feat(launcher): complete Campaign LA11 pre-gate support

This commit is contained in:
Erik 2026-08-15 00:02:04 +02:00
parent f881e5b467
commit 134edabed2
19 changed files with 433 additions and 55 deletions

View file

@ -160,18 +160,23 @@ static async Task<int> BootstrapProbeAsync(string[] arguments)
static int CanonicalProbe(string[] arguments)
{
if (arguments.Length != 1)
if (arguments.Length < 1)
{
return 2;
}
string suffix = arguments.Length == 1
? string.Empty
: Environment.NewLine
+ string.Join(Environment.NewLine, arguments[1..]);
File.WriteAllText(
Path.GetFullPath(arguments[0]),
Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture)
+ "|"
+ Path.GetFullPath(
Environment.ProcessPath
?? throw new InvalidOperationException("Process path is unavailable.")));
?? throw new InvalidOperationException("Process path is unavailable."))
+ suffix);
return 0;
}

View file

@ -279,25 +279,43 @@ public sealed class LauncherSelfUpdateManagerTests : IDisposable
public async Task BootstrapConfirmationAndOrdinaryStartupDoNotUseShellParsing()
{
using var harness = new Harness(_root);
string[] publicArguments =
[
"--config-dir", Path.Combine(_root, "config with spaces"),
"--data-dir", Path.Combine(_root, "data & literal"),
"--cache-dir", Path.Combine(_root, "cache"),
"--update-manifest-uri", "http://127.0.0.1:43119/manifest.json",
];
SelfUpdateStartupResult ordinary = await LauncherSelfUpdateBootstrap.HandleAsync(
["--literal", "argument with spaces & metacharacters"],
publicArguments,
harness.Manager,
harness.Target,
harness.LauncherPath);
Assert.False(ordinary.ShouldExit);
Assert.Equal(["--literal", "argument with spaces & metacharacters"],
ordinary.RemainingArguments);
Assert.Equal(publicArguments, ordinary.RemainingArguments);
SelfUpdateStartupResult deferred = await LauncherSelfUpdateBootstrap.HandleAsync(
[LauncherSelfUpdateBootstrap.DeferredArgument, .. publicArguments],
harness.Manager,
harness.Target,
harness.LauncherPath);
Assert.False(deferred.ShouldExit);
Assert.Equal(publicArguments, deferred.RemainingArguments);
_ = await harness.StageAsync();
SelfUpdatePlan applied = await harness.Manager.ApplyPendingAsync(harness.Target);
SelfUpdateStartupResult confirmation = await LauncherSelfUpdateBootstrap.HandleAsync(
[LauncherSelfUpdateBootstrap.ConfirmArgument, applied.TransactionId],
[
LauncherSelfUpdateBootstrap.ConfirmArgument,
applied.TransactionId,
.. publicArguments,
],
harness.Manager,
harness.Target,
harness.LauncherPath);
Assert.False(confirmation.ShouldExit);
Assert.Empty(confirmation.RemainingArguments);
Assert.Equal(publicArguments, confirmation.RemainingArguments);
Assert.False(File.Exists(harness.Manager.PendingPlanPath));
Assert.False(harness.Manager.IsConfirmed(applied.TransactionId));
}

View file

@ -34,6 +34,13 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
string ready = Path.Combine(_root, "crash.ready");
string launched = Path.Combine(_root, "replacement.ready");
string helperPidPath = Path.Combine(_root, "helper.pid");
string[] processLocalSuffix =
[
"--config-dir", Path.Combine(_root, "isolated config"),
"--data-dir", Path.Combine(_root, "isolated data"),
"--cache-dir", Path.Combine(_root, "isolated cache"),
"--update-manifest-uri", "http://127.0.0.1:43119/manifest.json",
];
Directory.CreateDirectory(_root);
string rid = LauncherRuntimeIdentity.DetectRid();
PreparedLauncher prepared = PrepareLauncherClosure(target, rid);
@ -76,7 +83,7 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
};
using Process canonical = StartProcess(
prepared.CanonicalPath,
["canonical-probe", launched],
["canonical-probe", launched, .. processLocalSuffix],
environment);
await canonical.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(20));
Assert.Equal(0, canonical.ExitCode);
@ -100,7 +107,12 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
SearchOption.TopDirectoryOnly));
Assert.Null(await manager.LoadPendingAsync());
int replacementPid = ParsePid(await File.ReadAllTextAsync(launched));
string launchMarker = await File.ReadAllTextAsync(launched);
Assert.EndsWith(
Environment.NewLine + string.Join(Environment.NewLine, processLocalSuffix),
launchMarker,
StringComparison.Ordinal);
int replacementPid = ParsePid(launchMarker);
int helperPid = int.Parse(
await File.ReadAllTextAsync(helperPidPath),
System.Globalization.CultureInfo.InvariantCulture);

View file

@ -44,6 +44,26 @@ public sealed class LauncherVersionTests
public sealed class ReleaseManifestClientTests
{
[Theory]
[InlineData("https://updates.example.test/manifest.json")]
[InlineData("http://127.0.0.1:43119/manifest.json")]
[InlineData("http://localhost:43119/manifest.json")]
public void LocalUpdateFeedOverrideAcceptsOnlySecureOrLoopbackFeeds(string value)
{
using ReleaseManifestClient source =
ReleaseManifestClient.CreateLocalUpdateFeedOverride(new Uri(value));
}
[Theory]
[InlineData("http://updates.example.test/manifest.json")]
[InlineData("file:///tmp/manifest.json")]
[InlineData("https://user:secret@updates.example.test/manifest.json")]
[InlineData("https://updates.example.test/manifest.json?token=secret")]
[InlineData("https://updates.example.test/manifest.json#fragment")]
public void LocalUpdateFeedOverrideRejectsRemoteHttpAndCredentialLikeUris(string value) =>
Assert.Throws<LauncherUpdateException>(() =>
ReleaseManifestClient.CreateLocalUpdateFeedOverride(new Uri(value)));
[Fact]
public async Task FetchesStrictManifestFromLoopbackAndPinsProductionFeed()
{

View file

@ -146,6 +146,78 @@ public sealed class LauncherStartupOptionsTests
Assert.Equal(helperOptions.UpdateManifestUri, confirmationOptions.UpdateManifestUri);
}
[Fact]
public void DeferredSelfUpdateRestartRetainsIsolationWithoutResolvingDefaults()
{
string root = Path.GetFullPath(
Path.Combine(Path.GetTempPath(), "acdream-la11-deferred"));
string[] suffix =
[
"--config-dir", Path.Combine(root, "config"),
"--data-dir", Path.Combine(root, "data"),
"--cache-dir", Path.Combine(root, "cache"),
"--update-manifest-uri", "http://127.0.0.1:43119/manifest.json",
];
LauncherStartupOptions options = LauncherStartupOptions.Parse(
["--acdream-self-update-deferred-v1", .. suffix],
() => throw new InvalidOperationException(
"canonical path resolver was touched"));
Assert.Equal(LauncherStartupMode.SelfUpdateDeferred, options.Mode);
Assert.Equal(suffix, options.PublicArguments);
Assert.Equal(Path.Combine(root, "config"), options.Paths.ConfigDirectory);
Assert.Equal(Path.Combine(root, "data"), options.Paths.DataDirectory);
Assert.Equal(Path.Combine(root, "cache"), options.Paths.CacheDirectory);
}
[Fact]
public void AvaloniaCompositionRetainsTheExactParsedOptionsAndPathSet()
{
string root = Path.GetFullPath(
Path.Combine(Path.GetTempPath(), "acdream-la11-app-composition"));
LauncherStartupOptions options = LauncherStartupOptions.Parse(
[
"--config-dir", Path.Combine(root, "config"),
"--data-dir", Path.Combine(root, "data"),
"--cache-dir", Path.Combine(root, "cache"),
"--update-manifest-uri", "https://updates.example.test/manifest.json",
],
() => throw new InvalidOperationException(
"canonical path resolver was touched"));
var app = new App(options);
Assert.Same(options, app.StartupOptions);
Assert.Same(options.Paths, app.StartupOptions.Paths);
Assert.Equal(
new Uri("https://updates.example.test/manifest.json"),
app.StartupOptions.UpdateManifestUri);
}
[Fact]
public void CompositionRejectsAnyBootstrapArgumentDrift()
{
var paths = new ApplicationPathSet("config", "data", "cache", null);
LauncherStartupOptions options = LauncherStartupOptions.Parse(
["--update-manifest-uri", "https://updates.example.test/manifest.json"],
() => paths);
Program.RequireUnchangedPublicArguments(
options,
new SelfUpdateStartupResult(
false,
0,
options.PublicArguments.ToArray()));
Assert.Throws<InvalidOperationException>(() =>
Program.RequireUnchangedPublicArguments(
options,
new SelfUpdateStartupResult(
false,
0,
["--update-manifest-uri", "https://other.example.test/manifest.json"])));
}
public static TheoryData<string[]> InvalidArguments()
{
string absolute = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "acdream-la11"));
@ -171,6 +243,10 @@ public sealed class LauncherStartupOptionsTests
data.Add(["--update-manifest-uri", "file:///tmp/manifest.json"]);
data.Add(
["--update-manifest-uri", "https://user:secret@example.test/manifest.json"]);
data.Add(
["--update-manifest-uri", "https://example.test/manifest.json?token=secret"]);
data.Add(
["--update-manifest-uri", "https://example.test/manifest.json#fragment"]);
data.Add(["--update-manifest-uri", "not-a-uri"]);
data.Add(
[

View file

@ -62,4 +62,44 @@ public sealed class LauncherUpdateCompositionTests : IDisposable
() => composition.Updater.CheckAsync());
Assert.Contains(exception.Message, updateError.Message, StringComparison.Ordinal);
}
[Theory]
[InlineData("https://updates.example.test/manifest.json")]
[InlineData("http://127.0.0.1:43119/manifest.json")]
public void ProcessLocalManifestOverrideReachesOnlyUpdateComposition(string value)
{
Directory.CreateDirectory(_root);
var paths = new ApplicationPathSet(
Path.Combine(_root, "config"),
Path.Combine(_root, "data"),
Path.Combine(_root, "cache"),
null);
var manifestUri = new Uri(value);
using LauncherUpdateComposition composition = LauncherUpdateComposition.Create(
paths,
LauncherRuntimeIdentity.DetectRid(),
LauncherVersion.Parse("1.0.0"),
_root,
() => false,
initialize: (_, _) => new ClientVersionResolution(
ClientVersionState.Missing,
"No client version is installed.",
null,
null,
null,
null),
updateManifestUri: manifestUri);
Assert.Same(manifestUri, composition.UpdateManifestUri);
Assert.Equal(
Path.Combine(paths.DataDirectory, "app"),
composition.Versions.AppDirectory);
Assert.False(File.Exists(
Path.Combine(paths.ConfigDirectory, "launcher-profiles.json")));
Assert.Empty(Directory.EnumerateFiles(
_root,
"*",
SearchOption.AllDirectories));
}
}