feat(launcher): Campaign LA add Avalonia desktop shell

This commit is contained in:
Erik 2026-08-14 18:15:14 +02:00
parent 6c4cd2bbc6
commit d0a9c65d85
31 changed files with 4831 additions and 16 deletions

View file

@ -2,13 +2,47 @@ using System.Runtime.ExceptionServices;
namespace AcDream.Launcher.Core.Launching;
/// <summary>
/// Test seam for one supervised launcher child. The Avalonia orchestration
/// layer owns this interface through a factory and never constructs or drives
/// <see cref="System.Diagnostics.Process"/> directly.
/// </summary>
public interface ILauncherProcessSupervisor : IDisposable
{
LauncherSessionState State { get; }
int? ExitCode { get; }
event EventHandler<LauncherSessionState>? StateChanged;
void Start(LauncherProcessSpec spec, string? password);
void Stop(TimeSpan timeout);
}
public interface ILauncherProcessSupervisorFactory
{
ILauncherProcessSupervisor Create();
}
public sealed class LauncherProcessSupervisorFactory(
ILauncherChildProcessFactory? childProcessFactory = null)
: ILauncherProcessSupervisorFactory
{
private readonly ILauncherChildProcessFactory _childProcessFactory =
childProcessFactory ?? new SystemChildProcessFactory();
public ILauncherProcessSupervisor Create() =>
new LauncherProcessSupervisor(_childProcessFactory);
}
/// <summary>
/// Spawns a host process (App/Headless), feeds the account password to
/// its stdin then closes it, and supervises its lifetime (Campaign LA
/// spec §3/§6). One supervisor instance owns exactly one child process
/// for its lifetime — start a new supervisor per launched session.
/// </summary>
public sealed class LauncherProcessSupervisor : IDisposable
public sealed class LauncherProcessSupervisor : ILauncherProcessSupervisor
{
private readonly ILauncherChildProcessFactory _factory;
private readonly object _gate = new();

View file

@ -13,6 +13,64 @@ public sealed record ComposedSessionConfig(
string StatusFilePath,
SessionConfigDocument Document);
/// <summary>
/// Injectable composition/write seam used by the canonical launcher
/// orchestrator. Production delegates to <see cref="SessionConfigComposer"/>;
/// tests can capture the exact request without writing a file or starting a
/// client process.
/// </summary>
public interface ILauncherSessionConfigService
{
ComposedSessionConfig ComposeAndWrite(
ServerProfile server,
AccountProfile account,
CharacterProfile character,
LauncherInstallRecord install,
ApplicationPathSet paths,
string sessionId,
int? loginCommandDelayMs = null);
ComposedSessionConfig ComposeProbeAndWrite(
ServerProfile server,
AccountProfile account,
LauncherInstallRecord install,
ApplicationPathSet paths,
string sessionId);
}
public sealed class LauncherSessionConfigService : ILauncherSessionConfigService
{
public ComposedSessionConfig ComposeAndWrite(
ServerProfile server,
AccountProfile account,
CharacterProfile character,
LauncherInstallRecord install,
ApplicationPathSet paths,
string sessionId,
int? loginCommandDelayMs = null) =>
SessionConfigComposer.ComposeAndWrite(
server,
account,
character,
install,
paths,
sessionId,
loginCommandDelayMs);
public ComposedSessionConfig ComposeProbeAndWrite(
ServerProfile server,
AccountProfile account,
LauncherInstallRecord install,
ApplicationPathSet paths,
string sessionId) =>
SessionConfigComposer.ComposeProbeAndWrite(
server,
account,
install,
paths,
sessionId);
}
/// <summary>
/// Builds the per-launch <see cref="SessionConfigDocument"/> from a
/// profile character + install record (Campaign LA spec §6). Passwords
@ -187,6 +245,23 @@ public static class SessionConfigComposer
sessionId,
loginCommandDelayMs);
return Write(composed);
}
/// <summary>Probe counterpart to <see cref="ComposeAndWrite"/>. It
/// writes the pinned <c>mode: "probe"</c> document and never includes
/// a character selector, policy, plugin set, login commands, or password.
/// </summary>
public static ComposedSessionConfig ComposeProbeAndWrite(
ServerProfile server,
AccountProfile account,
LauncherInstallRecord install,
ApplicationPathSet paths,
string sessionId) =>
Write(ComposeProbe(server, account, install, paths, sessionId));
private static ComposedSessionConfig Write(ComposedSessionConfig composed)
{
string? directory = Path.GetDirectoryName(composed.ConfigFilePath);
if (!string.IsNullOrEmpty(directory))
{