feat(headless): complete portable single-session host
This commit is contained in:
parent
fbebb91848
commit
f8cb840fb1
30 changed files with 3571 additions and 32 deletions
83
src/AcDream.Headless/Configuration/HeadlessCommandLine.cs
Normal file
83
src/AcDream.Headless/Configuration/HeadlessCommandLine.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
namespace AcDream.Headless.Configuration;
|
||||
|
||||
internal sealed record HeadlessCommandLine(
|
||||
string Command,
|
||||
string ConfigurationPath,
|
||||
HeadlessPathOverrides Paths)
|
||||
{
|
||||
internal static HeadlessCommandLine Parse(
|
||||
IReadOnlyList<string> arguments)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(arguments);
|
||||
if (arguments.Count < 3)
|
||||
throw new HeadlessCommandLineException("Missing command options.");
|
||||
|
||||
string command = arguments[0];
|
||||
if (command is not ("validate" or "run"))
|
||||
throw new HeadlessCommandLineException("Unknown command.");
|
||||
|
||||
string? configurationPath = null;
|
||||
string? configDirectory = null;
|
||||
string? dataDirectory = null;
|
||||
string? cacheDirectory = null;
|
||||
for (int index = 1; index < arguments.Count; index += 2)
|
||||
{
|
||||
if (index + 1 >= arguments.Count)
|
||||
{
|
||||
throw new HeadlessCommandLineException(
|
||||
"Every command option requires a value.");
|
||||
}
|
||||
|
||||
string name = arguments[index];
|
||||
string value = arguments[index + 1];
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new HeadlessCommandLineException(
|
||||
"Command option values cannot be empty.");
|
||||
}
|
||||
|
||||
switch (name)
|
||||
{
|
||||
case "--config":
|
||||
SetOnce(ref configurationPath, value);
|
||||
break;
|
||||
case "--config-dir":
|
||||
SetOnce(ref configDirectory, value);
|
||||
break;
|
||||
case "--data-dir":
|
||||
SetOnce(ref dataDirectory, value);
|
||||
break;
|
||||
case "--cache-dir":
|
||||
SetOnce(ref cacheDirectory, value);
|
||||
break;
|
||||
default:
|
||||
throw new HeadlessCommandLineException(
|
||||
"Unknown command option.");
|
||||
}
|
||||
}
|
||||
|
||||
if (configurationPath is null)
|
||||
{
|
||||
throw new HeadlessCommandLineException(
|
||||
"The --config option is required.");
|
||||
}
|
||||
|
||||
return new HeadlessCommandLine(
|
||||
command,
|
||||
configurationPath,
|
||||
new HeadlessPathOverrides(
|
||||
configDirectory,
|
||||
dataDirectory,
|
||||
cacheDirectory));
|
||||
}
|
||||
|
||||
private static void SetOnce(ref string? destination, string value)
|
||||
{
|
||||
if (destination is not null)
|
||||
{
|
||||
throw new HeadlessCommandLineException(
|
||||
"Command options cannot be repeated.");
|
||||
}
|
||||
destination = value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace AcDream.Headless.Configuration;
|
||||
|
||||
internal sealed class HeadlessCommandLineException : Exception
|
||||
{
|
||||
internal HeadlessCommandLineException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,73 @@ internal sealed class HeadlessConfiguration
|
|||
[JsonRequired]
|
||||
public int Version { get; init; }
|
||||
|
||||
public HeadlessProcessSettings Process { get; init; } = new();
|
||||
|
||||
[JsonRequired]
|
||||
public List<HeadlessSessionDescriptor?> Sessions { get; init; } = [];
|
||||
}
|
||||
|
||||
internal sealed class HeadlessProcessSettings
|
||||
{
|
||||
public HeadlessPathOverrides Paths { get; init; } = new();
|
||||
}
|
||||
|
||||
internal sealed class HeadlessSessionDescriptor
|
||||
{
|
||||
[JsonRequired]
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
[JsonRequired]
|
||||
public HeadlessEndpointDescriptor Endpoint { get; init; } = new();
|
||||
|
||||
[JsonRequired]
|
||||
public string Account { get; init; } = string.Empty;
|
||||
|
||||
[JsonRequired]
|
||||
public HeadlessCharacterSelector Character { get; init; } = new();
|
||||
|
||||
[JsonRequired]
|
||||
public HeadlessBotPolicyDescriptor Policy { get; init; } = new();
|
||||
|
||||
[JsonRequired]
|
||||
public HeadlessCredentialReference Credential { get; init; } = new();
|
||||
}
|
||||
|
||||
internal sealed class HeadlessEndpointDescriptor
|
||||
{
|
||||
[JsonRequired]
|
||||
public string Host { get; init; } = string.Empty;
|
||||
|
||||
[JsonRequired]
|
||||
public int Port { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class HeadlessCharacterSelector
|
||||
{
|
||||
public int? Index { get; init; }
|
||||
public uint? Id { get; init; }
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class HeadlessBotPolicyDescriptor
|
||||
{
|
||||
[JsonRequired]
|
||||
public string Id { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<HeadlessCredentialProviderKind>))]
|
||||
internal enum HeadlessCredentialProviderKind
|
||||
{
|
||||
Environment,
|
||||
StandardInput,
|
||||
File,
|
||||
}
|
||||
|
||||
internal sealed class HeadlessCredentialReference
|
||||
{
|
||||
[JsonRequired]
|
||||
public HeadlessCredentialProviderKind Provider { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public string Reference { get; init; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ internal static class HeadlessConfigurationLoader
|
|||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
ReadCommentHandling = JsonCommentHandling.Disallow,
|
||||
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(
|
||||
JsonNamingPolicy.CamelCase,
|
||||
allowIntegerValues: false),
|
||||
},
|
||||
};
|
||||
|
||||
internal static HeadlessConfiguration Load(string path)
|
||||
|
|
@ -47,6 +53,8 @@ internal static class HeadlessConfigurationLoader
|
|||
}
|
||||
|
||||
var sessionIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var credentialReferences = new HashSet<string>(
|
||||
StringComparer.Ordinal);
|
||||
foreach (HeadlessSessionDescriptor? session in configuration.Sessions)
|
||||
{
|
||||
if (session is null
|
||||
|
|
@ -61,8 +69,66 @@ internal static class HeadlessConfigurationLoader
|
|||
throw new HeadlessConfigurationException(
|
||||
$"Duplicate session id '{session.Id}'.");
|
||||
}
|
||||
|
||||
ValidateSession(session);
|
||||
string credentialKey =
|
||||
$"{session.Credential.Provider}:{session.Credential.Reference}";
|
||||
if (!credentialReferences.Add(credentialKey))
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Credential reference for session '{session.Id}' is already in use.");
|
||||
}
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private static void ValidateSession(HeadlessSessionDescriptor session)
|
||||
{
|
||||
if (session.Endpoint is null
|
||||
|| string.IsNullOrWhiteSpace(session.Endpoint.Host)
|
||||
|| session.Endpoint.Port is < 1 or > 65535)
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a host and a port from 1 through 65535.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(session.Account))
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a non-empty account.");
|
||||
}
|
||||
|
||||
if (session.Character is null)
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a character selector.");
|
||||
}
|
||||
|
||||
int selectorCount =
|
||||
(session.Character.Index.HasValue ? 1 : 0)
|
||||
+ (session.Character.Id.HasValue ? 1 : 0)
|
||||
+ (!string.IsNullOrWhiteSpace(session.Character.Name) ? 1 : 0);
|
||||
if (selectorCount != 1
|
||||
|| session.Character.Index is < 0
|
||||
|| session.Character.Id == 0u)
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' character selector must specify exactly one valid index, id, or name.");
|
||||
}
|
||||
|
||||
if (session.Policy is null
|
||||
|| string.IsNullOrWhiteSpace(session.Policy.Id))
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a non-empty policy id.");
|
||||
}
|
||||
|
||||
if (session.Credential is null
|
||||
|| string.IsNullOrWhiteSpace(session.Credential.Reference))
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a credential reference.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
src/AcDream.Headless/Configuration/HeadlessPathOverrides.cs
Normal file
13
src/AcDream.Headless/Configuration/HeadlessPathOverrides.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace AcDream.Headless.Configuration;
|
||||
|
||||
internal sealed record HeadlessPathOverrides(
|
||||
string? ConfigDirectory = null,
|
||||
string? DataDirectory = null,
|
||||
string? CacheDirectory = null)
|
||||
{
|
||||
internal HeadlessPathOverrides Merge(HeadlessPathOverrides commandLine) =>
|
||||
new(
|
||||
commandLine.ConfigDirectory ?? ConfigDirectory,
|
||||
commandLine.DataDirectory ?? DataDirectory,
|
||||
commandLine.CacheDirectory ?? CacheDirectory);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue