forked from Manuel/meeting-assistant
ci: prepare source-bound native macOS build and tests under TCG
This commit is contained in:
@@ -23,6 +23,9 @@ static class NativeDiagnostic
|
||||
const long GuestDiskBytes = 64L * 1024 * 1024 * 1024;
|
||||
const long ContainerMemoryBytes = 6L * 1024 * 1024 * 1024;
|
||||
const int MaximumCapturedCharacters = 8 * 1024 * 1024;
|
||||
const string SdkVersion = "10.0.401";
|
||||
const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0";
|
||||
const string FullState = "/storage/14/ci-state";
|
||||
static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
|
||||
const string OriginalBootstrap = "[ ! -e /tmp/m ]&&{ /sbin/mount_9p installstate >/dev/null 2>&1;exec /Volumes/installstate/launch.sh;};: >/tmp/m\n";
|
||||
const string MountOnlyBootstrap = "[ ! -e /tmp/m ]&& /sbin/mount_9p installstate >/dev/null 2>&1; : >/tmp/m\n";
|
||||
@@ -58,22 +61,25 @@ static class NativeDiagnostic
|
||||
{
|
||||
if (args.Length == 0 || args.Contains("--help"))
|
||||
{
|
||||
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--output artifacts/native-macos] [--source existing-dockur-clone]");
|
||||
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--full] [--output artifacts/native-macos] [--source existing-dockur-clone] [--compression-chunk readonly-qualified-chunk]");
|
||||
return 0;
|
||||
}
|
||||
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
|
||||
var full = args.Contains("--full");
|
||||
if (args.Contains("--validate"))
|
||||
{
|
||||
ValidateContracts();
|
||||
if (full) ValidateFullContracts();
|
||||
if (Option(args, "--source") is { } source)
|
||||
{
|
||||
await PrepareSource(Path.GetFullPath(source), output, "validation", false, CancellationToken.None);
|
||||
await PrepareSource(Path.GetFullPath(source), output, full ? new string('0', 32) : "validation", false, CancellationToken.None, full);
|
||||
await ValidateResourceRetention(output);
|
||||
await ValidateRecoveryPatch(output);
|
||||
await ValidateTcgPreflight(output, CancellationToken.None);
|
||||
if (full) await ValidateDiskSerialParser(output);
|
||||
Save(Path.Combine(output, "validation.json"), new
|
||||
{
|
||||
success = true, profile = Profile,
|
||||
success = true, profile = Profile, mode = full ? "full" : "readiness",
|
||||
helperSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "MacOsNativeDiagnostic.cs"))),
|
||||
udifChecksumBindingSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"))),
|
||||
baselineReadinessNormalized = true, readinessDiagnosticBlocksExcluded = 7,
|
||||
@@ -82,9 +88,13 @@ static class NativeDiagnostic
|
||||
resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12,
|
||||
independentFixtureCrc32Readback = true, resourceSnapshotRetention = true, cpuProfileSourceContractsVerified = true,
|
||||
preflightGateFixtureCases = 8, qemuRuntimePreflightExecuted = false, templateIsoDownloaded = false,
|
||||
fullResultContractsVerified = full, fullBootstrapFixtureCases = full ? 12 : 0,
|
||||
installerGuardFixtureCases = full ? 10 : 0, firstbootEvidenceFixtureCases = full ? 4 : 0, ownedDiskSerialFixtureCases = full ? 6 : 0,
|
||||
sourceModified = false, dockerExecuted = false, guestExecuted = false, completedUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
if (full && Option(args, "--source") is null) await ValidateDiskSerialParser(output);
|
||||
if (Option(args, "--compression-chunk") is { } chunk) await ValidateCompression(Path.GetFullPath(chunk), output);
|
||||
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
|
||||
return 0;
|
||||
}
|
||||
@@ -100,7 +110,9 @@ static class NativeDiagnostic
|
||||
Save(statePath, state);
|
||||
Directory.CreateDirectory(work);
|
||||
File.WriteAllText(Path.Combine(work, "run.owner"), token);
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(90));
|
||||
// Full leaves eight minutes within the existing three-hour job for capture/cleanup.
|
||||
var deadlineMinutes = full ? 172 : 90;
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(deadlineMinutes));
|
||||
using var signal = OperatingSystem.IsLinux() ? PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; deadline.Cancel(); }) : null;
|
||||
ConsoleCancelEventHandler cancelHandler = (_, context) => { context.Cancel = true; deadline.Cancel(); };
|
||||
Console.CancelKeyPress += cancelHandler;
|
||||
@@ -112,7 +124,7 @@ static class NativeDiagnostic
|
||||
throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only.");
|
||||
ValidateContracts();
|
||||
var sourceCommit = (await Command("git", ["rev-parse", "HEAD"], output, "candidate-commit", deadline.Token)).Output.Trim();
|
||||
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = Profile, causalSingleVariableTest = false, kvm = false, cpuModel = CpuModel, recoveryMajor = 14, cpuFlags = CpuFlags, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = 90 });
|
||||
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = Profile, causalSingleVariableTest = false, kvm = false, cpuModel = CpuModel, recoveryMajor = 14, cpuFlags = CpuFlags, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes, mode = full ? "full" : "readiness" });
|
||||
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
|
||||
using (var document = JsonDocument.Parse(info.Output))
|
||||
{
|
||||
@@ -131,13 +143,17 @@ static class NativeDiagnostic
|
||||
await Command("git", ["-C", source, "checkout", "--detach", DockurCommit], output, "dockur-checkout", deadline.Token);
|
||||
var actualCommit = (await Command("git", ["-C", source, "rev-parse", "HEAD"], output, "dockur-commit", deadline.Token)).Output.Trim();
|
||||
if (actualCommit != DockurCommit) throw new InvalidOperationException("Dockur source pin mismatch.");
|
||||
await PrepareSource(source, output, token, true, deadline.Token);
|
||||
if (full) await PreparePayload(source, output, token, sourceCommit, deadline.Token);
|
||||
await PrepareSource(source, output, token, true, deadline.Token, full);
|
||||
await Command("docker", ["build", "--platform", "linux/amd64", "--label", OwnerLabel + "=" + token, "--tag", state.ImageTag, source], output, "docker-build", deadline.Token, echo: true);
|
||||
var imageInspect = await Command("docker", ["image", "inspect", state.ImageTag], output, "image-inspect", deadline.Token);
|
||||
using (var image = JsonDocument.Parse(imageInspect.Output))
|
||||
state = state with { ImageId = image.RootElement[0].GetProperty("Id").GetString() };
|
||||
Save(statePath, state);
|
||||
var create = await Command("docker", ["create", "--name", state.ContainerName, "--label", OwnerLabel + "=" + token, "--memory", "6g", "--memory-swap", "6g", "--cpus", "2", "--shm-size", "512m", "--log-opt", "max-size=8m", "--log-opt", "max-file=1", "--env", "KVM=N", "--env", "CPU_MODEL=" + CpuModel, "--env", "NETWORK=slirp", "--env", "DISPLAY=web", "--env", "MANUAL=N", "--env", "VERSION=14", "--env", "RAM_SIZE=4G", "--env", "CPU_CORES=2", "--env", "DISK_SIZE=64G", "--env", "DISK_TYPE=sata", "--env", "ARGUMENTS=-object iothread,id=io2", state.ImageTag], output, "docker-create", deadline.Token);
|
||||
List<string> createArguments = ["create", "--name", state.ContainerName, "--label", OwnerLabel + "=" + token, "--memory", "6g", "--memory-swap", "6g", "--cpus", "2", "--shm-size", "512m", "--log-opt", "max-size=8m", "--log-opt", "max-file=1", "--env", "KVM=N", "--env", "CPU_MODEL=" + CpuModel, "--env", "NETWORK=slirp", "--env", "DISPLAY=web", "--env", "MANUAL=N", "--env", "VERSION=14", "--env", "RAM_SIZE=4G", "--env", "CPU_CORES=2", "--env", "DISK_SIZE=64G", "--env", "DISK_TYPE=sata", "--env", "ARGUMENTS=-object iothread,id=io2"];
|
||||
if (full) createArguments.AddRange(["--env", "ALLOCATE=N", "--env", "DISK_OPTIONS=serial=" + DiskSerial(token)]);
|
||||
createArguments.Add(state.ImageTag);
|
||||
var create = await Command("docker", createArguments.ToArray(), output, "docker-create", deadline.Token);
|
||||
var id = create.Output.Trim();
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(id, "^[0-9a-f]{64}$")) throw new InvalidOperationException("Docker did not return a container identity.");
|
||||
state = state with { ContainerId = id };
|
||||
@@ -146,14 +162,19 @@ static class NativeDiagnostic
|
||||
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
|
||||
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
|
||||
await CapturePressure(id, output, "before", deadline.Token);
|
||||
Console.WriteLine("The owned unprivileged TCG/Haswell macOS 14 guest is starting. Success requires native macOS 14+/x86_64 and a writable 64-GiB disk; no installer will run. This is not a single-variable causal test.");
|
||||
var recoveryStarted = Stopwatch.StartNew();
|
||||
Console.WriteLine(full ? "The owned unprivileged TCG/Haswell macOS 14 guest is starting. Installation requires fresh native readiness and an owned-disk permit; success requires all 577 tests with zero skips." : "The owned unprivileged TCG/Haswell macOS 14 guest is starting. Success requires native macOS 14+/x86_64 and a writable 64-GiB disk; no installer will run.");
|
||||
var phaseStarted = Stopwatch.StartNew();
|
||||
var phase = "recovery";
|
||||
var phaseBudget = TimeSpan.FromMinutes(90);
|
||||
var permitted = false;
|
||||
var heartbeat = Stopwatch.StartNew();
|
||||
var diskPressureCaptured = false;
|
||||
while (true)
|
||||
{
|
||||
deadline.Token.ThrowIfCancellationRequested();
|
||||
await CaptureGuest(id, output, deadline.Token);
|
||||
await CaptureGuest(id, output, deadline.Token, full);
|
||||
if (full && phaseStarted.Elapsed > phaseBudget)
|
||||
throw new InvalidOperationException("The bounded native " + phase + " phase exceeded " + phaseBudget.TotalMinutes + " minutes.");
|
||||
var proofPath = Path.Combine(output, "guest-proof.log");
|
||||
if (!diskPressureCaptured && File.Exists(proofPath) && File.ReadAllText(proofPath).Contains("[proof-start] disks", StringComparison.Ordinal))
|
||||
{
|
||||
@@ -161,19 +182,52 @@ static class NativeDiagnostic
|
||||
await CapturePressure(id, output, "during", deadline.Token);
|
||||
}
|
||||
var resultPath = Path.Combine(output, "guest-result.json");
|
||||
if (File.Exists(resultPath))
|
||||
if (File.Exists(resultPath) && !permitted)
|
||||
{
|
||||
var result = File.ReadAllText(resultPath);
|
||||
ValidateResult(result, token);
|
||||
Console.WriteLine("Native Recovery readiness passed. This run has not installed macOS, .NET, CLT, or run Meeting Assistant tests.");
|
||||
outcome = "readiness-passed";
|
||||
break;
|
||||
if (full)
|
||||
{
|
||||
await PermitInstallation(id, output, token, sourceCommit, result, deadline.Token);
|
||||
permitted = true;
|
||||
phase = "installation";
|
||||
phaseBudget = TimeSpan.FromMinutes(80);
|
||||
phaseStarted.Restart();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Native Recovery readiness passed. This run has not installed macOS, .NET, CLT, or run Meeting Assistant tests.");
|
||||
outcome = "readiness-passed";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (full)
|
||||
{
|
||||
var phasePath = Path.Combine(output, "guest-phase.json");
|
||||
if (File.Exists(phasePath))
|
||||
{
|
||||
using var nativePhase = JsonDocument.Parse(File.ReadAllText(phasePath));
|
||||
if (nativePhase.RootElement.GetProperty("token").GetString() != token) throw new InvalidOperationException("Stale native phase receipt.");
|
||||
var current = nativePhase.RootElement.GetProperty("phase").GetString();
|
||||
var next = !permitted ? phase : current == "toolchain-installing" ? "toolchain" : current is "tests-running" or "tests-passed" ? "tests" : phase;
|
||||
if (next != phase) { phase = next; phaseBudget = TimeSpan.FromMinutes(next == "toolchain" ? 30 : 25); phaseStarted.Restart(); Console.WriteLine("[native-diagnostic] phase: " + phase); }
|
||||
if (current is "tests-failed" or "bootstrap-failed" or "installation-failed") throw new InvalidOperationException("Guest phase failed: " + current);
|
||||
}
|
||||
var fullResult = Path.Combine(output, "full-result.json");
|
||||
if (permitted && File.Exists(fullResult))
|
||||
{
|
||||
ValidateFullResult(File.ReadAllText(fullResult), token, sourceCommit, File.ReadAllText(Path.Combine(output, "archive.sha256")).Trim());
|
||||
ValidateTrx(File.ReadAllBytes(Path.Combine(output, "native.trx")), File.ReadAllText(fullResult));
|
||||
outcome = "native-tests-passed";
|
||||
Console.WriteLine("Native macOS 577/577 tests passed, including all five native tests, with fresh Mach-O/x86_64 and codesign evidence.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
var running = await Command("docker", ["inspect", "--format", "{{.State.Running}}", id], output, "container-running", deadline.Token);
|
||||
if (running.Output.Trim() != "true") throw new InvalidOperationException("Guest container exited before a native readiness result.");
|
||||
if (heartbeat.Elapsed >= TimeSpan.FromSeconds(60))
|
||||
{
|
||||
Console.WriteLine($"[native-diagnostic] phase=recovery; elapsed={recoveryStarted.Elapsed.TotalMinutes:F1} minutes; container=running; readiness=pending");
|
||||
Console.WriteLine($"[native-diagnostic] phase={phase}; elapsed={phaseStarted.Elapsed.TotalMinutes:F1}/{phaseBudget.TotalMinutes:F0} minutes; container=running; readiness={(permitted ? "passed" : "pending")}");
|
||||
heartbeat.Restart();
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(20), deadline.Token);
|
||||
@@ -181,21 +235,22 @@ static class NativeDiagnostic
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = exception is OperationCanceledException ? "The explicit 90-minute diagnostic deadline or cancellation was reached." : exception.Message;
|
||||
error = exception is OperationCanceledException ? $"The explicit {deadlineMinutes}-minute diagnostic deadline or cancellation was reached." : exception.Message;
|
||||
Console.Error.WriteLine(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.CancelKeyPress -= cancelHandler;
|
||||
using var captureDeadline = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
try { await CaptureGuest(state.ContainerId ?? state.ContainerName, output, captureDeadline.Token, true, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Final evidence capture: " + exception.Message); }
|
||||
try { await CaptureGuest(state.ContainerId ?? state.ContainerName, output, captureDeadline.Token, full, true, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Final evidence capture: " + exception.Message); }
|
||||
await CapturePressure(state.ContainerId ?? state.ContainerName, output, "after", captureDeadline.Token);
|
||||
try { PrintGuestProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Native proof output: " + exception.Message); }
|
||||
if (full) try { PrintFullProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Full native proof output: " + exception.Message); }
|
||||
var clean = await Cleanup(output);
|
||||
if (!clean) { outcome = "failed"; error = (error ?? "") + " Owned-resource cleanup failed; inspect cleanup evidence."; }
|
||||
Save(Path.Combine(output, "outcome.json"), new { token, outcome, error, completedUtc = DateTimeOffset.UtcNow });
|
||||
}
|
||||
return outcome == "readiness-passed" ? 0 : 1;
|
||||
return outcome is "readiness-passed" or "native-tests-passed" ? 0 : 1;
|
||||
}
|
||||
|
||||
static string? Option(string[] args, string name)
|
||||
@@ -270,7 +325,7 @@ static class NativeDiagnostic
|
||||
return source.Remove(from, to + end.Length - from).Insert(from, originalSequence);
|
||||
}
|
||||
|
||||
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
|
||||
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false)
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
|
||||
@@ -304,13 +359,21 @@ static class NativeDiagnostic
|
||||
entry = ReplaceOnce(entry, "trap - ERR\n", "[[ \"$KVM_OPTS\" == ' -accel tcg,thread=multi' && \"$CPU_FLAGS\" == '" + CpuFlags + "' && \"$CPU_OPTS\" == \"-cpu $CPU_FLAGS -smp $SMP\" ]] || { error 'Supported profile refuses a CPU/accelerator fallback.'; exit 1; }\ninfo '[supported-profile] accelerator=tcg cpu=Haswell-noTSX recovery=14; AVX/AVX2 preflight passed; native guest gates still pending'\n\ntrap - ERR\n");
|
||||
var hookPath = Path.Combine("tools", "ci", "macos-native-readiness.sh");
|
||||
var hook = ReplaceOnce(File.ReadAllText(hookPath), "@@PROOF_TOKEN@@", token);
|
||||
var wrapper = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"));
|
||||
var wrapper = File.ReadAllText(Path.Combine("tools", "ci", full ? "macos-native-full-bootstrap.sh" : "macos-native-bootstrap.sh"));
|
||||
if (full) wrapper = ReplaceOnce(wrapper, "@@PROOF_TOKEN@@", token);
|
||||
var imagePath = Path.Combine(source, "src", "image.sh");
|
||||
var originalImage = File.ReadAllText(imagePath);
|
||||
if (Hash(Encoding.UTF8.GetBytes(originalImage)) != "c08bf9436fb8b72ea82fdf0e677641ab2fc42a0a59e2cf0309c00df519884c5c") throw new InvalidOperationException("Pinned Recovery staging script hash mismatch.");
|
||||
var image = ReplaceOnce(originalImage, " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\"; then\n", " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cp -f \"$IMAGE_TOOLS/recovery/readiness.sh\" \"${script%/*}/readiness.sh\"; then\n");
|
||||
image = ReplaceOnce(image, " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n", " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cmp -s \"$IMAGE_TOOLS/recovery/readiness.sh\" \"$state/readiness.sh\" ||\n");
|
||||
image = ReplaceOnce(image, " if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n", " info \"[recovery-original] bytes=$(stat -c%s -- \"$image\") sha256=$(sha256sum \"$image\" | awk '{print $1}')\"\n if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n");
|
||||
if (full)
|
||||
{
|
||||
await PrepareFullSource(source, output, token, writeSource, cancellation);
|
||||
hook = CreateFullReadiness(hook);
|
||||
dockerfile += "\n# Payload stays inside this image and its owned anonymous storage volume.\nCOPY ci-payload/ /assets/ci-payload/\n";
|
||||
entry = ReplaceOnce(entry, "free_kib >= 8 * 1024 * 1024", "free_kib >= 32 * 1024 * 1024").Replace("8-GiB diagnostic budget", "32-GiB full-run budget", StringComparison.Ordinal);
|
||||
}
|
||||
foreach (var pair in new[] { ("recovery-patch.py", patch), ("Dockerfile.patched", dockerfile), ("container-entry.sh", entry), ("guest-launch.sh", wrapper), ("guest-readiness.sh", hook), ("image.sh.patched", image), ("recoveryosd-original.plist", daemon), ("recoveryosd-diagnostic.plist", DiagnosticDaemon), ("recoveryosd-13-original.plist", OriginalDaemon13), ("recoveryosd-13-diagnostic.plist", DiagnosticDaemon13), ("early-bootstrap.sh", MountOnlyBootstrap), ("boot.sh.patched", boot), ("opencore-config.plist", File.ReadAllText(Path.Combine(source, "assets/config.plist"))), ("cpu.sh.patched", cpu), ("ci-cpu-preflight.asm", preflight) })
|
||||
File.WriteAllText(Path.Combine(output, pair.Item1), pair.Item2, new UTF8Encoding(false));
|
||||
Save(Path.Combine(output, "source-hashes.json"), Directory.GetFiles(output).Where(path => Path.GetFileName(path) is "recovery-patch.py" or "udif_checksums.py" or "Dockerfile.patched" or "container-entry.sh" or "guest-launch.sh" or "guest-readiness.sh" or "image.sh.patched" or "recoveryosd-original.plist" or "recoveryosd-diagnostic.plist" or "recoveryosd-13-original.plist" or "recoveryosd-13-diagnostic.plist" or "early-bootstrap.sh" or "boot.sh.patched" or "opencore-config.plist" or "cpu.sh.patched" or "ci-cpu-preflight.asm").ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
|
||||
@@ -321,6 +384,7 @@ static class NativeDiagnostic
|
||||
await Command("bash", ["-n", Path.Combine(output, "container-entry.sh")], output, "entry-syntax", cancellation);
|
||||
await Command("bash", ["-n", Path.Combine(output, "boot.sh.patched")], output, "boot-staging-syntax", cancellation);
|
||||
await Command("bash", ["-n", Path.Combine(output, "cpu.sh.patched")], output, "cpu-composition-syntax", cancellation);
|
||||
if (full && !writeSource) await ValidateFullBootstrap(wrapper, output, token, cancellation);
|
||||
if (!writeSource) return;
|
||||
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
|
||||
File.WriteAllText(Path.Combine(source, "src/install/recovery/udif_checksums.py"), checksumBinding, new UTF8Encoding(false));
|
||||
@@ -475,6 +539,531 @@ static class NativeDiagnostic
|
||||
}
|
||||
|
||||
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
|
||||
static string DiskSerial(string token) => token[..20];
|
||||
|
||||
static async Task ValidateFullBootstrap(string wrapper, string output, string token, CancellationToken cancellation)
|
||||
{
|
||||
// Execute the complete generated shell with only its external filesystem,
|
||||
// Apple daemon and probe-process boundaries mapped to harmless fixtures.
|
||||
const string commit = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
var ownMarker = token + ":" + commit + "\n";
|
||||
var cases = new[]
|
||||
{
|
||||
(Name: "restart", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 1, Failure: false, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "already-owned", Initial: ownMarker, ChildExit: 0, WriteFailure: false, Children: 0, Failure: false, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "foreign-token", Initial: ownMarker.Replace(token, new string('f', 32)), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "foreign-commit", Initial: ownMarker.Replace(commit, new string('f', 40)), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "empty", Initial: "", ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "extra-record", Initial: ownMarker + ownMarker, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "unterminated", Initial: ownMarker.TrimEnd('\n'), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "write-failure", Initial: (string?)null, ChildExit: 0, WriteFailure: true, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "first-child-failure", Initial: (string?)null, ChildExit: 1, WriteFailure: false, Children: 1, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "delayed-share", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 1, Failure: false, MountAfter: 3, Owner: (string?)token),
|
||||
(Name: "missing-share", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: -1, Owner: (string?)null),
|
||||
(Name: "foreign-owner", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)new string('f', 32))
|
||||
};
|
||||
foreach (var test in cases)
|
||||
{
|
||||
var state = Path.Combine(output, "full-bootstrap-" + test.Name + "-fixture");
|
||||
if (Directory.Exists(state)) throw new InvalidOperationException("Full bootstrap fixtures require fresh validation output: " + state);
|
||||
Directory.CreateDirectory(state);
|
||||
if (test.MountAfter == 0 && test.Owner is not null) File.WriteAllText(Path.Combine(state, "run.owner"), test.Owner + "\n");
|
||||
File.WriteAllText(Path.Combine(state, "source.commit"), commit + "\n");
|
||||
var active = JsonSerializer.Serialize(new { token, phase = "installation" });
|
||||
File.WriteAllText(Path.Combine(state, "guest-phase.json"), active);
|
||||
var marker = Path.Combine(state, "probe.started");
|
||||
if (test.Initial is not null) File.WriteAllText(marker, test.Initial);
|
||||
var mapped = wrapper.Replace("/Volumes/installstate", state, StringComparison.Ordinal);
|
||||
if (test.WriteFailure) mapped = ReplaceOnce(mapped, "MARKER=\"$STATE_DIR/probe.started\"", "MARKER=\"$STATE_DIR/absent-parent/probe.started\"");
|
||||
var script = """
|
||||
STATE_TEST="$1"; export STATE_TEST
|
||||
CHILD_EXIT="$2"; export CHILD_EXIT
|
||||
MOUNT_AFTER="$3"; MOUNT_OWNER="$4"; MOUNT_COMMIT="$5"
|
||||
/sbin/mount_9p() {
|
||||
local attempts=0
|
||||
[ ! -f "$STATE_TEST/mount-attempts" ] || attempts=$(cat "$STATE_TEST/mount-attempts")
|
||||
attempts=$((attempts + 1)); printf '%s\n' "$attempts" > "$STATE_TEST/mount-attempts"
|
||||
if (( MOUNT_AFTER > 0 && attempts >= MOUNT_AFTER )); then
|
||||
printf '%s\n' "$MOUNT_OWNER" > "$STATE_TEST/run.owner"
|
||||
printf '%s\n' "$MOUNT_COMMIT" > "$STATE_TEST/source.commit"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
sleep() { [ "$1" = 1 ] || return 1; printf 'sleep\n' >> "$STATE_TEST/mount-sleeps.log"; }
|
||||
/bin/bash() { cat "$STATE_TEST/probe.started" >> "$STATE_TEST/child-marker.log"; printf 'child\n' >> "$STATE_TEST/children.log"; return "$CHILD_EXIT"; }
|
||||
exec() { cat "$STATE_TEST/probe.started" >> "$STATE_TEST/apple-marker.log" 2>/dev/null || :; printf '%s\n' "$*" >> "$STATE_TEST/apple-exec.log"; return 0; }
|
||||
""" + "\n" + mapped + "\nwait\n";
|
||||
var errors = "";
|
||||
for (var start = 0; start < 2; start++)
|
||||
errors += (await Command("bash", ["-c", script, "full-bootstrap-contract", state, test.ChildExit.ToString(), test.MountAfter.ToString(), test.Owner ?? "", commit], output, "full-bootstrap-" + test.Name + "-start-" + start, cancellation)).Error;
|
||||
var childLog = Path.Combine(state, "children.log");
|
||||
var children = File.Exists(childLog) ? File.ReadAllLines(childLog).Length : 0;
|
||||
var appleExecutions = File.ReadAllLines(Path.Combine(state, "apple-exec.log"));
|
||||
var phase = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
|
||||
var phaseUnchanged = phase == active;
|
||||
using var receipt = JsonDocument.Parse(phase);
|
||||
var realFailure = receipt.RootElement.GetProperty("token").GetString() == token && receipt.RootElement.GetProperty("phase").GetString() == "bootstrap-failed";
|
||||
var markerPreserved = test.Initial is not null ? File.ReadAllText(marker) == test.Initial : test.Children == 0 ? !File.Exists(marker) : File.Exists(marker) && File.ReadAllText(marker) == ownMarker;
|
||||
var completeBeforeExec = test.Initial is null && test.Children == 1 ? File.Exists(Path.Combine(state, "child-marker.log")) && File.ReadAllText(Path.Combine(state, "apple-marker.log")) == ownMarker + ownMarker && File.ReadAllText(Path.Combine(state, "child-marker.log")) == ownMarker : true;
|
||||
var mountAttemptsPath = Path.Combine(state, "mount-attempts");
|
||||
var mountAttempts = File.Exists(mountAttemptsPath) ? int.Parse(File.ReadAllText(mountAttemptsPath)) : 0;
|
||||
var sleepPath = Path.Combine(state, "mount-sleeps.log");
|
||||
var mountSleeps = File.Exists(sleepPath) ? File.ReadAllLines(sleepPath).Length : 0;
|
||||
var expectedMounts = test.MountAfter < 0 ? 240 : test.MountAfter;
|
||||
var failureReported = test.Owner == token ? realFailure : phaseUnchanged && errors.Contains("[full-bootstrap] ERROR:", StringComparison.Ordinal);
|
||||
Save(Path.Combine(output, "full-bootstrap-" + test.Name + ".json"), new { children, appleExecutions = appleExecutions.Length, phaseUnchanged, realFailure, failureReported, markerPreserved, completeBeforeExec, mountAttempts, mountSleeps });
|
||||
if (children != test.Children || appleExecutions.Length != 2 || appleExecutions.Any(value => value != "/usr/libexec/recoveryosd") || test.Failure && !failureReported || !test.Failure && !phaseUnchanged || !markerPreserved || !completeBeforeExec || mountAttempts != expectedMounts || mountSleeps != expectedMounts)
|
||||
throw new InvalidOperationException($"Full bootstrap contract failed ({test.Name}): children={children}, appleExecutions={appleExecutions.Length}, phaseUnchanged={phaseUnchanged}, failureReported={failureReported}, markerPreserved={markerPreserved}, completeBeforeExec={completeBeforeExec}, mountAttempts={mountAttempts}, mountSleeps={mountSleeps}.");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task PreparePayload(string source, string output, string token, string commit, CancellationToken cancellation)
|
||||
{
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(commit, "^[0-9a-f]{40}$")) throw new InvalidOperationException("Candidate commit is not an exact Git SHA.");
|
||||
var status = await Command("git", ["status", "--porcelain", "--untracked-files=all"], output, "source-cleanliness", cancellation);
|
||||
if (!string.IsNullOrEmpty(status.Output)) throw new InvalidOperationException("Full CI requires a clean exact HEAD; commit the reviewable candidate before running it.");
|
||||
var payload = Path.Combine(source, "ci-payload");
|
||||
Directory.CreateDirectory(payload);
|
||||
var archive = Path.Combine(payload, "source.tar");
|
||||
await Command("git", ["archive", "--format=tar", "--output", archive, commit], output, "source-archive", cancellation);
|
||||
var archiveHash = Hash(File.ReadAllBytes(archive));
|
||||
File.WriteAllText(Path.Combine(output, "archive.sha256"), archiveHash + "\n");
|
||||
File.WriteAllText(Path.Combine(payload, "source.commit"), commit + "\n");
|
||||
Save(Path.Combine(payload, "payload.json"), new { runToken = token, sourceCommit = commit, archiveSha256 = archiveHash, sdkVersion = SdkVersion, sdkSha512 = SdkSha512, expectedTests = 577 });
|
||||
File.Copy(Path.Combine(payload, "payload.json"), Path.Combine(output, "payload.json"));
|
||||
foreach (var pair in new[] { ("MacOsNativeGuest.cs", "MacOsNativeGuest.cs"), ("macos-native-firstboot.sh", "native-firstboot-bootstrap.sh"), ("macos-native-disk-guard.sh", "macos-native-disk-guard.sh") })
|
||||
File.Copy(Path.Combine("tools", "ci", pair.Item1), Path.Combine(payload, pair.Item2));
|
||||
Console.WriteLine("[native-diagnostic] pinned-sdk-download");
|
||||
using var downloadDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
downloadDeadline.CancelAfter(TimeSpan.FromMinutes(15));
|
||||
using var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
|
||||
using var response = await client.GetAsync($"https://builds.dotnet.microsoft.com/dotnet/Sdk/{SdkVersion}/dotnet-sdk-{SdkVersion}-osx-x64.tar.gz", HttpCompletionOption.ResponseHeadersRead, downloadDeadline.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var sdkPath = Path.Combine(payload, "sdk.tar.gz");
|
||||
await using (var sdk = File.Create(sdkPath))
|
||||
await using (var stream = await response.Content.ReadAsStreamAsync(downloadDeadline.Token))
|
||||
await stream.CopyToAsync(sdk, downloadDeadline.Token);
|
||||
await using (var sdk = File.OpenRead(sdkPath))
|
||||
if (Convert.ToHexStringLower(await SHA512.HashDataAsync(sdk, downloadDeadline.Token)) != SdkSha512) throw new InvalidOperationException("Official macOS/x64 SDK SHA-512 mismatch.");
|
||||
Save(Path.Combine(output, "payload-hashes.json"), Directory.GetFiles(payload).ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
|
||||
}
|
||||
|
||||
static string ReadPinned(string source, string path, string expected)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(Path.Combine(source, path));
|
||||
if (Hash(bytes) != expected) throw new InvalidOperationException("Pinned full-install source hash mismatch: " + path);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
static string CreateFullReadiness(string hook) => ReplaceOnce(hook,
|
||||
" # Keep the service alive for the bounded host diagnostic to capture evidence.\n while :; do sleep 60; done",
|
||||
"""
|
||||
# Full mode waits for the host's independently validated fresh owned-disk permit.
|
||||
if [ "$success" = true ]; then
|
||||
permit_start=$SECONDS
|
||||
while (( SECONDS - permit_start < 300 )); do
|
||||
if [ -s "$STATE_DIR/install.permit" ]; then
|
||||
exec /bin/bash "$STATE_DIR/full-install.sh"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
printf '[full-install] host permit was not received in five minutes\n' >> "$PROOF_LOG"
|
||||
fi
|
||||
while :; do sleep 60; done
|
||||
""");
|
||||
|
||||
static async Task PrepareFullSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
|
||||
{
|
||||
var installer = ReadPinned(source, "src/install/recovery/launch.sh", "b44309d1056bbd0321251cc9f04a52cf6ad68209586f263b9619339bf7146b6c");
|
||||
var selectorStart = installer.IndexOf("select_target_disk() {", StringComparison.Ordinal);
|
||||
var selectorEnd = installer.IndexOf("find_startosinstall() {", StringComparison.Ordinal);
|
||||
if (selectorStart < 0 || selectorEnd <= selectorStart) throw new InvalidOperationException("Pinned installer selector boundaries changed.");
|
||||
var selector = """
|
||||
select_target_disk() {
|
||||
local permit_token permit_disk permit_commit extra
|
||||
{ IFS= read -r permit_token; IFS= read -r permit_disk; IFS= read -r permit_commit; IFS= read -r extra || :; } < "$STATE_DIR/install.permit" || return 1
|
||||
[ "$permit_token" = "$PROOF_TOKEN" ] && [ -z "${extra:-}" ] || return 1
|
||||
[ "$permit_commit" = "$(cat "$STATE_DIR/source.commit")" ] || return 1
|
||||
[[ "$permit_commit" =~ ^[0-9a-f]{40}$ ]] || return 1
|
||||
. "$STATE_DIR/macos-native-disk-guard.sh"
|
||||
verify_owned_disk "$permit_disk" "$STATE_DIR" "$PROOF_TOKEN" >&2 || return 1
|
||||
printf '%s\n' "$permit_disk"
|
||||
}
|
||||
|
||||
""" + "\n";
|
||||
installer = ReplaceOnce(installer, installer[selectorStart..selectorEnd], selector);
|
||||
installer = ReplaceOnce(installer, "MIN_TARGET_SIZE=$((16 * 1024 * 1024 * 1024))", "# Target policy is exclusively the own writable 64-GiB emulated disk.");
|
||||
installer = ReplaceOnce(installer, "no writable installation disk of at least 16 GiB was found", "the run-owned writable 64-GiB installation disk was not proved");
|
||||
installer = ReplaceOnce(installer, " local message=\"$1\"\n\n echo \"[log] ERROR: $message\"", " local message=\"$1\"\n\n mark_installation_failed || :\n echo \"[log] ERROR: $message\"");
|
||||
installer = ReplaceOnce(installer, "if (( rc != 0 )); then\n", "if (( rc != 0 )); then\n mark_installation_failed || :\n");
|
||||
installer = ReplaceAllExact(installer, "rm -f \"$STARTED\"", ": # Keep the owned erase guard on failure; never erase again.", 2);
|
||||
installer = ReplaceOnce(installer, "select_target_disk() {", """
|
||||
owns_started_guard() {
|
||||
local permit_token permit_disk permit_commit extra record
|
||||
[ -f "$STARTED" ] && [ ! -L "$STARTED" ] || return 1
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
|
||||
{ IFS= read -r permit_token; IFS= read -r permit_disk; IFS= read -r permit_commit; IFS= read -r extra || :; } < "$STATE_DIR/install.permit" || return 1
|
||||
[ "$permit_token" = "$PROOF_TOKEN" ] && [ -z "${extra:-}" ] || return 1
|
||||
[[ "$permit_commit" =~ ^[0-9a-f]{40}$ && "$permit_disk" =~ ^/dev/disk[0-9]+$ ]] || return 1
|
||||
[ "$permit_commit" = "$(cat "$STATE_DIR/source.commit")" ] || return 1
|
||||
[ -z "${TARGET_DISK:-}" ] || [ "$TARGET_DISK" = "$permit_disk" ] || return 1
|
||||
{
|
||||
IFS= read -r record || return 1
|
||||
if IFS= read -r extra || [ -n "$extra" ]; then return 1; fi
|
||||
} < "$STARTED"
|
||||
[ "$record" = "$PROOF_TOKEN:$permit_commit:$permit_disk" ]
|
||||
}
|
||||
|
||||
select_target_disk() {
|
||||
""");
|
||||
installer = ReplaceOnce(installer, " echo \"[log] installation was already started; refusing to erase the target disk again\"\n exec /usr/libexec/recoveryosd\n exit 1", " owns_started_guard || fail \"existing installation guard is not owned by this token, commit and disk\"\n echo \"[log] owned installation is already running; duplicate child exits without changing its phase\"\n exit 0");
|
||||
installer = ReplaceOnce(installer, ": > \"$STARTED\" || fail \"failed to create installation guard\"", """
|
||||
if ! ( set -o noclobber; printf '%s:%s:%s\n' "$PROOF_TOKEN" "$(cat "$STATE_DIR/source.commit")" "$TARGET_DISK" > "$STARTED" ); then
|
||||
if owns_started_guard; then
|
||||
echo "[log] another owned child claimed installation; duplicate exits without changing its phase"
|
||||
exit 0
|
||||
fi
|
||||
fail "failed to create the exclusive owned installation guard"
|
||||
fi
|
||||
""");
|
||||
// The Full bootstrap wrapper keeps Apple's original daemon running.
|
||||
// An installer child must terminate rather than launch another copy.
|
||||
installer = ReplaceAllExact(installer, " exec /usr/libexec/recoveryosd\n", "", 2);
|
||||
installer = ReplaceOnce(installer, "set -u\n", "set -u\nPROOF_TOKEN=\"" + token + "\"\n" + """
|
||||
mark_installation_failed() {
|
||||
local state="${STATE_DIR:-/Volumes/installstate}" temporary
|
||||
[ "$(cat "$state/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
|
||||
temporary="$state/guest-phase.install.$$.tmp"
|
||||
printf '{"token":"%s","phase":"installation-failed"}\n' "$PROOF_TOKEN" > "$temporary" &&
|
||||
/bin/mv -f "$temporary" "$state/guest-phase.json"
|
||||
}
|
||||
installer_parent=$$
|
||||
# Installer watchdog: 80 minutes, also bounded by the host's 172-minute total.
|
||||
(
|
||||
trap 'kill "$sleeper" 2>/dev/null || :; exit 0' TERM INT
|
||||
sleep 4800 & sleeper=$!; wait "$sleeper"; kill -TERM "$installer_parent" 2>/dev/null || :
|
||||
) & install_watchdog=$!
|
||||
trap 'kill -TERM "$install_watchdog" 2>/dev/null || :; wait "$install_watchdog" 2>/dev/null || :' EXIT
|
||||
trap 'kill "${STARTOSINSTALL_PID:-}" "${BOOTSTRAPPER_PID:-}" 2>/dev/null || :; mark_installation_failed || :; exit 1' TERM INT
|
||||
""" + "\n");
|
||||
var firstboot = ReadPinned(source, "src/install/firstboot/launch.sh", "d6b29bb42ffe99edda6b3be3faf6009c4e0b34e5b8bba6eb0855cf24a0c24307");
|
||||
firstboot = ReplaceOnce(firstboot, "LOG=\"/var/log/macos-unattended-firstboot.log\"\n", "LOG=\"/var/log/macos-unattended-firstboot.log\"\n" + FirstbootEvidence.Replace("@@OWNER@@", token, StringComparison.Ordinal) + "\n");
|
||||
firstboot = ReplaceOnce(firstboot, " printf '%s\\n' \"[firstboot] $1\" >> \"$LOG\" 2>/dev/null || :\n", " printf '%s\\n' \"[firstboot] $1\" >> \"$LOG\" 2>/dev/null || :\n publish_firstboot_log || :\n");
|
||||
firstboot = ReplaceOnce(firstboot, "log \"prebuilt account package installed successfully\"\n", "log \"prebuilt account package installed successfully\"\n/bin/bash /Volumes/installstate/native-firstboot-bootstrap.sh \"" + token + "\" || fail \"native CI bootstrap or tests failed\"\n");
|
||||
ReadPinned(source, "src/install/firstboot/com.dockur.macos.firstboot.plist", "29ef05388d962c236bdb87b2911e3b42e08c600035cfccf440d35ba64011c3d3");
|
||||
ReadPinned(source, "src/image.sh", "c08bf9436fb8b72ea82fdf0e677641ab2fc42a0a59e2cf0309c00df519884c5c");
|
||||
var initialize = ReadPinned(source, "src/install.sh", "19b4b27187de85148ad1de1c40d75a54738eb9890f02dbb9445155bb5ec44f90");
|
||||
initialize = ReplaceOnce(initialize, "INSTALL_STATE_DIR=\"$QEMU_DIR/installstate\"\nrm -rf \"$INSTALL_STATE_DIR\"", "INSTALL_STATE_DIR=\"$STORAGE/ci-state\"\n# Full CI preserves the own-volume erase guard and evidence across starts.");
|
||||
initialize = ReplaceOnce(initialize, " if ! prepareInstallationState \"$INSTALL_STATE_DIR\"; then\n exit 34\n fi", """
|
||||
if [ -e "$INSTALL_STATE_DIR/run.owner" ]; then
|
||||
[ "$(cat "$INSTALL_STATE_DIR/run.owner")" = "@@OWNER@@" ] || { error "CI storage belongs to another run."; exit 34; }
|
||||
else
|
||||
prepareInstallationState "$INSTALL_STATE_DIR" || exit 34
|
||||
cp -f /assets/ci-payload/* "$INSTALL_STATE_DIR/" || exit 34
|
||||
cp -f "$IMAGE_TOOLS/recovery/full-install.sh" "$INSTALL_STATE_DIR/full-install.sh" || exit 34
|
||||
cmp -s "$IMAGE_TOOLS/recovery/full-install.sh" "$INSTALL_STATE_DIR/full-install.sh" || exit 34
|
||||
for file in /assets/ci-payload/*; do cmp -s "$file" "$INSTALL_STATE_DIR/${file##*/}" || exit 34; done
|
||||
chmod 0755 "$INSTALL_STATE_DIR/full-install.sh" "$INSTALL_STATE_DIR/native-firstboot-bootstrap.sh" || exit 34
|
||||
printf '%s\n' '@@OWNER@@' > "$INSTALL_STATE_DIR/run.owner" || exit 34
|
||||
fi
|
||||
""".Replace("@@OWNER@@", token, StringComparison.Ordinal));
|
||||
foreach (var pair in new[] { ("full-install.sh", installer), ("full-firstboot.sh", firstboot), ("full-state-source.sh", initialize) })
|
||||
{
|
||||
File.WriteAllText(Path.Combine(output, pair.Item1), pair.Item2, new UTF8Encoding(false));
|
||||
await Command("bash", ["-n", Path.Combine(output, pair.Item1)], output, pair.Item1 + "-syntax", cancellation);
|
||||
}
|
||||
foreach (var name in new[] { "macos-native-firstboot.sh", "macos-native-disk-guard.sh" })
|
||||
await Command("bash", ["-n", Path.Combine("tools", "ci", name)], output, name + "-syntax", cancellation);
|
||||
Save(Path.Combine(output, "full-source-hashes.json"), new Dictionary<string, string> { ["full-install.sh"] = Hash(Encoding.UTF8.GetBytes(installer)), ["full-firstboot.sh"] = Hash(Encoding.UTF8.GetBytes(firstboot)), ["full-state-source.sh"] = Hash(Encoding.UTF8.GetBytes(initialize)), ["MacOsNativeGuest.cs"] = Hash(File.ReadAllBytes("tools/ci/MacOsNativeGuest.cs")), ["macos-native-firstboot.sh"] = Hash(File.ReadAllBytes("tools/ci/macos-native-firstboot.sh")), ["macos-native-disk-guard.sh"] = Hash(File.ReadAllBytes("tools/ci/macos-native-disk-guard.sh")) });
|
||||
if (!writeSource)
|
||||
{
|
||||
await ValidateInstallerFailureReceipt(installer, output, token, cancellation);
|
||||
await ValidateInstallGuardChild(installer, output, token, cancellation);
|
||||
await ValidateFirstbootEvidence(firstboot, output, token, cancellation);
|
||||
return;
|
||||
}
|
||||
File.WriteAllText(Path.Combine(source, "src/install/recovery/full-install.sh"), installer, new UTF8Encoding(false));
|
||||
File.WriteAllText(Path.Combine(source, "src/install/firstboot/launch.sh"), firstboot, new UTF8Encoding(false));
|
||||
File.WriteAllText(Path.Combine(source, "src/install.sh"), initialize, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
const string FirstbootEvidence = """
|
||||
# Mount and verify only this run's state before account setup can fail.
|
||||
PROOF_TOKEN="@@OWNER@@"
|
||||
STATE_DIR="/Volumes/installstate"
|
||||
owns_firstboot_state() {
|
||||
[ -f "$STATE_DIR/run.owner" ] && [ ! -L "$STATE_DIR/run.owner" ] &&
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ]
|
||||
}
|
||||
publish_firstboot_log() {
|
||||
owns_firstboot_state && [ -f "$LOG" ] || return 1
|
||||
local temporary="$STATE_DIR/unattended-firstboot.$$.tmp"
|
||||
/usr/bin/tail -c 1048576 "$LOG" > "$temporary" &&
|
||||
/bin/mv -f "$temporary" "$STATE_DIR/unattended-firstboot.log"
|
||||
}
|
||||
publish_firstboot_failure() {
|
||||
owns_firstboot_state || return 1
|
||||
local temporary="$STATE_DIR/guest-phase.firstboot.$$.tmp"
|
||||
printf '{"token":"%s","phase":"bootstrap-failed","stage":"account-firstboot"}\n' "$PROOF_TOKEN" > "$temporary" &&
|
||||
/bin/mv -f "$temporary" "$STATE_DIR/guest-phase.json"
|
||||
}
|
||||
trap 'firstboot_status=$?; publish_firstboot_log || :; (( firstboot_status == 0 )) || publish_firstboot_failure || :' EXIT
|
||||
count=0
|
||||
while ! owns_firstboot_state && (( count < 120 )); do
|
||||
if [ -e "$STATE_DIR/run.owner" ] || [ -L "$STATE_DIR/run.owner" ]; then
|
||||
printf '[firstboot] ERROR: foreign CI state owner\n' >> "$LOG"; exit 1
|
||||
fi
|
||||
/sbin/mount_9p installstate >/dev/null 2>&1 || :
|
||||
count=$((count + 1)); sleep 1
|
||||
done
|
||||
owns_firstboot_state || { printf '[firstboot] ERROR: owned CI state share did not mount\n' >> "$LOG"; exit 1; }
|
||||
""";
|
||||
|
||||
static async Task ValidateFirstbootEvidence(string firstboot, string output, string token, CancellationToken cancellation)
|
||||
{
|
||||
foreach (var test in new[] { ("missing-package", true, false, false), ("installer-failure", true, true, false), ("foreign-state", false, false, false), ("bounded-log", true, false, true) })
|
||||
{
|
||||
var work = Path.Combine(output, "firstboot-evidence-" + test.Item1);
|
||||
var state = Path.Combine(work, "state");
|
||||
var account = Path.Combine(work, "account");
|
||||
Directory.CreateDirectory(state); Directory.CreateDirectory(account);
|
||||
File.WriteAllText(Path.Combine(state, "run.owner"), test.Item2 ? token : new string('f', 32));
|
||||
var initial = JsonSerializer.Serialize(new { token, phase = "installation" });
|
||||
File.WriteAllText(Path.Combine(state, "guest-phase.json"), initial);
|
||||
if (test.Item3) File.WriteAllText(Path.Combine(account, "admin.pkg"), "owned harmless package fixture");
|
||||
Directory.CreateDirectory(Path.Combine(account, "users"));
|
||||
File.WriteAllText(Path.Combine(account, "admin.plist"), "owned harmless admin fixture");
|
||||
var log = Path.Combine(work, "local-firstboot.log");
|
||||
if (test.Item4) File.WriteAllText(log, new string('x', 1024 * 1024 + 512) + "\n");
|
||||
var mapped = firstboot.Replace("/Volumes/installstate", state, StringComparison.Ordinal)
|
||||
.Replace("/Library/Application Support/macos-unattended", account, StringComparison.Ordinal)
|
||||
.Replace("/private/var/db/dslocal/nodes/Default/users", Path.Combine(account, "users"), StringComparison.Ordinal)
|
||||
.Replace("/private/var/db/dslocal/nodes/Default/groups/admin.plist", Path.Combine(account, "admin.plist"), StringComparison.Ordinal)
|
||||
.Replace("/var/log/macos-unattended-firstboot.log", log, StringComparison.Ordinal)
|
||||
.Replace("[ -x /usr/sbin/installer ]", "true", StringComparison.Ordinal);
|
||||
var fixture = """
|
||||
/sbin/mount_9p() { return 1; }
|
||||
/usr/sbin/installer() { printf '%s\n' 'synthetic owned installer failure'; return 17; }
|
||||
sleep() { return 0; }
|
||||
""" + "\n" + mapped;
|
||||
var path = Path.Combine(work, "fixture.sh");
|
||||
File.WriteAllText(path, fixture, new UTF8Encoding(false));
|
||||
var result = await Command("bash", [path], work, "firstboot", cancellation, requireSuccess: false);
|
||||
var mirror = Path.Combine(state, "unattended-firstboot.log");
|
||||
var evidence = File.Exists(mirror) ? File.ReadAllText(mirror) : "";
|
||||
var phase = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
|
||||
using var receipt = JsonDocument.Parse(phase);
|
||||
var published = receipt.RootElement.GetProperty("token").GetString() == token && receipt.RootElement.GetProperty("phase").GetString() == "bootstrap-failed";
|
||||
var message = test.Item3 ? "synthetic owned installer failure" : "prebuilt account package is missing";
|
||||
var bounded = !File.Exists(mirror) || new FileInfo(mirror).Length <= 1024 * 1024;
|
||||
var success = result.ExitCode != 0 && (test.Item2 ? published && evidence.Contains(message, StringComparison.Ordinal) && bounded : phase == initial && !File.Exists(mirror));
|
||||
Save(Path.Combine(work, "receipt.json"), new { success, result.ExitCode, failurePhasePublished = published, mirroredLogBytes = File.Exists(mirror) ? new FileInfo(mirror).Length : 0, bounded, nativeCommandsExecuted = false });
|
||||
if (!success) throw new InvalidOperationException("Firstboot account-stage evidence fixture failed: " + test.Item1);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task ValidateInstallerFailureReceipt(string installer, string output, string token, CancellationToken cancellation)
|
||||
{
|
||||
const string start = "mark_installation_failed() {";
|
||||
const string end = "\n}\ninstaller_parent=$$";
|
||||
var begin = installer.IndexOf(start, StringComparison.Ordinal);
|
||||
var finish = begin < 0 ? -1 : installer.IndexOf(end, begin, StringComparison.Ordinal);
|
||||
if (begin < 0 || finish < begin || installer.Split("mark_installation_failed || :", StringSplitOptions.None).Length != 4)
|
||||
throw new InvalidOperationException("Pinned installer must publish its terminal failure phase from fail(), nonzero startosinstall and TERM/INT.");
|
||||
var function = installer[begin..(finish + 2)];
|
||||
var state = Path.Combine(output, "installer-failure-fixture");
|
||||
Directory.CreateDirectory(state);
|
||||
File.WriteAllText(Path.Combine(state, "run.owner"), token);
|
||||
var command = "set -u\n" + function + "\nPROOF_TOKEN=\"$1\"; STATE_DIR=\"$2\"; mark_installation_failed";
|
||||
await Command("bash", ["-c", command, "installer-failure-validation", token, state], output, "installer-terminal-failure", cancellation);
|
||||
var receipt = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
|
||||
using var json = JsonDocument.Parse(receipt);
|
||||
if (json.RootElement.GetProperty("token").GetString() != token || json.RootElement.GetProperty("phase").GetString() != "installation-failed" || Directory.GetFiles(state, "*.tmp").Length != 0)
|
||||
throw new InvalidOperationException("Installer failed to publish a complete atomic terminal phase.");
|
||||
File.WriteAllText(Path.Combine(state, "run.owner"), "foreign");
|
||||
var foreign = await Command("bash", ["-c", command, "installer-failure-validation", token, state], output, "installer-foreign-failure", cancellation, requireSuccess: false);
|
||||
if (foreign.ExitCode == 0 || File.ReadAllText(Path.Combine(state, "guest-phase.json")) != receipt)
|
||||
throw new InvalidOperationException("Installer terminal phase overwrote foreign run-owned state.");
|
||||
}
|
||||
|
||||
static async Task ValidateInstallGuardChild(string installer, string output, string token, CancellationToken cancellation)
|
||||
{
|
||||
// Exercise the generated Recovery shell's actual pre-erase control path.
|
||||
// Apple exec and rolling log snapshots are external boundaries; no VM,
|
||||
// native disk command, installer, daemon or application is invoked here.
|
||||
static string Between(string text, string start, string end)
|
||||
{
|
||||
var first = text.IndexOf(start, StringComparison.Ordinal);
|
||||
var last = first < 0 ? -1 : text.IndexOf(end, first, StringComparison.Ordinal);
|
||||
if (first < 0 || last <= first) throw new InvalidOperationException("Generated installer guard validation boundary changed: " + start);
|
||||
return text[first..last];
|
||||
}
|
||||
var functions = Between(installer, "mark_installation_failed() {", "installer_parent=$$") +
|
||||
Between(installer, "fail() {", "select_target_disk() {");
|
||||
var existing = Between(installer, "if [ -e \"$STARTED\" ]; then", "[ -s \"$ADMIN_PACKAGE\" ]");
|
||||
var claim = Between(installer, "# Everything needed for the unattended install", "if ! /usr/sbin/diskutil eraseDisk");
|
||||
const string commit = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
const string disk = "/dev/disk1";
|
||||
var ownGuard = token + ":" + commit + ":" + disk + "\n";
|
||||
var cases = new[]
|
||||
{
|
||||
(Name: "race", Initial: (string?)null, Race: true, WriteFailure: false, Success: true, Erase: false),
|
||||
(Name: "fresh-winner", Initial: (string?)null, Race: false, WriteFailure: false, Success: true, Erase: true),
|
||||
(Name: "already-owned", Initial: ownGuard, Race: false, WriteFailure: false, Success: true, Erase: false),
|
||||
(Name: "foreign-token", Initial: ownGuard.Replace(token, new string('f', 32)), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "foreign-commit", Initial: ownGuard.Replace(commit, new string('f', 40)), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "foreign-disk", Initial: ownGuard.Replace(disk, "/dev/disk2"), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "empty", Initial: "", Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "extra-record", Initial: ownGuard + ownGuard, Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "unterminated", Initial: ownGuard.TrimEnd('\n'), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "write-failure", Initial: (string?)null, Race: false, WriteFailure: true, Success: false, Erase: false)
|
||||
};
|
||||
foreach (var test in cases)
|
||||
{
|
||||
var state = Path.Combine(output, "installer-guard-" + test.Name + "-fixture");
|
||||
if (Directory.Exists(state)) throw new InvalidOperationException("Install-guard fixtures require a fresh validation output: " + state);
|
||||
Directory.CreateDirectory(state);
|
||||
File.WriteAllText(Path.Combine(state, "run.owner"), token + "\n");
|
||||
File.WriteAllText(Path.Combine(state, "source.commit"), commit + "\n");
|
||||
File.WriteAllText(Path.Combine(state, "install.permit"), token + "\n" + disk + "\n" + commit + "\n");
|
||||
var active = JsonSerializer.Serialize(new { token, phase = "installation" });
|
||||
File.WriteAllText(Path.Combine(state, "guest-phase.json"), active);
|
||||
var guard = Path.Combine(state, test.WriteFailure ? "absent-parent/started" : "started");
|
||||
if (test.Initial is not null) File.WriteAllText(guard, test.Initial);
|
||||
var script = """
|
||||
set -u
|
||||
STATE_DIR="$1"; PROOF_TOKEN="$2"; TARGET_DISK="$3"
|
||||
STARTED="$4"; LOCAL_LOG="$STATE_DIR/local.log"
|
||||
STATE_LOG="$STATE_DIR/install.log"; STATE_LOG_LIMIT=1024
|
||||
APPLE_INSTALL_LOG="$STATE_DIR/apple.log"; APPLE_INSTALL_LOG_LIMIT=1024
|
||||
snapshot_log() { :; }
|
||||
exec() { printf '%s\n' "$*" >> "$STATE_DIR/apple-exec.log"; return 0; }
|
||||
""" + "\n" + functions + "\n" + existing + "\n" +
|
||||
// Publish another child's complete guard after the initial check.
|
||||
(test.Race ? "printf '%s:%s:%s\\n' \"$PROOF_TOKEN\" \"$(cat \"$STATE_DIR/source.commit\")\" \"$TARGET_DISK\" > \"$STARTED\"\n" : "") + claim +
|
||||
"printf 'guard-acquired\\n' > \"$STATE_DIR/erase-boundary-reached\"\n";
|
||||
var command = await Command("bash", ["-c", script, "generated-install-guard-validation", state, token, disk, guard], output, "installer-guard-" + test.Name, cancellation, requireSuccess: false);
|
||||
var phase = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
|
||||
var phaseUnchanged = phase == active;
|
||||
using var receipt = JsonDocument.Parse(phase);
|
||||
var realFailure = receipt.RootElement.GetProperty("token").GetString() == token && receipt.RootElement.GetProperty("phase").GetString() == "installation-failed";
|
||||
var appleExec = File.Exists(Path.Combine(state, "apple-exec.log"));
|
||||
var eraseReached = File.Exists(Path.Combine(state, "erase-boundary-reached"));
|
||||
var guardPreserved = test.Initial is not null ? File.ReadAllText(guard) == test.Initial : test.WriteFailure ? !File.Exists(guard) : File.ReadAllText(guard) == ownGuard;
|
||||
Save(Path.Combine(output, "installer-guard-" + test.Name + ".json"), new { command.ExitCode, phaseUnchanged, realFailure, appleExec, eraseReached, guardPreserved });
|
||||
if ((command.ExitCode == 0) != test.Success || test.Success && !phaseUnchanged || !test.Success && !realFailure || appleExec || eraseReached != test.Erase || !guardPreserved)
|
||||
throw new InvalidOperationException($"Generated install child contract failed ({test.Name}): exit={command.ExitCode}, phaseUnchanged={phaseUnchanged}, realFailure={realFailure}, appleExec={appleExec}, eraseReached={eraseReached}, guardPreserved={guardPreserved}.");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task PermitInstallation(string id, string output, string token, string commit, string readiness, CancellationToken cancellation)
|
||||
{
|
||||
await Command("docker", ["inspect", id], output, "full-container-boundary", cancellation);
|
||||
AssertContainer(File.ReadAllText(Path.Combine(output, "full-container-boundary.stdout.log")), token);
|
||||
using (var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(output, "full-container-boundary.stdout.log"))))
|
||||
{
|
||||
var mounts = document.RootElement[0].GetProperty("Mounts").EnumerateArray().ToArray();
|
||||
if (mounts.Length != 1 || mounts[0].GetProperty("Type").GetString() != "volume" || mounts[0].GetProperty("Destination").GetString() != "/storage" || !mounts[0].GetProperty("RW").GetBoolean()) throw new InvalidOperationException("Full installer requires only the newly owned anonymous /storage volume.");
|
||||
}
|
||||
var drive = await Command("docker", ["exec", id, "qemu-img", "info", "--force-share", "--output=json", "/storage/14/data.img"], output, "owned-raw-disk", cancellation);
|
||||
using (var document = JsonDocument.Parse(drive.Output))
|
||||
if (document.RootElement.GetProperty("format").GetString() != "raw" || document.RootElement.GetProperty("virtual-size").GetInt64() != GuestDiskBytes) throw new InvalidOperationException("Owned VM raw-disk capacity/format mismatch.");
|
||||
var attachment = await Command("docker", ["exec", id, "sh", "-c", "qemu_pid=$(cat /dev/shm/qemu.pid); tr '\\0' '\\n' < /proc/\"$qemu_pid\"/cmdline | sed -n '/^file=\\/storage\\/13\\/data\\.img,/p; /^ide-hd,drive=data3,/p; /^local,id=installstatefs,/p'"], output, "owned-disk-attachment", cancellation);
|
||||
var lines = attachment.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lines.Length != 3 || !lines.Any(line => line.StartsWith("file=/storage/14/data.img,id=data3,format=raw,", StringComparison.Ordinal) && !line.Contains("readonly=on", StringComparison.Ordinal)) || !lines.Any(line => line.StartsWith("ide-hd,drive=data3,", StringComparison.Ordinal) && line.Contains("serial=" + DiskSerial(token), StringComparison.Ordinal)) || !lines.Contains("local,id=installstatefs,path=" + FullState + ",security_model=none")) throw new InvalidOperationException("QEMU did not attach the exact owned raw disk/serial and persistent state share.");
|
||||
var owner = await Command("docker", ["exec", id, "cat", FullState + "/run.owner"], output, "full-share-owner", cancellation);
|
||||
if (owner.Output.Trim() != token) throw new InvalidOperationException("Native state owner mismatch.");
|
||||
using var receipt = JsonDocument.Parse(readiness);
|
||||
var disk = receipt.RootElement.GetProperty("disk").GetString();
|
||||
var permit = Path.Combine(output, "install.permit");
|
||||
File.WriteAllText(permit, token + "\n" + disk + "\n" + commit + "\n");
|
||||
await Command("docker", ["cp", permit, id + ":" + FullState + "/install.permit.tmp"], output, "stage-owned-install-permit", cancellation);
|
||||
await Command("docker", ["exec", id, "mv", FullState + "/install.permit.tmp", FullState + "/install.permit"], output, "authorize-owned-guest-installation", cancellation);
|
||||
}
|
||||
|
||||
static readonly string[] NativeFacts = [
|
||||
"MeetingAssistant.Tests.MacOsMeetingAudioSourceTests.NativeAudioCaptureIsPackagedAsSignedMacOsAppForPersistentPrivacyGrant",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.MacOsCapabilityEndpointReportsEnabledRealProviders",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperCropsPngUsingOcrPixelCoordinates",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.CalendarClientFallsBackToCalendarAutomationWhenEventKitIsDenied"];
|
||||
static readonly string[] NativeArtifacts = ["MeetingAssistantAudioCapture.app/Contents/MacOS/macos-meeting-audio-capture", "macos-desktop-controls", "macos-meeting-integrations", "macos-meeting-assistant-launcher"];
|
||||
|
||||
static void ValidateFullResult(string json, string token, string commit, string archiveHash)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var result = document.RootElement;
|
||||
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || result.GetProperty("sourceCommit").GetString() != commit || result.GetProperty("archiveSha256").GetString() != archiveHash || result.GetProperty("sdkVersion").GetString() != SdkVersion || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 14 || result.GetProperty("architecture").GetString() != "x86_64" || new[] { "expectedTests", "total", "executed", "passed" }.Any(key => result.GetProperty(key).GetInt32() != 577) || new[] { "failed", "notExecuted", "audioCodeSignExit" }.Any(key => result.GetProperty(key).GetInt32() != 0) || !result.GetProperty("nativeTests").EnumerateArray().Select(value => value.GetString()).Order().SequenceEqual(NativeFacts.Order())) throw new InvalidOperationException("Native full receipt did not prove exact-source 577/577 with all five native tests and zero skips.");
|
||||
var artifacts = result.GetProperty("nativeArtifacts").EnumerateArray().ToArray();
|
||||
if (artifacts.Length != 4 || !artifacts.Select(item => item.GetProperty("name").GetString()).Order().SequenceEqual(NativeArtifacts.Order()) || artifacts.Any(item => item.GetProperty("architecture").GetString() != "x86_64" || !System.Text.RegularExpressions.Regex.IsMatch(item.GetProperty("sha256").GetString() ?? "", "^[0-9a-f]{64}$"))) throw new InvalidOperationException("Native Mach-O/x86_64 helper manifest is incomplete.");
|
||||
}
|
||||
|
||||
static void ValidateTrx(byte[] bytes, string json)
|
||||
{
|
||||
using var receipt = JsonDocument.Parse(json);
|
||||
if (Hash(bytes) != receipt.RootElement.GetProperty("trxSha256").GetString()) throw new InvalidOperationException("Returned native TRX SHA-256 mismatch.");
|
||||
var document = XDocument.Load(new MemoryStream(bytes));
|
||||
var counters = document.Descendants().Single(item => item.Name.LocalName == "Counters");
|
||||
foreach (var key in new[] { "total", "executed", "passed" }) if ((int?)counters.Attribute(key) != 577) throw new InvalidOperationException("Native TRX count mismatch: " + key);
|
||||
foreach (var key in new[] { "failed", "notExecuted" }) if ((int?)counters.Attribute(key) != 0) throw new InvalidOperationException("Native TRX failure/skip counter: " + key);
|
||||
var results = document.Descendants().Where(item => item.Name.LocalName == "UnitTestResult").ToArray();
|
||||
if (results.Length != 577 || results.Any(item => (string?)item.Attribute("outcome") != "Passed")) throw new InvalidOperationException("Native TRX contains missing, failed or skipped results.");
|
||||
var identities = document.Descendants().Where(item => item.Name.LocalName == "UnitTest").ToDictionary(item => (string)item.Attribute("id")!, item => { var method = item.Elements().Single(child => child.Name.LocalName == "TestMethod"); var className = ((string?)method.Attribute("className"))?.Split(',')[0].Trim() ?? ""; var name = (string?)method.Attribute("name") ?? ""; return name.StartsWith(className + ".", StringComparison.Ordinal) ? name : className + "." + name; });
|
||||
var passed = results.Select(item => identities[(string)item.Attribute("testId")!]).ToHashSet();
|
||||
if (NativeFacts.Any(name => !passed.Contains(name))) throw new InvalidOperationException("Native TRX does not explicitly pass every required macOS fact.");
|
||||
}
|
||||
|
||||
static void ValidateFullContracts()
|
||||
{
|
||||
var token = new string('a', 32); var commit = new string('b', 40); var hash = new string('c', 64);
|
||||
var trx = "<TestRun><TestDefinitions>" + string.Concat(Enumerable.Range(0, 577).Select(index => { var identity = index < 5 ? NativeFacts[index] : "MeetingAssistant.Tests.Validation.Test" + index; var split = identity.LastIndexOf('.'); return $"<UnitTest id='t{index}'><TestMethod className='{identity[..split]}' name='{identity[(split + 1)..]}' /></UnitTest>"; })) + "</TestDefinitions><Results>" + string.Concat(Enumerable.Range(0, 577).Select(index => $"<UnitTestResult testId='t{index}' outcome='Passed' />")) + "</Results><ResultSummary><Counters total='577' executed='577' passed='577' failed='0' notExecuted='0' /></ResultSummary></TestRun>";
|
||||
var good = JsonSerializer.Serialize(new { token, success = true, sourceCommit = commit, archiveSha256 = hash, sdkVersion = SdkVersion, osVersion = "14.6.1", architecture = "x86_64", expectedTests = 577, total = 577, executed = 577, passed = 577, failed = 0, notExecuted = 0, nativeTests = NativeFacts, nativeArtifacts = NativeArtifacts.Select(name => new { name, sha256 = hash, architecture = "x86_64" }), audioCodeSignExit = 0, trxSha256 = Hash(Encoding.UTF8.GetBytes(trx)) });
|
||||
ValidateFullResult(good, token, commit, hash); ValidateTrx(Encoding.UTF8.GetBytes(trx), good);
|
||||
byte[] bomTrx = [0xef, 0xbb, 0xbf, .. Encoding.UTF8.GetBytes(trx)];
|
||||
ValidateTrx(bomTrx, good.Replace(Hash(Encoding.UTF8.GetBytes(trx)), Hash(bomTrx), StringComparison.Ordinal));
|
||||
var qualifiedTrx = trx;
|
||||
foreach (var name in NativeFacts) qualifiedTrx = qualifiedTrx.Replace("name='" + name[(name.LastIndexOf('.') + 1)..] + "'", "name='" + name + "'", StringComparison.Ordinal);
|
||||
ValidateTrx(Encoding.UTF8.GetBytes(qualifiedTrx), good.Replace(Hash(Encoding.UTF8.GetBytes(trx)), Hash(Encoding.UTF8.GetBytes(qualifiedTrx)), StringComparison.Ordinal));
|
||||
foreach (var invalid in new[] { good.Replace("\"success\":true", "\"success\":false"), good.Replace("\"passed\":577", "\"passed\":572"), good.Replace("\"notExecuted\":0", "\"notExecuted\":5"), good.Replace("\"audioCodeSignExit\":0", "\"audioCodeSignExit\":1"), good.Replace("14.6.1", "13.6.1"), good.Replace("x86_64", "arm64"), good.Replace(token, new string('d', 32)), good.Replace(commit, new string('e', 40)), good.Replace("NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures", "UnrelatedTest") })
|
||||
{
|
||||
try { ValidateFullResult(invalid, token, commit, hash); } catch (InvalidOperationException) { continue; }
|
||||
throw new InvalidOperationException("Full native validator accepted an incomplete or stale proof.");
|
||||
}
|
||||
try { ValidateTrx(Encoding.UTF8.GetBytes(trx.Replace("outcome='Passed'", "outcome='NotExecuted'")), good); } catch (InvalidOperationException) { return; }
|
||||
throw new InvalidOperationException("Full native validator accepted changed TRX bytes.");
|
||||
}
|
||||
|
||||
static async Task ValidateCompression(string chunk, string output)
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
var bytes = File.ReadAllBytes(chunk);
|
||||
if (Hash(bytes) != "2770f06fe51f19ddc040cfad4ab870d7227f540d1ba4190a9ce631145c12fae0") throw new InvalidOperationException("Readonly retained Recovery qualification chunk hash mismatch.");
|
||||
var text = Encoding.Latin1.GetString(bytes);
|
||||
if (text.Split(DiagnosticDaemon, StringSplitOptions.None).Length != 2 || text.Split(MountOnlyBootstrap, StringSplitOptions.None).Length != 2) throw new InvalidOperationException("Qualification chunk does not contain this exact Recovery LaunchDaemon and mount-only patch.");
|
||||
// Upstream UDIF recompression uses Python zlib; .NET's compressor differs even on baseline.
|
||||
await Command("python3", ["-c", "import json,pathlib,sys,zlib; b=pathlib.Path(sys.argv[1]).read_bytes(); n=len(zlib.compress(b,9)); print(json.dumps({'runtime':zlib.ZLIB_RUNTIME_VERSION,'compressedBytes':n,'slotBytes':43266,'fits':n<=43266})); sys.exit(0 if n<=43266 else 1)", chunk], output, "readonly-recovery-compression", CancellationToken.None);
|
||||
}
|
||||
|
||||
static async Task ValidateDiskSerialParser(string output)
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
var guard = File.ReadAllText("tools/ci/macos-native-disk-guard.sh");
|
||||
const string start = "-v disk=\"${disk#/dev/}\" '\n";
|
||||
const string end = " ' \"$state/disk-ownership-ioreg.log\")";
|
||||
var program = guard[(guard.IndexOf(start, StringComparison.Ordinal) + start.Length)..guard.IndexOf(end, StringComparison.Ordinal)];
|
||||
var serial = new string('0', 20);
|
||||
var own = "+-o QEMU HARDDISK <class IOAHCIBlockStorageDevice>\n | \"Device Characteristics\" = {\"Serial Number\"=\"" + serial + "\"}\n +-o Media <class IOMedia>\n \"BSD Name\" = \"disk1\"\n";
|
||||
var reversed = "+-o QEMU HARDDISK <class IOAHCIBlockStorageDevice>\n +-o Media <class IOMedia>\n \"BSD Name\" = \"disk1\"\n | \"Device Characteristics\" = {\"Serial Number\"=\"" + serial + "\"}\n";
|
||||
var splitRoots = "+-o QEMU HARDDISK <class IOAHCIBlockStorageDevice>\n | \"Device Characteristics\" = {\"Serial Number\"=\"" + serial + "\"}\n+-o Other <class IOAHCIBlockStorageDevice>\n +-o Media <class IOMedia>\n \"BSD Name\" = \"disk1\"\n";
|
||||
foreach (var test in new[] { ("own", own, "1"), ("reversed-properties", reversed, "1"), ("split-roots", splitRoots, "0"), ("foreign-serial", own.Replace(serial, new string('a', 20)), "0"), ("foreign-disk", own.Replace("disk1", "disk2"), "0"), ("ambiguous", own + own, "2") })
|
||||
{
|
||||
var path = Path.Combine(output, "ioreg-" + test.Item1 + ".fixture");
|
||||
File.WriteAllText(path, test.Item2);
|
||||
var result = await Command("awk", ["-v", "expected=" + serial, "-v", "disk=disk1", program, path], output, "disk-serial-" + test.Item1, CancellationToken.None);
|
||||
if (result.Output.Trim() != test.Item3) throw new InvalidOperationException("Actual boot-seam IORegistry parser fixture failed: " + test.Item1);
|
||||
}
|
||||
}
|
||||
static string ReplaceAllExact(string text, string oldValue, string newValue, int expected)
|
||||
{
|
||||
var count = text.Split(oldValue, StringSplitOptions.None).Length - 1;
|
||||
@@ -519,15 +1108,28 @@ static class NativeDiagnostic
|
||||
throw new InvalidOperationException("Created container exceeds the owned unprivileged TCG/Haswell/macOS 14 boundary.");
|
||||
}
|
||||
|
||||
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool final = false, string? token = null)
|
||||
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool full = false, bool final = false, string? token = null)
|
||||
{
|
||||
if (final && token is not null) await CaptureMonitor(id, output, token, cancellation);
|
||||
var logs = await Command("docker", ["logs", "--tail", "3000", id], output, "container", cancellation, requireSuccess: false);
|
||||
foreach (var file in new[] { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") })
|
||||
ReportCpuPreflight(output, logs.Output);
|
||||
var files = new List<(string, string)> { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") };
|
||||
if (full) files.AddRange([("guest-phase.json", "guest-phase.json"), ("full-result.json", "full-result.json"), ("firstboot.log", "firstboot.log"), ("unattended-firstboot.log", "unattended-firstboot.log"), ("install.log", "install.log"), ("apple.log", "apple.log"), ("disk-ownership-ioreg.log", "disk-ownership-ioreg.log"), ("installed-root.plist", "installed-root.plist"), ("apfs-containers.plist", "apfs-containers.plist"), ("physical-store.plist", "physical-store.plist"), ("clt-catalog.log", "clt-catalog.log"), ("clt-install.log", "clt-install.log"), ("clt-sdk.log", "clt-sdk.log")]);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
|
||||
var result = await Command("docker", ["exec", id, "cat", (full ? FullState : "/dev/shm/installstate") + "/" + file.Item1], output, "capture-" + file.Item1.Replace('/', '-'), cancellation, requireSuccess: false);
|
||||
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
|
||||
}
|
||||
if (full)
|
||||
{
|
||||
var trx = await Command("docker", ["cp", id + ":" + FullState + "/test-results/native.trx", Path.Combine(output, "native.trx.tmp")], output, "capture-native-trx", cancellation, requireSuccess: false);
|
||||
if (trx.ExitCode == 0) File.Move(Path.Combine(output, "native.trx.tmp"), Path.Combine(output, "native.trx"), overwrite: true);
|
||||
if (final || File.Exists(Path.Combine(output, "full-result.json")))
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(output, "guest-logs"));
|
||||
await Command("docker", ["cp", id + ":" + FullState + "/guest-logs/.", Path.Combine(output, "guest-logs")], output, "capture-guest-logs", cancellation, requireSuccess: false);
|
||||
}
|
||||
}
|
||||
// The immutable Recovery image is complete only after this staging marker.
|
||||
// Hash it once instead of rereading the image on every twenty-second poll.
|
||||
if (logs.Output.Contains("[supported-profile] accelerator=tcg", StringComparison.Ordinal)
|
||||
@@ -535,6 +1137,23 @@ static class NativeDiagnostic
|
||||
await Command("docker", ["exec", id, "sh", "-c", "printf '[qemu]\n'; qemu-system-x86_64 --version | head -n 1; printf '[Recovery hash]\n'; test -f /storage/14/setup.dmg && sha256sum /storage/14/setup.dmg || exit 1; printf '[resources]\n'; df -Pk /storage; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true"], output, "guest-container-resources", cancellation, requireSuccess: false, retainSuccessful: true);
|
||||
}
|
||||
|
||||
static void ReportCpuPreflight(string output, string logs)
|
||||
{
|
||||
var receipt = Path.Combine(output, "cpu-preflight-runtime.json");
|
||||
if (File.Exists(receipt)) return;
|
||||
var expectedSuffix = " cpu=" + CpuFlags + "; actual AVX/AVX2 executed before Apple download";
|
||||
foreach (var line in logs.Split('\n'))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(line, @"\[cpu-preflight\] positive=33 negative=([0-9]{1,3})");
|
||||
if (!match.Success || match.Groups[1].Value == "33" || !line[(match.Index + match.Length)..].StartsWith(expectedSuffix, StringComparison.Ordinal)) continue;
|
||||
var sourceMarker = match.Value + expectedSuffix;
|
||||
var marker = "[cpu-preflight] positive=33 negative=" + match.Groups[1].Value + " accelerator=tcg cpu=" + CpuModel + " instructions=AVX/AVX2";
|
||||
Console.WriteLine(marker);
|
||||
Save(receipt, new { marker, markerSha256 = Hash(Encoding.UTF8.GetBytes(sourceMarker)), capturedUtc = DateTimeOffset.UtcNow, readinessGateSatisfied = false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task CapturePressure(string id, string output, string phase, CancellationToken cancellation)
|
||||
{
|
||||
using var snapshotDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
@@ -860,6 +1479,18 @@ static class NativeDiagnostic
|
||||
Console.WriteLine("[native-diagnostic] Final native guest proof:");
|
||||
Console.Write(proof);
|
||||
}
|
||||
static void PrintFullProof(string output, string token)
|
||||
{
|
||||
foreach (var name in new[] { "full-result.json", "firstboot.log", "guest-logs/build.stdout.log", "guest-logs/build.stderr.log", "guest-logs/test.stdout.log", "guest-logs/test.stderr.log" })
|
||||
{
|
||||
var path = Path.Combine(output, name);
|
||||
if (!File.Exists(path)) continue;
|
||||
var proof = File.ReadAllText(path).Replace(token, "<run-id>", StringComparison.Ordinal);
|
||||
if (proof.Length > 64 * 1024) proof = "[earlier output retained in artifact]\n" + proof[^(64 * 1024)..];
|
||||
Console.WriteLine("[native-diagnostic] Final native evidence: " + name);
|
||||
Console.WriteLine(proof);
|
||||
}
|
||||
}
|
||||
static void Save(string path, object value)
|
||||
{
|
||||
var temporary = path + ".tmp";
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
#:property PublishAot=false
|
||||
using System.Diagnostics;
|
||||
using System.Formats.Tar;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
// Installed-guest CI slice. --validate never invokes macOS, dotnet build/test, or a VM.
|
||||
return await NativeGuest.Execute(args);
|
||||
|
||||
static class NativeGuest
|
||||
{
|
||||
const string SdkVersion = "10.0.401";
|
||||
const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0";
|
||||
const int ExpectedTests = 577;
|
||||
const int MaximumLogBytes = 8 * 1024 * 1024;
|
||||
static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
|
||||
static readonly string[] RequiredNativeTests =
|
||||
[
|
||||
"MeetingAssistant.Tests.MacOsMeetingAudioSourceTests.NativeAudioCaptureIsPackagedAsSignedMacOsAppForPersistentPrivacyGrant",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.MacOsCapabilityEndpointReportsEnabledRealProviders",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperCropsPngUsingOcrPixelCoordinates",
|
||||
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.CalendarClientFallsBackToCalendarAutomationWhenEventKitIsDenied"
|
||||
];
|
||||
static readonly string[] NativeNames =
|
||||
[
|
||||
"MeetingAssistantAudioCapture.app/Contents/MacOS/macos-meeting-audio-capture",
|
||||
"macos-desktop-controls", "macos-meeting-integrations", "macos-meeting-assistant-launcher"
|
||||
];
|
||||
|
||||
public static async Task<int> Execute(string[] args)
|
||||
{
|
||||
if (args.Length == 0 || args.SequenceEqual(["--help"]))
|
||||
{
|
||||
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeGuest.cs -- --validate [--archive <source.tar> --source-commit <SHA>] | --run --state /Volumes/installstate --work /private/var/tmp/meeting-assistant-native-<runToken>");
|
||||
return 0;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (args.SequenceEqual(["--validate"]) || args.Length == 5 && args[0] == "--validate" && args[1] == "--archive" && args[3] == "--source-commit")
|
||||
{
|
||||
ValidateContracts();
|
||||
if (args.Length == 5)
|
||||
{
|
||||
if (!Hex(args[4], 40)) throw new ArgumentException("Source commit must be the full lowercase Git SHA.");
|
||||
using var archive = File.OpenRead(Path.GetFullPath(args[2]));
|
||||
ValidateArchive(archive, args[4]);
|
||||
}
|
||||
Console.WriteLine("Guest payload, safe Git tar, fresh TRX and native receipt contracts passed; no native execution occurred.");
|
||||
return 0;
|
||||
}
|
||||
if (args.Length != 5 || args[0] != "--run" || args[1] != "--state" || args[3] != "--work")
|
||||
throw new ArgumentException("Choose --validate or --run --state <mounted9pdir> --work <ownedAPFSdir>.");
|
||||
return await Run(Path.GetFullPath(args[2]), Path.GetFullPath(args[4]));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(exception.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<int> Run(string state, string work)
|
||||
{
|
||||
if (!OperatingSystem.IsMacOS() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
throw new InvalidOperationException("--run is restricted to the installed macOS x86_64 guest.");
|
||||
RequireNoLinks(state);
|
||||
RequireNoLinks(work);
|
||||
var manifestPath = Path.Combine(state, "payload.json");
|
||||
RequireNoLinks(manifestPath);
|
||||
var payload = ReadPayload(File.ReadAllText(manifestPath));
|
||||
if (work != "/private/var/tmp/meeting-assistant-native-" + payload.RunToken || !Directory.Exists(work))
|
||||
throw new InvalidOperationException("Guest work directory does not match this run's owned APFS location.");
|
||||
var owner = Path.Combine(work, "run.owner");
|
||||
RequireNoLinks(owner);
|
||||
if (File.ReadAllText(owner).TrimEnd('\r', '\n') != payload.RunToken)
|
||||
throw new InvalidOperationException("Guest work directory has a different run owner.");
|
||||
|
||||
var started = DateTimeOffset.UtcNow;
|
||||
var summary = new TestSummary(0, 0, 0, 0, 0, []);
|
||||
var native = new List<NativeArtifact>();
|
||||
var osVersion = "";
|
||||
var architecture = "";
|
||||
var actualSdk = "";
|
||||
var trxSha256 = "";
|
||||
var audioCodeSignExit = -1;
|
||||
var success = false;
|
||||
var reason = "";
|
||||
var logs = Path.Combine(state, "guest-logs");
|
||||
var results = Path.Combine(state, "test-results");
|
||||
var source = Path.Combine(work, "source");
|
||||
var dotnet = Path.Combine(work, "dotnet", "dotnet");
|
||||
// Guest checks/restore/build/tests: 25 minutes within the host's 172-minute total.
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(25));
|
||||
using var signal = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; deadline.Cancel(); });
|
||||
ConsoleCancelEventHandler cancelHandler = (_, context) => { context.Cancel = true; deadline.Cancel(); };
|
||||
Console.CancelKeyPress += cancelHandler;
|
||||
void Phase(string phase, string stage) => Save(Path.Combine(state, "guest-phase.json"), new { token = payload.RunToken, phase, stage, updatedUtc = DateTimeOffset.UtcNow });
|
||||
async Task<CommandResult> Cmd(string executable, string[] arguments, string label, bool requireSuccess = true) =>
|
||||
await Command(executable, arguments, source, work, logs, label, deadline.Token, requireSuccess);
|
||||
try
|
||||
{
|
||||
RequireAbsent(Path.Combine(state, "full-result.json"));
|
||||
RequireAbsent(logs);
|
||||
RequireAbsent(results);
|
||||
RequireAbsent(source);
|
||||
Directory.CreateDirectory(logs);
|
||||
Directory.CreateDirectory(results);
|
||||
foreach (var directory in new[] { "home", "packages", "tmp" })
|
||||
{
|
||||
RequireNoLinks(Path.Combine(work, directory));
|
||||
Directory.CreateDirectory(Path.Combine(work, directory));
|
||||
}
|
||||
Phase("tests-running", "installed-guest-checks");
|
||||
osVersion = (await Cmd("/usr/bin/sw_vers", ["-productVersion"], "os-version")).Output.Trim();
|
||||
architecture = (await Cmd("/usr/bin/uname", ["-m"], "architecture")).Output.Trim();
|
||||
var uid = (await Cmd("/usr/bin/id", ["-u"], "uid")).Output.Trim();
|
||||
RequirePlatform(osVersion, architecture, uid);
|
||||
foreach (var volume in new[] { ("/", "system-volume"), (work, "work-volume") })
|
||||
RequireApfs((await Cmd("/usr/sbin/diskutil", ["info", "-plist", volume.Item1], volume.Item2)).Output);
|
||||
await Cmd("/usr/bin/xcode-select", ["-p"], "clt-location");
|
||||
await Cmd("/usr/sbin/pkgutil", ["--pkg-info", "com.apple.pkg.CLTools_Executables"], "clt-package");
|
||||
await Cmd("/usr/bin/xcrun", ["swiftc", "--version"], "swift-version");
|
||||
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-version"], "apple-sdk-version");
|
||||
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], "apple-sdk-path");
|
||||
RequireNoLinks(dotnet);
|
||||
actualSdk = (await Cmd(dotnet, ["--version"], "sdk-version")).Output.Trim();
|
||||
var sdkInfo = (await Cmd(dotnet, ["--info"], "sdk-info")).Output;
|
||||
if (actualSdk != SdkVersion || !Regex.IsMatch(sdkInfo, @"(?m)^\s*Architecture:\s*x64\s*$"))
|
||||
throw new InvalidOperationException("Guest .NET SDK is not the pinned 10.0.401/x64 toolchain.");
|
||||
|
||||
Phase("tests-running", "payload-verification");
|
||||
var archive = Path.Combine(state, "source.tar");
|
||||
var sdkArchive = Path.Combine(state, "sdk.tar.gz");
|
||||
RequireNoLinks(archive);
|
||||
RequireNoLinks(sdkArchive);
|
||||
if (await HashFile(archive, false, deadline.Token) != payload.ArchiveSha256 || await HashFile(sdkArchive, true, deadline.Token) != payload.SdkSha512)
|
||||
throw new InvalidOperationException("Guest payload hash does not match the pinned manifest.");
|
||||
using (var stream = File.OpenRead(archive)) ValidateArchive(stream, payload.SourceCommit);
|
||||
// All members were checked before native tar can write anything; the destination is new.
|
||||
Directory.CreateDirectory(source);
|
||||
await Cmd("/usr/bin/tar", ["-xf", archive, "-C", source], "source-extraction");
|
||||
var project = Path.Combine(source, "MeetingAssistant.Tests", "MeetingAssistant.Tests.csproj");
|
||||
if (!File.Exists(project)) throw new InvalidOperationException("Source archive lacks the test project.");
|
||||
var nativeRoot = Path.Combine(source, "MeetingAssistant", "bin", "Release", "net10.0", "Native");
|
||||
RequireAbsent(nativeRoot);
|
||||
Phase("tests-running", "restore");
|
||||
await Cmd(dotnet, ["restore", project, "-p:EnableWindowsTargeting=true", "-p:TargetFramework=net10.0"], "restore");
|
||||
Phase("tests-running", "build");
|
||||
var buildStarted = DateTimeOffset.UtcNow;
|
||||
await Cmd(dotnet, ["build", project, "--no-restore", "-f", "net10.0", "-c", "Release", "-p:EnableWindowsTargeting=true"], "build");
|
||||
Phase("tests-running", "native-artifacts");
|
||||
foreach (var name in NativeNames)
|
||||
{
|
||||
var path = Path.Combine(nativeRoot, name);
|
||||
RequireNoLinks(path);
|
||||
RequireFreshFile(path, buildStarted);
|
||||
var fileOutput = (await Cmd("/usr/bin/file", ["-b", path], "native-file-" + native.Count)).Output;
|
||||
var arch = (await Cmd("/usr/bin/xcrun", ["lipo", "-archs", path], "native-architecture-" + native.Count)).Output.Trim();
|
||||
RequireNativeArtifact(fileOutput, arch);
|
||||
native.Add(new(name, await HashFile(path, false, deadline.Token), arch));
|
||||
}
|
||||
var signing = await Cmd("/usr/bin/codesign", ["--verify", "--deep", "--strict", Path.Combine(nativeRoot, "MeetingAssistantAudioCapture.app")], "audio-code-sign", false);
|
||||
audioCodeSignExit = signing.ExitCode;
|
||||
if (audioCodeSignExit != 0) throw new InvalidOperationException("Fresh audio app did not pass strict code-signature verification.");
|
||||
Phase("tests-running", "test");
|
||||
var testStarted = DateTimeOffset.UtcNow;
|
||||
await Cmd(dotnet, ["test", project, "--no-build", "--no-restore", "-f", "net10.0", "-c", "Release", "-p:EnableWindowsTargeting=true", "--logger", "trx;LogFileName=native.trx", "--results-directory", results], "test");
|
||||
var trxPath = Path.Combine(results, "native.trx");
|
||||
RequireNoLinks(trxPath);
|
||||
RequireFreshFile(trxPath, testStarted, 16 * 1024 * 1024);
|
||||
var trxBytes = File.ReadAllBytes(trxPath);
|
||||
summary = ValidateTrx(trxBytes, payload.ExpectedTests, testStarted);
|
||||
trxSha256 = Convert.ToHexStringLower(SHA256.HashData(trxBytes));
|
||||
success = true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
reason = exception is OperationCanceledException ? "The explicit 25-minute guest deadline or cancellation was reached." : exception.Message;
|
||||
if (reason.Length > 4096) reason = reason[..4096];
|
||||
Console.Error.WriteLine(reason);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.CancelKeyPress -= cancelHandler;
|
||||
var result = new FullResult(payload.RunToken, success, payload.SourceCommit, payload.ArchiveSha256, osVersion, architecture, actualSdk, payload.ExpectedTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, native.ToArray(), audioCodeSignExit, trxSha256, reason, started, DateTimeOffset.UtcNow);
|
||||
if (success)
|
||||
{
|
||||
try { ValidateResult(result, payload); }
|
||||
catch (InvalidOperationException exception)
|
||||
{
|
||||
success = false;
|
||||
result = result with { Success = false, Reason = exception.Message };
|
||||
}
|
||||
}
|
||||
Save(Path.Combine(state, "full-result.json"), result);
|
||||
Phase(success ? "tests-passed" : "tests-failed", "complete");
|
||||
}
|
||||
Console.WriteLine(success ? "Native macOS guest build, signed helpers and all 577 tests passed without skips." : "Native guest validation failed; full-result.json and bounded logs retain the evidence.");
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
static Payload ReadPayload(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object || document.RootElement.EnumerateObject().Select(property => property.Name).Distinct(StringComparer.Ordinal).Count() != document.RootElement.EnumerateObject().Count())
|
||||
throw new InvalidOperationException("Payload manifest must contain one unambiguous JSON object.");
|
||||
var payload = JsonSerializer.Deserialize<Payload>(json, JsonOptions) ?? throw new InvalidOperationException("Missing guest payload manifest.");
|
||||
if (!Hex(payload.RunToken, 32) || !Hex(payload.SourceCommit, 40) || !Hex(payload.ArchiveSha256, 64) || payload.SdkVersion != SdkVersion || payload.SdkSha512 != SdkSha512 || payload.ExpectedTests != ExpectedTests)
|
||||
throw new InvalidOperationException("Guest payload identity, SDK pin or expected test count is invalid.");
|
||||
return payload;
|
||||
}
|
||||
|
||||
static void ValidateArchive(Stream stream, string commit)
|
||||
{
|
||||
using var reader = new TarReader(stream, leaveOpen: true);
|
||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var commits = new List<string>();
|
||||
long totalBytes = 0;
|
||||
while (reader.GetNextEntry() is { } entry)
|
||||
{
|
||||
if (entry is PaxGlobalExtendedAttributesTarEntry global)
|
||||
{
|
||||
if (global.GlobalExtendedAttributes.TryGetValue("comment", out var value)) commits.Add(value);
|
||||
if (global.GlobalExtendedAttributes.Keys.Any(key => key is "path" or "linkpath"))
|
||||
throw new InvalidOperationException("Global tar path overrides are not supported.");
|
||||
continue;
|
||||
}
|
||||
if (entry.EntryType is not (TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.Directory))
|
||||
throw new InvalidOperationException("Source tar contains a link or unsupported entry type: " + entry.Name);
|
||||
var name = entry.Name.TrimEnd('/');
|
||||
if (name.Length == 0 || name.StartsWith('/') || name.Contains('\\') || name.Contains('\0') || name.Split('/').Any(part => part.Length == 0 || part is "." or ".." or ".git" or "bin" or "obj") || !names.Add(name))
|
||||
throw new InvalidOperationException("Source tar path is unsafe, repeated or contains generated output: " + entry.Name);
|
||||
totalBytes = checked(totalBytes + entry.Length);
|
||||
if (names.Count > 100_000 || totalBytes > 2L * 1024 * 1024 * 1024)
|
||||
throw new InvalidOperationException("Source tar exceeds its explicit entry/size budget.");
|
||||
}
|
||||
if (names.Count == 0 || commits.Count != 1 || commits[0] != commit || !names.Contains("MeetingAssistant.Tests/MeetingAssistant.Tests.csproj"))
|
||||
throw new InvalidOperationException("Source tar does not prove its exact Git commit and test project.");
|
||||
}
|
||||
|
||||
static TestSummary ValidateTrx(string xml, int expected, DateTimeOffset testStarted) => ValidateTrx(Encoding.UTF8.GetBytes(xml), expected, testStarted);
|
||||
static TestSummary ValidateTrx(byte[] xml, int expected, DateTimeOffset testStarted)
|
||||
{
|
||||
var document = ParseXml(xml);
|
||||
var root = document.Root ?? throw new InvalidOperationException("Empty TRX.");
|
||||
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
|
||||
if (root.Name != ns + "TestRun") throw new InvalidOperationException("Unexpected TRX namespace or root.");
|
||||
var times = root.Element(ns + "Times") ?? throw new InvalidOperationException("TRX lacks execution timestamps.");
|
||||
if (!DateTimeOffset.TryParse((string?)times.Attribute("start"), out var start) || !DateTimeOffset.TryParse((string?)times.Attribute("finish"), out var finish) || start < testStarted.AddSeconds(-2) || finish < start || finish > DateTimeOffset.UtcNow.AddSeconds(30))
|
||||
throw new InvalidOperationException("TRX belongs to a stale or invalid test execution.");
|
||||
var resultSummary = root.Element(ns + "ResultSummary") ?? throw new InvalidOperationException("TRX lacks a final result summary.");
|
||||
if ((string?)resultSummary.Attribute("outcome") != "Completed") throw new InvalidOperationException("TRX test run did not complete.");
|
||||
var counters = resultSummary.Element(ns + "Counters") ?? throw new InvalidOperationException("TRX lacks final counters.");
|
||||
int Count(string name) => int.TryParse((string?)counters.Attribute(name), out var value) && value >= 0 ? value : throw new InvalidOperationException("TRX lacks a valid counter: " + name);
|
||||
var total = Count("total");
|
||||
var executed = Count("executed");
|
||||
var passed = Count("passed");
|
||||
var failed = Count("failed");
|
||||
var notExecuted = Count("notExecuted");
|
||||
foreach (var name in new[] { "error", "timeout", "aborted", "inconclusive", "passedButRunAborted", "notRunnable", "disconnected", "inProgress", "pending" })
|
||||
if (counters.Attribute(name) is not null && Count(name) != 0) throw new InvalidOperationException("TRX contains an incomplete or unsuccessful execution: " + name);
|
||||
if (expected != ExpectedTests || total != expected || executed != expected || passed != expected || failed != 0 || notExecuted != 0)
|
||||
throw new InvalidOperationException($"Native TRX must prove {expected} total/executed/passed tests, zero failures and zero skips; got {total}/{executed}/{passed}/{failed}/{notExecuted}.");
|
||||
var definitions = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var unit in root.Element(ns + "TestDefinitions")?.Elements(ns + "UnitTest") ?? [])
|
||||
{
|
||||
var method = unit.Element(ns + "TestMethod") ?? throw new InvalidOperationException("TRX test definition lacks its method identity.");
|
||||
var className = ((string?)method.Attribute("className") ?? "").Split(',')[0].Trim();
|
||||
var methodName = (string?)method.Attribute("name") ?? "";
|
||||
var id = (string?)unit.Attribute("id") ?? "";
|
||||
if (id.Length == 0 || className.Length == 0 || methodName.Length == 0 || !definitions.TryAdd(id, methodName.StartsWith(className + ".", StringComparison.Ordinal) ? methodName : className + "." + methodName))
|
||||
throw new InvalidOperationException("TRX contains an ambiguous test definition.");
|
||||
}
|
||||
var results = root.Element(ns + "Results")?.Elements(ns + "UnitTestResult").ToArray() ?? [];
|
||||
var executionIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var testIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var native = new List<string>();
|
||||
foreach (var result in results)
|
||||
{
|
||||
var id = (string?)result.Attribute("testId") ?? "";
|
||||
var executionId = (string?)result.Attribute("executionId") ?? "";
|
||||
if ((string?)result.Attribute("outcome") != "Passed" || executionId.Length == 0 || !executionIds.Add(executionId) || !testIds.Add(id) || !definitions.TryGetValue(id, out var identity))
|
||||
throw new InvalidOperationException("TRX has a missing, duplicate or non-passed execution.");
|
||||
if (RequiredNativeTests.Contains(identity, StringComparer.Ordinal)) native.Add(identity);
|
||||
}
|
||||
if (definitions.Count != expected || results.Length != expected || native.Count != RequiredNativeTests.Length || !native.Order().SequenceEqual(RequiredNativeTests.Order()))
|
||||
throw new InvalidOperationException("TRX does not prove every expected test definition exactly once and the five explicit native macOS tests.");
|
||||
return new(total, executed, passed, failed, notExecuted, native.ToArray());
|
||||
}
|
||||
|
||||
static void ValidateResult(FullResult result, Payload payload)
|
||||
{
|
||||
if (result.Token != payload.RunToken || !result.Success || result.SourceCommit != payload.SourceCommit || result.ArchiveSha256 != payload.ArchiveSha256 || !Version.TryParse(result.OsVersion, out var version) || version.Major < 14 || result.Architecture != "x86_64" || result.SdkVersion != SdkVersion || result.ExpectedTests != ExpectedTests || result.Total != ExpectedTests || result.Executed != ExpectedTests || result.Passed != ExpectedTests || result.Failed != 0 || result.NotExecuted != 0 || !result.NativeTests.Order().SequenceEqual(RequiredNativeTests.Order()) || result.AudioCodeSignExit != 0 || !Hex(result.TrxSha256, 64) || result.NativeArtifacts.Length != NativeNames.Length || !result.NativeArtifacts.Select(artifact => artifact.Name).Order().SequenceEqual(NativeNames.Order()) || result.NativeArtifacts.Any(artifact => artifact.Architecture != "x86_64" || !Hex(artifact.Sha256, 64)) || result.CompletedUtc < result.StartedUtc || result.CompletedUtc - result.StartedUtc > TimeSpan.FromMinutes(25).Add(TimeSpan.FromSeconds(15)) || result.CompletedUtc > DateTimeOffset.UtcNow.AddSeconds(30) || result.CompletedUtc < DateTimeOffset.UtcNow.AddMinutes(-1) || result.Reason.Length != 0)
|
||||
throw new InvalidOperationException("Native guest receipt does not prove this source, toolchain, signed artifacts and all expected tests.");
|
||||
}
|
||||
|
||||
static void RequirePlatform(string version, string architecture, string uid)
|
||||
{
|
||||
if (!Version.TryParse(version, out var parsed) || parsed.Major < 14 || architecture != "x86_64" || uid != "0")
|
||||
throw new InvalidOperationException("Native build requires installed macOS 14+/x86_64 running as root.");
|
||||
}
|
||||
static void RequireApfs(string xml)
|
||||
{
|
||||
var elements = ParseXml(xml).Root?.Element("dict")?.Elements().ToArray() ?? [];
|
||||
var type = elements.Select((element, index) => (element, index)).FirstOrDefault(pair => pair.element.Name == "key" && pair.element.Value == "FilesystemType");
|
||||
if (type.element is null || type.index + 1 >= elements.Length || elements[type.index + 1].Name != "string" || elements[type.index + 1].Value != "apfs")
|
||||
throw new InvalidOperationException("Native build must run from the installed APFS system and owned APFS work volume.");
|
||||
}
|
||||
static void RequireNativeArtifact(string fileOutput, string architecture)
|
||||
{
|
||||
if (!fileOutput.Contains("Mach-O", StringComparison.Ordinal) || !fileOutput.Contains("x86_64", StringComparison.Ordinal) || architecture != "x86_64")
|
||||
throw new InvalidOperationException("Native helper is not a Mach-O executable containing exactly x86_64.");
|
||||
}
|
||||
static void RequireAbsent(string path)
|
||||
{
|
||||
if (Path.Exists(path) || GetAttributesSafe(path)?.HasFlag(FileAttributes.ReparsePoint) == true)
|
||||
throw new InvalidOperationException("Fresh guest execution refuses pre-existing output: " + path);
|
||||
}
|
||||
static void RequireFreshFile(string path, DateTimeOffset started, long maximumBytes = long.MaxValue)
|
||||
{
|
||||
RequireNoLinks(path);
|
||||
var file = new FileInfo(path);
|
||||
if (!file.Exists || file.Length == 0 || file.Length > maximumBytes || file.LastWriteTimeUtc < started.UtcDateTime.AddSeconds(-2))
|
||||
throw new InvalidOperationException("Expected a fresh, nonempty, bounded output from this execution: " + path);
|
||||
}
|
||||
static void RequireNoLinks(string path)
|
||||
{
|
||||
for (var current = Path.GetFullPath(path); current is not null; current = Path.GetDirectoryName(current))
|
||||
if (GetAttributesSafe(current)?.HasFlag(FileAttributes.ReparsePoint) == true)
|
||||
throw new InvalidOperationException("Guest ownership/extraction refuses a symbolic link: " + current);
|
||||
}
|
||||
static FileAttributes? GetAttributesSafe(string path)
|
||||
{
|
||||
try { return File.GetAttributes(path); }
|
||||
catch (FileNotFoundException) { return null; }
|
||||
catch (DirectoryNotFoundException) { return null; }
|
||||
}
|
||||
static XDocument ParseXml(string xml) => ParseXml(Encoding.UTF8.GetBytes(xml));
|
||||
static XDocument ParseXml(byte[] xml)
|
||||
{
|
||||
using var stream = new MemoryStream(xml, writable: false);
|
||||
using var reader = XmlReader.Create(stream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, XmlResolver = null, MaxCharactersInDocument = 16 * 1024 * 1024 });
|
||||
return XDocument.Load(reader);
|
||||
}
|
||||
static bool Hex(string? value, int length) => value is not null && Regex.IsMatch(value, "^[0-9a-f]{" + length + "}$");
|
||||
static async Task<string> HashFile(string path, bool sha512, CancellationToken cancellation)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = sha512 ? await SHA512.HashDataAsync(stream, cancellation) : await SHA256.HashDataAsync(stream, cancellation);
|
||||
return Convert.ToHexStringLower(hash);
|
||||
}
|
||||
static void Save(string path, object value)
|
||||
{
|
||||
RequireNoLinks(path);
|
||||
var temporary = path + ".tmp";
|
||||
RequireNoLinks(temporary);
|
||||
File.WriteAllText(temporary, JsonSerializer.Serialize(value, JsonOptions), new UTF8Encoding(false));
|
||||
File.Move(temporary, path, overwrite: true);
|
||||
}
|
||||
|
||||
static async Task<CommandResult> Command(string executable, string[] arguments, string source, string work, string logs, string label, CancellationToken cancellation, bool requireSuccess)
|
||||
{
|
||||
Console.WriteLine("[native-guest] " + label);
|
||||
using var commandCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
var token = commandCancellation.Token;
|
||||
var start = new ProcessStartInfo(executable) { WorkingDirectory = Directory.Exists(source) ? source : work, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
|
||||
foreach (var argument in arguments) start.ArgumentList.Add(argument);
|
||||
start.Environment.Clear();
|
||||
foreach (var pair in new Dictionary<string, string>
|
||||
{
|
||||
["PATH"] = Path.Combine(work, "dotnet") + ":/usr/bin:/bin:/usr/sbin:/sbin",
|
||||
["TMPDIR"] = Path.Combine(work, "tmp"),
|
||||
["DOTNET_ROOT"] = Path.Combine(work, "dotnet"), ["DOTNET_CLI_HOME"] = Path.Combine(work, "home"),
|
||||
["NUGET_PACKAGES"] = Path.Combine(work, "packages"), ["TZ"] = "Europe/Berlin",
|
||||
["LANG"] = "en_US.UTF-8", ["LC_ALL"] = "en_US.UTF-8", ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1",
|
||||
["DOTNET_NOLOGO"] = "1", ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1", ["MSBUILDDISABLENODEREUSE"] = "1"
|
||||
}) start.Environment[pair.Key] = pair.Value;
|
||||
using var process = Process.Start(start) ?? throw new InvalidOperationException("Cannot start guest command: " + label);
|
||||
long capturedBytes = 0;
|
||||
async Task<string> Read(StreamReader reader, string stream)
|
||||
{
|
||||
var captured = new StringBuilder();
|
||||
var buffer = new char[8192];
|
||||
using var log = new StreamWriter(Path.Combine(logs, label + "." + stream + ".log"), false, new UTF8Encoding(false));
|
||||
while (true)
|
||||
{
|
||||
var count = await reader.ReadAsync(buffer.AsMemory(), token);
|
||||
if (count == 0) break;
|
||||
if (Interlocked.Add(ref capturedBytes, Encoding.UTF8.GetByteCount(buffer.AsSpan(0, count))) > MaximumLogBytes)
|
||||
{
|
||||
commandCancellation.Cancel();
|
||||
throw new InvalidOperationException(label + " exceeded its combined 8-MiB output budget.");
|
||||
}
|
||||
captured.Append(buffer, 0, count);
|
||||
await log.WriteAsync(buffer.AsMemory(0, count), token);
|
||||
await log.FlushAsync(token);
|
||||
}
|
||||
return captured.ToString();
|
||||
}
|
||||
var stdout = Read(process.StandardOutput, "stdout");
|
||||
var stderr = Read(process.StandardError, "stderr");
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(token));
|
||||
var result = new CommandResult(process.ExitCode, await stdout, await stderr);
|
||||
if (requireSuccess && result.ExitCode != 0)
|
||||
throw new InvalidOperationException($"{label} exited {result.ExitCode}: {result.Error[..Math.Min(result.Error.Length, 1500)]}");
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { if (!process.HasExited) process.Kill(entireProcessTree: true); } catch (InvalidOperationException) { }
|
||||
using var killDeadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
try { await process.WaitForExitAsync(killDeadline.Token); } catch (OperationCanceledException) { }
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
static void ValidateContracts()
|
||||
{
|
||||
var payload = new Payload(new string('a', 32), new string('b', 40), new string('c', 64), SdkVersion, SdkSha512, ExpectedTests);
|
||||
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||
ReadPayload(json);
|
||||
Reject(() => ReadPayload(json.Replace(SdkVersion, "10.0.100", StringComparison.Ordinal)));
|
||||
Reject(() => ReadPayload(json.Replace(SdkSha512, new string('d', 128), StringComparison.Ordinal)));
|
||||
Reject(() => ReadPayload(json.Replace("577", "572", StringComparison.Ordinal)));
|
||||
Reject(() => ReadPayload(json.Replace(payload.RunToken, "stale", StringComparison.Ordinal)));
|
||||
Reject(() => ReadPayload(json.Replace(payload.ArchiveSha256, "invalid", StringComparison.Ordinal)));
|
||||
RequirePlatform("14.6.1", "x86_64", "0");
|
||||
Reject(() => RequirePlatform("13.6.1", "x86_64", "0"));
|
||||
Reject(() => RequirePlatform("14.6.1", "arm64", "0"));
|
||||
Reject(() => RequirePlatform("14.6.1", "x86_64", "501"));
|
||||
RequireApfs("<plist><dict><key>FilesystemType</key><string>apfs</string></dict></plist>");
|
||||
Reject(() => RequireApfs("<plist><dict><key>FilesystemType</key><string>hfs</string></dict></plist>"));
|
||||
RequireNativeArtifact("Mach-O 64-bit executable x86_64", "x86_64");
|
||||
Reject(() => RequireNativeArtifact("Mach-O universal binary x86_64 arm64", "x86_64 arm64"));
|
||||
Reject(() => RequireNativeArtifact("ELF 64-bit executable x86_64", "x86_64"));
|
||||
using (var archive = FixtureArchive(payload.SourceCommit)) ValidateArchive(archive, payload.SourceCommit);
|
||||
foreach (var path in new[] { "../escape", "/absolute", "safe/../escape", "safe\\escape", "MeetingAssistant/bin/stale" })
|
||||
Reject(() => { using var archive = FixtureArchive(payload.SourceCommit, path); ValidateArchive(archive, payload.SourceCommit); });
|
||||
Reject(() => { using var archive = FixtureArchive(payload.SourceCommit, "link", true); ValidateArchive(archive, payload.SourceCommit); });
|
||||
Reject(() => { using var archive = FixtureArchive(new string('d', 40)); ValidateArchive(archive, payload.SourceCommit); });
|
||||
var started = DateTimeOffset.UtcNow.AddMinutes(-1);
|
||||
var fixture = FixtureTrx(started);
|
||||
var summary = ValidateTrx(fixture.ToString(), ExpectedTests, started);
|
||||
var plainTrx = Encoding.UTF8.GetBytes(fixture.ToString());
|
||||
var bomTrx = new byte[] { 0xef, 0xbb, 0xbf }.Concat(plainTrx).ToArray();
|
||||
ValidateTrx(bomTrx, ExpectedTests, started);
|
||||
if (SHA256.HashData(plainTrx).SequenceEqual(SHA256.HashData(bomTrx))) throw new InvalidOperationException("TRX raw-byte hashing discarded its BOM.");
|
||||
Reject(() => ValidateTrx(fixture.ToString(), ExpectedTests, started.AddMinutes(2)));
|
||||
XNamespace ns = fixture.Root!.Name.Namespace;
|
||||
var skipped = new XDocument(fixture);
|
||||
skipped.Descendants(ns + "UnitTestResult").First().SetAttributeValue("outcome", "NotExecuted");
|
||||
Reject(() => ValidateTrx(skipped.ToString(), ExpectedTests, started));
|
||||
var missingNative = new XDocument(fixture);
|
||||
missingNative.Descendants(ns + "TestMethod").First().SetAttributeValue("name", "ManagedReplacement");
|
||||
Reject(() => ValidateTrx(missingNative.ToString(), ExpectedTests, started));
|
||||
var duplicate = new XDocument(fixture);
|
||||
duplicate.Descendants(ns + "UnitTestResult").Last().SetAttributeValue("executionId", "execution-0");
|
||||
Reject(() => ValidateTrx(duplicate.ToString(), ExpectedTests, started));
|
||||
var repeatedManaged = new XDocument(fixture);
|
||||
repeatedManaged.Descendants(ns + "UnitTestResult").Last().SetAttributeValue("testId", "test-5");
|
||||
Reject(() => ValidateTrx(repeatedManaged.ToString(), ExpectedTests, started));
|
||||
var unexecutedDefinition = new XDocument(fixture);
|
||||
var extraDefinition = new XElement(unexecutedDefinition.Descendants(ns + "UnitTest").Last());
|
||||
extraDefinition.SetAttributeValue("id", "unexecuted-test");
|
||||
unexecutedDefinition.Root!.Element(ns + "TestDefinitions")!.Add(extraDefinition);
|
||||
Reject(() => ValidateTrx(unexecutedDefinition.ToString(), ExpectedTests, started));
|
||||
var missingDefinition = new XDocument(fixture);
|
||||
missingDefinition.Descendants(ns + "UnitTest").Last().Remove();
|
||||
Reject(() => ValidateTrx(missingDefinition.ToString(), ExpectedTests, started));
|
||||
var theoryRows = new XDocument(fixture);
|
||||
foreach (var method in theoryRows.Descendants(ns + "TestMethod").Skip(RequiredNativeTests.Length).Take(2))
|
||||
method.SetAttributeValue("name", "TheoryWithDistinctRowIds");
|
||||
ValidateTrx(theoryRows.ToString(), ExpectedTests, started);
|
||||
var counters = new XDocument(fixture);
|
||||
counters.Descendants(ns + "Counters").Single().SetAttributeValue("passed", "572");
|
||||
Reject(() => ValidateTrx(counters.ToString(), ExpectedTests, started));
|
||||
var aborted = new XDocument(fixture);
|
||||
aborted.Descendants(ns + "ResultSummary").Single().SetAttributeValue("outcome", "Aborted");
|
||||
Reject(() => ValidateTrx(aborted.ToString(), ExpectedTests, started));
|
||||
var artifacts = NativeNames.Select(name => new NativeArtifact(name, new string('d', 64), "x86_64")).ToArray();
|
||||
var result = new FullResult(payload.RunToken, true, payload.SourceCommit, payload.ArchiveSha256, "14.6.1", "x86_64", SdkVersion, ExpectedTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, artifacts, 0, new string('e', 64), "", started, DateTimeOffset.UtcNow);
|
||||
ValidateResult(result, payload);
|
||||
foreach (var invalid in new[] { result with { Token = new string('f', 32) }, result with { Success = false }, result with { SourceCommit = new string('f', 40) }, result with { ArchiveSha256 = new string('f', 64) }, result with { NotExecuted = 5 }, result with { AudioCodeSignExit = 1 }, result with { NativeTests = RequiredNativeTests[..4] }, result with { NativeArtifacts = artifacts[..3] }, result with { NativeArtifacts = [artifacts[0] with { Architecture = "arm64" }, .. artifacts[1..]] }, result with { TrxSha256 = "" }, result with { SdkVersion = "10.0.100" }, result with { OsVersion = "13.6.1" }, result with { StartedUtc = started.AddHours(-1) }, result with { StartedUtc = started.AddHours(-1), CompletedUtc = started.AddHours(-1).AddSeconds(1) } })
|
||||
Reject(() => ValidateResult(invalid, payload));
|
||||
var temporary = Path.Combine(OperatingSystem.IsMacOS() ? "/private/tmp" : Path.GetTempPath(), "meeting-assistant-guest-validation-" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
RequireAbsent(temporary);
|
||||
Directory.CreateDirectory(temporary);
|
||||
Reject(() => RequireAbsent(temporary));
|
||||
var file = Path.Combine(temporary, "fresh.trx");
|
||||
File.WriteAllText(file, "fixture");
|
||||
RequireFreshFile(file, started);
|
||||
Reject(() => RequireFreshFile(file, started, 1));
|
||||
File.SetLastWriteTimeUtc(file, started.UtcDateTime.AddMinutes(-5));
|
||||
Reject(() => RequireFreshFile(file, started));
|
||||
File.Delete(file);
|
||||
}
|
||||
finally { if (Directory.Exists(temporary)) Directory.Delete(temporary, recursive: true); }
|
||||
}
|
||||
|
||||
static MemoryStream FixtureArchive(string commit, string? extra = null, bool link = false)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using (var writer = new TarWriter(stream, TarEntryFormat.Pax, leaveOpen: true))
|
||||
{
|
||||
writer.WriteEntry(new PaxGlobalExtendedAttributesTarEntry(new Dictionary<string, string> { ["comment"] = commit }));
|
||||
writer.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, "MeetingAssistant.Tests/MeetingAssistant.Tests.csproj") { DataStream = new MemoryStream(Encoding.UTF8.GetBytes("<Project />")) });
|
||||
if (extra is not null)
|
||||
{
|
||||
var entry = new PaxTarEntry(link ? TarEntryType.SymbolicLink : TarEntryType.RegularFile, extra);
|
||||
if (link) entry.LinkName = "../outside";
|
||||
else entry.DataStream = new MemoryStream([1]);
|
||||
writer.WriteEntry(entry);
|
||||
}
|
||||
}
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
static XDocument FixtureTrx(DateTimeOffset started)
|
||||
{
|
||||
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
|
||||
var definitions = new XElement(ns + "TestDefinitions");
|
||||
var results = new XElement(ns + "Results");
|
||||
for (var index = 0; index < ExpectedTests; index++)
|
||||
{
|
||||
var identity = index < RequiredNativeTests.Length ? RequiredNativeTests[index] : "MeetingAssistant.Tests.ManagedTests.Test" + index;
|
||||
var separator = identity.LastIndexOf('.');
|
||||
definitions.Add(new XElement(ns + "UnitTest", new XAttribute("id", "test-" + index), new XElement(ns + "TestMethod", new XAttribute("className", identity[..separator] + ", MeetingAssistant.Tests"), new XAttribute("name", identity[(separator + 1)..]))));
|
||||
results.Add(new XElement(ns + "UnitTestResult", new XAttribute("testId", "test-" + index), new XAttribute("executionId", "execution-" + index), new XAttribute("outcome", "Passed")));
|
||||
}
|
||||
return new XDocument(new XElement(ns + "TestRun", new XElement(ns + "Times", new XAttribute("start", started.ToString("O")), new XAttribute("finish", started.AddSeconds(1).ToString("O"))), definitions, results, new XElement(ns + "ResultSummary", new XAttribute("outcome", "Completed"), new XElement(ns + "Counters", new XAttribute("total", ExpectedTests), new XAttribute("executed", ExpectedTests), new XAttribute("passed", ExpectedTests), new XAttribute("failed", 0), new XAttribute("notExecuted", 0)))));
|
||||
}
|
||||
static void Reject(Action action)
|
||||
{
|
||||
try { action(); }
|
||||
catch (InvalidOperationException) { return; }
|
||||
throw new InvalidOperationException("Contract validation accepted an invalid or stale fixture.");
|
||||
}
|
||||
sealed record Payload(string RunToken, string SourceCommit, string ArchiveSha256, string SdkVersion, string SdkSha512, int ExpectedTests);
|
||||
sealed record NativeArtifact(string Name, string Sha256, string Architecture);
|
||||
sealed record TestSummary(int Total, int Executed, int Passed, int Failed, int NotExecuted, string[] NativeTests);
|
||||
sealed record FullResult(string Token, bool Success, string SourceCommit, string ArchiveSha256, string OsVersion, string Architecture, string SdkVersion, int ExpectedTests, int Total, int Executed, int Passed, int Failed, int NotExecuted, string[] NativeTests, NativeArtifact[] NativeArtifacts, int AudioCodeSignExit, string TrxSha256, string Reason, DateTimeOffset StartedUtc, DateTimeOffset CompletedUtc);
|
||||
sealed record CommandResult(int ExitCode, string Output, string Error);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Apple pre-.NET boot seam, sourced in Recovery and on installed first boot.
|
||||
# A match requires the exact run-owned emulated serial, one whole writable 64-GiB disk.
|
||||
verify_owned_disk() {
|
||||
local disk="$1" state="$2" token="$3" info serial matches bytes
|
||||
[[ "$disk" =~ ^/dev/disk[0-9]+$ && "$token" =~ ^[0-9a-f]{32}$ ]] || return 1
|
||||
[ "$(cat "$state/run.owner" 2>/dev/null)" = "$token" ] || return 1
|
||||
serial="${token:0:20}"
|
||||
/usr/sbin/diskutil list physical > "$state/disk-list-current.log" 2>&1 || return 1
|
||||
/usr/bin/grep -Eq "^$disk[[:space:]].*physical" "$state/disk-list-current.log" || return 1
|
||||
/usr/sbin/diskutil info "$disk" > "$state/disk-info-current.log" 2>&1 || return 1
|
||||
info=$(cat "$state/disk-info-current.log")
|
||||
printf '%s\n' "$info" | /usr/bin/grep -Eq '^[[:space:]]*Whole:[[:space:]]*Yes' || return 1
|
||||
printf '%s\n' "$info" | /usr/bin/grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*Yes' && return 1
|
||||
printf '%s\n' "$info" | /usr/bin/grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*No' || return 1
|
||||
bytes=$(printf '%s\n' "$info" | /usr/bin/sed -nE 's/^[[:space:]]*Disk Size:.*\(([0-9]+) Bytes\).*/\1/p')
|
||||
[ "$bytes" = 68719476736 ] || return 1
|
||||
/usr/sbin/ioreg -r -c IOBlockStorageDevice -l -w 0 > "$state/disk-ownership-ioreg.log" 2>&1 || return 1
|
||||
matches=$(/usr/bin/awk -v expected="$serial" -v disk="${disk#/dev/}" '
|
||||
function finish_root() { if (serialCount == 1 && serial == expected && ownedDisk) found++ }
|
||||
/^\+-o/ { finish_root(); serial=""; serialCount=0; ownedDisk=0 }
|
||||
/"Serial Number"[[:space:]]*=[[:space:]]*"/ {
|
||||
serialCount++; serial=$0; sub(/^.*"Serial Number"[[:space:]]*=[[:space:]]*"/, "", serial); sub(/".*$/, "", serial); sub(/[[:space:]]+$/, "", serial)
|
||||
}
|
||||
/"BSD Name"[[:space:]]*=[[:space:]]*"/ {
|
||||
name=$0; sub(/^.*"BSD Name"[[:space:]]*=[[:space:]]*"/, "", name); sub(/".*$/, "", name)
|
||||
if (name == disk) ownedDisk=1
|
||||
}
|
||||
END { finish_root(); print found+0 }
|
||||
' "$state/disk-ownership-ioreg.log")
|
||||
[ "$matches" = 1 ] || return 1
|
||||
printf '[owned-disk] %s serial=%s bytes=%s\n' "$disk" "$serial" "$bytes"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
# Apple LaunchDaemon bootstrap before the guest .NET SDK exists.
|
||||
# Runs only inside the isolated owned VM; installs CLT and expands the pinned SDK.
|
||||
set -u
|
||||
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH
|
||||
PROOF_TOKEN="${1:-}"
|
||||
STATE_DIR="/Volumes/installstate"
|
||||
WORK="/private/var/tmp/meeting-assistant-native-$PROOF_TOKEN"
|
||||
[[ "$PROOF_TOKEN" =~ ^[0-9a-f]{32}$ ]] || exit 1
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || exit 1
|
||||
exec >> "$STATE_DIR/firstboot.log" 2>&1
|
||||
|
||||
phase() {
|
||||
printf '{"token":"%s","phase":"%s","updatedUtc":"%s"}\n' "$PROOF_TOKEN" "$1" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATE_DIR/guest-phase.json.tmp"
|
||||
mv -f "$STATE_DIR/guest-phase.json.tmp" "$STATE_DIR/guest-phase.json"
|
||||
}
|
||||
fail() { printf '[firstboot] ERROR: %s\n' "$1"; phase bootstrap-failed; exit 1; }
|
||||
require_installed_macos_version() {
|
||||
# Numeric sw_vers product release with a major version of at least 14.
|
||||
[[ "${1:-}" =~ ^(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,})\.[0-9]+(\.[0-9]+)?$ ]]
|
||||
}
|
||||
phase toolchain-installing
|
||||
echo '[firstboot] verifying installed OS, APFS and owned physical store'
|
||||
uname -a; id
|
||||
installed_version=$(/usr/bin/sw_vers -productVersion) || fail installed_os_version_failed
|
||||
require_installed_macos_version "$installed_version" || fail installed_macos_14_or_newer_required
|
||||
printf '[firstboot] installed macOS version=%s\n' "$installed_version"
|
||||
[ "$(id -u)" = 0 ] && [ "$(uname -m)" = x86_64 ] || fail wrong_guest_platform
|
||||
/usr/sbin/diskutil info -plist / > "$STATE_DIR/installed-root.plist" || fail root_diskutil_failed
|
||||
[ "$(/usr/libexec/PlistBuddy -c 'Print :FilesystemType' "$STATE_DIR/installed-root.plist")" = apfs ] || fail root_is_not_installed_apfs
|
||||
container=$(/usr/libexec/PlistBuddy -c 'Print :APFSContainerReference' "$STATE_DIR/installed-root.plist") || fail root_container_missing
|
||||
/usr/sbin/diskutil apfs list -plist > "$STATE_DIR/apfs-containers.plist" || fail apfs_list_failed
|
||||
index=0
|
||||
physical=""
|
||||
while reference=$(/usr/libexec/PlistBuddy -c "Print :Containers:$index:ContainerReference" "$STATE_DIR/apfs-containers.plist" 2>/dev/null); do
|
||||
if [ "$reference" = "$container" ]; then
|
||||
[ -z "$physical" ] || fail ambiguous_root_containers
|
||||
physical=$(/usr/libexec/PlistBuddy -c "Print :Containers:$index:PhysicalStores:0:DeviceIdentifier" "$STATE_DIR/apfs-containers.plist") || fail physical_store_missing
|
||||
/usr/libexec/PlistBuddy -c "Print :Containers:$index:PhysicalStores:1" "$STATE_DIR/apfs-containers.plist" >/dev/null 2>&1 && fail multiple_physical_stores
|
||||
fi
|
||||
index=$((index + 1))
|
||||
done
|
||||
[[ "$physical" =~ ^disk[0-9]+s[0-9]+$ ]] || fail invalid_physical_store
|
||||
/usr/sbin/diskutil info -plist "/dev/$physical" > "$STATE_DIR/physical-store.plist" || fail physical_store_info_failed
|
||||
whole=$(/usr/libexec/PlistBuddy -c 'Print :ParentWholeDisk' "$STATE_DIR/physical-store.plist") || fail physical_parent_missing
|
||||
. "$STATE_DIR/macos-native-disk-guard.sh"
|
||||
verify_owned_disk "/dev/$whole" "$STATE_DIR" "$PROOF_TOKEN" || fail installed_root_is_not_the_owned_64g_disk
|
||||
|
||||
# The phase has an independent 30-minute watchdog; host outer deadline is 180 minutes.
|
||||
parent=$$
|
||||
(
|
||||
trap 'kill "$sleeper" 2>/dev/null || :; exit 0' TERM INT
|
||||
# Toolchain watchdog: 30 minutes, also bounded by the host's 172-minute total.
|
||||
sleep 1800 & sleeper=$!; wait "$sleeper"; kill -TERM "$parent" 2>/dev/null || :
|
||||
) & watchdog=$!
|
||||
clt_marker="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress"
|
||||
trap 'rm -f "$clt_marker"; kill -TERM "$watchdog" 2>/dev/null || :; wait "$watchdog" 2>/dev/null || :' EXIT
|
||||
trap 'fail toolchain_deadline_or_cancellation' TERM INT
|
||||
[ ! -e "$WORK" ] || fail guest_work_directory_already_exists
|
||||
mkdir -p "$WORK" || fail guest_work_directory_failed
|
||||
printf '%s\n' "$PROOF_TOKEN" > "$WORK/run.owner" || fail guest_work_owner_failed
|
||||
free_kib=$(df -Pk "$WORK" | awk 'NR==2 {print $4}')
|
||||
[[ "$free_kib" =~ ^[0-9]+$ ]] && (( free_kib >= 8 * 1024 * 1024 )) || fail insufficient_existing_guest_free_space
|
||||
|
||||
echo '[firstboot] installing a compatible Apple CLT catalog entry without a GUI'
|
||||
touch "$clt_marker" || fail clt_marker_failed
|
||||
/usr/sbin/softwareupdate -l > "$STATE_DIR/clt-catalog.log" 2>&1 || fail clt_catalog_failed
|
||||
label=$(grep -B 1 -E 'Command Line Tools' "$STATE_DIR/clt-catalog.log" | awk -F'*' '/^ *\*/ {print $2}' | sed -e 's/^ *Label: //' -e 's/^ *//' | sort -V | tail -n 1)
|
||||
[ -n "$label" ] || fail no_compatible_headless_clt_label
|
||||
printf '[firstboot] selected CLT: %s\n' "$label"
|
||||
/usr/sbin/softwareupdate -i "$label" > "$STATE_DIR/clt-install.log" 2>&1 || fail clt_install_failed
|
||||
/usr/bin/xcode-select --switch /Library/Developer/CommandLineTools || fail clt_switch_failed
|
||||
/usr/sbin/pkgutil --pkg-info=com.apple.pkg.CLTools_Executables || fail clt_receipt_failed
|
||||
/usr/bin/xcrun --find swiftc || fail swiftc_missing
|
||||
/usr/bin/xcrun swiftc --version || fail swiftc_version_failed
|
||||
{
|
||||
/usr/bin/xcrun --sdk macosx --show-sdk-version &&
|
||||
/usr/bin/xcrun --sdk macosx --show-sdk-path
|
||||
} > "$STATE_DIR/clt-sdk.log" 2>&1 || fail clt_sdk_identity_failed
|
||||
printf 'import AppKit\nimport AVFoundation\nimport ScreenCaptureKit\nimport EventKit\nimport WebKit\nprint("native SDK ready")\n' > "$WORK/toolchain-smoke.swift"
|
||||
/usr/bin/xcrun swiftc -target x86_64-apple-macos13.0 "$WORK/toolchain-smoke.swift" -o "$WORK/toolchain-smoke" || fail native_framework_compile_failed
|
||||
"$WORK/toolchain-smoke" || fail native_framework_execution_failed
|
||||
rm -f "$clt_marker"
|
||||
|
||||
echo '[firstboot] validating and expanding the pinned .NET SDK on owned APFS'
|
||||
expected_sdk=33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0
|
||||
actual_sdk=$(shasum -a 512 "$STATE_DIR/sdk.tar.gz" | awk '{print $1}')
|
||||
[ "$actual_sdk" = "$expected_sdk" ] || fail sdk_hash_mismatch
|
||||
mkdir "$WORK/dotnet" || fail sdk_directory_failed
|
||||
tar -xzf "$STATE_DIR/sdk.tar.gz" -C "$WORK/dotnet" || fail sdk_extract_failed
|
||||
[ "$("$WORK/dotnet/dotnet" --version)" = 10.0.401 ] || fail sdk_version_mismatch
|
||||
# Guest C# owns build/test deadlines from this point; stop the CLT-only watchdog.
|
||||
kill -TERM "$watchdog" 2>/dev/null || :
|
||||
wait "$watchdog" 2>/dev/null || :
|
||||
trap - TERM INT
|
||||
"$WORK/dotnet/dotnet" run --file "$STATE_DIR/MacOsNativeGuest.cs" -- --run --state "$STATE_DIR" --work "$WORK"
|
||||
result=$?
|
||||
(( result == 0 )) || fail native_tests_failed
|
||||
exit 0
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Full-only pre-.NET seam. Claim one persistent owned probe before forking;
|
||||
# launchd then execs the original Apple daemon, including on a real failure.
|
||||
set -u
|
||||
PROOF_TOKEN="@@PROOF_TOKEN@@"
|
||||
STATE_DIR="/Volumes/installstate"
|
||||
MARKER="$STATE_DIR/probe.started"
|
||||
|
||||
bootstrap_failed() {
|
||||
printf '[full-bootstrap] ERROR: %s\n' "$1" >&2
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
|
||||
printf '{"token":"%s","phase":"bootstrap-failed","reason":"%s"}\n' "$PROOF_TOKEN" "$1" > "$STATE_DIR/guest-phase.bootstrap.$$.tmp" &&
|
||||
/bin/mv -f "$STATE_DIR/guest-phase.bootstrap.$$.tmp" "$STATE_DIR/guest-phase.json"
|
||||
}
|
||||
|
||||
wait_for_owned_state() {
|
||||
local count=0
|
||||
while :; do
|
||||
if [ -e "$STATE_DIR/run.owner" ] || [ -L "$STATE_DIR/run.owner" ]; then
|
||||
[ -f "$STATE_DIR/run.owner" ] && [ ! -L "$STATE_DIR/run.owner" ] &&
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || {
|
||||
bootstrap_failed foreign_state_owner
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
fi
|
||||
if (( count >= 120 )); then
|
||||
bootstrap_failed state_share_mount_timeout
|
||||
return 1
|
||||
fi
|
||||
/sbin/mount_9p installstate >/dev/null 2>&1 || :
|
||||
count=$((count + 1))
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
owns_probe_marker() {
|
||||
local record extra
|
||||
[ -f "$MARKER" ] && [ ! -L "$MARKER" ] || return 1
|
||||
{
|
||||
IFS= read -r record || return 1
|
||||
if IFS= read -r extra || [ -n "$extra" ]; then return 1; fi
|
||||
} < "$MARKER"
|
||||
[ "$record" = "$PROOF_TOKEN:$source_commit" ]
|
||||
}
|
||||
|
||||
claim_probe() {
|
||||
[[ "$PROOF_TOKEN" =~ ^[0-9a-f]{32}$ ]] || { bootstrap_failed invalid_probe_token; return 1; }
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || { bootstrap_failed foreign_state_owner; return 1; }
|
||||
source_commit=$(cat "$STATE_DIR/source.commit" 2>/dev/null) || { bootstrap_failed source_commit_missing; return 1; }
|
||||
[[ "$source_commit" =~ ^[0-9a-f]{40}$ ]] || { bootstrap_failed source_commit_invalid; return 1; }
|
||||
if [ -e "$MARKER" ] || [ -L "$MARKER" ]; then
|
||||
owns_probe_marker || { bootstrap_failed foreign_or_invalid_probe_marker; return 1; }
|
||||
return 2
|
||||
fi
|
||||
# Keep partial/failed markers: an incomplete first start is an error, no retry.
|
||||
if ! ( set -o noclobber; printf '%s:%s\n' "$PROOF_TOKEN" "$source_commit" > "$MARKER" ); then
|
||||
bootstrap_failed probe_marker_write_failed
|
||||
return 1
|
||||
fi
|
||||
owns_probe_marker || { bootstrap_failed probe_marker_incomplete; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
if wait_for_owned_state && claim_probe; then
|
||||
(
|
||||
/bin/bash "$STATE_DIR/readiness.sh"
|
||||
child_exit=$?
|
||||
(( child_exit == 0 )) || bootstrap_failed first_probe_child_failed
|
||||
) &
|
||||
fi
|
||||
exec /usr/libexec/recoveryosd
|
||||
Reference in New Issue
Block a user