Files
meeting-assistant/tools/ci/MacOsNativeDiagnostic.cs
T

349 lines
27 KiB
C#

#:property PublishAot=false
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
// .NET 10 file-based CI diagnostic. See docs/macos-native-diagnostic.md.
return await NativeDiagnostic.Execute(args);
static class NativeDiagnostic
{
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
const string OwnerLabel = "org.meeting-assistant.native-diagnostic";
const long GuestDiskBytes = 64L * 1024 * 1024 * 1024;
const long ContainerMemoryBytes = 6L * 1024 * 1024 * 1024;
const int MaximumCapturedCharacters = 8 * 1024 * 1024;
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";
static readonly string OriginalDaemon = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>Label</key>
\t<string>com.apple.recoveryosd</string>
\t<key>OnDemand</key>
\t<false/>
\t<key>ProcessType</key>
\t<string>App</string>
\t<key>EnablePressuredExit</key>
\t<false/>
\t<key>ProgramArguments</key>
\t<array>
\t\t<string>/usr/libexec/recoveryosd</string>
\t</array>
</dict>
</plist>
""".Replace("\\t", "\t", StringComparison.Ordinal);
static readonly string DiagnosticDaemon = (OriginalDaemon + "\n")
.Replace("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n", "", StringComparison.Ordinal)
.Replace("\t\t<string>/usr/libexec/recoveryosd</string>", "\t\t<string>/bin/bash</string>\n\t\t<string>/Volumes/installstate/launch.sh</string>", StringComparison.Ordinal);
public static async Task<int> Execute(string[] args)
{
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]");
return 0;
}
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
if (args.Contains("--validate"))
{
ValidateContracts();
if (Option(args, "--source") is { } source)
await PrepareSource(Path.GetFullPath(source), output, "validation", false, CancellationToken.None);
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
return 0;
}
if (args.Contains("--cleanup"))
return await Cleanup(output) ? 0 : 1;
if (!args.Contains("--run")) throw new ArgumentException("Choose --run, --cleanup or --validate.");
Directory.CreateDirectory(output);
var statePath = Path.Combine(output, "owned-resources.json");
if (File.Exists(statePath)) throw new InvalidOperationException("Output already contains a run identity; choose a fresh directory or clean up its run first.");
var token = Guid.NewGuid().ToString("N");
var work = Path.Combine(Environment.GetEnvironmentVariable("RUNNER_TEMP") ?? Path.GetTempPath(), "meeting-assistant-native-" + token);
var state = new OwnedResources(token, "meeting-assistant-native-" + token, "meeting-assistant-native-diagnostic:" + token, work);
Save(statePath, state);
Directory.CreateDirectory(work);
File.WriteAllText(Path.Combine(work, "run.owner"), token);
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(40));
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;
var outcome = "failed";
string? error = null;
try
{
if (!OperatingSystem.IsLinux() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
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, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = 40 });
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
using (var document = JsonDocument.Parse(info.Output))
{
var data = document.RootElement;
if (data.GetProperty("OSType").GetString() != "linux" || data.GetProperty("Architecture").GetString() is not ("x86_64" or "amd64"))
throw new InvalidOperationException("The existing Docker daemon is not Linux/x64; this diagnostic does not reconfigure it.");
if (data.GetProperty("NCPU").GetInt32() < 2 || data.GetProperty("MemTotal").GetInt64() < ContainerMemoryBytes)
throw new InvalidOperationException("Existing Docker resources cannot fit this bounded 2-CPU/6-GiB diagnostic; no infrastructure change was requested.");
}
await Command("sh", ["-c", "cat /proc/meminfo; printf '\n[cgroup]\n'; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true; printf '\n[workspace disk]\n'; df -Pk ."], output, "runner-resources", deadline.Token);
var available = System.Text.RegularExpressions.Regex.Match(File.ReadAllText("/proc/meminfo"), @"(?m)^MemAvailable:\s+(\d+) kB$");
if (!available.Success || long.Parse(available.Groups[1].Value) < 5L * 1024 * 1024)
throw new InvalidOperationException("Existing runner memory has less than the 5-GiB available diagnostic budget; no infrastructure change was requested.");
var source = Path.Combine(work, "dockur");
await Command("git", ["clone", "--no-checkout", "https://github.com/dockur/macos.git", source], output, "dockur-clone", deadline.Token);
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);
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", "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);
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 };
Save(statePath, state);
await Command("docker", ["inspect", id], output, "container-created", deadline.Token);
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
Console.WriteLine("The owned unprivileged TCG guest is starting. Success requires native macOS 14+/x86_64 and a writable 64-GiB disk; no installer will run.");
while (true)
{
deadline.Token.ThrowIfCancellationRequested();
await CaptureGuest(id, output, deadline.Token);
var resultPath = Path.Combine(output, "guest-result.json");
if (File.Exists(resultPath))
{
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;
}
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.");
await Task.Delay(TimeSpan.FromSeconds(20), deadline.Token);
}
}
catch (Exception exception)
{
error = exception is OperationCanceledException ? "The explicit 40-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.ContainerName, output, captureDeadline.Token); } catch (Exception exception) { Console.Error.WriteLine("Final evidence capture: " + exception.Message); }
try { PrintGuestProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("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;
}
static string? Option(string[] args, string name)
{
var index = Array.IndexOf(args, name);
return index < 0 ? null : index + 1 < args.Length ? args[index + 1] : throw new ArgumentException("Missing value for " + name);
}
static void ValidateContracts()
{
XDocument.Parse(DiagnosticDaemon);
if (Encoding.UTF8.GetByteCount(DiagnosticDaemon) > Encoding.UTF8.GetByteCount(OriginalDaemon + "\n")) throw new InvalidOperationException("Daemon replacement exceeds original file.");
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "14.6.1", architecture = "x86_64", uid = 0, disk = "/dev/disk1", diskBytes = GuestDiskBytes, readOnly = false, systemExit = 0, diskArbitrationExit = 0, recoveryExit = 0, diskListExit = 0 });
ValidateResult(good, "validation");
foreach (var invalid in new[] { good.Replace("14.6.1", "13.6.1"), good.Replace("x86_64", "arm64"), good.Replace("\"readOnly\":false", "\"readOnly\":true"), good.Replace("\"success\":true", "\"success\":false"), good.Replace("68719476736", "17179869184"), good.Replace("validation", "stale") })
{
try { ValidateResult(invalid, "validation"); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Diagnostic validator accepted an invalid/stale result.");
}
}
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
{
Directory.CreateDirectory(output);
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
var originalPatch = File.ReadAllText(patchPath);
if (Hash(Encoding.UTF8.GetBytes(originalPatch)) != "84f13db88c02edbf5ce21a39571fe58f12bebf5b0886c2d012f16ddbaed45323") throw new InvalidOperationException("Pinned Recovery patcher hash mismatch.");
var patch = ReplaceOnce(originalPatch, OriginalBootstrap, MountOnlyBootstrap);
var oldConstants = "RECOVERY_ORIGINAL = b\"/usr/libexec/recoveryosd\"\nRECOVERY_REPLACEMENT = b\"/private/etc/rc.cdrom.sh\"";
var daemon = OriginalDaemon + "\n";
var constants = "RECOVERY_ORIGINAL = b'''" + daemon + "'''\nRECOVERY_REPLACEMENT = b'''" + DiagnosticDaemon + "'''.ljust(len(RECOVERY_ORIGINAL), b\" \")";
patch = ReplaceOnce(patch, oldConstants, constants);
var dockerPath = Path.Combine(source, "Dockerfile");
// The existing runner's BuildKit cannot checksum dangling manpage links during COPY /.
// This pinned filesystem image has an empty Config; FROM preserves the same runtime defaults.
var dockerfile = ReplaceOnce(File.ReadAllText(dockerPath), "FROM scratch AS base\nCOPY --from=qemux/qemu:7.50 --exclude=usr/bin/qemu-system-x86_64 / /\n", "FROM qemux/qemu:7.50@sha256:e7f6fda52503a546fd649670ba46e4bc23dc6dcef275bc3fac48877fbbc430df AS base\n");
dockerfile = ReplaceAllExact(dockerfile, "--from=qemux/qemu-macos:latest ", "--from=qemux/qemu-macos:latest@sha256:af64297171228f27d5f616249e18f6ad5e2fbc79c1cc517252521e8bcd8eadaa ", 2);
var entryPath = Path.Combine(source, "src/entry.sh");
var entry = ReplaceOnce(File.ReadAllText(entryPath), "set -Eeuo pipefail\n", "set -Eeuo pipefail\n\n# Diagnostic budget: inspect existing Docker storage before Recovery download/boot.\ndf -Pk /storage\nfree_kib=$(df -Pk /storage | awk 'NR==2 {print $4}')\n[[ \"$free_kib\" =~ ^[0-9]+$ ]] && (( free_kib >= 8 * 1024 * 1024 )) || { echo 'Existing Docker storage has less than the 8-GiB diagnostic budget.' >&2; exit 1; }\n");
var hookPath = Path.Combine("tools", "ci", "macos-native-readiness.sh");
var hook = ReplaceOnce(File.ReadAllText(hookPath), "@@PROOF_TOKEN@@", token);
foreach (var pair in new[] { ("recovery-patch.py", patch), ("Dockerfile.patched", dockerfile), ("container-entry.sh", entry), ("guest-launch.sh", hook), ("recoveryosd-original.plist", daemon), ("recoveryosd-diagnostic.plist", DiagnosticDaemon), ("early-bootstrap.sh", MountOnlyBootstrap) })
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 "Dockerfile.patched" or "container-entry.sh" or "guest-launch.sh" or "recoveryosd-original.plist" or "recoveryosd-diagnostic.plist" or "early-bootstrap.sh").ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
await Command("bash", ["-n", Path.Combine(output, "guest-launch.sh")], output, "guest-hook-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "container-entry.sh")], output, "entry-syntax", cancellation);
if (!writeSource) return;
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
File.WriteAllText(dockerPath, dockerfile, new UTF8Encoding(false));
File.WriteAllText(entryPath, entry, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/launch.sh"), hook, new UTF8Encoding(false));
}
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
static string ReplaceAllExact(string text, string oldValue, string newValue, int expected)
{
var count = text.Split(oldValue, StringSplitOptions.None).Length - 1;
if (count != expected) throw new InvalidOperationException($"Pinned source contract expected {expected} match(es), found {count}: {oldValue.Split('\n')[0]}");
return text.Replace(oldValue, newValue, StringComparison.Ordinal);
}
static void ValidateResult(string json, string token)
{
using var document = JsonDocument.Parse(json);
var result = document.RootElement;
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 14 || result.GetProperty("architecture").GetString() != "x86_64" || result.GetProperty("uid").GetInt32() != 0 || !System.Text.RegularExpressions.Regex.IsMatch(result.GetProperty("disk").GetString() ?? "", "^/dev/disk[0-9]+$") || result.GetProperty("diskBytes").GetInt64() != GuestDiskBytes || result.GetProperty("readOnly").GetBoolean() || new[] { "systemExit", "diskArbitrationExit", "recoveryExit", "diskListExit" }.Any(key => result.GetProperty(key).GetInt32() != 0))
throw new InvalidOperationException("The fresh guest receipt did not prove native macOS 14+/x86_64, service readiness and the writable 64-GiB disk.");
}
static void AssertContainer(string json, string token)
{
using var document = JsonDocument.Parse(json);
var container = document.RootElement[0];
var config = container.GetProperty("HostConfig");
if (container.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != token || config.GetProperty("Privileged").GetBoolean() || config.GetProperty("NetworkMode").GetString() != "default" && config.GetProperty("NetworkMode").GetString() != "bridge" || config.GetProperty("Memory").GetInt64() != ContainerMemoryBytes || new[] { "CapAdd", "Devices", "DeviceRequests", "Binds", "PortBindings" }.Any(key => config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null && (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any())))
throw new InvalidOperationException("Created container exceeds the owned/unprivileged diagnostic boundary.");
}
static async Task CaptureGuest(string id, string output, CancellationToken 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") })
{
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
}
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; 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);
}
static async Task<bool> Cleanup(string output)
{
var path = Path.Combine(output, "owned-resources.json");
if (!File.Exists(path)) return true;
var state = JsonSerializer.Deserialize<OwnedResources>(File.ReadAllText(path), JsonOptions) ?? throw new InvalidOperationException("Invalid owned-resource receipt.");
if (!System.Text.RegularExpressions.Regex.IsMatch(state.Token, "^[0-9a-f]{32}$") || state.ContainerName != "meeting-assistant-native-" + state.Token || state.ImageTag != "meeting-assistant-native-diagnostic:" + state.Token) throw new InvalidOperationException("Invalid cleanup ownership identity.");
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(90));
try
{
foreach (var kind in new[] { "container", "image" })
{
var name = kind == "container" ? state.ContainerName : state.ImageTag;
var inspect = await Command("docker", [kind, "inspect", name], output, "cleanup-" + kind + "-inspect", deadline.Token, requireSuccess: false);
if (inspect.ExitCode != 0)
{
if (inspect.Error.Contains("No such object", StringComparison.Ordinal) || inspect.Error.Contains("No such container", StringComparison.Ordinal) || inspect.Error.Contains("No such image", StringComparison.Ordinal)) continue;
throw new InvalidOperationException("Cannot establish owned " + kind + " absence: " + inspect.Error);
}
using var document = JsonDocument.Parse(inspect.Output);
var resource = document.RootElement[0];
if (resource.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != state.Token) throw new InvalidOperationException("Cleanup refuses a resource without this run's exact ownership label.");
var id = resource.GetProperty("Id").GetString()!;
var expectedId = kind == "container" ? state.ContainerId : state.ImageId;
if (expectedId is not null && expectedId != id) throw new InvalidOperationException("Cleanup refuses a resource whose ID changed after creation.");
await Command("docker", kind == "container" ? ["rm", "--force", "--volumes", id] : ["image", "rm", id], output, "cleanup-" + kind + "-remove", deadline.Token);
}
if (Path.GetFileName(state.WorkDirectory) == "meeting-assistant-native-" + state.Token && File.Exists(Path.Combine(state.WorkDirectory, "run.owner")) && File.ReadAllText(Path.Combine(state.WorkDirectory, "run.owner")) == state.Token) Directory.Delete(state.WorkDirectory, true);
Save(Path.Combine(output, "cleanup.json"), new { state.Token, success = true, completedUtc = DateTimeOffset.UtcNow });
return true;
}
catch (Exception exception)
{
Save(Path.Combine(output, "cleanup.json"), new { state.Token, success = false, error = exception.Message, completedUtc = DateTimeOffset.UtcNow });
Console.Error.WriteLine("Owned diagnostic cleanup failed: " + exception.Message);
return false;
}
}
static async Task<CommandResult> Command(string executable, string[] arguments, string output, string label, CancellationToken cancellation, bool requireSuccess = true, bool echo = false)
{
if (!label.StartsWith("capture-", StringComparison.Ordinal) && label is not "container" and not "container-running" and not "guest-container-resources")
Console.WriteLine("[native-diagnostic] " + label);
using var commandCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
var commandToken = commandCancellation.Token;
var start = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
foreach (var argument in arguments) start.ArgumentList.Add(argument);
start.Environment["GIT_TERMINAL_PROMPT"] = "0";
using var process = Process.Start(start) ?? throw new InvalidOperationException("Cannot start " + executable);
async Task<string> Read(StreamReader reader, string stream)
{
var captured = new StringBuilder();
var buffer = new char[8192];
using var log = new StreamWriter(Path.Combine(output, label + "." + stream + ".log"), false, new UTF8Encoding(false));
while (true)
{
var count = await reader.ReadAsync(buffer.AsMemory(), commandToken);
if (count == 0) break;
if (captured.Length + count > MaximumCapturedCharacters)
{
commandCancellation.Cancel();
throw new InvalidOperationException(label + " exceeded its bounded diagnostic log size.");
}
captured.Append(buffer, 0, count);
await log.WriteAsync(buffer.AsMemory(0, count), commandToken);
await log.FlushAsync(commandToken);
if (echo) Console.Write(new string(buffer, 0, count));
}
return captured.ToString();
}
var stdout = Read(process.StandardOutput, "stdout");
var stderr = Read(process.StandardError, "stderr");
try
{
await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(commandToken));
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) { }
throw;
}
}
static string Hash(byte[] bytes) => Convert.ToHexStringLower(SHA256.HashData(bytes));
static void PrintGuestProof(string output, string token)
{
var path = Path.Combine(output, "guest-proof.log");
if (!File.Exists(path)) { Console.WriteLine("[native-diagnostic] No native guest proof was captured."); return; }
var proof = File.ReadAllText(path).Replace(token, "<run-id>", StringComparison.Ordinal);
const int budget = 512 * 1024;
if (proof.Length > budget)
proof = proof[..(64 * 1024)] + "\n[native-diagnostic] Middle of proof omitted from CI stdout; complete bounded proof is retained in the artifact.\n" + proof[^((budget - 64 * 1024))..];
Console.WriteLine("[native-diagnostic] Final native guest proof:");
Console.Write(proof);
}
static void Save(string path, object value)
{
var temporary = path + ".tmp";
File.WriteAllText(temporary, JsonSerializer.Serialize(value, JsonOptions), new UTF8Encoding(false));
File.Move(temporary, path, overwrite: true);
}
sealed record OwnedResources(string Token, string ContainerName, string ImageTag, string WorkDirectory, string? ContainerId = null, string? ImageId = null);
sealed record CommandResult(int ExitCode, string Output, string Error);
}