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
+35 -4
View File
@@ -58,8 +58,11 @@ public sealed class BrokerService : IBrokerEvents
try
{
var result = await DispatchAsync(method, @params, ct).ConfigureAwait(false);
if (id is { } requestId)
var result = await DispatchAsync(method, @params, id, ct).ConfigureAwait(false);
// A null result means the handler already enqueued its own response because it had to
// send it before doing something else (proc.exec — see below). Everything else
// returns a value and is responded to here.
if (result is not null && id is { } requestId)
outbound.EnqueueJson(Rpc.Response(requestId, result));
}
catch (JsonException e)
@@ -80,7 +83,10 @@ public sealed class BrokerService : IBrokerEvents
}
}
private async Task<object> DispatchAsync(string method, JsonElement? p, CancellationToken ct)
/// <summary>Returns the result to respond with, or null when the handler has already
/// responded (it needed the response on the wire before continuing).</summary>
private async Task<object?> DispatchAsync(
string method, JsonElement? p, JsonElement? id, CancellationToken ct)
{
switch (method)
{
@@ -176,7 +182,32 @@ public sealed class BrokerService : IBrokerEvents
var procId = Interlocked.Increment(ref nextProcId);
var proc = await wslc.ExecAsync(procId, spec, ct).ConfigureAwait(false);
lock (procsLock) procs[procId] = proc;
return new { procId };
// Respond BEFORE running it. Output and exit are enqueued on the same ordered
// outbound queue as this response, so a command that finishes fast (`echo`) would
// otherwise put `proc.exit` on the wire ahead of the `procId` naming it — and a
// client that registers interest when it learns the procId then waits forever.
// Hit on the first live run, hardware-confirmed (docs/WINDOWS_PORT.md §13.3).
if (id is { } requestId)
outbound.EnqueueJson(Rpc.Response(requestId, new { procId }));
try
{
await proc.StartAsync(ct).ConfigureAwait(false);
}
catch (Exception e)
{
// The response is already on the wire, so this failure CANNOT be reported as a
// JSON-RPC error — that would put two responses under one id, which is a
// protocol violation and left the client waiting for an exit that never came.
// Report it the way the process itself would have: the reason on stderr, then
// an exit. 126 is the shell's "command found but not executable", which is
// what "could not start" means to every caller above.
lock (procsLock) procs.Remove(procId);
var reason = $"nucleic-brokerd: could not start process: {e.Message}\n";
ProcOutput(procId, stderr: true, System.Text.Encoding.UTF8.GetBytes(reason));
ProcExited(procId, 126);
}
return null;
}
case "proc.stdin":
{