c5656224
feat: muxd start — the setsid-nohup incantation becomes a subcommand
a73x 2026-08-09 14:02
Commit message
docs/superpowers/plans/2026-08-09-m10-quic-ergonomics.md
| Old | New | ||
|---|---|---|---|
| @@ -1153,7 +1153,7 @@ git commit -m "feat: spawn.ensureDaemon — probe, detach, poll; the start/auto- | |||
| 1153 | - Modify: `src/main.zig` (usage, `Cmd`, `parseArgs`, dispatch, new `startCmd`; parse test) | 1153 | - Modify: `src/main.zig` (usage, `Cmd`, `parseArgs`, dispatch, new `startCmd`; parse test) |
| 1154 | - Modify: `test/e2e.sh` (start scenario: fresh start, marker, rerun no-op, race, default-key QUIC attach) | 1154 | - Modify: `test/e2e.sh` (start scenario: fresh start, marker, rerun no-op, race, default-key QUIC attach) |
| 1155 | 1155 | ||
| 1156 | - [ ] **Step 1: Failing parse test** | 1156 | - [x] **Step 1: Failing parse test** |
| 1157 | 1157 | ||
| 1158 | ```zig | 1158 | ```zig |
| 1159 | test "parseArgs: start takes run's flags" { | 1159 | test "parseArgs: start takes run's flags" { |
| @@ -1167,7 +1167,7 @@ test "parseArgs: start takes run's flags" { | |||
| 1167 | 1167 | ||
| 1168 | Run: `make test` — compile error on `.start` (failing state). | 1168 | Run: `make test` — compile error on `.start` (failing state). |
| 1169 | 1169 | ||
| 1170 | - [ ] **Step 2: Implement** | 1170 | - [x] **Step 2: Implement** |
| 1171 | 1171 | ||
| 1172 | `src/main.zig`. `Cmd`: add `start`: | 1172 | `src/main.zig`. `Cmd`: add `start`: |
| 1173 | 1173 | ||
| @@ -1226,12 +1226,12 @@ fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const | |||
| 1226 | 1226 | ||
| 1227 | Usage: add `\\ muxd start [run's flags] (spawn a daemon detached; no-op if one is up)`. | 1227 | Usage: add `\\ muxd start [run's flags] (spawn a daemon detached; no-op if one is up)`. |
| 1228 | 1228 | ||
| 1229 | - [ ] **Step 3: Run tests, expect pass** | 1229 | - [x] **Step 3: Run tests, expect pass** |
| 1230 | 1230 | ||
| 1231 | Run: `make test` | 1231 | Run: `make test` |
| 1232 | Expected: pass. | 1232 | Expected: pass. |
| 1233 | 1233 | ||
| 1234 | - [ ] **Step 4: e2e scenarios** | 1234 | - [x] **Step 4: e2e scenarios** |
| 1235 | 1235 | ||
| 1236 | `test/e2e.sh`, new block. Declare near the other SOCK vars: `SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock"` and `SPID=""`, add `[ -n "$SPID" ] && kill "$SPID" 2>/dev/null` to the trap (kill by tracked pid) plus `wait` tolerance mirroring the existing daemons' cleanup. | 1236 | `test/e2e.sh`, new block. Declare near the other SOCK vars: `SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock"` and `SPID=""`, add `[ -n "$SPID" ] && kill "$SPID" 2>/dev/null` to the trap (kill by tracked pid) plus `wait` tolerance mirroring the existing daemons' cleanup. |
| 1237 | 1237 | ||
| @@ -1312,16 +1312,16 @@ echo "e2e OK: keygen + start --quic + mux quic:// with no --key anywhere" | |||
| 1312 | 1312 | ||
| 1313 | Track `GPID` in the trap like `SPID`. | 1313 | Track `GPID` in the trap like `SPID`. |
| 1314 | 1314 | ||
| 1315 | - [ ] **Step 5: Run e2e** | 1315 | - [x] **Step 5: Run e2e** |
| 1316 | 1316 | ||
| 1317 | Run: `make build && make e2e` | 1317 | Run: `make build && make e2e` |
| 1318 | Expected: both new OK lines; suite passes. | 1318 | Expected: both new OK lines; suite passes. |
| 1319 | 1319 | ||
| 1320 | - [ ] **Step 6: Mutation checks** | 1320 | - [x] **Step 6: Mutation checks** |
| 1321 | 1321 | ||
| 1322 | (a) Race/loser handling: in `server.zig`'s `claimSockPath`, invert the live-daemon refusal (`return error.DaemonAlreadyRunning` → `return`); `make build && make e2e`: the race scenario or the rerun scenario MUST fail (two daemons, marker lost). Restore. (b) Pinned lines: drop `pid={d}` from the up-line; e2e MUST fail on the empty `SPID`. Restore. | 1322 | (a) Race/loser handling: in `server.zig`'s `claimSockPath`, invert the live-daemon refusal (`return error.DaemonAlreadyRunning` → `return`); `make build && make e2e`: the race scenario or the rerun scenario MUST fail (two daemons, marker lost). Restore. (b) Pinned lines: drop `pid={d}` from the up-line; e2e MUST fail on the empty `SPID`. Restore. |
| 1323 | 1323 | ||
| 1324 | - [ ] **Step 7: Commit** | 1324 | - [x] **Step 7: Commit** |
| 1325 | 1325 | ||
| 1326 | ```bash | 1326 | ```bash |
| 1327 | git add src/main.zig test/e2e.sh | 1327 | git add src/main.zig test/e2e.sh |
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -8,6 +8,7 @@ const proxy = @import("proxy"); | |||
| 8 | const quic = @import("quic"); | 8 | const quic = @import("quic"); |
| 9 | const build_options = @import("build_options"); | 9 | const build_options = @import("build_options"); |
| 10 | const xdg = @import("xdg"); | 10 | const xdg = @import("xdg"); |
| 11 | const spawn = @import("spawn"); | ||
| 11 | 12 | ||
| 12 | const usage = | 13 | const usage = |
| 13 | \\usage: | 14 | \\usage: |
| @@ -17,6 +18,7 @@ const usage = | |||
| 17 | \\ muxd stats [--sock PATH] | 18 | \\ muxd stats [--sock PATH] |
| 18 | \\ muxd proxy [--sock PATH] (byte pump: stdio <-> session socket) | 19 | \\ muxd proxy [--sock PATH] (byte pump: stdio <-> session socket) |
| 19 | \\ muxd keygen (write a fresh key to ~/.config/mux/key) | 20 | \\ muxd keygen (write a fresh key to ~/.config/mux/key) |
| 21 | \\ muxd start [run's flags] (spawn a daemon detached; no-op if one is up) | ||
| 20 | \\ muxd --version | 22 | \\ muxd --version |
| 21 | \\ | 23 | \\ |
| 22 | ; | 24 | ; |
| @@ -41,7 +43,7 @@ fn envKey() ?[]const u8 { | |||
| 41 | /// for what the number means and why it lives there. | 43 | /// for what the number means and why it lives there. |
| 42 | const default_quic_idle_ms: u32 = quic.default_idle_ms; | 44 | const default_quic_idle_ms: u32 = quic.default_idle_ms; |
| 43 | 45 | ||
| 44 | const Cmd = enum { run, dump, stats, proxy, version, keygen }; | 46 | const Cmd = enum { run, dump, stats, proxy, version, keygen, start }; |
| 45 | 47 | ||
| 46 | /// Everything the command line can say, once. Parsed away from `main` so it | 48 | /// Everything the command line can say, once. Parsed away from `main` so it |
| 47 | /// can be tested without a process to exit from — the same reason | 49 | /// can be tested without a process to exit from — the same reason |
| @@ -92,6 +94,8 @@ fn parseArgs(args: []const [:0]const u8) ParseResult { | |||
| 92 | .proxy | 94 | .proxy |
| 93 | else if (std.mem.eql(u8, args[1], "keygen")) | 95 | else if (std.mem.eql(u8, args[1], "keygen")) |
| 94 | .keygen | 96 | .keygen |
| 97 | else if (std.mem.eql(u8, args[1], "start")) | ||
| 98 | .start | ||
| 95 | else | 99 | else |
| 96 | return .{ .err = .{ .unknown_command = args[1] } }; | 100 | return .{ .err = .{ .unknown_command = args[1] } }; |
| 97 | 101 | ||
| @@ -232,6 +236,7 @@ pub fn main() !u8 { | |||
| 232 | return 0; | 236 | return 0; |
| 233 | }, | 237 | }, |
| 234 | .keygen => return keygen(alloc), | 238 | .keygen => return keygen(alloc), |
| 239 | .start => return startCmd(alloc, sock_path, args[2..]), | ||
| 235 | .run => return run(alloc, o, sock_path), | 240 | .run => return run(alloc, o, sock_path), |
| 236 | .dump => return dump(alloc, sock_path, o.vt), | 241 | .dump => return dump(alloc, sock_path, o.vt), |
| 237 | .stats => return stats(alloc, sock_path), | 242 | .stats => return stats(alloc, sock_path), |
| @@ -410,6 +415,42 @@ fn stats(alloc: std.mem.Allocator, sock_path: []const u8) !u8 { | |||
| 410 | return 1; | 415 | return 1; |
| 411 | } | 416 | } |
| 412 | 417 | ||
| 418 | /// `muxd start` = ensureDaemon under an explicit flag. Everything after | ||
| 419 | /// `start` is forwarded to `run` verbatim — no re-serialization, so a flag | ||
| 420 | /// that parses here behaves identically there. parseArgs has already | ||
| 421 | /// validated the flags in THIS process; what it cannot validate (a bad | ||
| 422 | /// bind address, a missing key file) surfaces in the daemon's log, which | ||
| 423 | /// the failure path names. | ||
| 424 | fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const [:0]const u8) !u8 { | ||
| 425 | var exe_buf: [std.fs.max_path_bytes]u8 = undefined; | ||
| 426 | const exe = std.fs.selfExePath(&exe_buf) catch { | ||
| 427 | std.debug.print("muxd: cannot find own binary via /proc/self/exe\n", .{}); | ||
| 428 | return 1; | ||
| 429 | }; | ||
| 430 | const progress: spawn.Progress = .{ | ||
| 431 | .fd = std.posix.STDERR_FILENO, | ||
| 432 | .prefix = "muxd", | ||
| 433 | .tty = std.posix.isatty(std.posix.STDERR_FILENO), | ||
| 434 | }; | ||
| 435 | // null log path: the xdg default is the whole point for a real daemon. | ||
| 436 | const r = spawn.ensureDaemon(alloc, exe, forwarded, sock_path, progress, 2000, null) catch |err| switch (err) { | ||
| 437 | // The failure line, with the log path, was already printed by | ||
| 438 | // Progress — a second line here would say the same thing worse. | ||
| 439 | error.NeverAnswered => return 1, | ||
| 440 | error.BinaryNotFound, error.SpawnFailed => { | ||
| 441 | std.debug.print("muxd: could not spawn {s}: {s}\n", .{ exe, @errorName(err) }); | ||
| 442 | return 1; | ||
| 443 | }, | ||
| 444 | }; | ||
| 445 | if (r == .already_running) { | ||
| 446 | std.debug.print( | ||
| 447 | "muxd: already running on {s} (stop it first if you meant different flags)\n", | ||
| 448 | .{sock_path}, | ||
| 449 | ); | ||
| 450 | } | ||
| 451 | return 0; | ||
| 452 | } | ||
| 453 | |||
| 413 | fn keygen(alloc: std.mem.Allocator) !u8 { | 454 | fn keygen(alloc: std.mem.Allocator) !u8 { |
| 414 | const path = try xdg.keyPath(alloc); | 455 | const path = try xdg.keyPath(alloc); |
| 415 | defer alloc.free(path); | 456 | defer alloc.free(path); |
| @@ -608,3 +649,11 @@ test "pickKey: --key beats MUX_KEY_FILE beats the default path" { | |||
| 608 | // Nothing named anywhere is the triad-message case, not a silent skip. | 649 | // Nothing named anywhere is the triad-message case, not a silent skip. |
| 609 | try std.testing.expect(pickKey(null, null, null) == null); | 650 | try std.testing.expect(pickKey(null, null, null) == null); |
| 610 | } | 651 | } |
| 652 | |||
| 653 | test "parseArgs: start takes run's flags" { | ||
| 654 | const r = parse(&.{ "muxd", "start", "--sock", "/tmp/x.sock", "--cols", "100" }); | ||
| 655 | try std.testing.expect(r == .ok); | ||
| 656 | try std.testing.expect(r.ok.cmd == .start); | ||
| 657 | try std.testing.expectEqualStrings("/tmp/x.sock", r.ok.sock.?); | ||
| 658 | try std.testing.expectEqual(@as(u16, 100), r.ok.cols); | ||
| 659 | } | ||
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -49,6 +49,16 @@ SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock" | |||
| 49 | D10PID="" | 49 | D10PID="" |
| 50 | QPORT2=$(( 26000 + ($$ % 4000) )) | 50 | QPORT2=$(( 26000 + ($$ % 4000) )) |
| 51 | QPORT3=$(( 31000 + ($$ % 4000) )) | 51 | QPORT3=$(( 31000 + ($$ % 4000) )) |
| 52 | # M10 `muxd start`. These daemons are spawned DETACHED, so the suite never | ||
| 53 | # holds their pids as shell jobs — it reads them off the up-line and kills | ||
| 54 | # by that tracked pid, never by name. | ||
| 55 | SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock" | ||
| 56 | SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock" | ||
| 57 | SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock" | ||
| 58 | QPORT4=$(( 36000 + ($$ % 4000) )) | ||
| 59 | SPID="" | ||
| 60 | TPID="" | ||
| 61 | GPID="" | ||
| 52 | 62 | ||
| 53 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 63 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 54 | # such line on exit; every field is a key=value pair, so a rename or reorder | 64 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| @@ -84,6 +94,16 @@ wait_for() { | |||
| 84 | return 1 | 94 | return 1 |
| 85 | } | 95 | } |
| 86 | 96 | ||
| 97 | # Poll until nothing answers on a socket path (2s). Keyed off the daemon's | ||
| 98 | # own liveness rather than a fixed sleep, same reasoning as wait_for. | ||
| 99 | wait_gone() { | ||
| 100 | _i=0 | ||
| 101 | while "$MUXD" dump --sock "$1" > /dev/null 2>&1; do | ||
| 102 | _i=$((_i + 1)); [ "$_i" -lt 40 ] || { echo "e2e FAIL: daemon on $1 never died"; exit 1; } | ||
| 103 | sleep 0.05 | ||
| 104 | done | ||
| 105 | } | ||
| 106 | |||
| 87 | # The transport child for a given socket: a process named muxd running the | 107 | # The transport child for a given socket: a process named muxd running the |
| 88 | # `proxy` subcommand on that path. NEVER `pkill -f proxy` — the client's own | 108 | # `proxy` subcommand on that path. NEVER `pkill -f proxy` — the client's own |
| 89 | # argv contains the --via command string, so a pattern kill takes out the | 109 | # argv contains the --via command string, so a pattern kill takes out the |
| @@ -110,7 +130,13 @@ cleanup() { | |||
| 110 | [ -n "$D7PID" ] && kill "$D7PID" 2>/dev/null || true | 130 | [ -n "$D7PID" ] && kill "$D7PID" 2>/dev/null || true |
| 111 | [ -n "$D9PID" ] && kill "$D9PID" 2>/dev/null || true | 131 | [ -n "$D9PID" ] && kill "$D9PID" 2>/dev/null || true |
| 112 | [ -n "$D10PID" ] && kill "$D10PID" 2>/dev/null || true | 132 | [ -n "$D10PID" ] && kill "$D10PID" 2>/dev/null || true |
| 113 | rm -f "$SOCK9" "$SOCK10" "$OUT.env1" "$OUT.env2" \ | 133 | # Detached daemons: killed by the pid their own up-line reported. |
| 134 | [ -n "$SPID" ] && kill "$SPID" 2>/dev/null || true | ||
| 135 | [ -n "$TPID" ] && kill "$TPID" 2>/dev/null || true | ||
| 136 | [ -n "$GPID" ] && kill "$GPID" 2>/dev/null || true | ||
| 137 | rm -f "$SOCK8" "$SOCK8T" "$SOCK11" "$OUT.start" "$OUT.start2" "$OUT.s8" \ | ||
| 138 | "$OUT.ra" "$OUT.rb" "$OUT.goal" "$OUT.g9" \ | ||
| 139 | "$SOCK9" "$SOCK10" "$OUT.env1" "$OUT.env2" \ | ||
| 114 | "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \ | 140 | "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \ |
| 115 | "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.pr" \ | 141 | "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.pr" \ |
| 116 | "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \ | 142 | "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \ |
| @@ -809,6 +835,115 @@ echo "e2e OK: daemon honours MUX_KEY_FILE, and --key beats it" | |||
| 809 | 835 | ||
| 810 | rm -f "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" | 836 | rm -f "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" |
| 811 | 837 | ||
| 838 | # --- M10: muxd start — detached spawn, no-op rerun, race, pinned lines. | ||
| 839 | # | ||
| 840 | # Every scenario below asserts a MARKER through the session, never just $?. | ||
| 841 | # A client that fails to authenticate exits 0 by way of "aborted before | ||
| 842 | # attaching", so an exit code cannot distinguish a working session from a | ||
| 843 | # key or transport regression — only bytes coming back out of the shell can. | ||
| 844 | "$MUXD" start --sock "$SOCK8" 2> "$OUT.start" | ||
| 845 | grep -q '^muxd: starting' "$OUT.start" || { | ||
| 846 | echo "e2e FAIL: start printed no starting line"; cat "$OUT.start"; exit 1; } | ||
| 847 | grep -q ' up (' "$OUT.start" || { | ||
| 848 | echo "e2e FAIL: start printed no up line"; cat "$OUT.start"; exit 1; } | ||
| 849 | SPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start") | ||
| 850 | [ -n "$SPID" ] || { echo "e2e FAIL: up line carries no pid"; exit 1; } | ||
| 851 | kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; } | ||
| 852 | # Non-tty stderr: exactly two lines, no dots. | ||
| 853 | [ "$(wc -l < "$OUT.start")" = "2" ] || { | ||
| 854 | echo "e2e FAIL: non-tty start not exactly two lines:"; cat "$OUT.start"; exit 1; } | ||
| 855 | |||
| 856 | # The daemon it started serves a session (marker in, marker in dump). | ||
| 857 | { printf 'printf "start-%%s\\n" works\n'; sleep 2; printf '\034'; } | \ | ||
| 858 | "$MUX" --sock "$SOCK8" > "$OUT.s8" 2>&1 | ||
| 859 | "$MUXD" dump --sock "$SOCK8" | grep -q "start-works" || { | ||
| 860 | echo "e2e FAIL: auto-started daemon lost the marker"; cat "$OUT.s8"; exit 1; } | ||
| 861 | |||
| 862 | # The log the failure path names really is there, and is no more readable | ||
| 863 | # than the key is: it carries whatever the daemon says about its own start. | ||
| 864 | MUXLOG="$XDG_STATE_HOME/mux/muxd.log" | ||
| 865 | [ -f "$MUXLOG" ] || { echo "e2e FAIL: no daemon log at $MUXLOG"; exit 1; } | ||
| 866 | LPERMS=$(stat -c %a "$MUXLOG") | ||
| 867 | [ "$LPERMS" = "600" ] || { echo "e2e FAIL: daemon log perms $LPERMS, want 600"; exit 1; } | ||
| 868 | |||
| 869 | # Rerun: silent no-op beyond the already-running line, exit 0, same daemon. | ||
| 870 | "$MUXD" start --sock "$SOCK8" 2> "$OUT.start2" | ||
| 871 | grep -q "already running on $SOCK8" "$OUT.start2" || { | ||
| 872 | echo "e2e FAIL: rerun did not say already running"; cat "$OUT.start2"; exit 1; } | ||
| 873 | "$MUXD" dump --sock "$SOCK8" | grep -q "start-works" || { | ||
| 874 | echo "e2e FAIL: rerun replaced the daemon (marker gone)"; exit 1; } | ||
| 875 | |||
| 876 | # Truncated per spawn, not appended to: the log holds the CURRENT daemon's | ||
| 877 | # output, so a stale one must not be what an operator reads after a | ||
| 878 | # restart. Padded, then a real spawn on a fresh path must shrink it. | ||
| 879 | dd if=/dev/zero bs=1024 count=8 2>/dev/null >> "$MUXLOG" | ||
| 880 | PADDED=$(stat -c %s "$MUXLOG") | ||
| 881 | "$MUXD" start --sock "$SOCK8T" 2> "$OUT.start" | ||
| 882 | TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start") | ||
| 883 | [ -n "$TPID" ] || { echo "e2e FAIL: truncation spawn reported no pid"; exit 1; } | ||
| 884 | SHRUNK=$(stat -c %s "$MUXLOG") | ||
| 885 | [ "$SHRUNK" -lt "$PADDED" ] || { | ||
| 886 | echo "e2e FAIL: log not truncated on spawn ($PADDED -> $SHRUNK)"; exit 1; } | ||
| 887 | kill "$TPID" 2>/dev/null || true | ||
| 888 | TPID="" | ||
| 889 | |||
| 890 | # Race: two concurrent starts, both exit 0, still one session (the marker | ||
| 891 | # survives — a second daemon on the path would have started a fresh shell). | ||
| 892 | kill "$SPID" && wait_gone "$SOCK8" | ||
| 893 | SPID="" | ||
| 894 | "$MUXD" start --sock "$SOCK8" 2> "$OUT.ra" & RA=$! | ||
| 895 | "$MUXD" start --sock "$SOCK8" 2> "$OUT.rb" & RB=$! | ||
| 896 | set +e | ||
| 897 | wait "$RA"; RCA=$? | ||
| 898 | wait "$RB"; RCB=$? | ||
| 899 | set -e | ||
| 900 | { [ "$RCA" = "0" ] && [ "$RCB" = "0" ]; } || { | ||
| 901 | echo "e2e FAIL: race: exits $RCA/$RCB"; cat "$OUT.ra" "$OUT.rb"; exit 1; } | ||
| 902 | { printf 'printf "race-%%s\\n" one\n'; sleep 2; printf '\034'; } | \ | ||
| 903 | "$MUX" --sock "$SOCK8" > /dev/null 2>&1 | ||
| 904 | "$MUXD" dump --sock "$SOCK8" | grep -q "race-one" || { | ||
| 905 | echo "e2e FAIL: race: session unusable"; exit 1; } | ||
| 906 | SPID=$(cat "$OUT.ra" "$OUT.rb" | sed -n 's/.* pid=\([0-9]*\).*/\1/p' | while read -r p; do | ||
| 907 | kill -0 "$p" 2>/dev/null && echo "$p"; done | head -1) | ||
| 908 | |||
| 909 | # What actually makes two concurrent starts safe is that the LOSER exits | ||
| 910 | # instead of unlinking the winner's socket and binding over it. The race | ||
| 911 | # above cannot see that — both starts report success either way, and the | ||
| 912 | # marker comes back from whichever daemon owns the path — so it is pinned | ||
| 913 | # head-on: a second `muxd run` against a live socket must refuse. | ||
| 914 | set +e | ||
| 915 | timeout 10 "$MUXD" run --sock "$SOCK8" --shell /bin/sh > "$OUT.rb" 2>&1 | ||
| 916 | RCS=$? | ||
| 917 | set -e | ||
| 918 | [ "$RCS" -eq 1 ] || { | ||
| 919 | echo "e2e FAIL: second muxd run on a live socket exited $RCS (want 1; 124 means it took the path)" | ||
| 920 | cat "$OUT.rb"; exit 1; } | ||
| 921 | grep -q "already running" "$OUT.rb" || { | ||
| 922 | echo "e2e FAIL: second muxd run refused, but not as already-running"; cat "$OUT.rb"; exit 1; } | ||
| 923 | # ...and the session the winner was serving is undisturbed. | ||
| 924 | "$MUXD" dump --sock "$SOCK8" | grep -q "race-one" || { | ||
| 925 | echo "e2e FAIL: the refused daemon disturbed the live session"; exit 1; } | ||
| 926 | |||
| 927 | kill "$SPID" 2>/dev/null || true | ||
| 928 | SPID="" | ||
| 929 | echo "e2e OK: muxd start — spawn, no-op rerun, log truncation, race" | ||
| 930 | |||
| 931 | # --- M10: the goal commands, minus ssh: keygen'd default key on both ends, | ||
| 932 | # explicit loopback port (4433 on the suite machine is somebody's daemon). | ||
| 933 | # No --key on either side — the key is the one `muxd keygen` wrote at the | ||
| 934 | # hermetic default path near the top of this suite. | ||
| 935 | "$MUXD" start --sock "$SOCK11" --quic "127.0.0.1:$QPORT4" 2> "$OUT.goal" | ||
| 936 | GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal") | ||
| 937 | [ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; } | ||
| 938 | { printf 'printf "goal-%%s\\n" quic\n'; sleep 2; printf '\034'; } | \ | ||
| 939 | timeout 30 "$MUX" "quic://127.0.0.1:$QPORT4" > "$OUT.g9" 2>&1 | ||
| 940 | "$MUXD" dump --sock "$SOCK11" | grep -q "goal-quic" || { | ||
| 941 | echo "e2e FAIL: no-key-flag QUIC attach did not reach the session" | ||
| 942 | cat "$OUT.g9"; exit 1; } | ||
| 943 | kill "$GPID" 2>/dev/null || true | ||
| 944 | GPID="" | ||
| 945 | echo "e2e OK: keygen + start --quic + mux quic:// with no --key anywhere" | ||
| 946 | |||
| 812 | # ---- M9: prediction --------------------------------------------------- | 947 | # ---- M9: prediction --------------------------------------------------- |
| 813 | # | 948 | # |
| 814 | # Every scenario here asserts COUNTERS, because the screen cannot tell a | 949 | # Every scenario here asserts COUNTERS, because the screen cannot tell a |