fix(launcher): harden Campaign LA11 gate evidence
This commit is contained in:
parent
134edabed2
commit
accd01a008
16 changed files with 1820 additions and 210 deletions
|
|
@ -31,7 +31,7 @@ if (!string.IsNullOrWhiteSpace(selfUpdateData)
|
|||
SelfUpdateStartupResult startup = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
effectiveArgs,
|
||||
manager,
|
||||
Path.GetFullPath(selfUpdateTarget),
|
||||
Path.GetFullPath(AppContext.BaseDirectory),
|
||||
Path.GetFullPath(
|
||||
Environment.ProcessPath
|
||||
?? throw new InvalidOperationException("Process path is unavailable.")));
|
||||
|
|
@ -53,6 +53,8 @@ return effectiveArgs.FirstOrDefault() switch
|
|||
"stage-self-update" => await StageSelfUpdateAsync(effectiveArgs[1..]),
|
||||
"bootstrap-probe" => await BootstrapProbeAsync(effectiveArgs[1..]),
|
||||
"canonical-probe" => CanonicalProbe(effectiveArgs[1..]),
|
||||
"hold-campaign-la-process" =>
|
||||
await HoldCampaignLaProcessAsync(effectiveArgs[1..]),
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
|
|
@ -60,7 +62,7 @@ static bool IsBootstrapInvocation(string[] arguments) =>
|
|||
arguments.Length > 0
|
||||
&& arguments[0] is LauncherSelfUpdateBootstrap.HelperArgument
|
||||
or LauncherSelfUpdateBootstrap.ConfirmArgument
|
||||
or LauncherSelfUpdateBootstrap.DeferredArgument
|
||||
or "--acdream-self-update-deferred-v1"
|
||||
or "canonical-probe";
|
||||
|
||||
static ApplicationPathSet Paths(string dataDirectory)
|
||||
|
|
@ -180,6 +182,34 @@ static int CanonicalProbe(string[] arguments)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static async Task<int> HoldCampaignLaProcessAsync(string[] arguments)
|
||||
{
|
||||
if (arguments.Length != 4
|
||||
|| arguments[0] is not ("--config" or "--session-config"))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
string configPath = Path.GetFullPath(arguments[1]);
|
||||
string readyPath = Path.GetFullPath(arguments[2]);
|
||||
string releasePath = Path.GetFullPath(arguments[3]);
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
File.WriteAllText(
|
||||
readyPath,
|
||||
Environment.ProcessId.ToString(
|
||||
System.Globalization.CultureInfo.InvariantCulture));
|
||||
while (!File.Exists(releasePath))
|
||||
{
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static async Task<int> HoldUpdateLeaseAsync(string[] arguments)
|
||||
{
|
||||
if (arguments.Length != 4
|
||||
|
|
|
|||
|
|
@ -295,29 +295,228 @@ public sealed class LauncherSelfUpdateManagerTests : IDisposable
|
|||
Assert.Equal(publicArguments, ordinary.RemainingArguments);
|
||||
|
||||
SelfUpdateStartupResult deferred = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[LauncherSelfUpdateBootstrap.DeferredArgument, .. publicArguments],
|
||||
["--acdream-self-update-deferred-v1", .. publicArguments],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
Assert.False(deferred.ShouldExit);
|
||||
Assert.Equal(publicArguments, deferred.RemainingArguments);
|
||||
Assert.True(deferred.ShouldExit);
|
||||
Assert.Equal(64, deferred.ExitCode);
|
||||
Assert.Empty(deferred.RemainingArguments);
|
||||
|
||||
_ = await harness.StageAsync();
|
||||
SelfUpdatePlan applied = await harness.Manager.ApplyPendingAsync(harness.Target);
|
||||
SelfUpdateStartupResult confirmation = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[
|
||||
LauncherSelfUpdateBootstrap.ConfirmArgument,
|
||||
applied.TransactionId,
|
||||
.. publicArguments,
|
||||
],
|
||||
SelfUpdateStartupResult confirmation;
|
||||
using (UpdateSessionBarrier.ExclusiveLease helperLease =
|
||||
harness.Manager.Barrier.AcquireExclusive())
|
||||
{
|
||||
confirmation = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[
|
||||
LauncherSelfUpdateBootstrap.ConfirmArgument,
|
||||
applied.TransactionId,
|
||||
.. publicArguments,
|
||||
],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
}
|
||||
|
||||
Assert.False(confirmation.ShouldExit);
|
||||
Assert.Equal(publicArguments, confirmation.RemainingArguments);
|
||||
Assert.True(File.Exists(harness.Manager.PendingPlanPath));
|
||||
Assert.True(harness.Manager.IsConfirmed(applied.TransactionId));
|
||||
await harness.Manager.CompleteConfirmedAsync(applied.TransactionId, harness.Target);
|
||||
Assert.False(File.Exists(harness.Manager.PendingPlanPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContendedOrdinaryStartupAllowsOnlyNoPlanOrValidatedStagedPlan()
|
||||
{
|
||||
using var harness = new Harness(_root);
|
||||
using (UpdateSessionBarrier.SessionLease session =
|
||||
harness.Manager.Barrier.AcquireSession())
|
||||
{
|
||||
SelfUpdateStartupResult empty = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
["ordinary"],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
Assert.False(empty.ShouldExit);
|
||||
}
|
||||
|
||||
_ = await harness.StageAsync();
|
||||
using (UpdateSessionBarrier.SessionLease session =
|
||||
harness.Manager.Barrier.AcquireSession())
|
||||
{
|
||||
SelfUpdateStartupResult staged = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
["ordinary"],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
Assert.False(staged.ShouldExit);
|
||||
}
|
||||
|
||||
SelfUpdatePlan awaiting = await harness.Manager.ApplyPendingAsync(harness.Target);
|
||||
using (UpdateSessionBarrier.SessionLease session =
|
||||
harness.Manager.Barrier.AcquireSession())
|
||||
{
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
["ordinary"],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
}
|
||||
|
||||
SelfUpdatePlan rolledBack = await harness.Manager
|
||||
.RollbackAwaitingConfirmationAsync(harness.Target);
|
||||
using (UpdateSessionBarrier.SessionLease session =
|
||||
harness.Manager.Barrier.AcquireSession())
|
||||
{
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
["ordinary"],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
}
|
||||
|
||||
await SetPlanStateAsync(harness.Manager.PendingPlanPath, "applying");
|
||||
using (UpdateSessionBarrier.SessionLease session =
|
||||
harness.Manager.Barrier.AcquireSession())
|
||||
{
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
["ordinary"],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
}
|
||||
|
||||
Assert.Equal(SelfUpdatePlanState.AwaitingConfirmation, awaiting.State);
|
||||
Assert.Equal(SelfUpdatePlanState.RolledBack, rolledBack.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OrdinaryStartupRecoversApplyingAndFinalizesVerifiedRollback()
|
||||
{
|
||||
using var harness = new Harness(_root);
|
||||
_ = await harness.StageAsync();
|
||||
_ = await harness.Manager.ApplyPendingAsync(harness.Target);
|
||||
await SetPlanStateAsync(harness.Manager.PendingPlanPath, "applying");
|
||||
|
||||
SelfUpdateStartupResult result = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
["ordinary"],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
|
||||
Assert.False(confirmation.ShouldExit);
|
||||
Assert.Equal(publicArguments, confirmation.RemainingArguments);
|
||||
Assert.False(File.Exists(harness.Manager.PendingPlanPath));
|
||||
Assert.False(harness.Manager.IsConfirmed(applied.TransactionId));
|
||||
Assert.False(result.ShouldExit);
|
||||
Assert.Equal(["ordinary"], result.RemainingArguments);
|
||||
Assert.Null(await harness.Manager.LoadPendingAsync());
|
||||
Assert.Equal("old-launcher", await File.ReadAllTextAsync(harness.LauncherPath));
|
||||
Assert.Equal("old-support", await File.ReadAllTextAsync(harness.SupportPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InternalPrefixSpoofsCannotCrossPlanStateOrExecutableTrust()
|
||||
{
|
||||
using var harness = new Harness(_root);
|
||||
_ = await harness.StageAsync();
|
||||
SelfUpdatePlan staged = Assert.IsType<SelfUpdatePlan>(
|
||||
await harness.Manager.LoadPendingAsync());
|
||||
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[
|
||||
LauncherSelfUpdateBootstrap.HelperArgument,
|
||||
int.MaxValue.ToString(
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
harness.Target,
|
||||
staged.TransactionId,
|
||||
],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[LauncherSelfUpdateBootstrap.ConfirmArgument, staged.TransactionId],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
|
||||
SelfUpdatePlan awaiting = await harness.Manager.ApplyPendingAsync(harness.Target);
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[LauncherSelfUpdateBootstrap.ConfirmArgument, awaiting.TransactionId],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
Path.Combine(harness.Target, "spoof-launcher")));
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[
|
||||
LauncherSelfUpdateBootstrap.HelperArgument,
|
||||
int.MaxValue.ToString(
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
harness.Target,
|
||||
awaiting.TransactionId,
|
||||
],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
|
||||
SelfUpdatePlan rolledBack = await harness.Manager
|
||||
.RollbackAwaitingConfirmationAsync(harness.Target);
|
||||
foreach (string prefix in new[]
|
||||
{
|
||||
LauncherSelfUpdateBootstrap.HelperArgument,
|
||||
LauncherSelfUpdateBootstrap.ConfirmArgument,
|
||||
})
|
||||
{
|
||||
string[] arguments = prefix == LauncherSelfUpdateBootstrap.HelperArgument
|
||||
? [prefix, int.MaxValue.ToString(), harness.Target, rolledBack.TransactionId]
|
||||
: [prefix, rolledBack.TransactionId];
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
arguments,
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
}
|
||||
|
||||
SelfUpdateStartupResult deferred = await LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
["--acdream-self-update-deferred-v1"],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
Assert.True(deferred.ShouldExit);
|
||||
Assert.Equal(64, deferred.ExitCode);
|
||||
|
||||
await File.WriteAllTextAsync(harness.Manager.PendingPlanPath, "{ambiguous");
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[LauncherSelfUpdateBootstrap.ConfirmArgument, rolledBack.TransactionId],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
await Assert.ThrowsAsync<LauncherUpdateException>(() =>
|
||||
LauncherSelfUpdateBootstrap.HandleAsync(
|
||||
[
|
||||
LauncherSelfUpdateBootstrap.HelperArgument,
|
||||
int.MaxValue.ToString(),
|
||||
harness.Target,
|
||||
rolledBack.TransactionId,
|
||||
],
|
||||
harness.Manager,
|
||||
harness.Target,
|
||||
harness.LauncherPath));
|
||||
}
|
||||
|
||||
private static async Task SetPlanStateAsync(string path, string state)
|
||||
{
|
||||
JsonObject plan = Assert.IsType<JsonObject>(JsonNode.Parse(
|
||||
await File.ReadAllTextAsync(path)));
|
||||
plan["state"] = state;
|
||||
await File.WriteAllTextAsync(path, plan.ToJsonString());
|
||||
}
|
||||
|
||||
private sealed class Harness : IDisposable
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KilledAfterCanonicalReplaceCanInvokeCanonicalAndConvergeAutomatically()
|
||||
public async Task KilledApplyingPlanRecoversPriorAndContinuesCanonicalWithoutRetryLoop()
|
||||
{
|
||||
string data = Path.Combine(_root, "data");
|
||||
string target = Path.Combine(_root, "launcher");
|
||||
|
|
@ -94,13 +94,21 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
"The self-update journal did not converge.");
|
||||
|
||||
Assert.Equal(
|
||||
prepared.NewCanonicalHash,
|
||||
oldHash,
|
||||
await FileIntegrity.ComputeSha256HexAsync(prepared.CanonicalPath));
|
||||
Assert.True(File.Exists(Path.Combine(
|
||||
Assert.False(File.Exists(Path.Combine(
|
||||
target,
|
||||
LauncherSelfUpdateManager.InstallRecordFileName)));
|
||||
Assert.False(Directory.Exists(manager.GetTransactionDirectory(
|
||||
plan.TransactionId)));
|
||||
using (UpdateSessionBarrier.ExclusiveLease cleanupLease =
|
||||
manager.Barrier.AcquireExclusive())
|
||||
{
|
||||
Assert.True(manager.CleanupOwnedResidueUnderLease(
|
||||
pending: null,
|
||||
target,
|
||||
cleanupLease));
|
||||
}
|
||||
Assert.Empty(Directory.EnumerateDirectories(
|
||||
target,
|
||||
".acdream-self-update-*",
|
||||
|
|
@ -113,11 +121,8 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
launchMarker,
|
||||
StringComparison.Ordinal);
|
||||
int replacementPid = ParsePid(launchMarker);
|
||||
int helperPid = int.Parse(
|
||||
await File.ReadAllTextAsync(helperPidPath),
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
await WaitForProcessExitAsync(replacementPid, TimeSpan.FromSeconds(10));
|
||||
await WaitForProcessExitAsync(helperPid, TimeSpan.FromSeconds(10));
|
||||
Assert.False(File.Exists(helperPidPath));
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Assert.True(
|
||||
|
|
@ -156,13 +161,8 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
["canonical-probe", launched],
|
||||
BootstrapEnvironment(crashed, helperPidPath));
|
||||
await canonical.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(20));
|
||||
Assert.Equal(0, canonical.ExitCode);
|
||||
await WaitForFileAsync(helperPidPath, process: null, TimeSpan.FromSeconds(20));
|
||||
await WaitForProcessExitAsync(
|
||||
int.Parse(
|
||||
await File.ReadAllTextAsync(helperPidPath),
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
TimeSpan.FromSeconds(20));
|
||||
Assert.NotEqual(0, canonical.ExitCode);
|
||||
Assert.False(File.Exists(helperPidPath));
|
||||
|
||||
Assert.False(File.Exists(launched));
|
||||
SelfUpdatePlan preserved = Assert.IsType<SelfUpdatePlan>(
|
||||
|
|
@ -204,13 +204,8 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
["canonical-probe", launched],
|
||||
BootstrapEnvironment(crashed, helperPidPath));
|
||||
await canonical.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(20));
|
||||
Assert.Equal(0, canonical.ExitCode);
|
||||
await WaitForFileAsync(helperPidPath, process: null, TimeSpan.FromSeconds(20));
|
||||
await WaitForProcessExitAsync(
|
||||
int.Parse(
|
||||
await File.ReadAllTextAsync(helperPidPath),
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
TimeSpan.FromSeconds(20));
|
||||
Assert.NotEqual(0, canonical.ExitCode);
|
||||
Assert.False(File.Exists(helperPidPath));
|
||||
|
||||
Assert.False(File.Exists(launched));
|
||||
Assert.True(File.Exists(outsideCanonical));
|
||||
|
|
@ -297,8 +292,8 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
["bootstrap-probe", data, target, canonical, resultPath]);
|
||||
await startup.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.Equal(0, startup.ExitCode);
|
||||
Assert.Equal("ordinary", await File.ReadAllTextAsync(resultPath));
|
||||
Assert.NotEqual(0, startup.ExitCode);
|
||||
Assert.False(File.Exists(resultPath));
|
||||
Assert.True(Directory.Exists(transaction));
|
||||
Assert.False(File.Exists(observer.PendingPlanPath));
|
||||
|
||||
|
|
@ -328,9 +323,9 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
string helperPid = Path.Combine(_root, "helper.pid");
|
||||
Directory.CreateDirectory(target);
|
||||
string rid = LauncherRuntimeIdentity.DetectRid();
|
||||
string canonical = Path.Combine(target, LauncherName(rid));
|
||||
await File.WriteAllTextAsync(canonical, "old-launcher");
|
||||
byte[] archive = UpdateTestData.LauncherZip(rid, "new-launcher");
|
||||
PreparedLauncher prepared = PrepareLauncherClosure(target, rid);
|
||||
string canonical = prepared.CanonicalPath;
|
||||
byte[] archive = prepared.NewArchive;
|
||||
using var server = new LocalHttpFixture();
|
||||
server.Add("launcher.zip", archive);
|
||||
using var http = new HttpClient();
|
||||
|
|
@ -354,7 +349,8 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
[HelperPidEnvironment] = helperPid,
|
||||
};
|
||||
|
||||
using Process helper = StartFixture(
|
||||
using Process helper = StartProcess(
|
||||
manager.GetStagedLauncherPath(plan),
|
||||
[
|
||||
LauncherSelfUpdateBootstrap.HelperArgument,
|
||||
int.MaxValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
|
|
@ -365,16 +361,151 @@ public sealed class LauncherSelfUpdateProcessTests : IDisposable
|
|||
], environment);
|
||||
await helper.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.Equal(LauncherSelfUpdateBootstrap.DeferredLeaseExitCode, helper.ExitCode);
|
||||
string helperError = await helper.StandardError.ReadToEndAsync();
|
||||
string helperOutput = await helper.StandardOutput.ReadToEndAsync();
|
||||
Assert.True(
|
||||
helper.ExitCode == LauncherSelfUpdateBootstrap.UpdateLeaseBusyExitCode,
|
||||
$"helper exit {helper.ExitCode}; stdout: {helperOutput}; stderr: {helperError}");
|
||||
Assert.True(File.Exists(helperPid));
|
||||
Assert.False(File.Exists(unexpectedLaunch));
|
||||
Assert.Equal("old-launcher", await File.ReadAllTextAsync(canonical));
|
||||
Assert.NotEqual(
|
||||
prepared.NewCanonicalHash,
|
||||
await FileIntegrity.ComputeSha256HexAsync(canonical));
|
||||
SelfUpdatePlan deferred = Assert.IsType<SelfUpdatePlan>(
|
||||
await manager.LoadPendingAsync());
|
||||
Assert.Equal(SelfUpdatePlanState.Staged, deferred.State);
|
||||
Assert.Equal(plan.TransactionId, deferred.TransactionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SpoofedInternalPrefixesCannotBypassAnyDurablePlanState()
|
||||
{
|
||||
string data = Path.Combine(_root, "data");
|
||||
string target = Path.Combine(_root, "launcher");
|
||||
string rid = LauncherRuntimeIdentity.DetectRid();
|
||||
PreparedLauncher prepared = PrepareLauncherClosure(target, rid);
|
||||
using var server = new LocalHttpFixture();
|
||||
server.Add("launcher.zip", prepared.NewArchive);
|
||||
using var http = new HttpClient();
|
||||
var manager = new LauncherSelfUpdateManager(UpdateTestData.Paths(_root), http);
|
||||
_ = await manager.StageAsync(
|
||||
LauncherVersion.Parse("2.0.0"),
|
||||
rid,
|
||||
new ReleaseArtifact(
|
||||
server.UriFor("launcher.zip"),
|
||||
UpdateTestData.Sha256(prepared.NewArchive),
|
||||
prepared.NewArchive.LongLength),
|
||||
target,
|
||||
progress: null,
|
||||
CancellationToken.None);
|
||||
SelfUpdatePlan plan = Assert.IsType<SelfUpdatePlan>(await manager.LoadPendingAsync());
|
||||
var environment = new Dictionary<string, string>
|
||||
{
|
||||
[DataEnvironment] = data,
|
||||
[TargetEnvironment] = target,
|
||||
};
|
||||
|
||||
await AssertInternalSpoofsRejectedAsync(
|
||||
prepared.CanonicalPath,
|
||||
target,
|
||||
plan.TransactionId,
|
||||
environment,
|
||||
"staged");
|
||||
|
||||
plan = await manager.ApplyPendingAsync(target);
|
||||
await SetPlanStateAsync(manager.PendingPlanPath, "applying");
|
||||
await AssertInternalSpoofsRejectedAsync(
|
||||
prepared.CanonicalPath,
|
||||
target,
|
||||
plan.TransactionId,
|
||||
environment,
|
||||
"applying");
|
||||
|
||||
plan = await manager.RecoverApplyingAsync(target);
|
||||
plan = await manager.ApplyPendingAsync(target);
|
||||
Assert.Equal(SelfUpdatePlanState.AwaitingConfirmation, plan.State);
|
||||
await AssertInternalSpoofsRejectedAsync(
|
||||
prepared.CanonicalPath,
|
||||
target,
|
||||
plan.TransactionId,
|
||||
environment,
|
||||
"awaitingConfirmation");
|
||||
|
||||
plan = await manager.RollbackAwaitingConfirmationAsync(target);
|
||||
await AssertInternalSpoofsRejectedAsync(
|
||||
prepared.CanonicalPath,
|
||||
target,
|
||||
plan.TransactionId,
|
||||
environment,
|
||||
"rolledBack");
|
||||
|
||||
await File.WriteAllTextAsync(manager.PendingPlanPath, "{ambiguous");
|
||||
await AssertInternalSpoofsRejectedAsync(
|
||||
prepared.CanonicalPath,
|
||||
target,
|
||||
plan.TransactionId,
|
||||
environment,
|
||||
"ambiguous");
|
||||
}
|
||||
|
||||
private static async Task AssertInternalSpoofsRejectedAsync(
|
||||
string canonicalPath,
|
||||
string targetDirectory,
|
||||
string transactionId,
|
||||
IReadOnlyDictionary<string, string> environment,
|
||||
string state)
|
||||
{
|
||||
(string Name, string[] Arguments, int? ExactExit)[] attempts =
|
||||
[
|
||||
(
|
||||
"deferred",
|
||||
["--acdream-self-update-deferred-v1"],
|
||||
64),
|
||||
(
|
||||
"helper",
|
||||
[
|
||||
LauncherSelfUpdateBootstrap.HelperArgument,
|
||||
int.MaxValue.ToString(
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
targetDirectory,
|
||||
transactionId,
|
||||
],
|
||||
null),
|
||||
(
|
||||
"confirm",
|
||||
[LauncherSelfUpdateBootstrap.ConfirmArgument, transactionId],
|
||||
null),
|
||||
];
|
||||
|
||||
foreach ((string name, string[] arguments, int? exactExit) in attempts)
|
||||
{
|
||||
using Process process = StartProcess(canonicalPath, arguments, environment);
|
||||
await process.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
string stderr = await process.StandardError.ReadToEndAsync();
|
||||
if (exactExit.HasValue)
|
||||
{
|
||||
Assert.True(
|
||||
process.ExitCode == exactExit.Value,
|
||||
$"{state}/{name} exited {process.ExitCode}: {stderr}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(
|
||||
process.ExitCode != 0,
|
||||
$"{state}/{name} unexpectedly succeeded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SetPlanStateAsync(string path, string state)
|
||||
{
|
||||
System.Text.Json.Nodes.JsonObject plan = Assert.IsType<
|
||||
System.Text.Json.Nodes.JsonObject>(
|
||||
System.Text.Json.Nodes.JsonNode.Parse(await File.ReadAllTextAsync(path)));
|
||||
plan["state"] = state;
|
||||
await File.WriteAllTextAsync(path, plan.ToJsonString());
|
||||
}
|
||||
|
||||
private PreparedLauncher PrepareLauncherClosure(string target, string rid)
|
||||
{
|
||||
string fixtureDirectory = GetFixtureDirectory();
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ public sealed class LauncherStartupOptionsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void DeferredSelfUpdateRestartRetainsIsolationWithoutResolvingDefaults()
|
||||
public void LegacyDeferredSelfUpdatePrefixIsRejectedAsUntrustedInput()
|
||||
{
|
||||
string root = Path.GetFullPath(
|
||||
Path.Combine(Path.GetTempPath(), "acdream-la11-deferred"));
|
||||
|
|
@ -159,16 +159,11 @@ public sealed class LauncherStartupOptionsTests
|
|||
"--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);
|
||||
Assert.Throws<LauncherStartupOptionsException>(() =>
|
||||
LauncherStartupOptions.Parse(
|
||||
["--acdream-self-update-deferred-v1", .. suffix],
|
||||
() => throw new InvalidOperationException(
|
||||
"canonical path resolver was touched")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue