Merge nucleic/lucid-river-toad-6efj into dev
This commit is contained in:
+55
-22
@@ -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()));
|
||||
|
||||
Reference in New Issue
Block a user