520a8baa
feat: the daemon holds thirty-two sessions, wall-sized again
a73x 2026-08-25 16:50
Commit message
README.md
| Old | New | ||
|---|---|---|---|
| @@ -124,8 +124,8 @@ nothing — there is nowhere to step to. | |||
| 124 | 124 | ||
| 125 | To see which sessions are alive without attaching to one, ask the daemon: | 125 | To see which sessions are alive without attaching to one, ask the daemon: |
| 126 | `muxd stats` names every live session with the number of clients watching it. | 126 | `muxd stats` names every live session with the number of clients watching it. |
| 127 | That is the question to ask when `mux` will not let you in — four sessions is | 127 | That is the question to ask when `mux` will not let you in — thirty-two |
| 128 | the table, and a fifth name has nowhere to go. | 128 | sessions is the table, and the thirty-third name has nowhere to go. |
| 129 | 129 | ||
| 130 | Every session shell is told where it lives: `$MUX_SOCK` is the daemon's | 130 | Every session shell is told where it lives: `$MUX_SOCK` is the daemon's |
| 131 | socket path and `$MUX_SESSION` is the session's name. Scripts and prompts | 131 | socket path and `$MUX_SESSION` is the session's name. Scripts and prompts |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -6686,3 +6686,31 @@ stays silent. Someone who typed `muxd start` asked about a daemon and is owed a | |||
| 6686 | verdict on one; someone who typed `mux` asked for a session and is about to get | 6686 | verdict on one; someone who typed `mux` asked for a session and is about to get |
| 6687 | it. | 6687 | it. |
| 6688 | 6688 | ||
| 6689 | ## 2026-08-25 — max_sessions 4 → 32 | ||
| 6690 | |||
| 6691 | The 4 was "purpose bounds the surface" from the multi-session design: a number | ||
| 6692 | picked to keep the new thing small, never a measured limit. Multipane then grew | ||
| 6693 | the wall to `wallview.max_tiles = 32` and the daemon cap did not move with it, | ||
| 6694 | so a wall could ask for more sessions than the daemon would ever hold. It | ||
| 6695 | surfaced as a silently refused `Ctrl-\ c` — the client does say "cannot create a | ||
| 6696 | new session", but the relayout repaint that follows eats the notice, which is a | ||
| 6697 | separate follow-up. | ||
| 6698 | |||
| 6699 | Nothing about the 4 was performance. A session costs one engine, one pty and one | ||
| 6700 | shell, slots are filled lazily, and an empty slot polls fd -1. The two places | ||
| 6701 | the count does scale are worst cases that need a misbehaving peer to bite: the | ||
| 6702 | per-death drain is `max_sessions × 250ms` only when the whole table dies into | ||
| 6703 | stalled clients in one pass, and `pty.term_grace_ms` is paid only by a child | ||
| 6704 | that survives the master's close and then ignores SIGTERM. | ||
| 6705 | |||
| 6706 | `stats_text_len` is now derived from `max_sessions` rather than a literal with a | ||
| 6707 | comptime assert beside it, so the next bump cannot truncate a stats reply — the | ||
| 6708 | assert would have caught it, but only by failing the build, and deriving the | ||
| 6709 | number means there is nothing to catch. | ||
| 6710 | |||
| 6711 | Filling a 32-slot table from a shell script found one thing worth writing down: | ||
| 6712 | `acceptConn` parks every new connection in an OBSERVER slot and promotes it to a | ||
| 6713 | client only when its attach frame arrives, so simultaneous dials contend for | ||
| 6714 | `max_observers` (4), not `max_clients` (8) — 7 at once lost 11 of 31 fills to a | ||
| 6715 | closed connection, 3 at once lost none. `muxd stop` on a full table of `/bin/sh` | ||
| 6716 | took 51ms, which is the grace-cost claim above, measured. | ||
src/pty.zig
| Old | New | ||
|---|---|---|---|
| @@ -208,10 +208,11 @@ pub const Pty = struct { | |||
| 208 | return self.exit_status; | 208 | return self.exit_status; |
| 209 | } | 209 | } |
| 210 | 210 | ||
| 211 | /// How long the child gets to honour SIGTERM before SIGKILL. Generous | 211 | /// How long the child gets to honour SIGTERM before SIGKILL. Only a |
| 212 | /// for a shell that is only being asked to exit, and short enough that | 212 | /// child that survives the master's close (SIGHUP) AND ignores SIGTERM |
| 213 | /// four of them in a row (max_sessions) cannot hold a shutdown open for | 213 | /// — the leaked-fd case this bound exists for — pays it, so a full |
| 214 | /// a noticeable time. | 214 | /// table of well-behaved shells still exits in well under a second; |
| 215 | /// max_sessions stubborn ones cost max_sessions × term_grace_ms. | ||
| 215 | const term_grace_ms = 500; | 216 | const term_grace_ms = 500; |
| 216 | 217 | ||
| 217 | pub fn deinit(self: *Pty) void { | 218 | pub fn deinit(self: *Pty) void { |
src/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -254,7 +254,10 @@ const AwaitState = struct { | |||
| 254 | saw_busy: bool = false, | 254 | saw_busy: bool = false, |
| 255 | }; | 255 | }; |
| 256 | 256 | ||
| 257 | pub const max_sessions = 4; | 257 | // Matches `wallview.max_tiles`: the 4 was surface-bounding (decisions.md, |
| 258 | // 2026-08-25), never a measured limit, and a wall of 32 tiles could not | ||
| 259 | // fill it. | ||
| 260 | pub const max_sessions = 32; | ||
| 258 | 261 | ||
| 259 | /// The smallest grid a session may exist at. Two owners used to decide | 262 | /// The smallest grid a session may exist at. Two owners used to decide |
| 260 | /// this and disagreed: resolveSession created at anything nonzero while | 263 | /// this and disagreed: resolveSession created at anything nonzero while |
| @@ -1022,7 +1025,9 @@ pub const Server = struct { | |||
| 1022 | // stalled bystander client of a surviving session spends it | 1025 | // stalled bystander client of a surviving session spends it |
| 1023 | // too, since the drain waits on ALL owed bytes. Both are the | 1026 | // too, since the drain waits on ALL owed bytes. Both are the |
| 1024 | // price of draining per-death, bounded by a compile-time | 1027 | // price of draining per-death, bounded by a compile-time |
| 1025 | // constant of 4. A dropped client's outstanding await needs | 1028 | // constant of 32 (`max_sessions`) — and only ever paid in |
| 1029 | // full when the whole table dies into stalled peers in one | ||
| 1030 | // pass. A dropped client's outstanding await needs | ||
| 1026 | // nothing here: the connection dying IS muxa's answer | 1031 | // nothing here: the connection dying IS muxa's answer |
| 1027 | // (decision 8), which is also why checkAwaits' session-less | 1032 | // (decision 8), which is also why checkAwaits' session-less |
| 1028 | // `continue` stays unreachable — no client survives its | 1033 | // `continue` stays unreachable — no client survives its |
| @@ -3208,31 +3213,14 @@ pub const Server = struct { | |||
| 3208 | try proto.writeFrame(fd, .stats_reply, try self.statsText(&buf)); | 3213 | try proto.writeFrame(fd, .stats_reply, try self.statsText(&buf)); |
| 3209 | } | 3214 | } |
| 3210 | 3215 | ||
| 3211 | // 256 was sized for the single-session text; the per-session tail | 3216 | // Derived, not a literal, so no future max_sessions bump can truncate |
| 3212 | // can add several "session NAME clients=N seq=N" segments, one | 3217 | // somebody's stats reply. The 256 covers the main line, which measures |
| 3213 | // per live session up to max_sessions, and 256 stopped being enough | 3218 | // ~220: six u64 counters (snapshots, snapshot_bytes, deltas, |
| 3214 | // headroom for that plus the longest legal names. | 3219 | // delta_bytes, snapshot_equiv_bytes, attaches) plus two gauges (clients, |
| 3215 | // | 3220 | // sessions), each with its "name=" label and 20 digits for a u64 at its |
| 3216 | // The arithmetic the 576 answers to: the main line's six u64 counters | 3221 | // widest. The 44 is one per-session segment before its name: 23 of |
| 3217 | // (snapshots, snapshot_bytes, deltas, delta_bytes, snapshot_equiv_bytes, | 3222 | // literal text (" session " + " clients=" + " seq=") and 21 of digits. |
| 3218 | // attaches) plus the two gauges (clients, sessions), each with its | 3223 | pub const stats_text_len = 256 + max_sessions * (44 + proto.session_name_max); |
| 3219 | // "name=" label and a generous 20 digits for a u64 at its widest, comes | ||
| 3220 | // to roughly 220 bytes. `attaches` is what took it from 190 — and the | ||
| 3221 | // comptime assert below is what SAID so, failing the build the moment | ||
| 3222 | // the field was added rather than truncating somebody's stats reply at | ||
| 3223 | // four sessions. Each per-session segment is 44 bytes before the name: 23 | ||
| 3224 | // of literal text (" session " + " clients=" + " seq=", 9 + 9 + 5) and | ||
| 3225 | // 21 of digits — one for clients, which max_clients bounds at 8, and 20 | ||
| 3226 | // for seq at a u64's widest — plus up to session_name_max (32) bytes of | ||
| 3227 | // the name itself, so ~76 bytes | ||
| 3228 | // a session. Four sessions (max_sessions): 220 + 4*76 = 524 of 576 — | ||
| 3229 | // headroom, not a coincidence, and pinned below so a future | ||
| 3230 | // max_sessions bump (or another main-line field) fails the build | ||
| 3231 | // instead of silently truncating whoever asks for stats. | ||
| 3232 | pub const stats_text_len = 576; | ||
| 3233 | comptime { | ||
| 3234 | std.debug.assert(stats_text_len >= 220 + max_sessions * (44 + proto.session_name_max)); | ||
| 3235 | } | ||
| 3236 | 3224 | ||
| 3237 | /// Every live name plus one separator each — one more separator than a | 3225 | /// Every live name plus one separator each — one more separator than a |
| 3238 | /// join uses, which is the slack that lets sessionsText be infallible. | 3226 | /// join uses, which is the slack that lets sessionsText be infallible. |
| @@ -7598,7 +7586,7 @@ test "Server: a bare 20-byte attach lands in the default session" { | |||
| 7598 | for (srv.sessions[1..]) |slot| try std.testing.expect(slot == null); | 7586 | for (srv.sessions[1..]) |slot| try std.testing.expect(slot == null); |
| 7599 | } | 7587 | } |
| 7600 | 7588 | ||
| 7601 | test "Server: attach to a fifth name is refused with exit_status, sessions intact" { | 7589 | test "Server: an attach past max_sessions is refused with exit_status, sessions intact" { |
| 7602 | const alloc = std.testing.allocator; | 7590 | const alloc = std.testing.allocator; |
| 7603 | 7591 | ||
| 7604 | var tmp = try TmpDir.make(); | 7592 | var tmp = try TmpDir.make(); |
| @@ -7609,33 +7597,40 @@ test "Server: attach to a fifth name is refused with exit_status, sessions intac | |||
| 7609 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); | 7597 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); |
| 7610 | defer srv.deinit(); | 7598 | defer srv.deinit(); |
| 7611 | 7599 | ||
| 7612 | // The default session holds slot 0 from init, so three names fill the | 7600 | // The default session holds slot 0 from init, so max_sessions-1 more |
| 7613 | // table. Each attach is confirmed before the next so the refusal below | 7601 | // names fill the table. Each attach is confirmed by its own snapshot |
| 7614 | // is unambiguously "no session slot", not a race. | 7602 | // before the next, so the refusal below is unambiguously "no session |
| 7615 | const names = [_][]const u8{ "b", "c", "d" }; | 7603 | // slot", not a race — and each connection is closed once confirmed |
| 7616 | var conns: [names.len]std.net.Stream = undefined; | 7604 | // because max_clients (8) is smaller than max_sessions: holding them |
| 7617 | var opened: usize = 0; | 7605 | // all open would fill the CLIENT table first and refuse for the wrong |
| 7618 | defer for (conns[0..opened]) |s| s.close(); | 7606 | // reason. The session outlives its client, which is what makes that |
| 7619 | for (names) |nm| { | 7607 | // safe. |
| 7620 | conns[opened] = try std.net.connectUnixSocket(sock_path); | 7608 | for (1..max_sessions) |i| { |
| 7621 | const fd = conns[opened].handle; | 7609 | var nb: [8]u8 = undefined; |
| 7622 | opened += 1; | 7610 | const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i}); |
| 7623 | try attachNamed(fd, 80, 24, nm); | 7611 | const c = try std.net.connectUnixSocket(sock_path); |
| 7624 | const f = (try awaitFrame(alloc, &srv, fd, .snapshot, 400)) orelse | 7612 | defer c.close(); |
| 7613 | try attachNamed(c.handle, 80, 24, nm); | ||
| 7614 | const f = (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400)) orelse | ||
| 7625 | return error.NoSnapshotFillingTable; | 7615 | return error.NoSnapshotFillingTable; |
| 7626 | f.deinit(alloc); | 7616 | f.deinit(alloc); |
| 7627 | } | 7617 | } |
| 7618 | // The closes above are only visible to the daemon once it polls, and a | ||
| 7619 | // client slot it still believes is live would refuse the probe below | ||
| 7620 | // before the session table ever got asked. | ||
| 7621 | for (0..8) |_| _ = try srv.pumpOnce(1); | ||
| 7628 | 7622 | ||
| 7629 | // The fifth name gets the same honest no a full client table gives. | 7623 | // One name past the table gets the same honest no a full client table |
| 7624 | // gives. | ||
| 7630 | const extra = try std.net.connectUnixSocket(sock_path); | 7625 | const extra = try std.net.connectUnixSocket(sock_path); |
| 7631 | defer extra.close(); | 7626 | defer extra.close(); |
| 7632 | try attachNamed(extra.handle, 80, 24, "e"); | 7627 | try attachNamed(extra.handle, 80, 24, "extra"); |
| 7633 | const f = (try awaitFrame(alloc, &srv, extra.handle, .exit_status, 400)) orelse | 7628 | const f = (try awaitFrame(alloc, &srv, extra.handle, .exit_status, 400)) orelse |
| 7634 | return error.NoRefusal; | 7629 | return error.NoRefusal; |
| 7635 | defer f.deinit(alloc); | 7630 | defer f.deinit(alloc); |
| 7636 | try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1); | 7631 | try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1); |
| 7637 | 7632 | ||
| 7638 | // And it cost nobody anything: four live shells, none exited. | 7633 | // And it cost nobody anything: every slot's shell is live, none exited. |
| 7639 | for (0..max_sessions) |si| { | 7634 | for (0..max_sessions) |si| { |
| 7640 | try std.testing.expect(srv.sessions[si] != null); | 7635 | try std.testing.expect(srv.sessions[si] != null); |
| 7641 | try std.testing.expect(srv.sessions[si].?.pty.checkExited() == null); | 7636 | try std.testing.expect(srv.sessions[si].?.pty.checkExited() == null); |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -655,6 +655,42 @@ await_out() { | |||
| 655 | } | 655 | } |
| 656 | } | 656 | } |
| 657 | 657 | ||
| 658 | # fill_sessions SOCK STATE PREFIX FROM TO — one throwaway pipe attach per | ||
| 659 | # name so the daemon holds a slot for each. The session outlives the client | ||
| 660 | # that made it, which is what lets these detach immediately. | ||
| 661 | # | ||
| 662 | # THREE at a time, and the number is `max_observers` minus one, not | ||
| 663 | # `max_clients` minus one: `acceptConn` parks EVERY new connection in an | ||
| 664 | # observer slot and promotes it to a client only when its attach frame | ||
| 665 | # arrives, so simultaneous dials contend for the four observer slots, and | ||
| 666 | # the ones with nowhere to land are closed outright. Measured at batch 7 | ||
| 667 | # on a 32-slot daemon: 11 of 31 fills died with "connection to muxd lost" | ||
| 668 | # and the table never filled; at batch 3, 31 of 31 land. | ||
| 669 | # | ||
| 670 | # STATE is a scratch home so the fills never write a wall line the leg | ||
| 671 | # later counts. The last name is asked back through muxa so a fill that | ||
| 672 | # never landed fails here with its own log rather than as the refusal leg | ||
| 673 | # proving nothing. | ||
| 674 | fill_sessions() { | ||
| 675 | _fs_sock="$1"; _fs_state="$2"; _fs_pfx="$3"; _fs_i="$4"; _fs_to="$5" | ||
| 676 | while [ "$_fs_i" -le "$_fs_to" ]; do | ||
| 677 | _fs_pids=""; _fs_j=0 | ||
| 678 | while [ "$_fs_j" -lt 3 ] && [ "$_fs_i" -le "$_fs_to" ]; do | ||
| 679 | { sleep 1.5; printf '\034\034'; } | XDG_STATE_HOME="$_fs_state" timeout 40 \ | ||
| 680 | "$MUX" --sock "$_fs_sock" --session "$_fs_pfx$_fs_i" \ | ||
| 681 | > "$OUT.fill.$_fs_pfx$_fs_i" 2>&1 & | ||
| 682 | _fs_pids="$_fs_pids $!" | ||
| 683 | _fs_i=$((_fs_i + 1)); _fs_j=$((_fs_j + 1)) | ||
| 684 | done | ||
| 685 | for _fs_p in $_fs_pids; do wait "$_fs_p" || { | ||
| 686 | echo "e2e FAIL: fill_sessions $_fs_pfx: a fill attach on $_fs_sock exited nonzero:" | ||
| 687 | cat "$OUT".fill."$_fs_pfx"*; exit 1; }; done | ||
| 688 | done | ||
| 689 | "$MUXA" status --sock "$_fs_sock" --session "$_fs_pfx$_fs_to" > "$OUT.fill.$_fs_pfx.st" 2>&1 || { | ||
| 690 | echo "e2e FAIL: fill_sessions: $_fs_pfx$_fs_to was never created, the daemon is not full:" | ||
| 691 | cat "$OUT.fill.$_fs_pfx.st"; exit 1; } | ||
| 692 | } | ||
| 693 | |||
| 658 | # repaints FILE — how many full repaints (ESC[2J) a capture holds. The | 694 | # repaints FILE — how many full repaints (ESC[2J) a capture holds. The |
| 659 | # first paint after a reconnect is always a full one (interact.zig | 695 | # first paint after a reconnect is always a full one (interact.zig |
| 660 | # `repaint_after_resync`), so a count that rose is the resume itself, seen | 696 | # `repaint_after_resync`), so a count that rose is the resume itself, seen |
| @@ -1532,6 +1568,12 @@ cleanup() { | |||
| 1532 | "$OUT.whxa" "$OUT.whxa.err" "$OUT.whxb" "$OUT.whxb.err" \ | 1568 | "$OUT.whxa" "$OUT.whxa.err" "$OUT.whxb" "$OUT.whxb.err" \ |
| 1533 | "$OUT.whstop" | 1569 | "$OUT.whstop" |
| 1534 | rm -rf "$WHSTATE" "$WHBAD" "$WHXSTATE" | 1570 | rm -rf "$WHSTATE" "$WHBAD" "$WHXSTATE" |
| 1571 | # fill_sessions' captures, one per filled name, and the scratch state | ||
| 1572 | # homes the fills write their own wall lines into. A glob rather than a | ||
| 1573 | # list because the names are generated: 28 per caller, and a list would | ||
| 1574 | # go stale the moment max_sessions moves again. | ||
| 1575 | rm -f "$OUT".fill.* | ||
| 1576 | rm -rf "$OUT.nswfill" "$OUT.whfill" | ||
| 1535 | # ...and the wall-chord leg's own state home, which holds the two-line | 1577 | # ...and the wall-chord leg's own state home, which holds the two-line |
| 1536 | # wall that leg builds and reads back. | 1578 | # wall that leg builds and reads back. |
| 1537 | rm -rf "$M4STATE" | 1579 | rm -rf "$M4STATE" |
| @@ -5148,6 +5190,12 @@ ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches" | |||
| 5148 | "$MUXD" run --sock "$SOCK28" --shell /bin/sh > "$OUT.nsw.d" 2>&1 & | 5190 | "$MUXD" run --sock "$SOCK28" --shell /bin/sh > "$OUT.nsw.d" 2>&1 & |
| 5149 | D25PID=$! | 5191 | D25PID=$! |
| 5150 | wait_sock "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" | 5192 | wait_sock "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" |
| 5193 | # The refusal this leg's fourth chord asserts needs a FULL table. The | ||
| 5194 | # chords make four (0, 1, 2, 3); the other 28 are filled here. `fillN` is | ||
| 5195 | # not an integer, so nextFreeName still hands the chords 1, 2, 3 and | ||
| 5196 | # reaches for "4" on the refused one — which is the name the post-check | ||
| 5197 | # below asks about. | ||
| 5198 | fill_sessions "$SOCK28" "$OUT.nswfill" fill 4 31 | ||
| 5151 | 5199 | ||
| 5152 | set +e | 5200 | set +e |
| 5153 | # tall: Ctrl-\ c adds a second tile at 80x24 (80 >= 48) | 5201 | # tall: Ctrl-\ c adds a second tile at 80x24 (80 >= 48) |
| @@ -5199,8 +5247,8 @@ grep -q "m2a-pin" "$OUT.nswcap" || { | |||
| 5199 | grep -q "m2b-pin" "$OUT.nswcap" && { | 5247 | grep -q "m2b-pin" "$OUT.nswcap" && { |
| 5200 | echo "e2e FAIL: new session: the second marker ran in the OLD session" | 5248 | echo "e2e FAIL: new session: the second marker ran in the OLD session" |
| 5201 | cat "$OUT.nswcap"; exit 1; } | 5249 | cat "$OUT.nswcap"; exit 1; } |
| 5202 | # The fourth chord had nowhere to go: max_sessions is 4 and the leg filled | 5250 | # The fourth chord had nowhere to go: the fill above took 28 slots and the |
| 5203 | # the table (0, 1, 2, 3), so the daemon answered the attach with an | 5251 | # chords the other four (0, 1, 2, 3), so the daemon answered the attach with an |
| 5204 | # exit_status before a single frame of state. That refusal is NOT fatal — | 5252 | # exit_status before a single frame of state. That refusal is NOT fatal — |
| 5205 | # the tile the chord created knows which tile it was born from — and the | 5253 | # the tile the chord created knows which tile it was born from — and the |
| 5206 | # three claims below are what "fell back" means, none of which the log | 5254 | # three claims below are what "fell back" means, none of which the log |
| @@ -5230,9 +5278,9 @@ grep -q "m2d-pin" "$OUT.nswcap3" || { | |||
| 5230 | echo "e2e FAIL: new session: the refused switch did not come back to session 3:" | 5278 | echo "e2e FAIL: new session: the refused switch did not come back to session 3:" |
| 5231 | cat "$OUT.nswcap3"; exit 1; } | 5279 | cat "$OUT.nswcap3"; exit 1; } |
| 5232 | # * and the name it was reaching for was never created. Without this the | 5280 | # * and the name it was reaching for was never created. Without this the |
| 5233 | # leg would pass on a daemon that quietly grew a fifth slot. | 5281 | # leg would pass on a daemon that quietly grew one more slot. |
| 5234 | "$MUXA" status --sock "$SOCK28" --session 4 > "$OUT.nswst4" 2>&1 && { | 5282 | "$MUXA" status --sock "$SOCK28" --session 4 > "$OUT.nswst4" 2>&1 && { |
| 5235 | echo "e2e FAIL: new session: the daemon created a fifth session:" | 5283 | echo "e2e FAIL: new session: the daemon created a 33rd session:" |
| 5236 | cat "$OUT.nswst4"; exit 1; } | 5284 | cat "$OUT.nswst4"; exit 1; } |
| 5237 | assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop" | 5285 | assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop" |
| 5238 | D25PID="" | 5286 | D25PID="" |
| @@ -6253,13 +6301,16 @@ grep -q "nothing left to show" "$OUT.whxcap2" || { | |||
| 6253 | [ ! -s "$WHXSTATE/mux/wall" ] || { | 6301 | [ ! -s "$WHXSTATE/mux/wall" ] || { |
| 6254 | echo "e2e FAIL: x forgets: the last \\x1cx left lines behind:" | 6302 | echo "e2e FAIL: x forgets: the last \\x1cx left lines behind:" |
| 6255 | cat "$WHXSTATE/mux/wall"; exit 1; } | 6303 | cat "$WHXSTATE/mux/wall"; exit 1; } |
| 6256 | # The phantom tile, while this daemon is FULL (0, 1, xa, xb — max_sessions | 6304 | # The phantom tile, while this daemon is FULL: this leg's own four (0, 1, |
| 6257 | # is 4): a FIRST attach to a fifth name is refused, and a refusal must | 6305 | # xa, xb) plus the 28 filled here are max_sessions, so a FIRST attach to |
| 6258 | # leave no line behind. This is the case that made the seam move off "the | 6306 | # one more name is refused — and a refusal must leave no line behind. Its |
| 6259 | # dial succeeded" — a dial that comes up is not an attach that landed, and | 6307 | # state home is neither $WHSTATE nor $WHXSTATE, both of whose walls are |
| 6260 | # only a SWITCH's refusal ever had somewhere to undo the write from. The | 6308 | # asserted on. This is the case that made the seam move off "the dial |
| 6261 | # leg cannot pass vacuously: a daemon with room would accept the attach and | 6309 | # succeeded" — a dial that comes up is not an attach that landed, and only |
| 6310 | # a SWITCH's refusal ever had somewhere to undo the write from. The leg | ||
| 6311 | # cannot pass vacuously: a daemon with room would accept the attach and | ||
| 6262 | # the rc check below would fail loudly. | 6312 | # the rc check below would fail loudly. |
| 6313 | fill_sessions "$SOCK40" "$OUT.whfill" fill 4 31 | ||
| 6263 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | 6314 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) |
| 6264 | set +e | 6315 | set +e |
| 6265 | { sleep 1; printf '\034\034'; } | XDG_STATE_HOME="$WHSTATE" timeout 40 \ | 6316 | { sleep 1; printf '\034\034'; } | XDG_STATE_HOME="$WHSTATE" timeout 40 \ |
| @@ -6267,7 +6318,7 @@ set +e | |||
| 6267 | RC=$? | 6318 | RC=$? |
| 6268 | set -e | 6319 | set -e |
| 6269 | [ "$RC" -ne 0 ] || { | 6320 | [ "$RC" -ne 0 ] || { |
| 6270 | echo "e2e FAIL: x forgets: a fifth session was created on a full daemon," | 6321 | echo "e2e FAIL: x forgets: a 33rd session was created on a full daemon," |
| 6271 | echo " so the phantom-tile leg proved nothing:"; cat "$OUT.whph"; exit 1; } | 6322 | echo " so the phantom-tile leg proved nothing:"; cat "$OUT.whph"; exit 1; } |
| 6272 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | 6323 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { |
| 6273 | echo "e2e FAIL: x forgets: a REFUSED first attach recorded a phantom tile:" | 6324 | echo "e2e FAIL: x forgets: a REFUSED first attach recorded a phantom tile:" |