using System.Diagnostics; using System.Reflection; using AcDream.Bake; using AcDream.Launcher.Core.Installation; using AcDream.Launcher.Core.Updates; using AcDream.Platform; return args.FirstOrDefault() switch { "hold-install-lease" => await HoldInstallLeaseAsync(args[1..]), "hold-update-lease" => await HoldUpdateLeaseAsync(args[1..]), "orphan-parent" => await RunOrphanParentAsync(args[1..]), "orphan-child" => RunOrphanChild(args[1..]), _ => 2, }; static async Task 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 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 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 RunAsync( BakeProcessRequest request, Action 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"); } }