feat(launcher): implement verified atomic updates
This commit is contained in:
parent
2198a0cc8e
commit
2d2a5b5046
34 changed files with 6755 additions and 61 deletions
|
|
@ -1,18 +1,19 @@
|
|||
using AcDream.Launcher.Core.Launching;
|
||||
using AcDream.Launcher.Core.Profiles;
|
||||
using AcDream.Launcher.Core.Updates;
|
||||
|
||||
namespace AcDream.Launcher.Core.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves and validates the co-deployed graphical/headless hosts. LA10 will
|
||||
/// replace the directory lookup with its versioned-current resolver; until
|
||||
/// then a missing host disables the corresponding action instead of deferring
|
||||
/// failure until process creation.
|
||||
/// Resolves and validates the graphical/headless hosts. Production uses the
|
||||
/// verified <c>DataDirectory/app/current.json</c> resolver; the explicit-path
|
||||
/// constructor remains the injectable test seam.
|
||||
/// </summary>
|
||||
public sealed class LauncherExecutableSet
|
||||
{
|
||||
private readonly Func<string, bool> _fileExists;
|
||||
private readonly Func<string, bool> _hasUnixExecutePermission;
|
||||
private readonly Func<ExecutablePaths> _resolve;
|
||||
|
||||
public LauncherExecutableSet(
|
||||
string graphicalHostPath,
|
||||
|
|
@ -23,25 +24,50 @@ public sealed class LauncherExecutableSet
|
|||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(graphicalHostPath);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(headlessHostPath);
|
||||
GraphicalHostPath = graphicalHostPath;
|
||||
HeadlessHostPath = headlessHostPath;
|
||||
WorkingDirectory = workingDirectory;
|
||||
string graphical = graphicalHostPath;
|
||||
string headless = headlessHostPath;
|
||||
_resolve = () => new ExecutablePaths(graphical, headless, workingDirectory);
|
||||
_fileExists = fileExists ?? File.Exists;
|
||||
_hasUnixExecutePermission =
|
||||
hasUnixExecutePermission ?? HasUnixExecutePermission;
|
||||
}
|
||||
|
||||
public string GraphicalHostPath { get; }
|
||||
private LauncherExecutableSet(
|
||||
Func<ExecutablePaths> resolve,
|
||||
Func<string, bool>? fileExists = null,
|
||||
Func<string, bool>? hasUnixExecutePermission = null)
|
||||
{
|
||||
_resolve = resolve ?? throw new ArgumentNullException(nameof(resolve));
|
||||
_fileExists = fileExists ?? File.Exists;
|
||||
_hasUnixExecutePermission =
|
||||
hasUnixExecutePermission ?? HasUnixExecutePermission;
|
||||
}
|
||||
|
||||
public string HeadlessHostPath { get; }
|
||||
public string GraphicalHostPath => _resolve().GraphicalHostPath;
|
||||
|
||||
public string? WorkingDirectory { get; }
|
||||
public string HeadlessHostPath => _resolve().HeadlessHostPath;
|
||||
|
||||
public string? WorkingDirectory => _resolve().WorkingDirectory;
|
||||
|
||||
public LauncherCapability GetAvailability(LaunchMode mode)
|
||||
{
|
||||
ExecutablePaths paths;
|
||||
try
|
||||
{
|
||||
paths = _resolve();
|
||||
}
|
||||
catch (Exception ex) when (ex is LauncherUpdateException
|
||||
or InvalidOperationException
|
||||
or IOException
|
||||
or UnauthorizedAccessException)
|
||||
{
|
||||
return LauncherCapability.Unavailable(
|
||||
$"The active versioned client is unavailable: {ex.Message}");
|
||||
}
|
||||
|
||||
string path = mode == LaunchMode.Headless
|
||||
? HeadlessHostPath
|
||||
: GraphicalHostPath;
|
||||
? paths.HeadlessHostPath
|
||||
: paths.GraphicalHostPath;
|
||||
string host = mode == LaunchMode.Headless
|
||||
? "headless host"
|
||||
: "graphical client";
|
||||
|
|
@ -68,27 +94,27 @@ public sealed class LauncherExecutableSet
|
|||
string configFilePath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(configFilePath);
|
||||
RequireAvailable(mode);
|
||||
ExecutablePaths paths = RequireAvailable(mode);
|
||||
|
||||
return mode == LaunchMode.Headless
|
||||
? new LauncherProcessSpec(
|
||||
HeadlessHostPath,
|
||||
paths.HeadlessHostPath,
|
||||
["--config", configFilePath],
|
||||
WorkingDirectory)
|
||||
paths.WorkingDirectory)
|
||||
: new LauncherProcessSpec(
|
||||
GraphicalHostPath,
|
||||
paths.GraphicalHostPath,
|
||||
["--session-config", configFilePath],
|
||||
WorkingDirectory);
|
||||
paths.WorkingDirectory);
|
||||
}
|
||||
|
||||
public LauncherProcessSpec CreateProbeSpec(string configFilePath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(configFilePath);
|
||||
RequireAvailable(LaunchMode.Headless);
|
||||
ExecutablePaths paths = RequireAvailable(LaunchMode.Headless);
|
||||
return new LauncherProcessSpec(
|
||||
HeadlessHostPath,
|
||||
paths.HeadlessHostPath,
|
||||
["--config", configFilePath],
|
||||
WorkingDirectory);
|
||||
paths.WorkingDirectory);
|
||||
}
|
||||
|
||||
public static LauncherExecutableSet FromDirectory(string directory)
|
||||
|
|
@ -102,7 +128,28 @@ public sealed class LauncherExecutableSet
|
|||
fullDirectory);
|
||||
}
|
||||
|
||||
private void RequireAvailable(LaunchMode mode)
|
||||
/// <summary>
|
||||
/// Dynamic production resolver. The store cache is admitted only after a
|
||||
/// strict startup/update verification, and a pointer swap changes the
|
||||
/// binaries selected for the next session without replacing LA9 content.
|
||||
/// </summary>
|
||||
public static LauncherExecutableSet FromCurrentVersionStore(
|
||||
ClientVersionStore store)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
return new LauncherExecutableSet(() =>
|
||||
{
|
||||
ClientVersionResolution resolution = store.CachedResolution;
|
||||
if (!resolution.IsVerified || resolution.Directory is null)
|
||||
{
|
||||
throw new LauncherUpdateException(resolution.Status);
|
||||
}
|
||||
|
||||
return FromDirectoryPaths(resolution.Directory);
|
||||
});
|
||||
}
|
||||
|
||||
private ExecutablePaths RequireAvailable(LaunchMode mode)
|
||||
{
|
||||
LauncherCapability capability = GetAvailability(mode);
|
||||
if (!capability.IsAvailable)
|
||||
|
|
@ -110,6 +157,18 @@ public sealed class LauncherExecutableSet
|
|||
throw new LauncherOperationException(
|
||||
capability.Reason ?? "The selected launcher host is unavailable.");
|
||||
}
|
||||
|
||||
return _resolve();
|
||||
}
|
||||
|
||||
private static ExecutablePaths FromDirectoryPaths(string directory)
|
||||
{
|
||||
string fullDirectory = Path.GetFullPath(directory);
|
||||
string executableSuffix = OperatingSystem.IsWindows() ? ".exe" : string.Empty;
|
||||
return new ExecutablePaths(
|
||||
Path.Combine(fullDirectory, "AcDream.App" + executableSuffix),
|
||||
Path.Combine(fullDirectory, "acdream-headless" + executableSuffix),
|
||||
fullDirectory);
|
||||
}
|
||||
|
||||
private static bool HasUnixExecutePermission(string path)
|
||||
|
|
@ -134,4 +193,9 @@ public sealed class LauncherExecutableSet
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record ExecutablePaths(
|
||||
string GraphicalHostPath,
|
||||
string HeadlessHostPath,
|
||||
string? WorkingDirectory);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue