d057e034
refactor: an unzoomed browser tile claims no size, like every other watcher
a73x 2026-08-20 11:53
Commit message
src/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -784,15 +784,18 @@ pub const Server = struct { | |||
| 784 | 784 | ||
| 785 | /// Attach-or-create. Creation demands a size the session can actually | 785 | /// Attach-or-create. Creation demands a size the session can actually |
| 786 | /// live at, and that is the SAME threshold applySize enforces — not a | 786 | /// live at, and that is the SAME threshold applySize enforces — not a |
| 787 | /// weaker one. A 0x0 attach makes no size claim (muxa send) and a | 787 | /// weaker one. A 0x0 attach makes no size claim at all — muxa, the CLI |
| 788 | /// client with no size must never be the reason a shell spawns; but | 788 | /// wall's stripes and an unzoomed browser tile all spell passivity that |
| 789 | /// 1x1 is worse than 0x0, because it is a size a client really sends: | 789 | /// way — and a client with no size must never be the reason a shell |
| 790 | /// every unzoomed BROWSER wall tile attaches 1x1 (web/mux.js); the CLI | 790 | /// spawns. |
| 791 | /// wall's stripes attach 0x0. Gating on | 791 | /// |
| 792 | /// "nonzero" let `muxweb host#newname` fork a shell whose engine, pty | 792 | /// The threshold has to be the real one and not merely "nonzero", |
| 793 | /// because 1x1 is a size a client genuinely sends: a terminal can be | ||
| 794 | /// one column wide. Gating on nonzero forked a shell whose engine, pty | ||
| 793 | /// and winsize were all 1x1 — and then applySize refused to move it, | 795 | /// and winsize were all 1x1 — and then applySize refused to move it, |
| 794 | /// recordSize was skipped, the slot stayed 0x0, and claimGrid could | 796 | /// recordSize was skipped, the slot stayed 0x0, and claimGrid could |
| 795 | /// never claim, so the tile that caused the size could never fix it. | 797 | /// never claim, so the client that caused the size could never fix it. |
| 798 | /// A session nobody can use is worse than a refusal nobody can miss. | ||
| 796 | /// One rule, spelled once, in both places that decide it. | 799 | /// One rule, spelled once, in both places that decide it. |
| 797 | /// | 800 | /// |
| 798 | /// Null is a refusal (bad name, table full, too small to create at, or | 801 | /// Null is a refusal (bad name, table full, too small to create at, or |
| @@ -7420,13 +7423,16 @@ test "Server: a 1x1 attach joins but never creates" { | |||
| 7420 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); | 7423 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); |
| 7421 | defer srv.deinit(); | 7424 | defer srv.deinit(); |
| 7422 | 7425 | ||
| 7423 | // 1x1 is the size every unzoomed BROWSER wall tile attaches at | 7426 | // Unlike 0x0, nothing spells passivity as 1x1 — which is exactly why |
| 7424 | // (web/mux.js) — the CLI wall's stripes attach 0x0 — so | 7427 | // this case needs its own pin. 0x0 is a contract and gets refused by |
| 7425 | // unlike 0x0 this is a size a real client really sends. It must be | 7428 | // one; 1x1 is a real terminal, one column wide, and the daemon has to |
| 7426 | // refused for creation exactly as 0x0 is: applySize will not move a | 7429 | // stay unclaimable by it on the size threshold alone. e2e drives that |
| 7430 | // client for real, on a genuine 1x1 pty. | ||
| 7431 | // | ||
| 7432 | // Refused for creation exactly as 0x0 is: applySize will not move a | ||
| 7427 | // grid to 1x1, so a session created at 1x1 could never be resized by | 7433 | // grid to 1x1, so a session created at 1x1 could never be resized by |
| 7428 | // the tile that caused it — a shell nobody can use, spawned by opening | 7434 | // the client that caused it — a shell nobody can use, spawned by |
| 7429 | // a wall page with a name nobody had created yet. | 7435 | // attaching with a name nobody had created yet. |
| 7430 | const c1 = try std.net.connectUnixSocket(sock_path); | 7436 | const c1 = try std.net.connectUnixSocket(sock_path); |
| 7431 | defer c1.close(); | 7437 | defer c1.close(); |
| 7432 | try attachNamed(c1.handle, 1, 1, "tile"); | 7438 | try attachNamed(c1.handle, 1, 1, "tile"); |
| @@ -7445,8 +7451,9 @@ test "Server: a 1x1 attach joins but never creates" { | |||
| 7445 | f2.deinit(alloc); | 7451 | f2.deinit(alloc); |
| 7446 | const si = srv.findSession("tile") orelse return error.NotCreated; | 7452 | const si = srv.findSession("tile") orelse return error.NotCreated; |
| 7447 | 7453 | ||
| 7448 | // ...and the same 1x1 attach now JOINS it, leaving the grid alone. That | 7454 | // ...and the same 1x1 attach now JOINS it, leaving the grid alone. |
| 7449 | // is the passivity contract: a tile may watch any session, and may | 7455 | // Refusing the creation must not cost the client its view: a terminal |
| 7456 | // too small to spawn a session is still allowed to watch one, and may | ||
| 7450 | // never be the reason one exists or the reason one resizes. | 7457 | // never be the reason one exists or the reason one resizes. |
| 7451 | const c3 = try std.net.connectUnixSocket(sock_path); | 7458 | const c3 = try std.net.connectUnixSocket(sock_path); |
| 7452 | defer c3.close(); | 7459 | defer c3.close(); |
src/wasm_core.zig
| Old | New | ||
|---|---|---|---|
| @@ -390,9 +390,11 @@ export fn mux_mark_all_dirty() void { | |||
| 390 | // --------------------------------------------------------------------- | 390 | // --------------------------------------------------------------------- |
| 391 | 391 | ||
| 392 | /// The whole 20-byte attach payload into the output buffer, so JS never | 392 | /// The whole 20-byte attach payload into the output buffer, so JS never |
| 393 | /// hand-assembles a u64. `cols`/`rows` are what this client claims — a | 393 | /// hand-assembles a u64. `cols`/`rows` are what this client claims — an |
| 394 | /// wall tile passes 1x1 (the passivity contract), the zoomed tile its | 394 | /// unzoomed wall tile passes 0x0 and claims nothing (the passivity |
| 395 | /// real size. Quotes the replica's resume coordinates; pass fresh=1 to | 395 | /// contract), the zoomed tile its real size. Encoded as given: a size |
| 396 | /// this refused would be a second opinion on a rule server.zig owns. | ||
| 397 | /// Quotes the replica's resume coordinates; pass fresh=1 to | ||
| 396 | /// quote (0,0) instead (the resync re-attach). | 398 | /// quote (0,0) instead (the resync re-attach). |
| 397 | export fn mux_attach_payload(cols: u32, rows: u32, fresh: u32) i32 { | 399 | export fn mux_attach_payload(cols: u32, rows: u32, fresh: u32) i32 { |
| 398 | const c = core orelse return -1; | 400 | const c = core orelse return -1; |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -3998,7 +3998,7 @@ wait_sock "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound" | |||
| 3998 | # Two clients, two names, one socket. Each plants a marker its own shell | 3998 | # Two clients, two names, one socket. Each plants a marker its own shell |
| 3999 | # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only | 3999 | # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only |
| 4000 | # the output reads `m18a-pin`, so a grep that hits is the shell's work and | 4000 | # the output reads `m18a-pin`, so a grep that hits is the shell's work and |
| 4001 | # not an echo of our keystrokes (the M-web passivity block's trick). | 4001 | # not an echo of our keystrokes (the tiny-attach block's trick). |
| 4002 | # | 4002 | # |
| 4003 | # Both run in the foreground and DETACH (\034\034, the Ctrl-\ chord) rather than staying | 4003 | # Both run in the foreground and DETACH (\034\034, the Ctrl-\ chord) rather than staying |
| 4004 | # up: the session has to outlive its client for every assertion below to | 4004 | # up: the session has to outlive its client for every assertion below to |
| @@ -4131,7 +4131,7 @@ wait_for "$OUT.m18wh" "serving" 10 || { | |||
| 4131 | echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; } | 4131 | echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; } |
| 4132 | 4132 | ||
| 4133 | # Content for each session, planted by a CLI client and left behind: the | 4133 | # Content for each session, planted by a CLI client and left behind: the |
| 4134 | # tiles are passive 1x1 wall tiles and can never type anything themselves. | 4134 | # tiles are passive 0x0 wall tiles and can never type anything themselves. |
| 4135 | { printf 'printf "wall-a-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ | 4135 | { printf 'printf "wall-a-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ |
| 4136 | timeout 40 "$MUX" --sock "$SOCK22" --session a > "$OUT.m18wa" 2> "$OUT.m18wa.err" | 4136 | timeout 40 "$MUX" --sock "$SOCK22" --session a > "$OUT.m18wa" 2> "$OUT.m18wa.err" |
| 4137 | wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a | 4137 | wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a |
| @@ -4141,8 +4141,8 @@ wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b | |||
| 4141 | 4141 | ||
| 4142 | # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the | 4142 | # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the |
| 4143 | # name the way mux.js does — bytes appended after the fixed 20 — and | 4143 | # name the way mux.js does — bytes appended after the fixed 20 — and |
| 4144 | # attaches at 1x1, the passivity contract: both sessions already exist, so | 4144 | # attaches at 0x0, the passivity contract: both sessions already exist, so |
| 4145 | # these are joins, and applySize refuses to move an 80-wide grid to 1. | 4145 | # these are joins, and a slot that claimed no size can never move the grid. |
| 4146 | for _t in 0 1; do | 4146 | for _t in 0 1; do |
| 4147 | case "$_t" in | 4147 | case "$_t" in |
| 4148 | 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;; | 4148 | 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;; |
| @@ -4151,7 +4151,7 @@ for _t in 0 1; do | |||
| 4151 | set +e | 4151 | set +e |
| 4152 | timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \ | 4152 | timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \ |
| 4153 | --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF | 4153 | --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF |
| 4154 | attach 1 1 $_sn | 4154 | attach 0 0 $_sn |
| 4155 | expectstate up 10000 | 4155 | expectstate up 10000 |
| 4156 | expectgrid $_mine 15000 | 4156 | expectgrid $_mine 15000 |
| 4157 | settle 500 10000 | 4157 | settle 500 10000 |
| @@ -4298,17 +4298,20 @@ assert_stopped "$SOCK23" "$D20PID" "M18 quic multi-session" "$OUT.m18qstop" | |||
| 4298 | D20PID="" | 4298 | D20PID="" |
| 4299 | ok "quic: two dials are two sessions, and one name is one session on either transport" | 4299 | ok "quic: two dials are two sessions, and one name is one session on either transport" |
| 4300 | 4300 | ||
| 4301 | # --- M-web (a): a 1x1 attach is refused the grid and can never claim it. | 4301 | # --- a 1x1 attach is refused the grid and can never claim it. |
| 4302 | # The wall tile's passivity contract, pinned DAEMON-SIDE and hub-free on | 4302 | # The degenerate size a client can genuinely arrive with — a terminal one |
| 4303 | # purpose: the mechanism is applySize's cols<2 refusal plus claimGrid's | 4303 | # column wide — driven here on a real 1x1 pty. Nothing spells passivity |
| 4304 | # 0x0-slot refusal (server.zig), and it must hold whether or not muxweb | 4304 | # this way (that is 0x0, and it is refused by contract); this leg is the |
| 4305 | # exists. The probe for "the grid did not move" is textual: a marker | 4305 | # daemon staying unclaimable on the size threshold ALONE. Pinned |
| 4306 | # DAEMON-SIDE and hub-free on purpose: the mechanism is applySize's cols<2 | ||
| 4307 | # refusal plus claimGrid's 0x0-slot refusal (server.zig), and it must hold | ||
| 4308 | # for any client that sends a size nobody can live at. The probe for "the grid did not move" is textual: a marker | ||
| 4306 | # string typed at 80 wide can only appear CONTIGUOUS in `muxd dump` if | 4309 | # string typed at 80 wide can only appear CONTIGUOUS in `muxd dump` if |
| 4307 | # the grid is still 80 wide — a 1-column grid puts every glyph on its | 4310 | # the grid is still 80 wide — a 1-column grid puts every glyph on its |
| 4308 | # own row, so grep itself is the geometry assertion. | 4311 | # own row, so grep itself is the geometry assertion. |
| 4309 | "$MUXD" run --sock "$SOCK18" --shell /bin/sh > "$OUT.weba.d" 2>&1 & | 4312 | "$MUXD" run --sock "$SOCK18" --shell /bin/sh > "$OUT.weba.d" 2>&1 & |
| 4310 | D14PID=$! | 4313 | D14PID=$! |
| 4311 | wait_sock "$SOCK18" "$OUT.weba.d" "web passivity daemon never bound" | 4314 | wait_sock "$SOCK18" "$OUT.weba.d" "tiny attach daemon never bound" |
| 4312 | 4315 | ||
| 4313 | # The 80x24 client: types the first marker, holds the session open long | 4316 | # The 80x24 client: types the first marker, holds the session open long |
| 4314 | # enough for the 1x1 attacher to come and go, types the second marker | 4317 | # enough for the 1x1 attacher to come and go, types the second marker |
| @@ -4316,7 +4319,7 @@ wait_sock "$SOCK18" "$OUT.weba.d" "web passivity daemon never bound" | |||
| 4316 | { printf 'printf "wall-%%s\\n" pin\n'; sleep 4; printf 'printf "zz-%%s\\n" web\n'; sleep 2; printf '\034\034'; } | \ | 4319 | { printf 'printf "wall-%%s\\n" pin\n'; sleep 4; printf 'printf "zz-%%s\\n" web\n'; sleep 2; printf '\034\034'; } | \ |
| 4317 | timeout 40 "$MUX" --sock "$SOCK18" > "$OUT.weba" 2> "$OUT.weba.err" & | 4320 | timeout 40 "$MUX" --sock "$SOCK18" > "$OUT.weba" 2> "$OUT.weba.err" & |
| 4318 | WCLIPID=$! | 4321 | WCLIPID=$! |
| 4319 | wait_grid "$SOCK18" "wall-pin" "web passivity: the first marker" | 4322 | wait_grid "$SOCK18" "wall-pin" "tiny attach: the first marker" |
| 4320 | 4323 | ||
| 4321 | # The 1x1 attacher, on a real 1x1 pty. Its attach is answered with a | 4324 | # The 1x1 attacher, on a real 1x1 pty. Its attach is answered with a |
| 4322 | # unicast snapshot (the alt-screen enter proves the first frame came), | 4325 | # unicast snapshot (the alt-screen enter proves the first frame came), |
| @@ -4336,16 +4339,16 @@ EOF | |||
| 4336 | RC=$? | 4339 | RC=$? |
| 4337 | set -e | 4340 | set -e |
| 4338 | [ "$RC" -eq 0 ] || { | 4341 | [ "$RC" -eq 0 ] || { |
| 4339 | echo "e2e FAIL: web passivity: 1x1 client exited $RC" | 4342 | echo "e2e FAIL: tiny attach: 1x1 client exited $RC" |
| 4340 | cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; } | 4343 | cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; } |
| 4341 | 4344 | ||
| 4342 | # The geometry assertion: both markers contiguous AFTER the 1x1 client | 4345 | # The geometry assertion: both markers contiguous AFTER the 1x1 client |
| 4343 | # attached, typed, and left. A grid moved to 1 wide cannot hold either. | 4346 | # attached, typed, and left. A grid moved to 1 wide cannot hold either. |
| 4344 | "$MUXD" dump --sock "$SOCK18" | grep -q "wall-pin" || { | 4347 | "$MUXD" dump --sock "$SOCK18" | grep -q "wall-pin" || { |
| 4345 | echo "e2e FAIL: web passivity: first marker lost after 1x1 attach:" | 4348 | echo "e2e FAIL: tiny attach: first marker lost after 1x1 attach:" |
| 4346 | "$MUXD" dump --sock "$SOCK18"; exit 1; } | 4349 | "$MUXD" dump --sock "$SOCK18"; exit 1; } |
| 4347 | "$MUXD" dump --sock "$SOCK18" | grep -q "zz-web" || { | 4350 | "$MUXD" dump --sock "$SOCK18" | grep -q "zz-web" || { |
| 4348 | echo "e2e FAIL: web passivity: the 1x1 attacher moved the grid (marker not contiguous):" | 4351 | echo "e2e FAIL: tiny attach: the 1x1 attacher moved the grid (marker not contiguous):" |
| 4349 | "$MUXD" dump --sock "$SOCK18"; exit 1; } | 4352 | "$MUXD" dump --sock "$SOCK18"; exit 1; } |
| 4350 | 4353 | ||
| 4351 | set +e | 4354 | set +e |
| @@ -4353,10 +4356,10 @@ wait "$WCLIPID" | |||
| 4353 | RC=$? | 4356 | RC=$? |
| 4354 | set -e | 4357 | set -e |
| 4355 | WCLIPID="" | 4358 | WCLIPID="" |
| 4356 | [ "$RC" -eq 0 ] || { echo "e2e FAIL: web passivity: 80x24 client exited $RC"; cat "$OUT.weba.err"; exit 1; } | 4359 | [ "$RC" -eq 0 ] || { echo "e2e FAIL: tiny attach: 80x24 client exited $RC"; cat "$OUT.weba.err"; exit 1; } |
| 4357 | assert_converged "$OUT.weba" "$SOCK18" "web passivity: the 80x24 client never glitched" | 4360 | assert_converged "$OUT.weba" "$SOCK18" "tiny attach: the 80x24 client never glitched" |
| 4358 | 4361 | ||
| 4359 | assert_stopped "$SOCK18" "$D14PID" "web passivity" "$OUT.webstop" | 4362 | assert_stopped "$SOCK18" "$D14PID" "tiny attach" "$OUT.webstop" |
| 4360 | D14PID="" | 4363 | D14PID="" |
| 4361 | ok "a 1x1 attach is refused the grid and can never claim it" | 4364 | ok "a 1x1 attach is refused the grid and can never claim it" |
| 4362 | 4365 | ||
| @@ -4374,12 +4377,12 @@ wait_for "$OUT.webh" "serving" 10 || { | |||
| 4374 | WCLIPID=$! | 4377 | WCLIPID=$! |
| 4375 | wait_grid "$SOCK19" "web-b1" "web hub: the typing client's marker" | 4378 | wait_grid "$SOCK19" "web-b1" "web hub: the typing client's marker" |
| 4376 | 4379 | ||
| 4377 | # The browser stand-in: a passive 1x1 wall tile through the hub. Its | 4380 | # The browser stand-in: a passive 0x0 wall tile through the hub. Its |
| 4378 | # final grid is the daemon's replica as REPLAYED THROUGH the WebSocket | 4381 | # final grid is the daemon's replica as REPLAYED THROUGH the WebSocket |
| 4379 | # leg, dumped in muxd dump's own format. | 4382 | # leg, dumped in muxd dump's own format. |
| 4380 | set +e | 4383 | set +e |
| 4381 | timeout 40 "$WSCLIENT" --port "$WPORT" --tile 0 --out "$OUT.webws" --err "$OUT.webws.err" <<'EOF' | 4384 | timeout 40 "$WSCLIENT" --port "$WPORT" --tile 0 --out "$OUT.webws" --err "$OUT.webws.err" <<'EOF' |
| 4382 | attach 1 1 | 4385 | attach 0 0 |
| 4383 | expectstate up 10000 | 4386 | expectstate up 10000 |
| 4384 | expectgrid web-b1 15000 | 4387 | expectgrid web-b1 15000 |
| 4385 | settle 500 10000 | 4388 | settle 500 10000 |
| @@ -4445,12 +4448,12 @@ wait_for "$OUT.webh2" "serving" 10 || { | |||
| 4445 | wait_grid "$SOCK20" "web-c1" "web tear: the first session's marker" | 4448 | wait_grid "$SOCK20" "web-c1" "web tear: the first session's marker" |
| 4446 | 4449 | ||
| 4447 | timeout 90 "$WSCLIENT" --port "$WPORT2" --tile 0 --out "$OUT.webws2" --err "$OUT.webws2.err" <<'EOF' & | 4450 | timeout 90 "$WSCLIENT" --port "$WPORT2" --tile 0 --out "$OUT.webws2" --err "$OUT.webws2.err" <<'EOF' & |
| 4448 | attach 1 1 | 4451 | attach 0 0 |
| 4449 | expectstate up 10000 | 4452 | expectstate up 10000 |
| 4450 | expectgrid web-c1 15000 | 4453 | expectgrid web-c1 15000 |
| 4451 | expectstate reconnecting 25000 | 4454 | expectstate reconnecting 25000 |
| 4452 | expectstate up 25000 | 4455 | expectstate up 25000 |
| 4453 | attach 1 1 | 4456 | attach 0 0 |
| 4454 | expectgrid web-c2 30000 | 4457 | expectgrid web-c2 30000 |
| 4455 | settle 500 10000 | 4458 | settle 500 10000 |
| 4456 | dumpexit | 4459 | dumpexit |
| @@ -4549,9 +4552,10 @@ RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X POST \ | |||
| 4549 | 4552 | ||
| 4550 | # A POSTed tile is a REAL tile: a marker typed through the CLI door is read | 4553 | # A POSTed tile is a REAL tile: a marker typed through the CLI door is read |
| 4551 | # back through the WebSocket door of the tile the page just created. The | 4554 | # back through the WebSocket door of the tile the page just created. The |
| 4552 | # stand-in attaches at 1x1 and the CLI client has already detached, which is | 4555 | # stand-in attaches at 0x0 and the CLI client has already detached, so the |
| 4553 | # the M-web (a) contract in passing — a 1x1 attach never claims the grid, so | 4556 | # passivity contract is in play in passing — a tile that claimed no size |
| 4554 | # the 80-wide marker is still one contiguous string to expect. | 4557 | # never moves the grid, and the 80-wide marker is still one contiguous |
| 4558 | # string to expect. | ||
| 4555 | set +e | 4559 | set +e |
| 4556 | { printf 'printf "dyn-%%s\\n" w1\n'; sleep 4; printf '\034\034'; } | \ | 4560 | { printf 'printf "dyn-%%s\\n" w1\n'; sleep 4; printf '\034\034'; } | \ |
| 4557 | timeout 40 "$MUX" --sock "$SOCK25" > "$OUT.dwcli" 2> "$OUT.dwcli.err" | 4561 | timeout 40 "$MUX" --sock "$SOCK25" > "$OUT.dwcli" 2> "$OUT.dwcli.err" |
| @@ -4562,7 +4566,7 @@ set -e | |||
| 4562 | wait_grid "$SOCK25" "dyn-w1" "dyn wall: CLI marker" | 4566 | wait_grid "$SOCK25" "dyn-w1" "dyn wall: CLI marker" |
| 4563 | set +e | 4567 | set +e |
| 4564 | timeout 40 "$WSCLIENT" --port "$WPORT4" --tile 0 --out "$OUT.dwws" --err "$OUT.dwws.err" <<'EOF' | 4568 | timeout 40 "$WSCLIENT" --port "$WPORT4" --tile 0 --out "$OUT.dwws" --err "$OUT.dwws.err" <<'EOF' |
| 4565 | attach 1 1 | 4569 | attach 0 0 |
| 4566 | expectstate up 10000 | 4570 | expectstate up 10000 |
| 4567 | expectgrid dyn-w1 15000 | 4571 | expectgrid dyn-w1 15000 |
| 4568 | dumpexit | 4572 | dumpexit |
test/wsclient.zig
| Old | New | ||
|---|---|---|---|
| @@ -158,15 +158,16 @@ const Client = struct { | |||
| 158 | } | 158 | } |
| 159 | 159 | ||
| 160 | /// The ONE place an attach frame is built, which is what keeps the | 160 | /// The ONE place an attach frame is built, which is what keeps the |
| 161 | /// passivity contract honest. A wall tile attaches at its own size — | 161 | /// passivity contract honest. An unzoomed wall tile attaches at 0x0 — |
| 162 | /// 1x1, refused the grid by applySize and refused claimGrid forever | 162 | /// no size claim at all, refused the grid by applySize and refused |
| 163 | /// after — and every LATER attach has to quote that same size. The | 163 | /// claimGrid forever after — and every LATER attach has to quote the |
| 164 | /// resync path is the trap: re-attaching at the grid the snapshot | 164 | /// same size the script chose. The resync path is the trap: re-attaching |
| 165 | /// taught us (80x24) is an attach at a differing size, which moves the | 165 | /// at the grid the snapshot taught us (80x24) is an attach at a |
| 166 | /// shared grid and repaints every other client, so a stand-in that did | 166 | /// differing size, which moves the shared grid and repaints every other |
| 167 | /// that would be pinning the opposite of the contract. The browser has | 167 | /// client, so a stand-in that did that would be pinning the opposite of |
| 168 | /// the same rule structurally: mux.js routes every attach through | 168 | /// the contract. The browser has the same rule structurally: mux.js |
| 169 | /// sendAttach, which quotes the tile's scripted size. | 169 | /// routes every attach through sendAttach, which quotes the tile's |
| 170 | /// scripted size. | ||
| 170 | /// | 171 | /// |
| 171 | /// `fresh` quotes (0,0) instead of the replica's resume coordinates — | 172 | /// `fresh` quotes (0,0) instead of the replica's resume coordinates — |
| 172 | /// what a resync needs, since there the replica is the suspect part. | 173 | /// what a resync needs, since there the replica is the suspect part. |
| @@ -574,12 +575,12 @@ test "ws reader: split delivery reassembles; server frames arrive unmasked" { | |||
| 574 | 575 | ||
| 575 | test "the resync re-attach quotes the TILE's size, never the grid it learned" { | 576 | test "the resync re-attach quotes the TILE's size, never the grid it learned" { |
| 576 | // The passivity contract, pinned at the WS WRITE SEAM: a wall tile | 577 | // The passivity contract, pinned at the WS WRITE SEAM: a wall tile |
| 577 | // attaches at 1x1, learns the authoritative 80x24 grid from the | 578 | // attaches at 0x0, learns the authoritative 80x24 grid from the |
| 578 | // snapshot, and then hits a garbled delta. The re-attach that follows | 579 | // snapshot, and then hits a garbled delta. The re-attach that follows |
| 579 | // must still say 1x1 — quoting the learned grid would be an attach at | 580 | // must still claim nothing — quoting the learned grid would be an |
| 580 | // a differing size, which claims the shared session and repaints every | 581 | // attach at a differing size, which claims the shared session and |
| 581 | // other client, i.e. the exact opposite of the contract scenario (a) | 582 | // repaints every other client, i.e. the exact opposite of what a |
| 582 | // exists to hold. | 583 | // passive tile is for. |
| 583 | const alloc = std.testing.allocator; | 584 | const alloc = std.testing.allocator; |
| 584 | const fds = try std.posix.pipe(); | 585 | const fds = try std.posix.pipe(); |
| 585 | defer std.posix.close(fds[0]); | 586 | defer std.posix.close(fds[0]); |
web/mux.js
| Old | New | ||
|---|---|---|---|
| @@ -427,11 +427,15 @@ class Tile { | |||
| 427 | } | 427 | } |
| 428 | } | 428 | } |
| 429 | sendAttach(fresh) { | 429 | sendAttach(fresh) { |
| 430 | // THE PASSIVITY CONTRACT (spec amendment 1): a wall tile attaches at | 430 | // THE PASSIVITY CONTRACT: an unzoomed wall tile attaches at 0x0, which |
| 431 | // 1x1. The daemon refuses the degenerate size, answers a unicast | 431 | // is how the CLI wall's stripes and muxa spell the same thing — no size |
| 432 | // snapshot carrying the true grid, and the slot stays 0x0 forever — | 432 | // claim at all. The daemon answers a unicast snapshot carrying the true |
| 433 | // this tile can never move the shared session. Only the zoomed tile | 433 | // grid and the slot stays 0x0 forever, so this tile can never move the |
| 434 | // claims its real size. | 434 | // shared session. Only the zoomed tile claims its real size. |
| 435 | // | ||
| 436 | // 0x0 rather than a small size on purpose: 1x1 is a size a client on a | ||
| 437 | // tiny terminal genuinely sends, so it cannot mean "no claim" without | ||
| 438 | // also meaning "this client wants 1x1" (server.zig attachOrCreate). | ||
| 435 | // ONE gate for every attach, wherever it comes from — the `up` | 439 | // ONE gate for every attach, wherever it comes from — the `up` |
| 436 | // control message included. A tile that gave up on replaying must | 440 | // control message included. A tile that gave up on replaying must |
| 437 | // not be talked back into asking for the same frame again. | 441 | // not be talked back into asking for the same frame again. |
| @@ -441,8 +445,8 @@ class Tile { | |||
| 441 | this.cancelScrollRequest(); | 445 | this.cancelScrollRequest(); |
| 442 | this.clearSelection(); | 446 | this.clearSelection(); |
| 443 | if (this.replayDead) return; | 447 | if (this.replayDead) return; |
| 444 | const cols = this.zoomed ? this.zoomCols() : 1; | 448 | const cols = this.zoomed ? this.zoomCols() : 0; |
| 445 | const rows = this.zoomed ? this.zoomRows() : 1; | 449 | const rows = this.zoomed ? this.zoomRows() : 0; |
| 446 | const n = this.core.mux_attach_payload(cols, rows, fresh ? 1 : 0); | 450 | const n = this.core.mux_attach_payload(cols, rows, fresh ? 1 : 0); |
| 447 | if (n > 0) { | 451 | if (n > 0) { |
| 448 | // The session name is transport dressing, appended AFTER the payload | 452 | // The session name is transport dressing, appended AFTER the payload |
| @@ -1637,11 +1641,12 @@ function buildAddTile() { | |||
| 1637 | const body = await res.json().catch(() => null); | 1641 | const body = await res.json().catch(() => null); |
| 1638 | input.value = ''; | 1642 | input.value = ''; |
| 1639 | await refetchWall(); | 1643 | await refetchWall(); |
| 1640 | // A wall tile attaches 1x1 passive and the daemon refuses to CREATE a | 1644 | // A wall tile attaches passive at 0x0 and the daemon refuses to |
| 1641 | // session at that size, so a tile added for a session that does not | 1645 | // CREATE a session for a client claiming no size, so a tile added for |
| 1642 | // exist yet would sit on a badge that never resolves. Zoom attaches at | 1646 | // a session that does not exist yet would sit on a badge that never |
| 1643 | // the real size, and that is what creates it — "new session here" has | 1647 | // resolves. Zoom attaches at the real size, and that is what creates |
| 1644 | // to land the user in the shell they asked for. | 1648 | // it — "new session here" has to land the user in the shell they |
| 1649 | // asked for. | ||
| 1645 | const fresh = body === null ? null : tilesById.get(body.id); | 1650 | const fresh = body === null ? null : tilesById.get(body.id); |
| 1646 | if (fresh) { | 1651 | if (fresh) { |
| 1647 | await fresh.startup; // refetchWall fires start(), it does not await it | 1652 | await fresh.startup; // refetchWall fires start(), it does not await it |
web/verify.js
| Old | New | ||
|---|---|---|---|
| @@ -2548,7 +2548,7 @@ async function main() { | |||
| 2548 | // shell never wanted them (it cannot hand-assemble a u64 anyway), so | 2548 | // shell never wanted them (it cannot hand-assemble a u64 anyway), so |
| 2549 | // the payload IS the interface, and what asserts on it is this. | 2549 | // the payload IS the interface, and what asserts on it is this. |
| 2550 | const resumeArgs = () => { | 2550 | const resumeArgs = () => { |
| 2551 | e.mux_attach_payload(1, 1, 0); | 2551 | e.mux_attach_payload(0, 0, 0); |
| 2552 | const b = outBytes(); | 2552 | const b = outBytes(); |
| 2553 | return { seq: b.readBigUInt64LE(4), epoch: b.readBigUInt64LE(12) }; | 2553 | return { seq: b.readBigUInt64LE(4), epoch: b.readBigUInt64LE(12) }; |
| 2554 | }; | 2554 | }; |
| @@ -2755,11 +2755,11 @@ async function main() { | |||
| 2755 | clientAction.selection, | 2755 | clientAction.selection, |
| 2756 | ); | 2756 | ); |
| 2757 | 2757 | ||
| 2758 | // --- attach payload before any state: quotes (0,0); wall spelling 1x1 --- | 2758 | // --- attach payload before any state: quotes (0,0); wall spelling 0x0 --- |
| 2759 | check('attach len', e.mux_attach_payload(1, 1, 0), 20); | 2759 | check('attach len', e.mux_attach_payload(0, 0, 0), 20); |
| 2760 | let att = outBytes(); | 2760 | let att = outBytes(); |
| 2761 | check('attach cols', att.readUInt16LE(0), 1); | 2761 | check('attach cols', att.readUInt16LE(0), 0); |
| 2762 | check('attach rows', att.readUInt16LE(2), 1); | 2762 | check('attach rows', att.readUInt16LE(2), 0); |
| 2763 | check('attach seq0', att.readBigUInt64LE(4), 0n); | 2763 | check('attach seq0', att.readBigUInt64LE(4), 0n); |
| 2764 | check('attach epoch0', att.readBigUInt64LE(12), 0n); | 2764 | check('attach epoch0', att.readBigUInt64LE(12), 0n); |
| 2765 | 2765 | ||
| @@ -2802,6 +2802,10 @@ async function main() { | |||
| 2802 | // --- attach after state: quotes what we hold; fresh=1 re-quotes zero --- | 2802 | // --- attach after state: quotes what we hold; fresh=1 re-quotes zero --- |
| 2803 | e.mux_attach_payload(80, 24, 0); | 2803 | e.mux_attach_payload(80, 24, 0); |
| 2804 | att = outBytes(); | 2804 | att = outBytes(); |
| 2805 | // The size really is encoded and not merely left zero — which the wall | ||
| 2806 | // spelling above, being 0x0, can no longer tell you on its own. | ||
| 2807 | check('attach cols held', att.readUInt16LE(0), 80); | ||
| 2808 | check('attach rows held', att.readUInt16LE(2), 24); | ||
| 2805 | check('attach seq held', att.readBigUInt64LE(4), 8n); | 2809 | check('attach seq held', att.readBigUInt64LE(4), 8n); |
| 2806 | check('attach epoch held', att.readBigUInt64LE(12), 0xabcdn); | 2810 | check('attach epoch held', att.readBigUInt64LE(12), 0xabcdn); |
| 2807 | e.mux_attach_payload(80, 24, 1); | 2811 | e.mux_attach_payload(80, 24, 1); |