c0d8a460
fix: review round 1 — comments that name what fails when they are wrong
a73x 2026-08-28 19:53
Commit message
src/cli/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -544,12 +544,11 @@ fn resumeRun(alloc: std.mem.Allocator, o: Opts, resume_fd: std.posix.fd_t) !u8 { | |||
| 544 | } | 544 | } |
| 545 | 545 | ||
| 546 | /// `muxd run`: the daemon, in the foreground. Every code this function picks | 546 | /// `muxd run`: the daemon, in the foreground. Every code this function picks |
| 547 | /// is 1, and every one of them is a boot failure — an operator mistake caught | 547 | /// is a boot failure — an operator mistake caught before anything bound. |
| 548 | /// before anything bound. Reaching `srv.run()` means the daemon served, and | 548 | /// Reaching `srv.run()` means the daemon served, and it answers 0 whenever |
| 549 | /// it answers 0 whenever something asks it to stop. No session's exit is ever | 549 | /// something asks it to stop. No session's exit is ever reported here: a |
| 550 | /// reported here: a shell's code goes to that shell's own clients | 550 | /// shell's code goes to that shell's own clients (`exit_status`), and an |
| 551 | /// (`exit_status`), and an emptied daemon is one with nothing on it rather | 551 | /// emptied daemon is one with nothing on it rather than one that is leaving. |
| 552 | /// than one that is leaving. | ||
| 553 | fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 { | 552 | fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 { |
| 554 | // Address and key are settled before anything binds: a mistyped address | 553 | // Address and key are settled before anything binds: a mistyped address |
| 555 | // or an unreadable key must not first leave a session socket and a live | 554 | // or an unreadable key must not first leave a session socket and a live |
src/server_test_session.zig
| Old | New | ||
|---|---|---|---|
| @@ -1091,14 +1091,21 @@ fn awaitFrameThreaded( | |||
| 1091 | return null; | 1091 | return null; |
| 1092 | } | 1092 | } |
| 1093 | 1093 | ||
| 1094 | /// Ask the daemon for its stats until `want` appears in the text. The | 1094 | /// Ask the daemon for its stats until `want` appears in the text — what |
| 1095 | /// counter is the daemon's own account of its table — what `muxd stats` | 1095 | /// `muxd stats` shows an operator looking at a box with nothing on it. |
| 1096 | /// shows an operator looking at a box with nothing running on it. | 1096 | /// |
| 1097 | fn waitStats(alloc: std.mem.Allocator, fd: std.posix.fd_t, want: []const u8) !void { | 1097 | /// A connection of its OWN, per call, and that is the assertion's teeth: a |
| 1098 | /// reply that misses its 200 ms stays buffered, so a shared socket would let | ||
| 1099 | /// a later question be answered by an earlier question's frame — one | ||
| 1100 | /// generated before the very event being waited on, which is how | ||
| 1101 | /// `sessions=1` after a rebirth passes without the rebirth. | ||
| 1102 | fn waitStats(alloc: std.mem.Allocator, sock_path: []const u8, want: []const u8) !void { | ||
| 1103 | const obs = try std.net.connectUnixSocket(sock_path); | ||
| 1104 | defer obs.close(); | ||
| 1098 | var tries: usize = 0; | 1105 | var tries: usize = 0; |
| 1099 | while (tries < 40) : (tries += 1) { | 1106 | while (tries < 40) : (tries += 1) { |
| 1100 | try proto.writeFrame(fd, .stats_req, ""); | 1107 | try proto.writeFrame(obs.handle, .stats_req, ""); |
| 1101 | const frame = (try awaitFrameThreaded(alloc, fd, .stats_reply, 200)) orelse continue; | 1108 | const frame = (try awaitFrameThreaded(alloc, obs.handle, .stats_reply, 200)) orelse continue; |
| 1102 | defer frame.deinit(alloc); | 1109 | defer frame.deinit(alloc); |
| 1103 | if (std.mem.indexOf(u8, frame.payload, want) != null) return; | 1110 | if (std.mem.indexOf(u8, frame.payload, want) != null) return; |
| 1104 | } | 1111 | } |
| @@ -1110,20 +1117,19 @@ fn waitStats(alloc: std.mem.Allocator, fd: std.posix.fd_t, want: []const u8) !vo | |||
| 1110 | /// its own thread, so a daemon that RETURNED from `run` answers none of it | 1117 | /// its own thread, so a daemon that RETURNED from `run` answers none of it |
| 1111 | /// and each one times out. | 1118 | /// and each one times out. |
| 1112 | fn probeEmptiedDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !void { | 1119 | fn probeEmptiedDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !void { |
| 1113 | const obs = try std.net.connectUnixSocket(sock_path); | ||
| 1114 | defer obs.close(); | ||
| 1115 | |||
| 1116 | // Attach the default session, then hang up its shell — the daemon's | 1120 | // Attach the default session, then hang up its shell — the daemon's |
| 1117 | // only one, so the table empties behind it. | 1121 | // only one, so the table empties behind it. 7 rather than 0 so the |
| 1122 | // caller's exit-code pin can tell the two contracts apart: under the | ||
| 1123 | // retired one this shell's code WAS the daemon's. | ||
| 1118 | const c1 = try std.net.connectUnixSocket(sock_path); | 1124 | const c1 = try std.net.connectUnixSocket(sock_path); |
| 1119 | defer c1.close(); | 1125 | defer c1.close(); |
| 1120 | try attachNamed(c1.handle, 80, 24, proto.default_session); | 1126 | try attachNamed(c1.handle, 80, 24, proto.default_session); |
| 1121 | const snap = (try awaitFrameThreaded(alloc, c1.handle, .snapshot, 4000)) orelse | 1127 | const snap = (try awaitFrameThreaded(alloc, c1.handle, .snapshot, 4000)) orelse |
| 1122 | return error.NoFirstSnapshot; | 1128 | return error.NoFirstSnapshot; |
| 1123 | snap.deinit(alloc); | 1129 | snap.deinit(alloc); |
| 1124 | try proto.writeFrame(c1.handle, .input, "die 0\n"); | 1130 | try proto.writeFrame(c1.handle, .input, "die 7\n"); |
| 1125 | 1131 | ||
| 1126 | try waitStats(alloc, obs.handle, "sessions=0"); | 1132 | try waitStats(alloc, sock_path, "sessions=0"); |
| 1127 | 1133 | ||
| 1128 | // Still serving with nothing to serve: a birth on the emptied daemon | 1134 | // Still serving with nothing to serve: a birth on the emptied daemon |
| 1129 | // takes the default name back, which is the attach `mux --sock PATH` | 1135 | // takes the default name back, which is the attach `mux --sock PATH` |
| @@ -1134,7 +1140,7 @@ fn probeEmptiedDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !void { | |||
| 1134 | const reborn = (try awaitFrameThreaded(alloc, c2.handle, .snapshot, 4000)) orelse | 1140 | const reborn = (try awaitFrameThreaded(alloc, c2.handle, .snapshot, 4000)) orelse |
| 1135 | return error.EmptyDaemonRefusedABirth; | 1141 | return error.EmptyDaemonRefusedABirth; |
| 1136 | reborn.deinit(alloc); | 1142 | reborn.deinit(alloc); |
| 1137 | try waitStats(alloc, obs.handle, "sessions=1"); | 1143 | try waitStats(alloc, sock_path, "sessions=1"); |
| 1138 | } | 1144 | } |
| 1139 | 1145 | ||
| 1140 | test "Server: a daemon outlives its last session and ends only on stop" { | 1146 | test "Server: a daemon outlives its last session and ends only on stop" { |
| @@ -1168,9 +1174,10 @@ test "Server: a daemon outlives its last session and ends only on stop" { | |||
| 1168 | th.join(); | 1174 | th.join(); |
| 1169 | try probed; | 1175 | try probed; |
| 1170 | 1176 | ||
| 1171 | // Not the code of any shell: the one this daemon was born with was hung | 1177 | // 0 and not 7. The shell this daemon was born with exited 7, and under |
| 1172 | // up with `die 0` two stats replies ago, and the one born after it is | 1178 | // the retired contract that WAS the daemon's exit code — so this line |
| 1173 | // still alive. This is the shutdown's own code. | 1179 | // fails on a reap that still answers, not merely on a shutdown that |
| 1180 | // returns the wrong thing. | ||
| 1174 | try std.testing.expectEqual(@as(?u8, 0), code); | 1181 | try std.testing.expectEqual(@as(?u8, 0), code); |
| 1175 | } | 1182 | } |
| 1176 | 1183 | ||
test/e2e_05_session.sh
| Old | New | ||
|---|---|---|---|
| @@ -173,10 +173,12 @@ wait_sessions "$SOCK21" 0 "M18: every one of the three shells has exited" | |||
| 173 | 173 | ||
| 174 | # The daemon is asked of the OS, not of itself: `stats` answering above | 174 | # The daemon is asked of the OS, not of itself: `stats` answering above |
| 175 | # already needed a live daemon, but only a pid can say the PROCESS is still | 175 | # already needed a live daemon, but only a pid can say the PROCESS is still |
| 176 | # here. The two seconds are not a wait for anything — the table emptied | 176 | # here. The two seconds are not a wait for anything and are not derived from |
| 177 | # before `stats` said so — they are the window the old exit-on-empty arm | 177 | # anything — the retired arm had no grace, it handed the code straight from |
| 178 | # used its grace on, so a daemon that still has one is caught here and not | 178 | # `reap` to `run` in the same pass. They are a settle window, chosen, so |
| 179 | # by the next scenario finding a dead socket. | 179 | # that a daemon leaving a BEAT after its table empties — through `deinit`, |
| 180 | # or through some later arm nobody has written yet — is caught here rather | ||
| 181 | # than by the next scenario finding a dead socket. | ||
| 180 | sleep 2 | 182 | sleep 2 |
| 181 | kill -0 "$D18PID" 2>/dev/null || { | 183 | kill -0 "$D18PID" 2>/dev/null || { |
| 182 | echo "e2e FAIL: M18: the daemon left with its last session — only muxd stop ends one" | 184 | echo "e2e FAIL: M18: the daemon left with its last session — only muxd stop ends one" |
test/xversion.sh
| Old | New | ||
|---|---|---|---|
| @@ -84,8 +84,10 @@ done | |||
| 84 | TMP="${TMPDIR:-/tmp}/mux-xver-$$" | 84 | TMP="${TMPDIR:-/tmp}/mux-xver-$$" |
| 85 | # Every named attach records a DAEMON in $XDG_STATE_HOME/mux/hosts; without | 85 | # Every named attach records a DAEMON in $XDG_STATE_HOME/mux/hosts; without |
| 86 | # a hermetic home this suite's throwaway sockets pile up on the developer's | 86 | # a hermetic home this suite's throwaway sockets pile up on the developer's |
| 87 | # own wall, one dead stripe each. Legs that take a terminal narrow it | 87 | # own wall as host lines — tiles for every session while a container is |
| 88 | # further, one dir per leg — see `side_channel_session`. | 88 | # still up, and after that a line nothing times out and only `mux hosts rm` |
| 89 | # removes. Legs that take a terminal narrow it further, one dir per leg — | ||
| 90 | # see `side_channel_session`. | ||
| 89 | XDG_STATE_HOME="$TMP/state"; XDG_CONFIG_HOME="$TMP/cfg"; XDG_CACHE_HOME="$TMP/cache" | 91 | XDG_STATE_HOME="$TMP/state"; XDG_CONFIG_HOME="$TMP/cfg"; XDG_CACHE_HOME="$TMP/cache" |
| 90 | export XDG_STATE_HOME XDG_CONFIG_HOME XDG_CACHE_HOME | 92 | export XDG_STATE_HOME XDG_CONFIG_HOME XDG_CACHE_HOME |
| 91 | RUN_OLD="$TMP/o" | 93 | RUN_OLD="$TMP/o" |