376 lines
12 KiB
C#
376 lines
12 KiB
C#
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using AcDream.Bake;
|
|
using AcDream.Launcher.Core.Installation;
|
|
using AcDream.Launcher.Core.Updates;
|
|
using AcDream.Platform;
|
|
|
|
const string SelfUpdateDataEnvironment = "ACDREAM_SELF_UPDATE_FIXTURE_DATA";
|
|
const string SelfUpdateTargetEnvironment = "ACDREAM_SELF_UPDATE_FIXTURE_TARGET";
|
|
const string SelfUpdateHelperPidEnvironment = "ACDREAM_SELF_UPDATE_FIXTURE_HELPER_PID";
|
|
|
|
string[] effectiveArgs = args;
|
|
string? selfUpdateData = Environment.GetEnvironmentVariable(SelfUpdateDataEnvironment);
|
|
string? selfUpdateTarget = Environment.GetEnvironmentVariable(SelfUpdateTargetEnvironment);
|
|
if (!string.IsNullOrWhiteSpace(selfUpdateData)
|
|
&& !string.IsNullOrWhiteSpace(selfUpdateTarget)
|
|
&& IsBootstrapInvocation(effectiveArgs))
|
|
{
|
|
if (effectiveArgs[0] == LauncherSelfUpdateBootstrap.HelperArgument
|
|
&& Environment.GetEnvironmentVariable(SelfUpdateHelperPidEnvironment) is string helperPid
|
|
&& !string.IsNullOrWhiteSpace(helperPid))
|
|
{
|
|
File.WriteAllText(
|
|
Path.GetFullPath(helperPid),
|
|
Environment.ProcessId.ToString(
|
|
System.Globalization.CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
using var http = new HttpClient();
|
|
var manager = new LauncherSelfUpdateManager(Paths(selfUpdateData), http);
|
|
SelfUpdateStartupResult startup = await LauncherSelfUpdateBootstrap.HandleAsync(
|
|
effectiveArgs,
|
|
manager,
|
|
Path.GetFullPath(selfUpdateTarget),
|
|
Path.GetFullPath(
|
|
Environment.ProcessPath
|
|
?? throw new InvalidOperationException("Process path is unavailable.")));
|
|
if (startup.ShouldExit)
|
|
{
|
|
return startup.ExitCode;
|
|
}
|
|
|
|
effectiveArgs = startup.RemainingArguments;
|
|
}
|
|
|
|
return effectiveArgs.FirstOrDefault() switch
|
|
{
|
|
"hold-install-lease" => await HoldInstallLeaseAsync(effectiveArgs[1..]),
|
|
"hold-update-lease" => await HoldUpdateLeaseAsync(effectiveArgs[1..]),
|
|
"orphan-parent" => await RunOrphanParentAsync(effectiveArgs[1..]),
|
|
"orphan-child" => RunOrphanChild(effectiveArgs[1..]),
|
|
"crash-self-update" => await CrashSelfUpdateAsync(effectiveArgs[1..]),
|
|
"stage-self-update" => await StageSelfUpdateAsync(effectiveArgs[1..]),
|
|
"bootstrap-probe" => await BootstrapProbeAsync(effectiveArgs[1..]),
|
|
"canonical-probe" => CanonicalProbe(effectiveArgs[1..]),
|
|
_ => 2,
|
|
};
|
|
|
|
static bool IsBootstrapInvocation(string[] arguments) =>
|
|
arguments.Length > 0
|
|
&& arguments[0] is LauncherSelfUpdateBootstrap.HelperArgument
|
|
or LauncherSelfUpdateBootstrap.ConfirmArgument
|
|
or LauncherSelfUpdateBootstrap.DeferredArgument
|
|
or "canonical-probe";
|
|
|
|
static ApplicationPathSet Paths(string dataDirectory)
|
|
{
|
|
string data = Path.GetFullPath(dataDirectory);
|
|
return new ApplicationPathSet(
|
|
Path.Combine(data, "fixture-config"),
|
|
data,
|
|
Path.Combine(data, "fixture-cache"),
|
|
null);
|
|
}
|
|
|
|
static async Task<int> CrashSelfUpdateAsync(string[] arguments)
|
|
{
|
|
if (arguments.Length != 4)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
string dataDirectory = Path.GetFullPath(arguments[0]);
|
|
string targetDirectory = Path.GetFullPath(arguments[1]);
|
|
string readyPath = Path.GetFullPath(arguments[2]);
|
|
string canonicalName = arguments[3];
|
|
using var http = new HttpClient();
|
|
var manager = new LauncherSelfUpdateManager(
|
|
Paths(dataDirectory),
|
|
http,
|
|
null,
|
|
observation =>
|
|
{
|
|
if (observation.Boundary == SelfUpdateApplyBoundary.AfterTargetMutation
|
|
&& string.Equals(
|
|
observation.Path,
|
|
canonicalName,
|
|
StringComparison.Ordinal))
|
|
{
|
|
if (!File.Exists(Path.Combine(targetDirectory, canonicalName)))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The canonical launcher vanished at the apply boundary.");
|
|
}
|
|
|
|
File.WriteAllText(readyPath, Environment.ProcessId.ToString(
|
|
System.Globalization.CultureInfo.InvariantCulture));
|
|
Thread.Sleep(Timeout.Infinite);
|
|
}
|
|
});
|
|
using UpdateSessionBarrier.ExclusiveLease lease = manager.Barrier.AcquireExclusive();
|
|
_ = await manager.ApplyPendingAsync(targetDirectory);
|
|
return 0;
|
|
}
|
|
|
|
static async Task<int> StageSelfUpdateAsync(string[] arguments)
|
|
{
|
|
if (arguments.Length != 7
|
|
|| !long.TryParse(
|
|
arguments[6],
|
|
System.Globalization.NumberStyles.None,
|
|
System.Globalization.CultureInfo.InvariantCulture,
|
|
out long size))
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
string dataDirectory = Path.GetFullPath(arguments[0]);
|
|
using var http = new HttpClient();
|
|
var manager = new LauncherSelfUpdateManager(Paths(dataDirectory), http);
|
|
_ = await manager.StageAsync(
|
|
LauncherVersion.Parse(arguments[2]),
|
|
arguments[3],
|
|
new ReleaseArtifact(new Uri(arguments[4]), arguments[5], size),
|
|
Path.GetFullPath(arguments[1]),
|
|
progress: null,
|
|
CancellationToken.None);
|
|
return 0;
|
|
}
|
|
|
|
static async Task<int> BootstrapProbeAsync(string[] arguments)
|
|
{
|
|
if (arguments.Length != 4)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
using var http = new HttpClient();
|
|
var manager = new LauncherSelfUpdateManager(Paths(arguments[0]), http);
|
|
SelfUpdateStartupResult result = await LauncherSelfUpdateBootstrap.HandleAsync(
|
|
["ordinary"],
|
|
manager,
|
|
Path.GetFullPath(arguments[1]),
|
|
Path.GetFullPath(arguments[2]));
|
|
File.WriteAllText(
|
|
Path.GetFullPath(arguments[3]),
|
|
result.ShouldExit ? "exit" : string.Join("\n", result.RemainingArguments));
|
|
return result.ShouldExit ? 3 : 0;
|
|
}
|
|
|
|
static int CanonicalProbe(string[] arguments)
|
|
{
|
|
if (arguments.Length != 1)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
File.WriteAllText(
|
|
Path.GetFullPath(arguments[0]),
|
|
Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
|
+ "|"
|
|
+ Path.GetFullPath(
|
|
Environment.ProcessPath
|
|
?? throw new InvalidOperationException("Process path is unavailable.")));
|
|
return 0;
|
|
}
|
|
|
|
static async Task<int> HoldUpdateLeaseAsync(string[] arguments)
|
|
{
|
|
if (arguments.Length != 4
|
|
|| arguments[0] is not ("session" or "exclusive"))
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
var barrier = new UpdateSessionBarrier(Path.GetFullPath(arguments[1]));
|
|
using IDisposable lease = arguments[0] == "session"
|
|
? barrier.AcquireSession()
|
|
: barrier.AcquireExclusive();
|
|
string readyPath = Path.GetFullPath(arguments[2]);
|
|
string releasePath = Path.GetFullPath(arguments[3]);
|
|
File.WriteAllText(readyPath, arguments[0]);
|
|
while (!File.Exists(releasePath))
|
|
{
|
|
await Task.Delay(10);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static async Task<int> HoldInstallLeaseAsync(string[] arguments)
|
|
{
|
|
if (arguments.Length != 3)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
string lockPath = Path.GetFullPath(arguments[0]);
|
|
string stagingPath = Path.GetFullPath(arguments[1]);
|
|
string readyPath = Path.GetFullPath(arguments[2]);
|
|
Directory.CreateDirectory(
|
|
Path.GetDirectoryName(lockPath)
|
|
?? throw new InvalidOperationException("lock path has no parent"));
|
|
Directory.CreateDirectory(
|
|
Path.GetDirectoryName(stagingPath)
|
|
?? throw new InvalidOperationException("staging path has no parent"));
|
|
|
|
using var lease = new FileStream(
|
|
lockPath,
|
|
FileMode.OpenOrCreate,
|
|
FileAccess.ReadWrite,
|
|
FileShare.None);
|
|
File.WriteAllText(stagingPath, "abandoned bake staging");
|
|
File.WriteAllText(readyPath, "ready");
|
|
await Task.Delay(Timeout.InfiniteTimeSpan);
|
|
return 0;
|
|
}
|
|
|
|
static async Task<int> RunOrphanParentAsync(string[] arguments)
|
|
{
|
|
if (arguments.Length != 8)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
string dataDirectory = Path.GetFullPath(arguments[0]);
|
|
string datDirectory = Path.GetFullPath(arguments[1]);
|
|
string bakeMarker = Path.GetFullPath(arguments[2]);
|
|
string schedule = arguments[3];
|
|
string childReadyPath = Path.GetFullPath(arguments[4]);
|
|
string childReleasePath = Path.GetFullPath(arguments[5]);
|
|
string childPidPath = Path.GetFullPath(arguments[6]);
|
|
string childExitPath = Path.GetFullPath(arguments[7]);
|
|
var paths = new ApplicationPathSet(
|
|
Path.Combine(dataDirectory, "fixture-config"),
|
|
dataDirectory,
|
|
Path.Combine(dataDirectory, "fixture-cache"),
|
|
null);
|
|
var runner = new OrphanBakeProcessRunner(
|
|
schedule,
|
|
childReadyPath,
|
|
childReleasePath,
|
|
childPidPath,
|
|
childExitPath);
|
|
var installer = new LauncherInstaller(
|
|
paths,
|
|
bakeMarker,
|
|
processRunner: runner);
|
|
|
|
try
|
|
{
|
|
await installer.InstallAsync(datDirectory, 1);
|
|
return 0;
|
|
}
|
|
catch
|
|
{
|
|
return 9;
|
|
}
|
|
}
|
|
|
|
static int RunOrphanChild(string[] arguments)
|
|
{
|
|
if (arguments.Length != 5)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
string outputPath = Path.GetFullPath(arguments[0]);
|
|
string schedule = arguments[1];
|
|
string readyPath = Path.GetFullPath(arguments[2]);
|
|
string releasePath = Path.GetFullPath(arguments[3]);
|
|
string exitPath = Path.GetFullPath(arguments[4]);
|
|
Action barrier = () =>
|
|
{
|
|
File.WriteAllText(readyPath, schedule);
|
|
while (!File.Exists(releasePath))
|
|
{
|
|
Thread.Sleep(10);
|
|
}
|
|
};
|
|
|
|
int exitCode;
|
|
try
|
|
{
|
|
BakeOutputTransaction.WriteValidateAndPublish(
|
|
outputPath,
|
|
temporaryPath =>
|
|
{
|
|
File.WriteAllText(temporaryPath, "orphan replacement");
|
|
return 1;
|
|
},
|
|
(temporaryPath, _) =>
|
|
{
|
|
if (File.ReadAllText(temporaryPath) != "orphan replacement")
|
|
{
|
|
throw new InvalidDataException("staging content changed");
|
|
}
|
|
},
|
|
beforePublicationLock: schedule == "late" ? barrier : null,
|
|
beforePromotion: schedule == "holds" ? barrier : null,
|
|
CancellationToken.None);
|
|
exitCode = 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
File.WriteAllText(exitPath + ".error", ex.Message);
|
|
exitCode = 17;
|
|
}
|
|
|
|
File.WriteAllText(exitPath, exitCode.ToString(
|
|
System.Globalization.CultureInfo.InvariantCulture));
|
|
return exitCode;
|
|
}
|
|
|
|
file sealed class OrphanBakeProcessRunner(
|
|
string schedule,
|
|
string childReadyPath,
|
|
string childReleasePath,
|
|
string childPidPath,
|
|
string childExitPath) : IBakeProcessRunner
|
|
{
|
|
public async Task<BakeProcessResult> RunAsync(
|
|
BakeProcessRequest request,
|
|
Action<string> onStandardOutput,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
string dotnetHost = Environment.ProcessPath
|
|
?? throw new InvalidOperationException("dotnet host path is unavailable");
|
|
string fixtureDll = Assembly.GetExecutingAssembly().Location;
|
|
var startInfo = new ProcessStartInfo(dotnetHost)
|
|
{
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
};
|
|
startInfo.ArgumentList.Add(fixtureDll);
|
|
startInfo.ArgumentList.Add("orphan-child");
|
|
startInfo.ArgumentList.Add(request.OutputPath);
|
|
startInfo.ArgumentList.Add(schedule);
|
|
startInfo.ArgumentList.Add(childReadyPath);
|
|
startInfo.ArgumentList.Add(childReleasePath);
|
|
startInfo.ArgumentList.Add(childExitPath);
|
|
startInfo.Environment.Remove(
|
|
BakePublicationGuardPaths.NonceEnvironmentVariable);
|
|
startInfo.Environment[
|
|
BakePublicationGuardPaths.NonceEnvironmentVariable] =
|
|
request.PublicationNonce
|
|
?? throw new InvalidOperationException("publication nonce is missing");
|
|
|
|
using Process child = Process.Start(startInfo)
|
|
?? throw new InvalidOperationException("orphan child did not start");
|
|
File.WriteAllText(
|
|
childPidPath,
|
|
child.Id.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
|
await child.WaitForExitAsync(cancellationToken);
|
|
if (child.ExitCode == 0)
|
|
{
|
|
long bytes = new FileInfo(request.OutputPath).Length;
|
|
onStandardOutput("{\"v\":1,\"e\":\"started\",\"bakeToolVersion\":4}\n");
|
|
onStandardOutput($"{{\"v\":1,\"e\":\"completed\","
|
|
+ $"\"bakeToolVersion\":4,\"outputBytes\":{bytes},"
|
|
+ "\"failures\":0}\n");
|
|
}
|
|
|
|
return new BakeProcessResult(child.ExitCode, "orphan fixture child");
|
|
}
|
|
}
|