Merge nucleic/lucid-river-toad-6efj into dev

This commit is contained in:
2026-07-29 17:09:35 -07:00
parent 1e6068e273
commit e41bafdb7c
7 changed files with 305 additions and 44 deletions
+55 -22
View File
@@ -40,6 +40,7 @@ internal static class Program
// 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");
var verbose = args.Contains("--verbose");
if (broker is null || !File.Exists(broker))
{
@@ -57,7 +58,7 @@ internal static class Program
try
{
await RunScenarioAsync(broker, sessionName, image, container, repo, iterations, sleepInit);
await RunScenarioAsync(broker, sessionName, image, container, repo, iterations, sleepInit, verbose);
if (!skipRecovery) await RunRecoveryAsync(broker, sessionName);
return 0;
}
@@ -73,9 +74,10 @@ internal static class Program
private static async Task RunScenarioAsync(
string brokerPath, string sessionName, string image, string containerName,
string repo, int iterations, bool sleepInit)
string repo, int iterations, bool sleepInit, bool verbose)
{
await using var broker = BrokerClient.Spawn(brokerPath);
broker.Verbose = verbose;
// Pull progress is high-rate; collapse it to one line per phase change so the transcript
// stays readable but a stalled pull is still visible.
@@ -164,13 +166,23 @@ internal static class Program
Console.WriteLine($" exit {code}: {output.Trim()}");
Step("exec: uid drop (setpriv wrapper, §3.2)");
// ProcessSettings has no uid/gid, so the facade wraps argv in setpriv. If the image lacks
// util-linux this is where that shows, and the fallback is `su agent -c`.
var (idCode, idOut) = await ExecAsync(
broker, containerName, ["/bin/sh", "-c", "id -u; id -g"], uid: 501, gid: 501);
Console.WriteLine(idCode == 0
? $" uid/gid inside container: {idOut.Replace("\n", "/").Trim('/')}"
: $" setpriv wrapper FAILED (exit {idCode}): {idOut.Trim()} — try `su agent -c`");
// ProcessSettings has no uid/gid, so the facade wraps argv in setpriv. A BusyBox image has
// a setpriv that cannot change uid, and the facade now REFUSES rather than silently
// running the agent as root — so `unsupported` here is correct behaviour on such an image,
// not a failure of the run.
try
{
var (idCode, idOut) = await ExecAsync(
broker, containerName, ["/bin/sh", "-c", "id -u; id -g"], uid: 501, gid: 501);
Console.WriteLine(idCode == 0
? $" uid/gid inside container: {idOut.Replace("\n", "/").Trim('/')}"
: $" ran but reported failure (exit {idCode}): {idOut.Trim()}");
}
catch (BrokerError e) when (e.Kind == "unsupported")
{
Console.WriteLine($" REFUSED (correctly): {e.Message}");
Console.WriteLine(" → expected on a BusyBox image; narOS ships util-linux.");
}
Step("the mounted worktree");
var (lsCode, lsOut) = await ExecAsync(broker, containerName,
@@ -200,10 +212,26 @@ internal static class Program
/// </summary>
private static async Task MeasureNinePAsync(BrokerClient broker, string container, int iterations)
{
Step($"9P vs ext4 — `git status` x{iterations} (§15 risk, D8)");
// `find -type f` rather than `git status`, because it needs only busybox and measures the
// same thing that makes git slow over a mount: a full lstat() traversal of the tree. Using
// git would make the number depend on the image having git, which is what derailed the
// first attempt at this measurement.
var probe = "find . -type f | wc -l";
var (gitCode, _) = await ExecAsync(broker, container, ["/bin/sh", "-c", "command -v git"]);
if (gitCode == 0)
{
probe = "git status --porcelain >/dev/null";
Console.WriteLine(" (git present — measuring `git status`)");
}
else
{
Console.WriteLine(" (no git in this image — measuring `find -type f`, which is the "
+ "lstat traversal that dominates `git status`)");
}
var mounted = await TimeCommandAsync(
broker, container, "cd /work && git status --porcelain >/dev/null", iterations);
Step($"9P vs ext4 — `{probe}` x{iterations} (§15 risk, D8)");
var mounted = await TimeCommandAsync(broker, container, $"cd /work && {probe}", iterations);
Report("/work (NTFS via 9P)", mounted);
Console.WriteLine(" copying the worktree to container-local ext4…");
@@ -217,8 +245,7 @@ internal static class Program
}
Console.WriteLine($" copied in {copyWatch.Elapsed.TotalSeconds:F1}s");
var local = await TimeCommandAsync(
broker, container, "cd /tmp/ext4 && git status --porcelain >/dev/null", iterations);
var local = await TimeCommandAsync(broker, container, $"cd /tmp/ext4 && {probe}", iterations);
Report("/tmp/ext4 (container-local)", local);
if (mounted.Count > 0 && local.Count > 0)
@@ -330,12 +357,22 @@ internal static class Program
{
var output = new MemoryStream();
var exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
long procId = -1;
// Deliberately NOT filtered by procId.
//
// The broker now guarantees the `proc.exec` response precedes any notification, but that
// is not sufficient for a client: completing the response's TaskCompletionSource only
// SCHEDULES the awaiting continuation, so the reader thread can dispatch the very next
// line — proc.stdout, or proc.exit — before the continuation has recorded the procId.
// Filtering on a not-yet-assigned procId silently drops the exit and hangs forever, which
// is exactly how this spike hung twice (docs/WINDOWS_PORT.md §13.3).
//
// Safe here because the spike runs one process at a time and awaits each to completion.
// A real client cannot take this shortcut: it must buffer notifications for procIds it
// has not yet learned. Worth checking `WslcBrokerClient.swift` for the same race.
void OnNotification(string method, JsonElement args)
{
if (!args.TryGetProperty("procId", out var idElement)) return;
if (idElement.GetInt64() != Volatile.Read(ref procId)) return;
if (!args.TryGetProperty("procId", out _)) return;
switch (method)
{
case "proc.stdout":
@@ -355,11 +392,7 @@ internal static class Program
object spec = uid is null
? new { container, argv, tty = false }
: new { container, argv, uid, gid = gid ?? uid, tty = false };
var result = await broker.CallAsync("proc.exec", spec);
// Set procId only after exec returns — but the broker may already have emitted output
// by then. That race is why WslcProcessHandle exists on the Swift side; here it costs
// at most a few dropped bytes of a diagnostic, so it is accepted rather than solved.
Volatile.Write(ref procId, result.GetProperty("procId").GetInt64());
await broker.CallAsync("proc.exec", spec);
var code = await exited.Task.WaitAsync(TimeSpan.FromSeconds(timeoutSeconds));
lock (output) return (code, System.Text.Encoding.UTF8.GetString(output.ToArray()));