From 1e6068e2739c4c5c15d55bc6faacd6a7b2f338b4 Mon Sep 17 00:00:00 2001 From: Nucleic Date: Wed, 29 Jul 2026 04:33:55 -0700 Subject: [PATCH] Merge nucleic/lucid-river-toad-6efj into dev --- spikes/WslcSpike/Program.cs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/spikes/WslcSpike/Program.cs b/spikes/WslcSpike/Program.cs index f177161..0277fe8 100644 --- a/spikes/WslcSpike/Program.cs +++ b/spikes/WslcSpike/Program.cs @@ -36,6 +36,10 @@ internal static class Program var iterations = int.TryParse(Arg(args, "--iterations"), out var n) ? n : 5; var broker = Arg(args, "--broker") ?? FindBroker(); var skipRecovery = args.Contains("--no-recovery"); + // narOS runs `naros-init` as PID 1 and stays up (docs/NAROS.md). A stock image usually + // does not: alpine's PID 1 is /bin/sh, which exits immediately with no tty, so the + // container is `Exited` before the first exec and every later step fails `not_running`. + var sleepInit = args.Contains("--sleep-init"); if (broker is null || !File.Exists(broker)) { @@ -53,7 +57,7 @@ internal static class Program try { - await RunScenarioAsync(broker, sessionName, image, container, repo, iterations); + await RunScenarioAsync(broker, sessionName, image, container, repo, iterations, sleepInit); if (!skipRecovery) await RunRecoveryAsync(broker, sessionName); return 0; } @@ -69,7 +73,7 @@ internal static class Program private static async Task RunScenarioAsync( string brokerPath, string sessionName, string image, string containerName, - string repo, int iterations) + string repo, int iterations, bool sleepInit) { await using var broker = BrokerClient.Spawn(brokerPath); @@ -131,19 +135,29 @@ internal static class Program Console.WriteLine($" pulled in {pullWatch.Elapsed.TotalSeconds:F1}s"); Step($"container.create {containerName}"); - await broker.CallAsync("container.create", new + var create = new Dictionary { - name = containerName, - image, - volumes = new[] { new { host = repo, guest = "/work", ro = false } }, - networkingMode = "Bridged", - hostname = "nucleic-spike", - env = new Dictionary { ["NUCLEIC_SPIKE"] = "1" }, - }); + ["name"] = containerName, + ["image"] = image, + ["volumes"] = new[] { new { host = repo, guest = "/work", ro = false } }, + ["networkingMode"] = "Bridged", + ["hostname"] = "nucleic-spike", + ["env"] = new Dictionary { ["NUCLEIC_SPIKE"] = "1" }, + }; + if (sleepInit) create["initArgv"] = new[] { "/bin/sh", "-c", "sleep 3600" }; + await broker.CallAsync("container.create", create); await broker.CallAsync("container.start", new { name = containerName }); var state = (await broker.CallAsync("container.state", new { name = containerName })) .GetProperty("state").GetString(); Console.WriteLine($" state: {state}"); + // Check here rather than letting the first exec fail `not_running`: a container whose + // PID 1 exited looks identical to a container that failed to start, and the remedy + // (supply a long-running init) is nothing like the remedy for a real start failure. + if (state != "running") + throw new InvalidOperationException( + $"container is '{state}' immediately after start — its PID 1 exited. narOS runs " + + "naros-init and stays up; a stock image (alpine's PID 1 is /bin/sh) does not. " + + "Re-run with --sleep-init to give it a long-running init process."); Step("exec: stdio round-trip"); var (code, output) = await ExecAsync(broker, containerName, ["/bin/sh", "-c", "echo hello from \"$(uname -n)\"; echo to-stderr 1>&2"]);