a73x

76fd930e

refactor: the link layer's docs name the failure, not the incident

a73x   2026-08-30 19:40

Commit message
refactor: the link layer's docs name the failure, not the incident

56 essays to 15 in client.zig. Target and Transport fields say what they
decide and what goes wrong when they are wrong; the paragraphs restating
which bug a test was written for, what an earlier cap measured, and how
three ssh runs became one are gone.

3301 -> 3045 lines, 1135 -> 879 comment lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSUFuYHqU9wr4J5NC8EkWV

docscheck.blocks
Old New
@@ -1,7 +1,7 @@
1 askpass.zig 10 1 askpass.zig 10
2 client_core_wasm_check.zig 0 2 client_core_wasm_check.zig 0
3 client_core.zig 1 3 client_core.zig 1
4 client.zig 56 4 client.zig 15
5 cmd.zig 2 5 cmd.zig 2
6 delta.zig 8 6 delta.zig 8
7 docscheck.zig 4 7 docscheck.zig 4
src/client/client.zig
Old New
@@ -1,17 +1,11 @@
1 //! The LINK, and nothing above it. 1 //! The LINK, and nothing above it: how a client reaches a daemon and what it
2 //! says on arrival. The `Target` grammar, the dial (unix socket, `--via` pipe,
3 //! direct QUIC, the ssh→QUIC handoff), the words a failed dial exits with, the
4 //! attach frame, the reconnect backoff, and the pure answers a chord needs.
2 //! 5 //!
3 //! How a client reaches a daemon and what it says on arrival: the `Target` 6 //! Not a SESSION. There is ONE interaction loop and it is wallview.zig's tile
4 //! grammar, the dial (unix socket, `--via` pipe, direct QUIC, the ssh→QUIC 7 //! pump. What crosses the seam is `Transport` — opened by whoever holds the
5 //! handoff), the vocabulary a dial that never came up exits with, the attach 8 //! tty, `adopt`ed by the thread that will own it — and nothing else.
6 //! frame, the reconnect backoff, the wall-file record an attach earns, and
7 //! the pure answers a `Ctrl-\ c`/`n`/`p` needs out of a `sessions_reply`.
8 //!
9 //! Not a SESSION. There is ONE interaction loop and it is wallview.zig's
10 //! tile pump, driving the entry tile exactly as every other; a converged
11 //! binary with a second copy of that loop linked here is not converged. What
12 //! crosses the seam is `Transport` — opened by whoever holds the tty,
13 //! `adopt`ed by the thread that will own it — plus the pure helpers the pump
14 //! asks for by name. Everything at the terminal is interact.zig's.
15 const std = @import("std"); 9 const std = @import("std");
16 const Replica = @import("term").replica.Replica; 10 const Replica = @import("term").replica.Replica;
17 const proto = @import("term").protocol; 11 const proto = @import("term").protocol;
@@ -59,13 +53,9 @@ pub fn validPick(pick: ?[]const u8) ?SessionName {
59 return .of(name); 53 return .of(name);
60 } 54 }
61 55
62 /// The name `Ctrl-\ c` creates: the lowest non-negative integer that is not 56 /// The name `Ctrl-\ c` creates: the lowest non-negative integer not already a
63 /// already a session name. `list` is a `sessions_reply` payload — names 57 /// session name, out of a `sessions_reply` payload. Numbering rather than
64 /// separated by '\n'. Names that are not integers (`work`, `dev`) are 58 /// `new-1` because the default session is already called "0".
65 /// simply names that do not match a candidate, so they narrow nothing.
66 ///
67 /// Numbering rather than `new-1`, `new-2` because the default session is
68 /// already called "0": the series a user sees stays one series.
69 pub fn nextFreeName(out: *[proto.session_name_max]u8, list: []const u8) []const u8 { 59 pub fn nextFreeName(out: *[proto.session_name_max]u8, list: []const u8) []const u8 {
70 // Each existing name can rule out at most one candidate, so the first 60 // Each existing name can rule out at most one candidate, so the first
71 // free number is somewhere in 0..count — no cap constant needed, and in 61 // free number is somewhere in 0..count — no cap constant needed, and in
@@ -89,24 +79,16 @@ pub fn nextFreeName(out: *[proto.session_name_max]u8, list: []const u8) []const
89 } 79 }
90 80
91 /// Which chord is waiting for the daemon to answer. One field rather than a 81 /// Which chord is waiting for the daemon to answer. One field rather than a
92 /// flag per chord: a client can only have one question outstanding, so the 82 /// flag per chord, so "one question outstanding" is a fact and not a rule.
93 /// enum makes that a fact instead of a rule every new chord must obey. 83 /// They share the wait because they share the deadline: each is a verb an
94 /// 84 /// older daemon simply does not hear.
95 /// `.new` waits on a `sessions_reply`; `.end`/`.end_force` on an
96 /// `end_reply`. They share the wait because they share the deadline: each
97 /// is a verb an older daemon simply does not hear.
98 pub const SwitchIntent = enum { none, new, end, end_force }; 85 pub const SwitchIntent = enum { none, new, end, end_force };
99 86
100 /// A chord that has asked the daemon for its session list, and the moment 87 /// A chord that has asked the daemon for its session list, and when it stops
101 /// it stops waiting for the answer. 88 /// waiting. The DEADLINE is why this is a struct: a daemon older than
102 /// 89 /// `sessions_req` drops frames it does not recognise, so the question is never
103 /// The deadline is the whole reason this is a struct rather than the bare 90 /// heard — and an intent with no expiry stays armed, making every later chord
104 /// enum. A daemon older than `sessions_req` (0x0c) drops frames it does not 91 /// a silent no-op the user blames on their keyboard.
105 /// recognise — `else => {}` in its dispatch — so the question is not
106 /// refused, it is never heard. An intent with no expiry then stays armed
107 /// for the rest of the session: every `Ctrl-\ c/n/p` after it is a silent
108 /// no-op, and the user is given no reason to suspect the daemon rather than
109 /// their keyboard.
110 pub const PendingSwitch = struct { 92 pub const PendingSwitch = struct {
111 /// How long a chord waits. Far longer than any round trip a switch is 93 /// How long a chord waits. Far longer than any round trip a switch is
112 /// usable over, and short enough that the keystroke is still in the 94 /// usable over, and short enough that the keystroke is still in the
@@ -186,11 +168,9 @@ pub const QuicTarget = struct {
186 deadline_ms: u32 = handoff.deadline_ms, 168 deadline_ms: u32 = handoff.deadline_ms,
187 }; 169 };
188 170
189 /// The bare-HOST recipe: everything a (re)connect needs to run the 171 /// The bare-HOST recipe: everything a (re)connect needs to run the ssh→QUIC
190 /// ssh→QUIC handoff again. It lives at the Transport layer, not in 172 /// handoff again. At the Transport layer so the reconnect loop re-runs the
191 /// mux_main, precisely so the reconnect loop re-runs the WHOLE flow — a 173 /// WHOLE flow — a daemon restart invalidates the cached port.
192 /// daemon restart invalidates the cached port, and only a fresh ssh fetch
193 /// can heal that.
194 pub const AskPass = struct { sock: []const u8, exe: []const u8 }; 174 pub const AskPass = struct { sock: []const u8, exe: []const u8 };
195 175
196 pub const HandoffTarget = struct { 176 pub const HandoffTarget = struct {
@@ -214,61 +194,36 @@ pub const HandoffTarget = struct {
214 deadline_ms: u32 = handoff.deadline_ms, 194 deadline_ms: u32 = handoff.deadline_ms,
215 idle_ms: u32 = quic_idle_ms_default, 195 idle_ms: u32 = quic_idle_ms_default,
216 /// Whether a USER asked for this dial: the entry attach and a picker 196 /// Whether a USER asked for this dial: the entry attach and a picker
217 /// birth, never a poll, a reconnect or a restored tile. The two things 197 /// birth, never a poll, a reconnect or a restored tile. It decides two
218 /// `asked` decides: 198 /// things.
219 /// 199 ///
220 /// SAY SO when the session falls back to ssh. The line reports a 200 /// SAY SO when the session falls back to ssh — but only once, because a
221 /// choice the user has not seen made, and a reconnect re-runs this 201 /// reconnect re-runs this recipe forever and a line per retry would scroll
222 /// recipe forever against dropped UDP: one line per retry would scroll 202 /// a live session's stderr onto the alternate screen.
223 /// a live session's stderr into the alternate screen and corrupt the
224 /// paint, to say what the [reconnecting] banner is already saying.
225 /// 203 ///
226 /// PICK THE ARGV — `asked_argv` rather than `ssh_argv`, unless there is 204 /// PICK THE ARGV: `asked_argv` rather than `ssh_argv`, unless there is
227 /// no `asked_argv`, which is a target nothing can start from. The two 205 /// none. The two are one ssh line a flag apart, so the far end decides
228 /// are the same ssh line one flag apart, so the far end decides whether 206 /// whether to start — and a poll's bare word cannot undo a `mux d stop`.
229 /// to start and answers in the same run; a wall polls every listed host
230 /// once a second, and its bare word cannot start anything to undo a
231 /// `mux d stop` with.
232 /// 207 ///
233 /// The trade, stated plainly: a session that degrades to ssh mid-life 208 /// Defaults to the harmless half: a missing start is a `mux HOST` that
234 /// says nothing at all about it. The user who wants to know can detach 209 /// says so, a spurious one is a daemon on someone else's box.
235 /// and reattach, which reports it on the next attach; the alternative
236 /// is noise on every backoff cycle for as long as the link stays bad.
237 ///
238 /// Defaults to the harmless half: a dial path that forgets the field
239 /// gets it wrong SILENTLY either way, and of the two silences a missing
240 /// start is a `mux HOST` that says so, while a spurious one is a daemon
241 /// on someone else's box that nothing reports at all.
242 asked: bool = false, 210 asked: bool = false,
243 /// Whether the caller is a human at a bare prompt who is owed ssh's 211 /// Whether the caller is a human at a bare prompt owed ssh's narration as
244 /// narration as it happens: relay every stderr byte to mux's own fd 2. 212 /// it happens: relay every stderr byte to mux's own fd 2. The remote's
245 /// The remote's `mux d endpoint: starting\u{2026}` progress, a dot per 213 /// progress rides that stderr, and the entry dial's user is sitting
246 /// interval and an up-line, ride that stderr, as do ssh's own 214 /// through the wait it describes.
247 /// diagnostics, and the entry dial's user is sitting through the wait
248 /// they describe.
249 ///
250 /// Nobody else relays. The bytes are READ either way — that is
251 /// `Reason`, and it is what a picker row quotes — but under a wall's
252 /// alternate screen they would sit over tiles and rails, where the
253 /// tile's own `connecting` label is the narration.
254 /// 215 ///
255 /// Defaults to the harmless half, the way `asked` does: a dial path 216 /// Nobody else relays. The bytes are READ either way — that is `Reason` —
256 /// that forgets this field is silent, and of the two silences a 217 /// but under a wall's alternate screen they would sit over tiles and
257 /// missing line is a wait that says nothing while a spurious one 218 /// rails. Defaults to the harmless half, as `asked` does: a spurious line
258 /// corrupts a paint nobody can repair from. 219 /// corrupts a paint nobody can repair from.
259 narrate: bool = false, 220 narrate: bool = false,
260 /// Where this dial's ssh sends its prompts, and what carries them. 221 /// Where this dial's ssh sends its prompts, and what carries them. Null is
261 /// 222 /// "ssh keeps its own": the entry dial's user has a /dev/tty right there,
262 /// Null is "ssh keeps its own", which is the OLD behaviour and a 223 /// and a poll spells `BatchMode` and asks nothing. A wall dial that forgets
263 /// visible one: the entry dial's user is at a bare prompt with a 224 /// these reads /dev/tty under the alternate screen — a wait nobody can see.
264 /// /dev/tty right there, and a poll spells `BatchMode` and asks 225 /// Two fields because `spawn.selfExe` is under `src/cli/`, which a client
265 /// nothing. A wall dial that forgets these two reads /dev/tty under 226 /// module may not name; the wall is tui and fills both in one place.
266 /// the alternate screen — a wait nobody can see, which is loud in the
267 /// way a wrong default has to be.
268 ///
269 /// Two fields because `spawn.selfExe` lives under `src/cli/`, which a
270 /// client module may not name (folder rule 1). The wall is tui and may,
271 /// so it fills both, together, in one place.
272 ask_sock: ?[]const u8 = null, 227 ask_sock: ?[]const u8 = null,
273 ask_exe: []const u8 = "", 228 ask_exe: []const u8 = "",
274 229
@@ -305,23 +260,15 @@ pub const Target = union(enum) {
305 quic: QuicTarget, 260 quic: QuicTarget,
306 hand: HandoffTarget, 261 hand: HandoffTarget,
307 262
308 /// The one road from a spelling's `hosts.Spec` to the dial it names: 263 /// The one road from a spelling's `hosts.Spec` to the dial it names. Every
309 /// the wall's host lines and the hub's tiles resolve here, and the 264 /// slice is OWNED by `alloc`: the spelling a caller parsed may be a scratch
310 /// CLI's `.host` arm shares `fromRecipe`. What can fail is 265 /// buffer, and a target outlives the read that made it. `asked` is
311 /// `SpecError`; the word for it is each door's own. 266 /// required, never defaulted — see `HandoffTarget.fromRecipe`.
312 ///
313 /// Every slice is OWNED by `alloc` — the spelling a caller parsed may
314 /// be a scratch buffer, and a target outlives the read that made it.
315 ///
316 /// `asked` is required, never defaulted: see `HandoffTarget.fromRecipe`.
317 pub fn fromSpec(alloc: std.mem.Allocator, spec: hosts.Spec, key: ?[]const u8, idle_ms: u32, asked: bool) SpecError!Target { 267 pub fn fromSpec(alloc: std.mem.Allocator, spec: hosts.Spec, key: ?[]const u8, idle_ms: u32, asked: bool) SpecError!Target {
318 return switch (spec) { 268 return switch (spec) {
319 // No length guard here, deliberately: `Address.initUnix` 269 // No length guard: `Address.initUnix` answers `NameTooLong` and
320 // answers `NameTooLong` and truncates nothing, so a doomed 270 // truncates nothing, so a doomed path fails as a dial like any
321 // path fails as a dial like any other and the picker row 271 // other. The one BINDER is where a path is refused by name.
322 // quotes the kernel's own word. The one binder is where a
323 // path is refused by name, because it is the only place a
324 // refusal saves anything.
325 .sock => |path| .{ .sock = try alloc.dupe(u8, path) }, 272 .sock => |path| .{ .sock = try alloc.dupe(u8, path) },
326 .host => |h| blk: { 273 .host => |h| blk: {
327 const hd = try alloc.dupe(u8, h); 274 const hd = try alloc.dupe(u8, h);
@@ -398,11 +345,9 @@ pub const Transport = struct {
398 /// a tile shows `connecting`, and the row quotes the POLL's copy — so 345 /// a tile shows `connecting`, and the row quotes the POLL's copy — so
399 /// a reader that appears is free to define what it means. 346 /// a reader that appears is free to define what it means.
400 reason: handoff.Reason = .{}, 347 reason: handoff.Reason = .{},
401 /// `HandoffTarget.narrate`, carried past the dial so a link that came 348 /// `HandoffTarget.narrate`, carried past the dial so a link that came up on
402 /// up on the pipe goes on relaying. The entry dial CLEARS it when the 349 /// the pipe goes on relaying. The entry dial CLEARS it when the wall takes
403 /// wall takes the screen (`wallview.runAttach`): past that point fd 2 350 /// the screen: past that, fd 2 is the alternate screen.
404 /// is the alternate screen, and a late `Connection to box closed by
405 /// remote host.` would be exactly the foreign writer this removed.
406 narrate: bool = false, 351 narrate: bool = false,
407 352
408 /// The handoff's coordination ssh, stderr included. `ask` non-null is 353 /// The handoff's coordination ssh, stderr included. `ask` non-null is
@@ -424,11 +369,8 @@ pub const Transport = struct {
424 ask: ?AskPass, 369 ask: ?AskPass,
425 ) !std.process.Child { 370 ) !std.process.Child {
426 // The product runs `ssh` and the user's own `--via` program, and 371 // The product runs `ssh` and the user's own `--via` program, and
427 // neither is worth a shell's expansions between us and it. 372 // neither is worth a shell's expansions between us and it. `argv` need
428 // 373 // not outlive the call: `std.process.Child` copies it before the fork.
429 // `argv` need not outlive the call: `std.process.Child` copies it
430 // into its own arena before the fork. "spawnPipe: the child is
431 // exec'd from a copy" is that claim, asserted.
432 var child = std.process.Child.init(argv, alloc); 374 var child = std.process.Child.init(argv, alloc);
433 child.stdin_behavior = .Pipe; 375 child.stdin_behavior = .Pipe;
434 child.stdout_behavior = .Pipe; 376 child.stdout_behavior = .Pipe;
@@ -508,11 +450,9 @@ pub const Transport = struct {
508 /// waits: stdin in the CLI, -1 (no abort channel) in a hub that 450 /// waits: stdin in the CLI, -1 (no abort channel) in a hub that
509 /// has no terminal — its stray fd 0 must never be read. 451 /// has no terminal — its stray fd 0 must never be read.
510 abort_fd: std.posix.fd_t, 452 abort_fd: std.posix.fd_t,
511 /// What the handoff leaves behind: ssh's last line, so a dial that 453 /// What the handoff leaves behind: ssh's last line, so a failed dial is
512 /// failed is reported in ssh's own words, and the pid of the ssh 454 /// reported in ssh's own words, and the pid that said it, so a prompt
513 /// that said it, so a prompt that ssh raised can be answered — and 455 /// can be answered against the one dial it belongs to.
514 /// a refusal remembered — against the one dial it belongs to. Null
515 /// is every caller that has nowhere to show either.
516 dial: ?*handoff.Dial, 456 dial: ?*handoff.Dial,
517 ) !Transport { 457 ) !Transport {
518 switch (target) { 458 switch (target) {
@@ -601,69 +541,42 @@ pub const Transport = struct {
601 } 541 }
602 }, 542 },
603 .spawn_ssh => blk: { 543 .spawn_ssh => blk: {
604 // ONE run, and `asked` is the whole of what picks it. The 544 // ONE run, picked by `asked` alone: the asking word ensures
605 // asking word ensures a daemon on the far side and 545 // a daemon and announces on the same stdout, so there is no
606 // announces on the same stdout, so there is no refusal 546 // refusal to read and no second run. `and len > 0` keeps
607 // for this side to read, no exit code to tell from ssh's 547 // `asked_argv`'s doc true — an empty argv is not a no-op at
608 // own 255, and no second run. A read spells the bare 548 // the exec, the child null-unwraps `argv[0]` and dies.
609 // word, which starts nothing — the rule holds by argv
610 // rather than by a branch.
611 //
612 // `and len > 0` is what keeps `asked_argv`'s doc true:
613 // empty means "nothing to start", and an empty argv is
614 // not a no-op at the exec — the forked child null-unwraps
615 // argv[0] and dies, which arrives here as an announce
616 // that never came. Falling back to the reading word makes
617 // such a target a dial that starts nothing, which is what
618 // the field says it is.
619 const argv = if (h.asked and h.asked_argv.len > 0) h.asked_argv else h.ssh_argv; 549 const argv = if (h.asked and h.asked_argv.len > 0) h.asked_argv else h.ssh_argv;
620 child = spawnPipe(alloc, argv, h.askpassFor()) catch |err| { 550 child = spawnPipe(alloc, argv, h.askpassFor()) catch |err| {
621 last_err = err; 551 last_err = err;
622 break :blk .failed; 552 break :blk .failed;
623 }; 553 };
624 // Taken OFF the child: `Child.kill` closes `stderr` 554 // Taken OFF the child: `Child.kill` closes `stderr` with the
625 // with the other pipes, and this fd has to outlive the 555 // other pipes, and this fd outlives the kill on `use_pipe`.
626 // kill on the `use_pipe` path, where the session runs 556 // `if` rather than an unwrap, so a spawn that stopped piping
627 // on and ssh goes on talking. 557 // stderr FAILS the test named for it instead of panicking.
628 //
629 // `if` rather than an unwrap `spawnPipe` makes safe: a
630 // spawn that stopped piping stderr would panic here,
631 // and the test named for the pipe would never get to
632 // say so. Leaving the fd at -1 fails that test instead.
633 if (child.?.stderr) |f| { 558 if (child.?.stderr) |f| {
634 errp.fd = f.handle; 559 errp.fd = f.handle;
635 child.?.stderr = null; 560 child.?.stderr = null;
636 } 561 }
637 // Recorded HERE rather than on the way out, because the 562 // Recorded HERE, not on the way out: the caller that needs it
638 // caller that needs it is the one this call is about to 563 // is the one this call is about to fail, and a refused
639 // fail: a dial whose ssh asked for a password the user 564 // password leaves no transport to read a pid off.
640 // refused has no transport to read a pid off, and the
641 // refusal is keyed on exactly this number.
642 out.ssh_pid = child.?.id; 565 out.ssh_pid = child.?.id;
643 break :blk .ok; 566 break :blk .ok;
644 }, 567 },
645 .read_announce => blk: { 568 .read_announce => blk: {
646 // Bounded by nothing but ssh itself — the same trust 569 // ssh owns the terminal while it runs: its prompts read
647 // `--via` already extends to the command it spawns. 570 // /dev/tty, and a competing stdin reader here steals whole
648 // 571 // cooked lines — auth fails on an empty password and the
649 // ssh owns the terminal while it runs: password and 572 // stolen line replays INTO the session via carry. On a first
650 // host-key prompts read /dev/tty, and a competing stdin 573 // attach the tty is still cooked, so Ctrl-C aborts the group;
651 // reader here steals whole cooked lines from them — auth 574 // the byte-read abort is only real on reconnect.
652 // fails on an empty password, and the stolen line replays
653 // INTO THE SESSION via carry. On a first attach
654 // (carry != null) the tty is still cooked, so Ctrl-C's
655 // SIGINT already aborts this whole foreground group; the
656 // byte-read abort is only real on reconnect, where raw
657 // mode is live and carry is null by policy. Unconsumed
658 // type-ahead is not lost: it waits in the kernel's tty
659 // buffer for the session's first read.
660 const announce_abort_fd: std.posix.fd_t = if (carry != null) -1 else abort_fd; 575 const announce_abort_fd: std.posix.fd_t = if (carry != null) -1 else abort_fd;
661 const got = readAnnounceAbortable(child.?.stdout.?.handle, alloc, null, announce_abort_fd, &errp) catch |err| { 576 const got = readAnnounceAbortable(child.?.stdout.?.handle, alloc, null, announce_abort_fd, &errp) catch |err| {
662 // ssh says why on its way out, and its stdout's 577 // ssh says why on its way out, and its stdout's EOF can
663 // EOF can be the same poll pass as the last of it. 578 // be the same poll pass as the last of it: read what is
664 // Read what is left BEFORE this error becomes the 579 // left BEFORE this error becomes the caller's answer.
665 // caller's answer, or the reason is the sentence
666 // ssh printed one syscall too late.
667 errp.drainReady(); 580 errp.drainReady();
668 last_err = err; 581 last_err = err;
669 break :blk .announce_failed; 582 break :blk .announce_failed;
@@ -671,20 +584,11 @@ pub const Transport = struct {
671 break :blk if (got) |ep| .{ .announced = ep } else .none; 584 break :blk if (got) |ep| .{ .announced = ep } else .none;
672 }, 585 },
673 .write_cache => |ep| blk: { 586 .write_cache => |ep| blk: {
674 // A cache write that fails costs a cold attach next time 587 // A failed cache write costs one cold attach and nothing
675 // and nothing else, so it is not worth a line of the 588 // else. Serialized because two tiles naming one host share a
676 // user's attention. 589 // cache path and `writeCache` truncates in place: unserialized
677 // 590 // writers tear the line, and the likelier miss is the loser
678 // Serialized because openHandoff is no longer 591 // writing the STALER endpoint.
679 // single-threaded: the web hub runs one of these per
680 // tile, and two tiles naming the same host share a cache
681 // path. writeCache truncates in place — there is no
682 // rename — so unserialized writers can tear the line. A
683 // torn one costs an attach, not a host: readCache refuses
684 // it and the next cold path rewrites it. The likelier
685 // miss is the loser writing the STALER endpoint, a cold
686 // attach that looks like a bug — and a mutex is cheaper
687 // than the afternoon.
688 cache_write_mu.lock(); 592 cache_write_mu.lock();
689 defer cache_write_mu.unlock(); 593 defer cache_write_mu.unlock();
690 handoff.writeCache(h.cache_path.?, ep) catch {}; 594 handoff.writeCache(h.cache_path.?, ep) catch {};
@@ -850,13 +754,10 @@ pub const Transport = struct {
850 } 754 }
851 } 755 }
852 756
853 /// Idempotent, and it has to be: a re-dial releases the dead transport 757 /// Idempotent, and it has to be: a re-dial releases the dead transport on
854 /// on entry, and if the user then aborts, the pump's `defer 758 /// entry, and an abort then closes the same value again through the pump's
855 /// transport.close()` closes the very same value again. A second 759 /// `defer`. A second `close(2)` on a stale fd is EBADF, which `std.posix`
856 /// close(2) on a stale fd is EBADF, which std.posix maps to 760 /// maps to `unreachable` — a panic that `--sock` does not hide.
857 /// `unreachable` — a panic, not an error. `--via` hides that (killing a
858 /// reaped child is harmless), `--sock` does not, and `--sock` is exactly
859 /// the locally-killed-daemon case.
860 pub fn close(self: *Transport) void { 761 pub fn close(self: *Transport) void {
861 if (self.conn.r == -1) return; // already released 762 if (self.conn.r == -1) return; // already released
862 defer self.conn = .{ .r = -1, .w = -1 }; 763 defer self.conn = .{ .r = -1, .w = -1 };
@@ -922,17 +823,10 @@ fn waitReady(
922 cl.pump(); 823 cl.pump();
923 if (cl.isReady()) return; 824 if (cl.isReady()) return;
924 if (cl.dead) return error.QuicHandshakeFailed; 825 if (cl.dead) return error.QuicHandshakeFailed;
925 // The abort fd (stdin, in the CLI) is watched alongside the 826 // The abort fd is watched alongside the socket, and it has to be: this
926 // socket, and it has to be. This wait runs INSIDE Transport.open, 827 // wait runs INSIDE `Transport.open`, where nothing else is looking for
927 // where for its whole length nothing else is looking for the 828 // the abort key, and a re-dial's terminal is raw so `Ctrl-\` is the
928 // abort key — and during a re-dial the terminal is in raw mode, so 829 // only way out. The uncapped retry loop is justified by that key.
929 // Ctrl-C is just a byte and Ctrl-\ is the only way out. Watching only
930 // the socket left the user with no way to stop for as long as the
931 // handshake bound allows: measured at 14.6s on the default idle
932 // timeout, against an endpoint that was never going to answer. The
933 // uncapped retry loop is justified by the user having an abort key,
934 // so an abort key that stops working mid-handshake takes the
935 // justification with it.
936 var fds = abortPoll(cl.pollFd(), abort_fd, watch_stdin); 830 var fds = abortPoll(cl.pollFd(), abort_fd, watch_stdin);
937 _ = std.posix.poll(&fds, cl.timeoutMs(50)) catch break; 831 _ = std.posix.poll(&fds, cl.timeoutMs(50)) catch break;
938 // On a first attach the bytes drained here are the user's first 832 // On a first attach the bytes drained here are the user's first
@@ -954,11 +848,10 @@ fn abortPoll(main_fd: std.posix.fd_t, abort_fd: std.posix.fd_t, watching: bool)
954 }; 848 };
955 } 849 }
956 850
957 /// Drain the abort fd; false means stop watching it, because a closed one 851 /// Drain the abort fd; false means stop watching it, since a closed one stays
958 /// stays readable forever and the caller's wait would spin hot. Bytes that 852 /// readable forever and the wait would spin hot. Non-abort bytes are the
959 /// are not the abort key are the caller's policy: `carry` keeps them, null 853 /// caller's policy: `carry` keeps them, null drops them, and a reconnect
960 /// drops them — a reconnect drops, since replaying a burst of stale 854 /// drops — replaying stale keystrokes on resume is worse than losing them.
961 /// keystrokes on resume is worse than losing them.
962 fn drainAbortFd(abort_fd: std.posix.fd_t, alloc: std.mem.Allocator, carry: ?*std.ArrayList(u8)) error{UserAbort}!bool { 855 fn drainAbortFd(abort_fd: std.posix.fd_t, alloc: std.mem.Allocator, carry: ?*std.ArrayList(u8)) error{UserAbort}!bool {
963 var buf: [1024]u8 = undefined; 856 var buf: [1024]u8 = undefined;
964 const n = std.posix.read(abort_fd, &buf) catch 0; 857 const n = std.posix.read(abort_fd, &buf) catch 0;
@@ -1002,12 +895,9 @@ const ErrPipe = struct {
1002 895
1003 /// Everything the pipe holds NOW, without waiting for more. 896 /// Everything the pipe holds NOW, without waiting for more.
1004 fn drainReady(self: *ErrPipe) void { 897 fn drainReady(self: *ErrPipe) void {
1005 // BYTES, not reads: the bound wants to be "everything a writer 898 // BYTES, not reads: the bound is "everything a dead writer can have
1006 // that has already died can have left behind", which is the pipe's 899 // left behind", which is the pipe's capacity and not a syscall count. A
1007 // capacity and nothing to do with how many syscalls it takes. A 900 // reads-shaped cap left the reason a line from the middle.
1008 // reads-shaped cap read a fixed 8k, so a remote whose ssh logs at
1009 // DEBUG3 and dies fast left the reason a line from the middle. A
1010 // live flooder is still cut off rather than holding a failed dial.
1011 var total: usize = 0; 901 var total: usize = 0;
1012 while (total < pipe_capacity and self.fd >= 0) { 902 while (total < pipe_capacity and self.fd >= 0) {
1013 var fds = [_]std.posix.pollfd{ 903 var fds = [_]std.posix.pollfd{
@@ -1098,18 +988,13 @@ fn announceFailed(err: anyerror) bool {
1098 return false; 988 return false;
1099 } 989 }
1100 990
1101 /// What `wallview.runAttach` says when the open fails, and what it exits 991 /// What `wallview.runAttach` says when the open fails, and what it exits with.
1102 /// with. A pair 992 /// A pair rather than a message, because the abort paths print a line AND exit
1103 /// rather than a bare message because the abort paths are not silent: they 993 /// 0 — two facts that would drift if they lived in two functions.
1104 /// print a line AND exit 0, and splitting those two facts across two
1105 /// functions would let one drift from the other.
1106 pub const OpenFailure = struct { 994 pub const OpenFailure = struct {
1107 /// Points into the buffer the caller passed `openFailure`, and stays 995 /// Points into the buffer passed to `openFailure` and lives only until the
1108 /// valid only until the next `openFailure` call on that same buffer — 996 /// next call on it — EXCEPT the abort paths, whose message is static. Print
1109 /// EXCEPT on the abort paths, whose message is static and outlives it. 997 /// before reusing; hold one across a second call and only some survive.
1110 /// Print it before reusing the buffer and the non-uniformity cannot
1111 /// bite; hold one across a second call and only some of them survive,
1112 /// which is the worse kind of bug to find.
1113 msg: []const u8, 998 msg: []const u8,
1114 /// 1 everywhere except the abort paths, where the user pressed Ctrl-\ 999 /// 1 everywhere except the abort paths, where the user pressed Ctrl-\
1115 /// and stopping when asked is not a failure to exit nonzero over. 1000 /// and stopping when asked is not a failure to exit nonzero over.
@@ -1118,14 +1003,10 @@ pub const OpenFailure = struct {
1118 1003
1119 /// The buffer `wallview.runAttach` hands `openFailure`. 1004 /// The buffer `wallview.runAttach` hands `openFailure`.
1120 /// 1005 ///
1121 /// Not derived, and it cannot be: PATH_MAX would bound the key and socket 1006 /// Not derived, and it cannot be: the longest operands are argv strings whose
1122 /// paths, but the longest operands are argv strings — a `--via` command 1007 /// only ceiling is ARG_MAX, and sizing for that puts a megabyte on the stack to
1123 /// line, a handoff's ssh argv, a `host_port` — whose only ceiling is 1008 /// print one line. A chosen number, made safe by `failedMsg`'s truncation:
1124 /// ARG_MAX, and sizing for that would put a megabyte on the stack to 1009 /// past this length the message clips instead of anything going wrong.
1125 /// print one line. So this is a chosen number, picked to put truncation
1126 /// out of reach of any real command, and it is `failedMsg`'s truncation
1127 /// policy that makes choosing rather than deriving safe: past this length
1128 /// the message clips instead of anything going wrong.
1129 pub const open_err_len = 8192; 1010 pub const open_err_len = 8192;
1130 1011
1131 /// Truncating, not failing: this is the user's only account of why the 1012 /// Truncating, not failing: this is the user's only account of why the
@@ -1141,27 +1022,19 @@ fn failedMsg(buf: []u8, comptime fmt: []const u8, args: anytype) OpenFailure {
1141 /// reported as a failure — and the exit says so too. 1022 /// reported as a failure — and the exit says so too.
1142 const open_aborted: OpenFailure = .{ .msg = "mux: aborted before attaching\n", .exit = 0 }; 1023 const open_aborted: OpenFailure = .{ .msg = "mux: aborted before attaching\n", .exit = 0 };
1143 1024
1144 /// What the entry dial prints and exits with when `Transport.open` fails. 1025 /// What the entry dial prints and exits with when `Transport.open` fails. Pure,
1145 /// Pure so the whole error policy can be pinned the way `lostMsg` is; the 1026 /// so the whole error policy can be pinned; the caller owns only the printing.
1146 /// caller owns only the printing. 1027 /// `err` is `anyerror` by design: open unions error sets from five sources and
1147 /// 1028 /// this classifies by VALUE. The cost is that a misspelled prong falls to
1148 /// `err` is `anyerror` by design, not by laziness: open unions error sets 1029 /// `else` rather than failing to compile — the literal pins below refuse that.
1149 /// from the key loader, the resolver, the QUIC client, ssh's spawn and
1150 /// handoff's parser, and this function classifies by VALUE across all of
1151 /// them. The cost is that a misspelled prong is not a compile error — it
1152 /// just falls to the `else` arm and quietly loses its class. What refuses
1153 /// that is the twelve literal pins below, never the signature.
1154 pub fn openFailure(buf: []u8, target: Target, err: anyerror, reason: []const u8) OpenFailure { 1030 pub fn openFailure(buf: []u8, target: Target, err: anyerror, reason: []const u8) OpenFailure {
1155 return switch (target) { 1031 return switch (target) {
1156 // A key the daemon would also have refused, said in the same 1032 // A key the daemon would also have refused, said in the same
1157 // words, because the user's mistake is the same one. 1033 // words, because the user's mistake is the same one.
1158 .quic => |q| switch (err) { 1034 .quic => |q| switch (err) {
1159 // Only the three key classes go through the shared body. The 1035 // Only the three key classes take the shared body. The `else` below
1160 // `else` below is NOT its catch-all and must not become it: 1036 // is NOT its catch-all: down here an unclassified error is far more
1161 // down here an unclassified error is far more often a dial that 1037 // often a failed dial than an unreadable file, so it names the endpoint.
1162 // failed than a file that would not read, so it names the
1163 // endpoint. The daemon-side callers, whose errors can only have
1164 // come from the load, use the body's fourth sentence instead.
1165 error.KeyFileMissing, 1038 error.KeyFileMissing,
1166 error.KeyFilePermissive, 1039 error.KeyFilePermissive,
1167 error.KeyFileMalformed, 1040 error.KeyFileMalformed,
@@ -1209,21 +1082,11 @@ pub fn openFailure(buf: []u8, target: Target, err: anyerror, reason: []const u8)
1209 "mux: {s} over ssh: {s}\n", 1082 "mux: {s} over ssh: {s}\n",
1210 .{ h.host, reason }, 1083 .{ h.host, reason },
1211 ) else if (announceFailed(err)) 1084 ) else if (announceFailed(err))
1212 // All this observes is that we waited for an announce 1085 // All this observes is that no announce came. Whether ssh
1213 // and did not get one. Whether ssh reached the host is 1086 // reached the host is NOT knowable here: a clean EOF is equally
1214 // NOT knowable from here: a parse failure does prove 1087 // a refused connection, a rejected key, or a remote with no
1215 // something spoke, but a clean EOF is equally what a 1088 // `mux` on PATH. So the line claims only the observation, which
1216 // refused connection, a rejected key, and a remote 1089 // is weakly true where "cannot reach" was strongly false.
1217 // without `mux` on its PATH all look like — the
1218 // shell's complaint goes to stderr and its stdout
1219 // closes clean.
1220 //
1221 // So the line claims only the observation, which is
1222 // weakly true in every one of those cases, where
1223 // "cannot reach" was strongly false in some of them.
1224 //
1225 // Reached only when ssh finished no line at all: with one,
1226 // the arm above quotes it instead.
1227 failedMsg( 1090 failedMsg(
1228 buf, 1091 buf,
1229 "mux: no endpoint announce from {s} over ssh ({s})\n", 1092 "mux: no endpoint announce from {s} over ssh ({s})\n",
@@ -1287,11 +1150,9 @@ pub fn spellingCap(target: Target) usize {
1287 return "--sock ".len + operand + 1 + proto.session_name_max; 1150 return "--sock ".len + operand + 1 + proto.session_name_max;
1288 } 1151 }
1289 1152
1290 // The grid a birth asks for. It is `main.Opts`'s own default — the size 1153 // The grid a birth asks for: `main.Opts`'s own default, the size `mux d start`
1291 // `mux d start` gives session 0 — because a session created for a client 1154 // gives session 0. A session created for a client that claims no size has to be
1292 // that claims no size has to be born at SOMETHING, and the daemon's own 1155 // born at SOMETHING, and the daemon's own answer needs no explaining.
1293 // answer to that question is the one nobody has to explain. Wrong, and a
1294 // restored browser tile comes back at a shape no client ever asked for.
1295 pub const birth_cols: u16 = 80; 1156 pub const birth_cols: u16 = 80;
1296 pub const birth_rows: u16 = 24; 1157 pub const birth_rows: u16 = 24;
1297 1158
@@ -1301,11 +1162,10 @@ pub const birth_rows: u16 = 24;
1301 // the tile reads [refused], which is where it stood before. 1162 // the tile reads [refused], which is where it stood before.
1302 const birth_budget_ms: i64 = 3000; 1163 const birth_budget_ms: i64 = 3000;
1303 1164
1304 /// One bounded wait on a side connection, shared by every ask that opens 1165 /// One bounded wait on a side connection: false means loop again without
1305 /// its own: false means loop again without reading, `error.Timeout` means 1166 /// reading, `error.Timeout` means the budget is spent. No EINTR arm —
1306 /// the budget is spent. No EINTR arm — `std.posix.poll` retries INTR 1167 /// `std.posix.poll` retries INTR itself, and its error set holds only failures
1307 /// itself, and its error set holds only failures the fd never recovers 1168 /// the fd never recovers from.
1308 /// from, so waiting the deadline out on one would be a stall, not a wait.
1309 fn awaitFrames(tr: *Transport, deadline: i64) !bool { 1169 fn awaitFrames(tr: *Transport, deadline: i64) !bool {
1310 const left = deadline - std.time.milliTimestamp(); 1170 const left = deadline - std.time.milliTimestamp();
1311 if (left <= 0) return error.Timeout; 1171 if (left <= 0) return error.Timeout;
@@ -1327,21 +1187,11 @@ pub fn birthSession(
1327 cols: u16, 1187 cols: u16,
1328 rows: u16, 1188 rows: u16,
1329 ) !void { 1189 ) !void {
1330 // `Refused` is the daemon's no — a full table, a name it will not 1190 // `Refused` is the daemon's no, `Timeout` a daemon that never answered,
1331 // make; `Timeout` is a daemon that never answered; `Transport` is a 1191 // `Transport` a wire that broke — they differ only in what a log can say.
1332 // wire that broke under the question. The caller forwards the refusal 1192 // The budget below is the ANSWER budget and starts after the dial, since
1333 // it was already holding in every case, so they differ only in what a 1193 // each target bounds its own. A SIDE connection, not the caller's: a tile
1334 // log can say. 1194 // that claimed a size to get its session made would keep claiming it.
1335 //
1336 // The budget below is the ANSWER budget and starts after the dial:
1337 // `Transport.open` is not interruptible from here, and each target
1338 // bounds its own dial already (`QuicTarget.deadline_ms`,
1339 // `HandoffTarget.deadline_ms`; a `.sock` connect is one syscall).
1340 //
1341 // A SIDE connection, not the caller's: a tile that claimed a size to
1342 // get its session made would keep claiming it, and the grid would
1343 // follow whichever browser last reloaded. The birth pays one attach
1344 // and hands the session back at a size nobody is bound to.
1345 var tr = try Transport.open(alloc, target, null, -1, null); 1195 var tr = try Transport.open(alloc, target, null, -1, null);
1346 defer tr.close(); 1196 defer tr.close();
1347 var buf: [proto.attach_max_len]u8 = undefined; 1197 var buf: [proto.attach_max_len]u8 = undefined;
@@ -1399,15 +1249,10 @@ pub fn listSessions(
1399 /// ssh's line, so a box that is down says why instead of `unreachable`. 1249 /// ssh's line, so a box that is down says why instead of `unreachable`.
1400 dial: ?*handoff.Dial, 1250 dial: ?*handoff.Dial,
1401 ) ![]const u8 { 1251 ) ![]const u8 {
1402 // A fresh connection per poll: the observer idle deadline and the 1252 // A fresh connection per poll, so the observer idle deadline and the redial
1403 // redial backoff stay the pump's problem, and a `--via` host — an ssh 1253 // backoff stay the pump's problem. Recorded BEFORE the open, because a
1404 // per open — is the case that will make a kept side connection worth 1254 // `hand` target's COST is the target's and not the reply's: an ssh login is
1405 // its state. Measure before adding one. 1255 // spent either way, and only the caller's backoff can stop paying.
1406 // BEFORE the open, because what a `hand` target COST is decided by the
1407 // target, not by the reply — an ssh login is spent either way, and the
1408 // caller's backoff is the only thing that can stop paying. A failed open
1409 // left this untouched, so a host that refuses the login was re-dialled at
1410 // the reachable interval forever.
1411 if (answered) |a| a.* = switch (target) { 1256 if (answered) |a| a.* = switch (target) {
1412 .hand, .via => .pipe, 1257 .hand, .via => .pipe,
1413 .quic => .quic, 1258 .quic => .quic,
@@ -1491,15 +1336,10 @@ pub fn pollDelayMs(link: std.meta.Tag(Link)) u64 {
1491 return if (link == .pipe) host_poll_ms * 10 else host_poll_ms; 1336 return if (link == .pipe) host_poll_ms * 10 else host_poll_ms;
1492 } 1337 }
1493 1338
1494 /// ONE host's session poll, for every front that shows a wall: the CLI 1339 /// ONE host's session poll, for every front that shows a wall, so a tile born
1495 /// wall's `wall_host.Host` and the browser hub's host row both run this 1340 /// in a terminal and one born in a browser come from the same question. Whether
1496 /// loop, so a tile born in a terminal and a tile born in a browser come 1341 /// to keep going and how to wake the reader are the CALLER's; everything else,
1497 /// from the same question asked the same way. 1342 /// including riding out a blip, is the same on both fronts.
1498 ///
1499 /// Whether to keep going and how to wake the reader are the caller's —
1500 /// the wall answers `shared.running and !forgotten` and rings the keyboard;
1501 /// the hub answers "the hub is serving" and applies the list. Everything
1502 /// else, including riding out a blip, is the same on both fronts.
1503 pub const SessionPoll = struct { 1343 pub const SessionPoll = struct {
1504 list_mu: std.Thread.Mutex = .{}, 1344 list_mu: std.Thread.Mutex = .{},
1505 list: [proto.sessions_text_max]u8 = undefined, 1345 list: [proto.sessions_text_max]u8 = undefined,
@@ -1602,11 +1442,9 @@ test "pollDelayMs: a poll that cost an ssh login is asked ten times less often"
1602 } 1442 }
1603 1443
1604 test "SessionPoll.run: a keep that says stop is felt within one sleep slice, not one poll interval" { 1444 test "SessionPoll.run: a keep that says stop is felt within one sleep slice, not one poll interval" {
1605 // The wall's teardown and the picker's `x` both end a poller by 1445 // The wall's teardown and the picker's `x` both end a poller by answering
1606 // answering `keep` with false. A loop that only re-read it once per 1446 // `keep` false. Re-reading it once per `pollDelayMs` would hold the wall's
1607 // `pollDelayMs` would hold the wall's exit for a second per host — so 1447 // exit for a second per host, so the 50 ms slice is the claim.
1608 // the 50 ms slice is the claim, and the socket nobody serves is what
1609 // makes the poll itself fail fast enough to reach the sleep.
1610 var tmp = try TmpDir.make(); 1448 var tmp = try TmpDir.make();
1611 defer tmp.cleanup(); 1449 defer tmp.cleanup();
1612 const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/nobody.sock", .{tmp.path()}); 1450 const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/nobody.sock", .{tmp.path()});
@@ -1715,11 +1553,9 @@ test "Transport.close is idempotent: the abort path closes what a re-dial alread
1715 1553
1716 var transport = try Transport.open(alloc, .{ .sock = sock_path }, null, -1, null); 1554 var transport = try Transport.open(alloc, .{ .sock = sock_path }, null, -1, null);
1717 1555
1718 // A re-dial closes the dead transport at entry; if the user then aborts, 1556 // A re-dial closes the dead transport at entry, and an abort then closes it
1719 // the pump's `defer transport.close()` closes it a second time. Without a 1557 // again through the pump's `defer`. Without a sentinel that is `close(2)` on
1720 // sentinel that is close(2) on a stale fd — EBADF, which std.posix.close 1558 // a stale fd — EBADF, which `std.posix.close` maps to `unreachable`.
1721 // maps to `unreachable`, i.e. a panic, and on --sock (the locally-killed
1722 // daemon case) nothing masks it.
1723 transport.close(); 1559 transport.close();
1724 transport.close(); 1560 transport.close();
1725 transport.close(); 1561 transport.close();
@@ -1756,21 +1592,11 @@ test "connectAgent: a live socket connects, a dead path returns null" {
1756 } 1592 }
1757 1593
1758 test "Transport.open: a --via target yields a pipe, a --sock target an fd" { 1594 test "Transport.open: a --via target yields a pipe, a --sock target an fd" {
1759 // The dispatch decision itself, which nothing else in this file pinned: 1595 // The dispatch decision itself, which nothing else pinned: every other open
1760 // every other open test asserts what the handoff CHOSE, and the 1596 // test asserts what the handoff CHOSE, so swapping open's two arms used to
1761 // idempotence test above opens a `.sock` but only reads the sentinel — 1597 // pass the whole suite. Ordered AFTER the idempotence test, because the
1762 // which a pipe link satisfies exactly as well as an fd one. So swapping 1598 // double close below is safe only while the sentinel holds — and a panic
1763 // the two arms of open's switch used to pass the whole unit suite. 1599 // prints no assertion, so running first would hide the pin that names it.
1764 //
1765 // Ordered AFTER that idempotence test, and it has to be: the double
1766 // close below is safe only while the `conn.r == -1` sentinel holds, so
1767 // if the sentinel regresses this test panics on close(2) of a stale fd.
1768 // A panic prints no assertion, so running first would hide the very pin
1769 // that names the regression.
1770 //
1771 // Both halves print before they fail. A bare `expect` here would say
1772 // "TestUnexpectedResult" about a decision, and the decision is the
1773 // whole content of the test.
1774 const alloc = std.testing.allocator; 1600 const alloc = std.testing.allocator;
1775 1601
1776 // A command that stays alive on stdin, so the link is unambiguously a 1602 // A command that stays alive on stdin, so the link is unambiguously a
@@ -1954,11 +1780,9 @@ test "spawnPipe: an askpass dial hands ssh the three variables, and a plain dial
1954 } 1780 }
1955 1781
1956 test "the announce reader consumes the newline and NOT the byte after it" { 1782 test "the announce reader consumes the newline and NOT the byte after it" {
1957 // The property the announce-then-frames protocol stands on, pinned on 1783 // The property the announce-then-frames protocol stands on. A buffered read
1958 // the one reader the product still has. A buffered read here would take 1784 // here would take 'X' — the frame stream's first byte — into a buffer that
1959 // 'X' — the frame stream's first byte — into a buffer that is then 1785 // is thrown away, and the session would hang with nothing to point at.
1960 // thrown away, and the session would hang or desynchronise with nothing
1961 // to point at.
1962 const alloc = std.testing.allocator; 1786 const alloc = std.testing.allocator;
1963 const fds = try std.posix.pipe(); 1787 const fds = try std.posix.pipe();
1964 defer std.posix.close(fds[0]); 1788 defer std.posix.close(fds[0]);
@@ -1967,12 +1791,9 @@ test "the announce reader consumes the newline and NOT the byte after it" {
1967 const ep: handoff.Endpoint = .{ .port = 4433, .key = [_]u8{0xAB} ** 32 }; 1791 const ep: handoff.Endpoint = .{ .port = 4433, .key = [_]u8{0xAB} ** 32 };
1968 _ = try std.posix.write(fds[1], try handoff.formatAnnounce(&buf, ep)); 1792 _ = try std.posix.write(fds[1], try handoff.formatAnnounce(&buf, ep));
1969 _ = try std.posix.write(fds[1], "X"); 1793 _ = try std.posix.write(fds[1], "X");
1970 // Closed before the read, deliberately: with the write end open, a 1794 // Closed before the read: with the write end open, a reader that swallowed
1971 // reader that had already swallowed the 'X' would leave the assertion 1795 // the 'X' would block the assertion below forever, and a hung suite names
1972 // below blocking forever, and this test's whole job is to catch exactly 1796 // nothing. EOF turns the catch into a printed "expected 1, found 0".
1973 // that implementation. A hung suite names nothing; EOF here turns the
1974 // catch into a printed "expected 1, found 0". The bytes are already in
1975 // the pipe, so the close costs the correct implementation nothing.
1976 std.posix.close(fds[1]); 1797 std.posix.close(fds[1]);
1977 1798
1978 const got = (try readAnnounceAbortable(fds[0], alloc, null, -1, null)).?; 1799 const got = (try readAnnounceAbortable(fds[0], alloc, null, -1, null)).?;
@@ -2054,25 +1875,14 @@ test "handoff: endpoint-none rides the open pipe with no deadline paid" {
2054 } 1875 }
2055 1876
2056 test "handoff: dead coordinates are a fast no, and the pipe is the fallback" { 1877 test "handoff: dead coordinates are a fast no, and the pipe is the fallback" {
2057 // A well-formed announce naming 127.0.0.1:1, where nothing listens. 1878 // A well-formed announce naming 127.0.0.1:1, where nothing listens: the
2058 // The refusal is REAL — an ICMP unreachable comes back — and both 1879 // ICMP refusal is real, so this dial dies in one loopback round trip
2059 // socket paths in the quic client act on it, so this dial dies 1880 // instead of running `deadline_ms` out. The upper bound proves it — a
2060 // in about one loopback round trip instead of running deadline_ms out. 1881 // build that swallowed the refusal spends the whole 300 ms.
2061 // The upper bound below is what proves that: it sits far under the
2062 // budget, so an implementation that swallowed the refusal again (the
2063 // bug this pins: drain's `catch return` discarding ECONNREFUSED) would
2064 // spend the whole 300ms and fail here. No lower bound — this measures
2065 // 2ms, so any floor would be pinning scheduler noise.
2066 // 1882 //
2067 // Which means this test no longer witnesses that a dial happened AT 1883 // With no cache here, this cannot witness that a dial happened at all: a
2068 // ALL: there is no cache here (cache_path = null), so the build it 1884 // build ignoring the coordinates lands on the child link just as fast.
2069 // cannot tell apart is one that IGNORED the announced coordinates and 1885 // That half is e2e's. Pinned HERE: the fallback DECISION and the speed.
2070 // went straight to the pipe — that lands on the child link just as
2071 // quickly. Dial-happened lives only in e2e now, in scenario (b), where
2072 // a warm attach must spawn no ssh; (c)'s ceiling covers the speed half,
2073 // not that half. What is pinned HERE is the fallback DECISION and the
2074 // speed of the refusal. The stderr line is e2e's too, where stderr is
2075 // capturable.
2076 const alloc = std.testing.allocator; 1886 const alloc = std.testing.allocator;
2077 var carry: std.ArrayList(u8) = .empty; 1887 var carry: std.ArrayList(u8) = .empty;
2078 defer carry.deinit(alloc); 1888 defer carry.deinit(alloc);
@@ -2094,13 +1904,9 @@ test "handoff: dead coordinates are a fast no, and the pipe is the fallback" {
2094 const elapsed = std.time.milliTimestamp() - t0; 1904 const elapsed = std.time.milliTimestamp() - t0;
2095 try std.testing.expect(t.link != .quic); 1905 try std.testing.expect(t.link != .quic);
2096 try std.testing.expect(t.link == .pipe); 1906 try std.testing.expect(t.link == .pipe);
2097 // The refusal was SEEN, not waited out. 150ms is derived from both 1907 // The refusal was SEEN, not waited out. 150 ms is derived from both ends:
2098 // ends: the failure it catches spends the full 300ms, so half the 1908 // the failure spends the full 300 ms, and the 2 ms this measures leaves 75x
2099 // budget still separates the two by 2x, while the 2ms this measures 1909 // of headroom — which it needs, since `elapsed` also covers a shell spawn.
2100 // leaves 75x of headroom. It needs that headroom — `elapsed` also
2101 // covers spawning /bin/sh and reading the announce out of it, and the
2102 // sibling endpoint-none test budgets 1000ms for that same spawn under
2103 // load.
2104 if (elapsed >= 150) std.debug.print( 1910 if (elapsed >= 150) std.debug.print(
2105 "refused dial took {d}ms of a 300ms budget: the ICMP refusal was swallowed, not acted on\n", 1911 "refused dial took {d}ms of a 300ms budget: the ICMP refusal was swallowed, not acted on\n",
2106 .{elapsed}, 1912 .{elapsed},
@@ -2109,14 +1915,9 @@ test "handoff: dead coordinates are a fast no, and the pipe is the fallback" {
2109 } 1915 }
2110 1916
2111 /// Put `bytes` on fd 0 for the duration of a test and give back a restorer. 1917 /// Put `bytes` on fd 0 for the duration of a test and give back a restorer.
2112 /// 1918 /// The CLI passes STDIN_FILENO as the abort fd, so testing that spelling means
2113 /// The CLI passes STDIN_FILENO as the abort fd — that is where the abort 1919 /// briefly owning fd 0. Zig runs a file's tests one at a time, so this is safe
2114 /// key arrives in production — so testing that spelling means briefly 1920 /// as long as every caller restores — which `deinit` makes a defer.
2115 /// owning fd 0. (The fd is injectable; the -1 test leans on this same
2116 /// harness to prove fd 0 is NOT read in that case.) Zig runs a file's
2117 /// tests one at a time in one process, so this is safe as long as every
2118 /// caller restores; `deinit`
2119 /// exists to make that a defer rather than a discipline.
2120 const FakeStdin = struct { 1921 const FakeStdin = struct {
2121 saved: std.posix.fd_t, 1922 saved: std.posix.fd_t,
2122 w: std.posix.fd_t, 1923 w: std.posix.fd_t,
@@ -2143,18 +1944,11 @@ const FakeStdin = struct {
2143 }; 1944 };
2144 1945
2145 test "handoff: the announce wait still answers the abort key" { 1946 test "handoff: the announce wait still answers the abort key" {
2146 // The property the QUIC handshake wait keeps, applied to the ssh half: an 1947 // An announce that never comes must not cost the user their way out: a
2147 // announce that never comes must not cost the user their way out. A 1948 // blocking read would sit in `read(2)` with nothing watching stdin, and on
2148 // blocking read here would sit in read(2) with nothing watching stdin, 1949 // the reconnect path `Ctrl-\` is the only way out. RECONNECT spelling on
2149 // and on the reconnect path — raw mode, ISIG off — Ctrl-\ is the only 1950 // purpose. The script exits on its own, so a blocking implementation FAILS
2150 // way out there is. RECONNECT spelling (carry = null) on purpose: on a 1951 // with a printed expectation rather than hanging the suite.
2151 // first attach the announce wait deliberately leaves stdin to ssh's
2152 // password prompt, and cooked-mode Ctrl-C is the way out instead.
2153 //
2154 // The script exits on its own rather than hanging, deliberately: a
2155 // blocking implementation must FAIL this test with a printed
2156 // "expected error.UserAbort, found error.UnterminatedLine", not hang the
2157 // suite. A test that catches its bug by never finishing names nothing.
2158 const alloc = std.testing.allocator; 1952 const alloc = std.testing.allocator;
2159 1953
2160 // The abort byte is already in the pipe when the wait starts, so the 1954 // The abort byte is already in the pipe when the wait starts, so the
@@ -2175,12 +1969,10 @@ test "handoff: the announce wait still answers the abort key" {
2175 } 1969 }
2176 1970
2177 test "handoff: a first attach leaves stdin to ssh while the announce is pending — a typed password must not be swallowed" { 1971 test "handoff: a first attach leaves stdin to ssh while the announce is pending — a typed password must not be swallowed" {
2178 // The competing-reader bug: ssh reads its password prompt from 1972 // ssh reads its password prompt from /dev/tty, and a client polling stdin
2179 // /dev/tty, and a client polling stdin during the announce wait steals 1973 // during the announce wait steals whole cooked lines — auth fails, and the
2180 // whole cooked lines from it — auth fails, and the stolen line later 1974 // stolen line replays INTO the session via carry. A first attach must leave
2181 // replays INTO THE SESSION via carry. A first attach (carry != null) 1975 // stdin untouched.
2182 // must leave stdin untouched; the bytes stay in the kernel's tty
2183 // buffer for whoever legitimately reads next.
2184 const alloc = std.testing.allocator; 1976 const alloc = std.testing.allocator;
2185 var carry: std.ArrayList(u8) = .empty; 1977 var carry: std.ArrayList(u8) = .empty;
2186 defer carry.deinit(alloc); 1978 defer carry.deinit(alloc);
@@ -2276,11 +2068,10 @@ fn boxScript(buf: []u8, dir: []const u8) ![]const u8 {
2276 , .{ .d = dir }); 2068 , .{ .d = dir });
2277 } 2069 }
2278 2070
2279 /// The dial must not have reached a daemon, said so by `claim`. NOT 2071 /// The dial must not have reached a daemon. NOT `expectError`: it renders the
2280 /// `expectError`: it renders the success value with `{any}`, and a live 2072 /// success value with `{any}`, and a live `Transport` holds an allocator vtable
2281 /// `Transport` holds an allocator vtable formatting FAULTS on — the runner 2073 /// that FAULTS on formatting — the runner dies inside the message and names no
2282 /// dies inside the message and names no test. The session it should not 2074 /// test. The session it should not have is closed here; it owns a child.
2283 /// have is closed here; it owns a child.
2284 fn expectNoSession(claim: []const u8, r: anytype) !void { 2075 fn expectNoSession(claim: []const u8, r: anytype) !void {
2285 if (r) |t| { 2076 if (r) |t| {
2286 var live = t; 2077 var live = t;
@@ -2304,14 +2095,10 @@ fn refusingBoxScript(buf: []u8, dir: []const u8) ![]const u8 {
2304 } 2095 }
2305 2096
2306 test "openHandoff: a HandoffTarget nobody configured runs the reading word, never the asking one" { 2097 test "openHandoff: a HandoffTarget nobody configured runs the reading word, never the asking one" {
2307 // The default is the value a NEW dial path inherits by forgetting the 2098 // The default is what a NEW dial path inherits by forgetting the line, and
2308 // line, and nothing fails loudly when it does: the symptom is a daemon 2099 // nothing fails loudly: the symptom is a daemon on someone else's box. So
2309 // (and a shell in session 0) appearing on someone else's box. So the 2100 // the default is the harmless half. `asked` picks the ARGV, so the oracle
2310 // default is the harmless half, and the ask is what has to be written 2101 // is the word the fake was handed — what RAN, not what was meant.
2311 // down.
2312 //
2313 // `asked` picks the ARGV now, so the oracle is the word the fake was
2314 // handed — a fact about what ran, not about what the client meant.
2315 const alloc = std.testing.allocator; 2102 const alloc = std.testing.allocator;
2316 var carry: std.ArrayList(u8) = .empty; 2103 var carry: std.ArrayList(u8) = .empty;
2317 defer carry.deinit(alloc); 2104 defer carry.deinit(alloc);
@@ -2338,11 +2125,9 @@ test "openHandoff: a HandoffTarget nobody configured runs the reading word, neve
2338 } 2125 }
2339 2126
2340 test "openHandoff: the dial a user ASKED for runs the asking word ONCE and rides the announce that comes back on it" { 2127 test "openHandoff: the dial a user ASKED for runs the asking word ONCE and rides the announce that comes back on it" {
2341 // Three ssh runs became one, and this is the claim that says so. The 2128 // Three ssh runs became one: the remote ensures the daemon and announces on
2342 // remote ensures the daemon and announces on the same stdout, so there 2129 // the same stdout, so there is no refusal to read, no exit code to tell from
2343 // is no refusal for the client to read, no exit code to tell from 2130 // ssh's own 255, and no second dial to pay a password prompt for.
2344 // ssh's own 255, and no second dial to pay a connect timeout or a
2345 // password prompt for.
2346 const alloc = std.testing.allocator; 2131 const alloc = std.testing.allocator;
2347 var carry: std.ArrayList(u8) = .empty; 2132 var carry: std.ArrayList(u8) = .empty;
2348 defer carry.deinit(alloc); 2133 defer carry.deinit(alloc);
@@ -2408,11 +2193,9 @@ test "openHandoff: an asked dial whose box still announces nothing fails after t
2408 } 2193 }
2409 2194
2410 test "openHandoff: a dial nobody asked for, against a box with nothing, reports the failure and ran the bare word only" { 2195 test "openHandoff: a dial nobody asked for, against a box with nothing, reports the failure and ran the bare word only" {
2411 // The regression this exists for: the wall polls every listed host once 2196 // The wall polls every listed host once a second, and a poll that started a
2412 // a second, and a poll that started a daemon gave a listed box one (and 2197 // daemon gave a listed box one from a READ — undoing a `mux d stop` a second
2413 // a shell in session 0) from a READ — undoing a `mux d stop` on the far 2198 // after it was typed. The rule is argv's, so the log of what ran IS the proof.
2414 // end a second after it was typed. The rule is argv's now, so the log
2415 // of what ran IS the proof.
2416 const alloc = std.testing.allocator; 2199 const alloc = std.testing.allocator;
2417 var carry: std.ArrayList(u8) = .empty; 2200 var carry: std.ArrayList(u8) = .empty;
2418 defer carry.deinit(alloc); 2201 defer carry.deinit(alloc);
@@ -2439,16 +2222,10 @@ test "openHandoff: a dial nobody asked for, against a box with nothing, reports
2439 } 2222 }
2440 2223
2441 test "openHandoff: an asked target with no asking argv runs the reading word, and starts nothing" { 2224 test "openHandoff: an asked target with no asking argv runs the reading word, and starts nothing" {
2442 // `asked_argv` defaults to empty and its doc says empty means "nothing 2225 // `asked_argv` defaults to empty, and nothing in the type stops a caller
2443 // to start". Nothing in the type stops a caller setting `asked` on such 2226 // setting `asked` on such a target — an empty argv is not a no-op at the
2444 // a target — and an empty argv is not a no-op at the exec: the forked 2227 // exec: the child null-unwraps `argv[0]` and dies, which looks exactly like
2445 // child null-unwraps `argv[0]` and dies, which reaches the parent as an 2228 // a box with no daemon. This pins the DOC, since `fromRecipe` always fills it.
2446 // announce that never terminated. Indistinguishable, from the outside,
2447 // from a box that had no daemon.
2448 //
2449 // `fromRecipe` is the only production builder and it always fills the
2450 // field, so this pins the DOC rather than a live path: the fallback is
2451 // what keeps the sentence above true as the type is used later.
2452 const alloc = std.testing.allocator; 2229 const alloc = std.testing.allocator;
2453 var carry: std.ArrayList(u8) = .empty; 2230 var carry: std.ArrayList(u8) = .empty;
2454 defer carry.deinit(alloc); 2231 defer carry.deinit(alloc);
@@ -2475,11 +2252,9 @@ test "openHandoff: an asked target with no asking argv runs the reading word, an
2475 try std.testing.expectEqualStrings(read_word, try shimSaid(tmp.path(), "runs", &buf)); 2252 try std.testing.expectEqualStrings(read_word, try shimSaid(tmp.path(), "runs", &buf));
2476 } 2253 }
2477 2254
2478 /// fd 2, captured into a pipe this test owns and restored on `take`. 2255 /// fd 2, captured into a pipe this test owns and restored on `take` —
2479 /// 2256 /// `FakeStdin`'s shape for the other direction. fd 2 and never fd 1: a byte on
2480 /// `FakeStdin`'s shape for the other direction. fd 2 and never fd 1: a 2257 /// the runner's stdout wedges `zig build test` silently, at 0 CPU.
2481 /// byte on the runner's stdout wedges `zig build test` silently, at 0 CPU,
2482 /// with no output at all.
2483 const CapturedStderr = struct { 2258 const CapturedStderr = struct {
2484 saved: std.posix.fd_t, 2259 saved: std.posix.fd_t,
2485 r: std.posix.fd_t, 2260 r: std.posix.fd_t,
@@ -2508,21 +2283,11 @@ const CapturedStderr = struct {
2508 }; 2283 };
2509 2284
2510 test "openHandoff: the handoff ssh's stderr is a pipe, and only `narrate` relays it" { 2285 test "openHandoff: the handoff ssh's stderr is a pipe, and only `narrate` relays it" {
2511 // The bug this closes: a hosts line naming a box that is down puts 2286 // A hosts line naming a box that is down used to put ssh's `No route to
2512 // `ssh: connect to host ... No route to host` onto the wall's 2287 // host` onto the wall's alternate screen every poll, because the child's
2513 // alternate screen every poll, over tiles and rails, because the ssh 2288 // stderr was INHERITED. It is a pipe mux reads now, whoever dialled. The
2514 // child's stderr was INHERITED. It is a pipe mux reads now — always, 2289 // fake records where its stderr POINTED, off `/proc/$$/fd/2`, so "piped" is
2515 // whoever dialled — and the one caller that relays the bytes onward is 2290 // exact. BOTH values of `asked`, because the rule is the spawn's.
2516 // the entry dial, where a human is at a bare prompt waiting.
2517 //
2518 // The fake records where its stderr POINTED, off `/proc/$$/fd/2` — the
2519 // shell's own link, not a redirect's — so "piped" is exact rather than
2520 // a guess about what a test runner's stderr happens to be.
2521 //
2522 // BOTH values of `asked`, because the rule is the spawn's and not the
2523 // ask's: a picker-born tile clears `asked` for every redial and keeps
2524 // whatever the spawn does with stderr, so the redial is the half that
2525 // runs for as long as the tile lives.
2526 const alloc = std.testing.allocator; 2291 const alloc = std.testing.allocator;
2527 var stdin = try FakeStdin.install(""); 2292 var stdin = try FakeStdin.install("");
2528 defer stdin.deinit(); 2293 defer stdin.deinit();
@@ -2597,17 +2362,10 @@ test "lostMsg: only a --via transport that never connected gets the new wording"
2597 // `openFailure` is pure, so each class is one call with a literal answer. 2362 // `openFailure` is pure, so each class is one call with a literal answer.
2598 2363
2599 test "Transport.drainErr: a session whose ssh floods stderr keeps serving" { 2364 test "Transport.drainErr: a session whose ssh floods stderr keeps serving" {
2600 // The reason the poll set grew a third fd rather than the reason 2365 // A pipe holds 64k; past that the writer BLOCKS, and ssh blocked on stderr
2601 // growing a sentence. A pipe holds 64k; past that the writer BLOCKS, 2366 // is ssh not moving the session's bytes either — nothing on screen says so,
2602 // and ssh blocked on stderr is ssh not moving the session's bytes 2367 // the tile simply stops. The fixture floods in the FOREGROUND, before it
2603 // either. Nothing on screen says so — the tile simply stops. 2368 // serves, so an owner that never drains cannot get past it.
2604 //
2605 // The fixture floods in the FOREGROUND, before it starts serving, so
2606 // an owner that never drains cannot get past it. (Backgrounding the
2607 // flood would let `cat` serve regardless, and the test would pass with
2608 // the drain deleted — which is the shape this claim exists to catch.)
2609 // Verified by deleting the `drainErr` call below: the round trip then
2610 // never completes and the 2 s budget is what ends the test.
2611 const alloc = std.testing.allocator; 2369 const alloc = std.testing.allocator;
2612 var tmp = try TmpDir.make(); 2370 var tmp = try TmpDir.make();
2613 defer tmp.cleanup(); 2371 defer tmp.cleanup();
@@ -2834,24 +2592,17 @@ test "openFailure: --via and --sock say what they know and nothing more" {
2834 } 2592 }
2835 2593
2836 test "openFailure: a message too long for the buffer clips, and still fails" { 2594 test "openFailure: a message too long for the buffer clips, and still fails" {
2837 // `open_err_len` is chosen rather than derived — an argv string has no 2595 // `open_err_len` is chosen rather than derived, so what makes choosing safe
2838 // useful ceiling — so the thing that makes choosing safe is this: past 2596 // is this: past the buffer the line CLIPS and everything else holds. A
2839 // the buffer the line clips and everything else holds. Unpinned, a 2597 // future `bufPrint` here would answer an empty message or a crash instead.
2840 // future `bufPrint` here would turn the same input into an empty
2841 // message or a crash, and no other test goes near the boundary.
2842 const cmd = "x" ** (open_err_len + 808); 2598 const cmd = "x" ** (open_err_len + 808);
2843 var buf: [open_err_len]u8 = undefined; 2599 var buf: [open_err_len]u8 = undefined;
2844 const f = openFailure(&buf, .{ .via = cmd }, error.FileNotFound, ""); 2600 const f = openFailure(&buf, .{ .via = cmd }, error.FileNotFound, "");
2845 2601
2846 // Full buffer, and it really is the message's own prefix: the fixed 2602 // Full buffer, and really the message's own prefix. The LENGTH goes first
2847 // text first, then as much of the command as fit. 2603 // and the slices below are cut to what came back: an expected slice sized
2848 // 2604 // from the constant is a comptime bounds error the day this input stops
2849 // The length goes FIRST and the slices below are cut to what came 2605 // overflowing, and that fails the whole module instead of one property.
2850 // back, never to `open_err_len`: an expected slice sized from the
2851 // constant is a COMPTIME bounds error the day this input stops
2852 // overflowing the buffer, and a compile error fails the whole module
2853 // — every other pin in this file included — instead of printing which
2854 // property broke. Checked by shrinking the input, not assumed.
2855 try std.testing.expectEqual(@as(usize, open_err_len), f.msg.len); 2606 try std.testing.expectEqual(@as(usize, open_err_len), f.msg.len);
2856 const head = "mux: cannot start --via command: "; 2607 const head = "mux: cannot start --via command: ";
2857 try std.testing.expectEqualStrings(head, f.msg[0..head.len]); 2608 try std.testing.expectEqualStrings(head, f.msg[0..head.len]);
@@ -2961,11 +2712,9 @@ test "client: a target spells itself back as one wall argument per session" {
2961 } 2712 }
2962 2713
2963 test "client: every spelling this writes, the host grammar reads back the same" { 2714 test "client: every spelling this writes, the host grammar reads back the same" {
2964 // Writer/reader identity, pinned across the module boundary. A tile's 2715 // Writer/reader identity across the module boundary: a tile's label and the
2965 // label and the sidecar leaf keyed by it are this string, and the 2716 // sidecar leaf keyed by it are this string, and a drift in either half heals
2966 // host half of it is what `hosts.parse` answers for — a drift in 2717 // a saved layout onto the wrong tile. `refAllDecls` compiles both.
2967 // either half heals a saved layout onto the wrong tile.
2968 // `refAllDecls` compiles both; only this executes both.
2969 var buf: [256]u8 = undefined; 2718 var buf: [256]u8 = undefined;
2970 const cases = .{ 2719 const cases = .{
2971 .{ Target{ .sock = "/run/user/1000/muxd.sock" }, "0" }, 2720 .{ Target{ .sock = "/run/user/1000/muxd.sock" }, "0" },
@@ -3224,13 +2973,10 @@ test "listSessions: a poll that failed still reports the login it paid for" {
3224 } 2973 }
3225 2974
3226 test "Target.fromSpec: asked is the caller's word, never a default" { 2975 test "Target.fromSpec: asked is the caller's word, never a default" {
3227 // The COMPILE-TIME half of this claim cannot be written as a runtime 2976 // The COMPILE-TIME half cannot be a runtime assertion: `fromSpec` and
3228 // assertion: `fromSpec` and `HandoffTarget.fromRecipe` take `asked` as 2977 // `fromRecipe` take `asked` with no default, so a call that omits it does
3229 // a parameter with no default, so a call that omits it does not build. 2978 // not build. What IS runtime-checkable is that the word is carried rather
3230 // That is the pin — the field's `= false` default stays for a road 2979 // than dropped and re-defaulted between here and the recipe.
3231 // that never heard of the field, and this door has no such road.
3232 // What is runtime-checkable is that the word is CARRIED, not dropped
3233 // and re-defaulted somewhere between here and the recipe.
3234 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 2980 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
3235 defer arena.deinit(); 2981 defer arena.deinit();
3236 const alloc = arena.allocator(); 2982 const alloc = arena.allocator();
@@ -3266,11 +3012,9 @@ test "Target.fromSpec: the target owns every slice, so a scratch spelling may be
3266 } 3012 }
3267 3013
3268 test "Target.fromSpec: a quic spelling with no key frees the path it refused" { 3014 test "Target.fromSpec: a quic spelling with no key frees the path it refused" {
3269 // `std.testing.allocator` IS the assertion: `xdg.resolveKeyPath`'s 3015 // `std.testing.allocator` IS the assertion: `resolveKeyPath`'s `.missing`
3270 // `.missing` arm hands back an allocated path, and the refusal that 3016 // arm hands back an allocated path, and the refusal that does not keep it
3271 // does not keep it must free it. The two live callers pass arenas or a 3017 // must free it. The live callers pass arenas, so nothing else would say so.
3272 // long-lived allocator, so nothing else in the product would ever say
3273 // so out loud.
3274 var tmp = try TmpDir.make(); 3018 var tmp = try TmpDir.make();
3275 defer tmp.cleanup(); 3019 defer tmp.cleanup();
3276 3020