9ad104a9
refactor: the daemon is `mux d`, the agent `mux a`, the hub `mux web`
a73x 2026-08-28 22:34
Commit message
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,6 +1,6 @@ | |||
| 1 | //! The LINK, and nothing above it. | 1 | //! The LINK, and nothing above it. |
| 2 | //! | 2 | //! |
| 3 | //! How a `mux` reaches a muxd and what it says on arrival: the `Target` | 3 | //! How a client reaches a daemon and what it says on arrival: the `Target` |
| 4 | //! grammar, the dial (unix socket, `--via` pipe, direct QUIC, the ssh→QUIC | 4 | //! grammar, the dial (unix socket, `--via` pipe, direct QUIC, the ssh→QUIC |
| 5 | //! handoff), the vocabulary a dial that never came up exits with, the attach | 5 | //! handoff), the vocabulary a dial that never came up exits with, the attach |
| 6 | //! frame, the reconnect backoff, the wall-file record an attach earns, and | 6 | //! frame, the reconnect backoff, the wall-file record an attach earns, and |
| @@ -156,7 +156,7 @@ pub const Incoming = union(enum) { | |||
| 156 | pub fn lostMsg(target: Target, session_epoch: u64) []const u8 { | 156 | pub fn lostMsg(target: Target, session_epoch: u64) []const u8 { |
| 157 | if (target == .via and session_epoch == 0) | 157 | if (target == .via and session_epoch == 0) |
| 158 | return "mux: transport command failed before a session started"; | 158 | return "mux: transport command failed before a session started"; |
| 159 | return "mux: connection to muxd lost"; | 159 | return "mux: connection to the daemon lost"; |
| 160 | } | 160 | } |
| 161 | 161 | ||
| 162 | /// The client's name for the shared default; see `quic.default_idle_ms` | 162 | /// The client's name for the shared default; see `quic.default_idle_ms` |
| @@ -186,10 +186,10 @@ pub const HandoffTarget = struct { | |||
| 186 | /// The word the user typed. ssh's business entirely (aliases, `user@`, | 186 | /// The word the user typed. ssh's business entirely (aliases, `user@`, |
| 187 | /// ProxyJump); the QUIC dial uses `handoff.dialHost(host)`. | 187 | /// ProxyJump); the QUIC dial uses `handoff.dialHost(host)`. |
| 188 | host: []const u8, | 188 | host: []const u8, |
| 189 | /// `ssh <host> muxd endpoint`, prebuilt by mux_main — it has the | 189 | /// `ssh <host> mux d endpoint`, prebuilt by mux_main — it has the |
| 190 | /// allocator, and it builds this once for the whole session. | 190 | /// allocator, and it builds this once for the whole session. |
| 191 | ssh_cmd: []const u8, | 191 | ssh_cmd: []const u8, |
| 192 | /// `ssh <host> muxd start`, from the same `handoff.recipeFor` call. | 192 | /// `ssh <host> mux d start`, from the same `handoff.recipeFor` call. |
| 193 | /// Empty is "nothing to start": the dial either finds a daemon or does | 193 | /// Empty is "nothing to start": the dial either finds a daemon or does |
| 194 | /// not. | 194 | /// not. |
| 195 | start_cmd: []const u8 = "", | 195 | start_cmd: []const u8 = "", |
| @@ -211,9 +211,9 @@ pub const HandoffTarget = struct { | |||
| 211 | /// a live session's stderr into the alternate screen and corrupt the | 211 | /// a live session's stderr into the alternate screen and corrupt the |
| 212 | /// paint, to say what the [reconnecting] banner is already saying. | 212 | /// paint, to say what the [reconnecting] banner is already saying. |
| 213 | /// | 213 | /// |
| 214 | /// START a daemon that was not there, via `start_cmd`. `muxd endpoint` | 214 | /// START a daemon that was not there, via `start_cmd`. `mux d endpoint` |
| 215 | /// no longer does it: a wall polls every listed host once a second, and | 215 | /// no longer does it: a wall polls every listed host once a second, and |
| 216 | /// a poll that starts daemons undoes a `muxd stop` a second after it is | 216 | /// a poll that starts daemons undoes a `mux d stop` a second after it is |
| 217 | /// typed. | 217 | /// typed. |
| 218 | /// | 218 | /// |
| 219 | /// The trade, stated plainly: a session that degrades to ssh mid-life | 219 | /// The trade, stated plainly: a session that degrades to ssh mid-life |
| @@ -227,7 +227,7 @@ pub const HandoffTarget = struct { | |||
| 227 | /// on someone else's box that nothing reports at all. | 227 | /// on someone else's box that nothing reports at all. |
| 228 | asked: bool = false, | 228 | asked: bool = false, |
| 229 | /// Whether the caller owns a screen the start's progress would land on. | 229 | /// Whether the caller owns a screen the start's progress would land on. |
| 230 | /// `muxd start` writes `starting\u{2026}`, a dot per interval and an | 230 | /// `mux d start` writes `starting\u{2026}`, a dot per interval and an |
| 231 | /// up-line to stderr; under the wall's alternate screen those bytes sit | 231 | /// up-line to stderr; under the wall's alternate screen those bytes sit |
| 232 | /// over tiles and rails, and the tile's own `connecting` label is the | 232 | /// over tiles and rails, and the tile's own `connecting` label is the |
| 233 | /// narration there. False on the entry dial, which runs before any wall | 233 | /// narration there. False on the entry dial, which runs before any wall |
| @@ -260,7 +260,7 @@ pub const Link = union(enum) { | |||
| 260 | quic: *quic_client.Client, | 260 | quic: *quic_client.Client, |
| 261 | }; | 261 | }; |
| 262 | 262 | ||
| 263 | /// One live connection to a muxd, however it was reached. The point of the | 263 | /// One live connection to a daemon, however it was reached. The point of the |
| 264 | /// struct is that it can be closed and opened again from the same `Target`, | 264 | /// struct is that it can be closed and opened again from the same `Target`, |
| 265 | /// which is what lets a session outlive its transport instead of exiting | 265 | /// which is what lets a session outlive its transport instead of exiting |
| 266 | /// with it. | 266 | /// with it. |
| @@ -281,7 +281,7 @@ pub const Transport = struct { | |||
| 281 | child.stdin_behavior = .Pipe; | 281 | child.stdin_behavior = .Pipe; |
| 282 | child.stdout_behavior = .Pipe; | 282 | child.stdout_behavior = .Pipe; |
| 283 | // Inherited, not piped: ssh's diagnostics (auth failure, unknown | 283 | // Inherited, not piped: ssh's diagnostics (auth failure, unknown |
| 284 | // host, connection refused), and `muxd endpoint`'s own one-liners, | 284 | // host, connection refused), and `mux d endpoint`'s own one-liners, |
| 285 | // are the user's only clue when the transport never comes up, and we | 285 | // are the user's only clue when the transport never comes up, and we |
| 286 | // would otherwise swallow them. | 286 | // would otherwise swallow them. |
| 287 | child.stderr_behavior = .Inherit; | 287 | child.stderr_behavior = .Inherit; |
| @@ -453,7 +453,7 @@ pub const Transport = struct { | |||
| 453 | } | 453 | } |
| 454 | } | 454 | } |
| 455 | 455 | ||
| 456 | /// One announce, or — for a dial the user ASKED for — a `muxd start` | 456 | /// One announce, or — for a dial the user ASKED for — a `mux d start` |
| 457 | /// on the far end and one more. | 457 | /// on the far end and one more. |
| 458 | fn announceOrStart( | 458 | fn announceOrStart( |
| 459 | alloc: std.mem.Allocator, | 459 | alloc: std.mem.Allocator, |
| @@ -469,7 +469,7 @@ pub const Transport = struct { | |||
| 469 | // reached and had nothing to say. | 469 | // reached and had nothing to say. |
| 470 | if (!h.asked or h.start_cmd.len == 0 or !announceFailed(err)) return err; | 470 | if (!h.asked or h.start_cmd.len == 0 or !announceFailed(err)) return err; |
| 471 | // The remote's refusal, told apart from ssh's own by the code | 471 | // The remote's refusal, told apart from ssh's own by the code |
| 472 | // it exited with: `muxd endpoint` refuses an empty box with 1, | 472 | // it exited with: `mux d endpoint` refuses an empty box with 1, |
| 473 | // and ssh reports its OWN failures as 255 while passing a | 473 | // and ssh reports its OWN failures as 255 while passing a |
| 474 | // remote code through. Both look identical from the announce — | 474 | // remote code through. Both look identical from the announce — |
| 475 | // nothing arrived either way — so without this a typo'd host or | 475 | // nothing arrived either way — so without this a typo'd host or |
| @@ -481,7 +481,7 @@ pub const Transport = struct { | |||
| 481 | else => return err, | 481 | else => return err, |
| 482 | } | 482 | } |
| 483 | // A start that did not take is reported as the announce failure | 483 | // A start that did not take is reported as the announce failure |
| 484 | // it began as: `muxd start` has already said its own piece on | 484 | // it began as: `mux d start` has already said its own piece on |
| 485 | // stderr, and a second guess over the top of it would be worse. | 485 | // stderr, and a second guess over the top of it would be worse. |
| 486 | if (!runStart(alloc, h.start_cmd, h.quiet)) return err; | 486 | if (!runStart(alloc, h.start_cmd, h.quiet)) return err; |
| 487 | child.* = try spawnPipe(alloc, h.ssh_cmd); | 487 | child.* = try spawnPipe(alloc, h.ssh_cmd); |
| @@ -504,7 +504,7 @@ pub const Transport = struct { | |||
| 504 | // stdout hits EOF when the channel closes, and the exit-status and | 504 | // stdout hits EOF when the channel closes, and the exit-status and |
| 505 | // any ControlMaster or ProxyJump hop come after. The verdict is then | 505 | // any ControlMaster or ProxyJump hop come after. The verdict is then |
| 506 | // `.Signal`, which the caller reads as "do not start": `mux HOST` | 506 | // `.Signal`, which the caller reads as "do not start": `mux HOST` |
| 507 | // failing on precisely the box `muxd start` exists for. | 507 | // failing on precisely the box `mux d start` exists for. |
| 508 | var waited: u64 = 0; | 508 | var waited: u64 = 0; |
| 509 | while (waited < announce_reap_ms) : (waited += announce_reap_step_ms) { | 509 | while (waited < announce_reap_ms) : (waited += announce_reap_step_ms) { |
| 510 | const res = std.posix.waitpid(child.id, std.posix.W.NOHANG); | 510 | const res = std.posix.waitpid(child.id, std.posix.W.NOHANG); |
| @@ -767,7 +767,7 @@ fn waitReady( | |||
| 767 | return error.QuicHandshakeFailed; | 767 | return error.QuicHandshakeFailed; |
| 768 | } | 768 | } |
| 769 | 769 | ||
| 770 | /// Deliberately NO deadline: a timer here races a cold `muxd endpoint` | 770 | /// Deliberately NO deadline: a timer here races a cold `mux d endpoint` |
| 771 | /// spawn, and the abort key already covers a hung ssh. | 771 | /// spawn, and the abort key already covers a hung ssh. |
| 772 | fn readAnnounceAbortable( | 772 | fn readAnnounceAbortable( |
| 773 | fd: std.posix.fd_t, | 773 | fd: std.posix.fd_t, |
| @@ -928,7 +928,7 @@ pub fn openFailure(buf: []u8, target: Target, err: anyerror) OpenFailure { | |||
| 928 | // both is more honest than guessing which it was. | 928 | // both is more honest than guessing which it was. |
| 929 | error.QuicHandshakeFailed => failedMsg( | 929 | error.QuicHandshakeFailed => failedMsg( |
| 930 | buf, | 930 | buf, |
| 931 | "mux: quic://{s} did not answer (wrong key, or no muxd --quic there)\n", | 931 | "mux: quic://{s} did not answer (wrong key, or no mux d --quic there)\n", |
| 932 | .{q.host_port}, | 932 | .{q.host_port}, |
| 933 | ), | 933 | ), |
| 934 | error.UserAbort => open_aborted, | 934 | error.UserAbort => open_aborted, |
| @@ -951,7 +951,7 @@ pub fn openFailure(buf: []u8, target: Target, err: anyerror) OpenFailure { | |||
| 951 | // NOT knowable from here: a parse failure does prove | 951 | // NOT knowable from here: a parse failure does prove |
| 952 | // something spoke, but a clean EOF is equally what a | 952 | // something spoke, but a clean EOF is equally what a |
| 953 | // refused connection, a rejected key, and a remote | 953 | // refused connection, a rejected key, and a remote |
| 954 | // without `muxd` on its PATH all look like — the | 954 | // without `mux` on its PATH all look like — the |
| 955 | // shell's complaint goes to stderr and its stdout | 955 | // shell's complaint goes to stderr and its stdout |
| 956 | // closes clean. | 956 | // closes clean. |
| 957 | // | 957 | // |
| @@ -977,7 +977,7 @@ pub fn openFailure(buf: []u8, target: Target, err: anyerror) OpenFailure { | |||
| 977 | ), | 977 | ), |
| 978 | }, | 978 | }, |
| 979 | .via => |cmd| failedMsg(buf, "mux: cannot start --via command: {s}\n", .{cmd}), | 979 | .via => |cmd| failedMsg(buf, "mux: cannot start --via command: {s}\n", .{cmd}), |
| 980 | // No "is muxd running?": auto-start checked that moments ago. | 980 | // No "is the daemon running?": auto-start checked that moments ago. |
| 981 | // Reaching here means a daemon answered the probe (or was just | 981 | // Reaching here means a daemon answered the probe (or was just |
| 982 | // spawned) and then vanished before this connect — the path is | 982 | // spawned) and then vanished before this connect — the path is |
| 983 | // the whole of what we know, so the path is all we say. | 983 | // the whole of what we know, so the path is all we say. |
| @@ -1040,7 +1040,7 @@ pub fn hydratedCreates(target: Target) bool { | |||
| 1040 | } | 1040 | } |
| 1041 | 1041 | ||
| 1042 | // The grid a birth asks for. It is `main.Opts`'s own default — the size | 1042 | // The grid a birth asks for. It is `main.Opts`'s own default — the size |
| 1043 | // `muxd run` gives session 0 — because a session created for a client | 1043 | // `mux d run` gives session 0 — because a session created for a client |
| 1044 | // that claims no size has to be born at SOMETHING, and the daemon's own | 1044 | // that claims no size has to be born at SOMETHING, and the daemon's own |
| 1045 | // answer to that question is the one nobody has to explain. Wrong, and a | 1045 | // answer to that question is the one nobody has to explain. Wrong, and a |
| 1046 | // restored browser tile comes back at a shape no client ever asked for. | 1046 | // restored browser tile comes back at a shape no client ever asked for. |
| @@ -1346,7 +1346,7 @@ test "Transport.open: a --via target yields a pipe, a --sock target an fd" { | |||
| 1346 | } | 1346 | } |
| 1347 | 1347 | ||
| 1348 | test "handoff: endpoint-none rides the open pipe with no deadline paid" { | 1348 | test "handoff: endpoint-none rides the open pipe with no deadline paid" { |
| 1349 | // A fake `ssh HOST muxd endpoint` that announces `none`. What this pins | 1349 | // A fake `ssh HOST mux d endpoint` that announces `none`. What this pins |
| 1350 | // is Transport.open's DECISION — the announce-less remote gets a | 1350 | // is Transport.open's DECISION — the announce-less remote gets a |
| 1351 | // child-backed transport, immediately — not a whole session, which | 1351 | // child-backed transport, immediately — not a whole session, which |
| 1352 | // would need a daemon on the far end of the pipe. | 1352 | // would need a daemon on the far end of the pipe. |
| @@ -1612,7 +1612,7 @@ test "openHandoff: a HandoffTarget nobody configured starts nothing" { | |||
| 1612 | } | 1612 | } |
| 1613 | 1613 | ||
| 1614 | test "openHandoff: the dial a user ASKED for starts the daemon it did not find, and re-runs the ssh line exactly once" { | 1614 | test "openHandoff: the dial a user ASKED for starts the daemon it did not find, and re-runs the ssh line exactly once" { |
| 1615 | // `muxd endpoint` on a box with no daemon exits 1 having written | 1615 | // `mux d endpoint` on a box with no daemon exits 1 having written |
| 1616 | // nothing, which arrives here as an announce that never terminated. | 1616 | // nothing, which arrives here as an announce that never terminated. |
| 1617 | // The shim is that box: the first run finds no `started` flag and | 1617 | // The shim is that box: the first run finds no `started` flag and |
| 1618 | // fails the same way; the start command drops the flag; the second run | 1618 | // fails the same way; the start command drops the flag; the second run |
| @@ -1653,7 +1653,7 @@ test "openHandoff: the dial a user ASKED for starts the daemon it did not find, | |||
| 1653 | test "openHandoff: a dial nobody asked for reports the failure and starts nothing — the wall polls every listed host once a second" { | 1653 | test "openHandoff: a dial nobody asked for reports the failure and starts nothing — the wall polls every listed host once a second" { |
| 1654 | // The regression this exists for: a poll that starts a daemon gives a | 1654 | // The regression this exists for: a poll that starts a daemon gives a |
| 1655 | // listed box one (and a shell in session 0) from a READ, and undoes a | 1655 | // listed box one (and a shell in session 0) from a READ, and undoes a |
| 1656 | // `muxd stop` on the far end a second after it is typed. | 1656 | // `mux d stop` on the far end a second after it is typed. |
| 1657 | const alloc = std.testing.allocator; | 1657 | const alloc = std.testing.allocator; |
| 1658 | var carry: std.ArrayList(u8) = .empty; | 1658 | var carry: std.ArrayList(u8) = .empty; |
| 1659 | defer carry.deinit(alloc); | 1659 | defer carry.deinit(alloc); |
| @@ -1770,7 +1770,7 @@ test "openHandoff: a start that does not help is tried once — a second announc | |||
| 1770 | } | 1770 | } |
| 1771 | 1771 | ||
| 1772 | test "openHandoff: ssh's own failure is not a box without a daemon — no start, and the timeout is paid once" { | 1772 | test "openHandoff: ssh's own failure is not a box without a daemon — no start, and the timeout is paid once" { |
| 1773 | // `announceFailed` cannot tell the two apart: a `muxd endpoint` that | 1773 | // `announceFailed` cannot tell the two apart: a `mux d endpoint` that |
| 1774 | // refused (exit 1, no output) and an ssh that never got there (exit | 1774 | // refused (exit 1, no output) and an ssh that never got there (exit |
| 1775 | // 255, no output) both arrive as an announce that never terminated. | 1775 | // 255, no output) both arrive as an announce that never terminated. |
| 1776 | // The exit code can, and it has to: a typo'd host, a down box or a | 1776 | // The exit code can, and it has to: a typo'd host, a down box or a |
| @@ -1803,7 +1803,7 @@ test "openHandoff: ssh's own failure is not a box without a daemon — no start, | |||
| 1803 | } | 1803 | } |
| 1804 | 1804 | ||
| 1805 | test "runStart: a caller that owns a screen gets a quiet start; one that does not sees the progress" { | 1805 | test "runStart: a caller that owns a screen gets a quiet start; one that does not sees the progress" { |
| 1806 | // `muxd start` writes `starting…`, a dot per interval and an up-line to | 1806 | // `mux d start` writes `starting…`, a dot per interval and an up-line to |
| 1807 | // stderr. On the entry dial that is a wait the user is sitting through | 1807 | // stderr. On the entry dial that is a wait the user is sitting through |
| 1808 | // and is owed; under the wall's alternate screen the same bytes land | 1808 | // and is owed; under the wall's alternate screen the same bytes land |
| 1809 | // over tiles and rails, and the tile's own `connecting` label is the | 1809 | // over tiles and rails, and the tile's own `connecting` label is the |
| @@ -1865,14 +1865,14 @@ test "lostMsg: only a --via transport that never connected gets the new wording" | |||
| 1865 | // no such binary). The lower layer spoke; this line must not talk over it. | 1865 | // no such binary). The lower layer spoke; this line must not talk over it. |
| 1866 | try std.testing.expectEqualStrings( | 1866 | try std.testing.expectEqualStrings( |
| 1867 | "mux: transport command failed before a session started", | 1867 | "mux: transport command failed before a session started", |
| 1868 | lostMsg(.{ .via = "ssh box muxd proxy" }, 0), | 1868 | lostMsg(.{ .via = "ssh box mux d proxy" }, 0), |
| 1869 | ); | 1869 | ); |
| 1870 | // Same transport, but a session existed — there WAS a connection, and | 1870 | // Same transport, but a session existed — there WAS a connection, and |
| 1871 | // saying otherwise would be the new lie in place of the old one. | 1871 | // saying otherwise would be the new lie in place of the old one. |
| 1872 | try std.testing.expectEqualStrings("mux: connection to muxd lost", lostMsg(.{ .via = "ssh box muxd proxy" }, 7)); | 1872 | try std.testing.expectEqualStrings("mux: connection to the daemon lost", lostMsg(.{ .via = "ssh box mux d proxy" }, 7)); |
| 1873 | // No command to have failed: a socket or quic:// target keeps the | 1873 | // No command to have failed: a socket or quic:// target keeps the |
| 1874 | // original wording however early it dies. | 1874 | // original wording however early it dies. |
| 1875 | try std.testing.expectEqualStrings("mux: connection to muxd lost", lostMsg(.{ .sock = "/run/muxd.sock" }, 0)); | 1875 | try std.testing.expectEqualStrings("mux: connection to the daemon lost", lostMsg(.{ .sock = "/run/muxd.sock" }, 0)); |
| 1876 | } | 1876 | } |
| 1877 | 1877 | ||
| 1878 | // The three tests below pin every line the entry dial can print when the | 1878 | // The three tests below pin every line the entry dial can print when the |
| @@ -1915,7 +1915,7 @@ test "openFailure: a quic:// target names the key or the address, and only an ab | |||
| 1915 | // absent daemon are indistinguishable from here, and dropping either | 1915 | // absent daemon are indistinguishable from here, and dropping either |
| 1916 | // half would turn an honest ambiguity into a wrong guess. | 1916 | // half would turn an honest ambiguity into a wrong guess. |
| 1917 | try std.testing.expectEqualStrings( | 1917 | try std.testing.expectEqualStrings( |
| 1918 | "mux: quic://box:4433 did not answer (wrong key, or no muxd --quic there)\n", | 1918 | "mux: quic://box:4433 did not answer (wrong key, or no mux d --quic there)\n", |
| 1919 | openFailure(&buf, q, error.QuicHandshakeFailed).msg, | 1919 | openFailure(&buf, q, error.QuicHandshakeFailed).msg, |
| 1920 | ); | 1920 | ); |
| 1921 | 1921 | ||
| @@ -1941,7 +1941,7 @@ test "openFailure: a handoff separates a missing announce from an ssh that never | |||
| 1941 | var buf: [open_err_len]u8 = undefined; | 1941 | var buf: [open_err_len]u8 = undefined; |
| 1942 | const h: Target = .{ .hand = .{ | 1942 | const h: Target = .{ .hand = .{ |
| 1943 | .host = "box", | 1943 | .host = "box", |
| 1944 | .ssh_cmd = "ssh box muxd endpoint", | 1944 | .ssh_cmd = "ssh box mux d endpoint", |
| 1945 | .cache_path = null, | 1945 | .cache_path = null, |
| 1946 | } }; | 1946 | } }; |
| 1947 | 1947 | ||
| @@ -1978,17 +1978,17 @@ test "openFailure: --via and --sock say what they know and nothing more" { | |||
| 1978 | 1978 | ||
| 1979 | // One line whatever the error was: the command's own stderr is | 1979 | // One line whatever the error was: the command's own stderr is |
| 1980 | // inherited and has already named the cause. | 1980 | // inherited and has already named the cause. |
| 1981 | const v: Target = .{ .via = "ssh box muxd proxy" }; | 1981 | const v: Target = .{ .via = "ssh box mux d proxy" }; |
| 1982 | try std.testing.expectEqualStrings( | 1982 | try std.testing.expectEqualStrings( |
| 1983 | "mux: cannot start --via command: ssh box muxd proxy\n", | 1983 | "mux: cannot start --via command: ssh box mux d proxy\n", |
| 1984 | openFailure(&buf, v, error.FileNotFound).msg, | 1984 | openFailure(&buf, v, error.FileNotFound).msg, |
| 1985 | ); | 1985 | ); |
| 1986 | try std.testing.expectEqualStrings( | 1986 | try std.testing.expectEqualStrings( |
| 1987 | "mux: cannot start --via command: ssh box muxd proxy\n", | 1987 | "mux: cannot start --via command: ssh box mux d proxy\n", |
| 1988 | openFailure(&buf, v, error.AccessDenied).msg, | 1988 | openFailure(&buf, v, error.AccessDenied).msg, |
| 1989 | ); | 1989 | ); |
| 1990 | 1990 | ||
| 1991 | // No "is muxd running?" — auto-start checked that moments ago, so the | 1991 | // No "is the daemon running?" — auto-start checked that moments ago, so the |
| 1992 | // path is the whole of what is known and the whole of what is said. | 1992 | // path is the whole of what is known and the whole of what is said. |
| 1993 | const s: Target = .{ .sock = "/run/muxd.sock" }; | 1993 | const s: Target = .{ .sock = "/run/muxd.sock" }; |
| 1994 | try std.testing.expectEqualStrings( | 1994 | try std.testing.expectEqualStrings( |
| @@ -2124,7 +2124,7 @@ test "client: a target spells itself back as one wall argument per session" { | |||
| 2124 | "vm1#work", | 2124 | "vm1#work", |
| 2125 | try wallSpelling(&buf, .{ .hand = .{ | 2125 | try wallSpelling(&buf, .{ .hand = .{ |
| 2126 | .host = "vm1", | 2126 | .host = "vm1", |
| 2127 | .ssh_cmd = "ssh vm1 muxd endpoint", | 2127 | .ssh_cmd = "ssh vm1 mux d endpoint", |
| 2128 | .cache_path = null, | 2128 | .cache_path = null, |
| 2129 | } }, "work"), | 2129 | } }, "work"), |
| 2130 | ); | 2130 | ); |
| @@ -2178,7 +2178,7 @@ test "client: a --via target has no wall spelling at all" { | |||
| 2178 | // that this transport cannot be walled. | 2178 | // that this transport cannot be walled. |
| 2179 | try std.testing.expectError( | 2179 | try std.testing.expectError( |
| 2180 | error.NoSpelling, | 2180 | error.NoSpelling, |
| 2181 | wallSpelling(&buf, .{ .via = "ssh h muxd proxy" }, "0"), | 2181 | wallSpelling(&buf, .{ .via = "ssh h mux d proxy" }, "0"), |
| 2182 | ); | 2182 | ); |
| 2183 | } | 2183 | } |
| 2184 | 2184 | ||
| @@ -2218,13 +2218,13 @@ test "hydratedCreates: a saved LOCAL line may create the session; every remote s | |||
| 2218 | } })); | 2218 | } })); |
| 2219 | try std.testing.expect(!hydratedCreates(.{ .hand = .{ | 2219 | try std.testing.expect(!hydratedCreates(.{ .hand = .{ |
| 2220 | .host = "box", | 2220 | .host = "box", |
| 2221 | .ssh_cmd = "ssh box muxd endpoint", | 2221 | .ssh_cmd = "ssh box mux d endpoint", |
| 2222 | .cache_path = null, | 2222 | .cache_path = null, |
| 2223 | } })); | 2223 | } })); |
| 2224 | // `.via` has no wall spelling at all, so it can never come off the | 2224 | // `.via` has no wall spelling at all, so it can never come off the |
| 2225 | // saved file — but a caller holding one must still not create through | 2225 | // saved file — but a caller holding one must still not create through |
| 2226 | // an arbitrary command it cannot even name. | 2226 | // an arbitrary command it cannot even name. |
| 2227 | try std.testing.expect(!hydratedCreates(.{ .via = "ssh box muxd run" })); | 2227 | try std.testing.expect(!hydratedCreates(.{ .via = "ssh box mux d run" })); |
| 2228 | } | 2228 | } |
| 2229 | 2229 | ||
| 2230 | /// A daemon stand-in for the birth tests: accepts once, records every frame | 2230 | /// A daemon stand-in for the birth tests: accepts once, records every frame |
src/client/client_core_wasm_check.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,7 +1,7 @@ | |||
| 1 | const client_core = @import("client_core"); | 1 | const client_core = @import("client_core"); |
| 2 | 2 | ||
| 3 | /// Compile-only canary: keep a real typed mode receive call in the wasm | 3 | /// Compile-only canary: keep a real typed mode receive call in the wasm |
| 4 | /// build, without exporting or embedding this check in muxweb. | 4 | /// build, without exporting or embedding this check in the hub. |
| 5 | export fn clientCoreWasmModesCheck() void { | 5 | export fn clientCoreWasmModesCheck() void { |
| 6 | var core = client_core.ClientCore{}; | 6 | var core = client_core.ClientCore{}; |
| 7 | const payload = [_]u8{ 1, 0, 0, 0 }; | 7 | const payload = [_]u8{ 1, 0, 0, 0 }; |
src/client/quic_client.zig
| Old | New | ||
|---|---|---|---|
| @@ -123,7 +123,7 @@ fn ackedStreamDataCb( | |||
| 123 | return 0; | 123 | return 0; |
| 124 | } | 124 | } |
| 125 | 125 | ||
| 126 | /// One connection to a muxd's QUIC listener. | 126 | /// One connection to a daemon's QUIC listener. |
| 127 | pub const Client = struct { | 127 | pub const Client = struct { |
| 128 | alloc: std.mem.Allocator, | 128 | alloc: std.mem.Allocator, |
| 129 | fd: std.posix.fd_t, | 129 | fd: std.posix.fd_t, |
src/client/wall.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,5 +1,5 @@ | |||
| 1 | //! The wall: an ordered list of TARGET spellings — attach history. mux | 1 | //! The wall: an ordered list of TARGET spellings — attach history. mux |
| 2 | //! records a grid-claiming attach on its first state; muxweb records its | 2 | //! records a grid-claiming attach on its first state; the hub records its |
| 3 | //! wall mutations (argv tiles, POST /tiles), never an attach. One owner | 3 | //! wall mutations (argv tiles, POST /tiles), never an attach. One owner |
| 4 | //! for the spelling grammar, the session split, and the persisted file, | 4 | //! for the spelling grammar, the session split, and the persisted file, |
| 5 | //! so the wall built in a browser is the wall the CLI sees. | 5 | //! so the wall built in a browser is the wall the CLI sees. |
| @@ -749,7 +749,7 @@ test "spellingFromArgv: a flag is not a target" { | |||
| 749 | } | 749 | } |
| 750 | 750 | ||
| 751 | test "parseSpelling: a HOST spelling carries no shell metacharacter — the hub's POST body reaches /bin/sh" { | 751 | test "parseSpelling: a HOST spelling carries no shell metacharacter — the hub's POST body reaches /bin/sh" { |
| 752 | // muxweb's POST /tiles body is a spelling, and a HOST spelling ends up | 752 | // the hub's POST /tiles body is a spelling, and a HOST spelling ends up |
| 753 | // interpolated unquoted into `handoff.sshLine`, which `client` runs as | 753 | // interpolated unquoted into `handoff.sshLine`, which `client` runs as |
| 754 | // `/bin/sh -c`. hosts.zig refuses these on the CLI's file; the browser | 754 | // `/bin/sh -c`. hosts.zig refuses these on the CLI's file; the browser |
| 755 | // mouth reaches the same ssh line and must be refused in the same words. | 755 | // mouth reaches the same ssh line and must be refused in the same words. |
src/client/wasm_core.zig
| Old | New | ||
|---|---|---|---|
| @@ -655,7 +655,7 @@ export fn mux_output_len() u32 { | |||
| 655 | } | 655 | } |
| 656 | 656 | ||
| 657 | /// Plain-text dump of the live viewport (verify.js's referee; matches | 657 | /// Plain-text dump of the live viewport (verify.js's referee; matches |
| 658 | /// Engine.dumpPlain, the same text muxd dump prints). | 658 | /// Engine.dumpPlain, the same text mux d dump prints). |
| 659 | export fn mux_dump_plain() i32 { | 659 | export fn mux_dump_plain() i32 { |
| 660 | const c = core orelse return -1; | 660 | const c = core orelse return -1; |
| 661 | output_len = 0; | 661 | output_len = 0; |
src/client/webhub.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,4 +1,4 @@ | |||
| 1 | //! The muxweb hub's HTTP/WebSocket layer: route table, | 1 | //! The `mux web` hub's HTTP/WebSocket layer: route table, |
| 2 | //! Origin gate, and the WS endpoint naming — the decisions std.http does | 2 | //! Origin gate, and the WS endpoint naming — the decisions std.http does |
| 3 | //! NOT make for us. The connection loop and per-tile pump build on this; | 3 | //! NOT make for us. The connection loop and per-tile pump build on this; |
| 4 | //! the assets are @embedFile'd by webhub_main.zig (the exe | 4 | //! the assets are @embedFile'd by webhub_main.zig (the exe |
| @@ -101,7 +101,7 @@ fn resolveTile( | |||
| 101 | const target: client.Target = switch (p.spec) { | 101 | const target: client.Target = switch (p.spec) { |
| 102 | // sun_path is a fixed array in the kernel's struct: a longer path | 102 | // sun_path is a fixed array in the kernel's struct: a longer path |
| 103 | // cannot be dialed at all, so it is refused here rather than at a | 103 | // cannot be dialed at all, so it is refused here rather than at a |
| 104 | // connect that fails with a truncated name nobody typed. muxweb's | 104 | // connect that fails with a truncated name nobody typed. The hub's |
| 105 | // argv path made this check before the Hub existed; a POSTed | 105 | // argv path made this check before the Hub existed; a POSTed |
| 106 | // spelling gets the same answer. | 106 | // spelling gets the same answer. |
| 107 | .sock => |path| if (path.len > sockpath.max_sun_path) | 107 | .sock => |path| if (path.len > sockpath.max_sun_path) |
| @@ -968,9 +968,9 @@ fn dialLoop( | |||
| 968 | } | 968 | } |
| 969 | 969 | ||
| 970 | /// Labels are argv, not hostile input, but a path with a quote in it must | 970 | /// Labels are argv, not hostile input, but a path with a quote in it must |
| 971 | /// not break the page. Deliberately NOT muxa's jsonEscape: this one sends | 971 | /// not break the page. Deliberately NOT `mux a`'s jsonEscape: this one sends |
| 972 | /// every control byte to `\u00XX` (one rule, no table to get wrong) while | 972 | /// every control byte to `\u00XX` (one rule, no table to get wrong) while |
| 973 | /// muxa spells the short forms. Both parse identically, the bytes differ, | 973 | /// `mux a` spells the short forms. Both parse identically, the bytes differ, |
| 974 | /// and each is pinned by its own test. | 974 | /// and each is pinned by its own test. |
| 975 | fn appendJsonString(alloc: std.mem.Allocator, out: *std.ArrayList(u8), s: []const u8) !void { | 975 | fn appendJsonString(alloc: std.mem.Allocator, out: *std.ArrayList(u8), s: []const u8) !void { |
| 976 | try out.append(alloc, '"'); | 976 | try out.append(alloc, '"'); |
| @@ -1134,7 +1134,7 @@ pub fn serveConn( | |||
| 1134 | error.EmptySpec => .{ .bad_request, "empty target\n" }, | 1134 | error.EmptySpec => .{ .bad_request, "empty target\n" }, |
| 1135 | error.BadByte => .{ .bad_request, "control byte in target\n" }, | 1135 | error.BadByte => .{ .bad_request, "control byte in target\n" }, |
| 1136 | error.ShellMeta => .{ .bad_request, "shell metacharacter in host: the tile runs this spelling through /bin/sh\n" }, | 1136 | error.ShellMeta => .{ .bad_request, "shell metacharacter in host: the tile runs this spelling through /bin/sh\n" }, |
| 1137 | error.MissingKey => .{ .bad_request, "no key for quic:// target (muxd keygen, or MUX_KEY_FILE)\n" }, | 1137 | error.MissingKey => .{ .bad_request, "no key for quic:// target (mux d keygen, or MUX_KEY_FILE)\n" }, |
| 1138 | error.SockPathTooLong => .{ .bad_request, "socket path too long\n" }, | 1138 | error.SockPathTooLong => .{ .bad_request, "socket path too long\n" }, |
| 1139 | // The tile is NOT live: addTile rolls back on a | 1139 | // The tile is NOT live: addTile rolls back on a |
| 1140 | // failed save, so 500 is the whole truth here. | 1140 | // failed save, so 500 is the whole truth here. |
src/engine/protocol.zig
| Old | New | ||
|---|---|---|---|
| @@ -561,7 +561,7 @@ pub const AwaitReq = struct { | |||
| 561 | /// Borrowed from the payload; valid only while the frame lives. Empty | 561 | /// Borrowed from the payload; valid only while the frame lives. Empty |
| 562 | /// means the default session — see the note beside `session_name_max`. | 562 | /// means the default session — see the note beside `session_name_max`. |
| 563 | /// Defaulted (unlike `AttachReq.name`) so the fixed-part struct | 563 | /// Defaulted (unlike `AttachReq.name`) so the fixed-part struct |
| 564 | /// literals already in the tree (muxa) keep building without every | 564 | /// literals already in the tree (`mux a`) keep building without every |
| 565 | /// call site learning about names; `AttachReq.name` has no such | 565 | /// call site learning about names; `AttachReq.name` has no such |
| 566 | /// default because the only place that constructs an `AttachReq` is | 566 | /// default because the only place that constructs an `AttachReq` is |
| 567 | /// `decodeAttach` itself, so there is no literal to keep compiling. | 567 | /// `decodeAttach` itself, so there is no literal to keep compiling. |
| @@ -620,7 +620,7 @@ pub fn decodeAwaitReply(payload: []const u8) !AwaitReply { | |||
| 620 | }; | 620 | }; |
| 621 | } | 621 | } |
| 622 | 622 | ||
| 623 | /// One structured snapshot for `muxa status`: what a driving agent needs | 623 | /// One structured snapshot for `mux a status`: what a driving agent needs |
| 624 | /// before deciding how to interact. | 624 | /// before deciding how to interact. |
| 625 | pub const StatusReply = struct { | 625 | pub const StatusReply = struct { |
| 626 | cols: u16, | 626 | cols: u16, |
| @@ -790,7 +790,7 @@ pub const clipboard_base64_max: usize = 64 * 1024; | |||
| 790 | /// handler already truncates a title to before it reaches `Terminal.title` | 790 | /// handler already truncates a title to before it reaches `Terminal.title` |
| 791 | /// (terminal/stream_terminal.zig, `max_title_len`), so a title read off | 791 | /// (terminal/stream_terminal.zig, `max_title_len`), so a title read off |
| 792 | /// the engine can never exceed it today. Both ends check it anyway: the peer is not | 792 | /// the engine can never exceed it today. Both ends check it anyway: the peer is not |
| 793 | /// necessarily this version of muxd, and the engine's truncation is not | 793 | /// necessarily this version of the daemon, and the engine's truncation is not |
| 794 | /// part of any contract mux is entitled to lean on. | 794 | /// part of any contract mux is entitled to lean on. |
| 795 | pub const term_title_max: usize = 1024; | 795 | pub const term_title_max: usize = 1024; |
| 796 | 796 | ||
| @@ -890,7 +890,7 @@ pub fn wireName(name: []const u8) []const u8 { | |||
| 890 | pub const sock_env = "MUX_SOCK"; | 890 | pub const sock_env = "MUX_SOCK"; |
| 891 | pub const session_env = "MUX_SESSION"; | 891 | pub const session_env = "MUX_SESSION"; |
| 892 | 892 | ||
| 893 | /// A name a user may spell: printable ASCII, no space; '#' is muxweb's | 893 | /// A name a user may spell: printable ASCII, no space; '#' is the hub's |
| 894 | /// TARGET separator and '/' is reserved. The empty string is valid ON THE | 894 | /// TARGET separator and '/' is reserved. The empty string is valid ON THE |
| 895 | /// WIRE (it means default) but not as a user-supplied name. | 895 | /// WIRE (it means default) but not as a user-supplied name. |
| 896 | pub fn validSessionName(name: []const u8) bool { | 896 | pub fn validSessionName(name: []const u8) bool { |
| @@ -2130,12 +2130,12 @@ test "encodeUpgradeReq/parseUpgradeReq round-trip preserves flag, version, path" | |||
| 2130 | const encoded = try encodeUpgradeReq(&buf, .{ | 2130 | const encoded = try encodeUpgradeReq(&buf, .{ |
| 2131 | .allow_same_version = true, | 2131 | .allow_same_version = true, |
| 2132 | .version = "0.0.1-14", | 2132 | .version = "0.0.1-14", |
| 2133 | .path = "/home/user/bin/muxd", | 2133 | .path = "/home/user/bin/mux", |
| 2134 | }); | 2134 | }); |
| 2135 | const decoded = try parseUpgradeReq(encoded); | 2135 | const decoded = try parseUpgradeReq(encoded); |
| 2136 | try std.testing.expect(decoded.allow_same_version); | 2136 | try std.testing.expect(decoded.allow_same_version); |
| 2137 | try std.testing.expectEqualStrings("0.0.1-14", decoded.version); | 2137 | try std.testing.expectEqualStrings("0.0.1-14", decoded.version); |
| 2138 | try std.testing.expectEqualStrings("/home/user/bin/muxd", decoded.path); | 2138 | try std.testing.expectEqualStrings("/home/user/bin/mux", decoded.path); |
| 2139 | } | 2139 | } |
| 2140 | 2140 | ||
| 2141 | test "parseUpgradeReq: a payload with no NUL is BadPayload" { | 2141 | test "parseUpgradeReq: a payload with no NUL is BadPayload" { |
src/proxy.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,4 +1,4 @@ | |||
| 1 | //! `muxd proxy`: a bidirectional byte pump between stdio and the local | 1 | //! `mux d proxy`: a bidirectional byte pump between stdio and the local |
| 2 | //! daemon socket. Deliberately frame-agnostic — it contains no protocol | 2 | //! daemon socket. Deliberately frame-agnostic — it contains no protocol |
| 3 | //! knowledge at all. That is the transport thesis: if an opaque byte | 3 | //! knowledge at all. That is the transport thesis: if an opaque byte |
| 4 | //! pipe suffices to carry the protocol over SSH, transport is a swap, | 4 | //! pipe suffices to carry the protocol over SSH, transport is a swap, |
| @@ -18,7 +18,7 @@ pub fn ignoreSigpipe() void { | |||
| 18 | std.posix.sigaction(std.posix.SIG.PIPE, &ign, null); | 18 | std.posix.sigaction(std.posix.SIG.PIPE, &ign, null); |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | /// `muxd proxy` proper: pump between this process's stdio and `sock_path`. | 21 | /// `mux d proxy` proper: pump between this process's stdio and `sock_path`. |
| 22 | pub fn run(sock_path: []const u8) !u8 { | 22 | pub fn run(sock_path: []const u8) !u8 { |
| 23 | return pump(std.posix.STDIN_FILENO, std.posix.STDOUT_FILENO, sock_path); | 23 | return pump(std.posix.STDIN_FILENO, std.posix.STDOUT_FILENO, sock_path); |
| 24 | } | 24 | } |
| @@ -29,7 +29,7 @@ pub fn run(sock_path: []const u8) !u8 { | |||
| 29 | /// socket cannot be reached or a read or write fails. | 29 | /// socket cannot be reached or a read or write fails. |
| 30 | pub fn pump(in_fd: std.posix.fd_t, out_fd: std.posix.fd_t, sock_path: []const u8) !u8 { | 30 | pub fn pump(in_fd: std.posix.fd_t, out_fd: std.posix.fd_t, sock_path: []const u8) !u8 { |
| 31 | const stream = std.net.connectUnixSocket(sock_path) catch { | 31 | const stream = std.net.connectUnixSocket(sock_path) catch { |
| 32 | std.debug.print("muxd proxy: cannot connect to {s}\n", .{sock_path}); | 32 | std.debug.print("mux d proxy: cannot connect to {s}\n", .{sock_path}); |
| 33 | return 1; | 33 | return 1; |
| 34 | }; | 34 | }; |
| 35 | defer stream.close(); | 35 | defer stream.close(); |
src/quic.zig
| Old | New | ||
|---|---|---|---|
| @@ -40,8 +40,8 @@ pub const default_port: u16 = 4433; | |||
| 40 | /// within a few seconds of keepalives failing — keepalives run at a third | 40 | /// within a few seconds of keepalives failing — keepalives run at a third |
| 41 | /// of it, so an idle session is never the thing that trips it. | 41 | /// of it, so an idle session is never the thing that trips it. |
| 42 | /// `--quic-idle-ms` tunes it on the three binaries that dial or listen for | 42 | /// `--quic-idle-ms` tunes it on the three binaries that dial or listen for |
| 43 | /// a human — `muxd`, `mux`, `muxweb` — because the reconnect tests need | 43 | /// a human — every mode of `mux` — because the reconnect tests need |
| 44 | /// death declared on a schedule they can wait for. `muxa` deliberately has | 44 | /// death declared on a schedule they can wait for. `mux a` deliberately has |
| 45 | /// no such flag and always takes this default: an agent's wait is bounded | 45 | /// no such flag and always takes this default: an agent's wait is bounded |
| 46 | /// by `--timeout` already, and a second knob over the same wait is one | 46 | /// by `--timeout` already, and a second knob over the same wait is one |
| 47 | /// more thing for a driver to get wrong. | 47 | /// more thing for a driver to get wrong. |
| @@ -74,12 +74,12 @@ pub const alpn = "\x03mux"; | |||
| 74 | // --------------------------------------------------------------------------- | 74 | // --------------------------------------------------------------------------- |
| 75 | 75 | ||
| 76 | /// `HOST[:PORT]`, as a client types it: `mux quic://HOST:PORT` and | 76 | /// `HOST[:PORT]`, as a client types it: `mux quic://HOST:PORT` and |
| 77 | /// `muxa --quic HOST:PORT` accept exactly these spellings, brackets and | 77 | /// `mux a --quic HOST:PORT` accept exactly these spellings, brackets and |
| 78 | /// all. One owner for the same reason `default_port` has one — an agent | 78 | /// all. One owner for the same reason `default_port` has one — an agent |
| 79 | /// and a human pointing at the same daemon must be able to type the same | 79 | /// and a human pointing at the same daemon must be able to type the same |
| 80 | /// thing, and two copies of a grammar drift into two dialects of one flag. | 80 | /// thing, and two copies of a grammar drift into two dialects of one flag. |
| 81 | /// | 81 | /// |
| 82 | /// A name is resolved rather than refused: unlike muxd's `--quic`, which | 82 | /// A name is resolved rather than refused: unlike the daemon's `--quic`, which |
| 83 | /// names an address to BIND, this one names a box to reach, and a box is | 83 | /// names an address to BIND, this one names a box to reach, and a box is |
| 84 | /// normally spelled with a name. | 84 | /// normally spelled with a name. |
| 85 | pub fn parseAddr(alloc: std.mem.Allocator, host_port: []const u8) !std.net.Address { | 85 | pub fn parseAddr(alloc: std.mem.Allocator, host_port: []const u8) !std.net.Address { |
| @@ -141,7 +141,7 @@ test "parseAddr: literals, brackets, and the spellings that are refused" { | |||
| 141 | try std.testing.expectEqual(@as(u16, 4433), (try parseAddr(alloc, "[::1]")).getPort()); | 141 | try std.testing.expectEqual(@as(u16, 4433), (try parseAddr(alloc, "[::1]")).getPort()); |
| 142 | 142 | ||
| 143 | // An unbracketed IPv6 literal would have its last group read as a | 143 | // An unbracketed IPv6 literal would have its last group read as a |
| 144 | // port. Refused rather than guessed at — the same refusal muxd's | 144 | // port. Refused rather than guessed at — the same refusal the daemon's |
| 145 | // splitHostPort makes about its bind address. | 145 | // splitHostPort makes about its bind address. |
| 146 | try std.testing.expectError(error.MalformedAddress, parseAddr(alloc, "fe80::1:4433")); | 146 | try std.testing.expectError(error.MalformedAddress, parseAddr(alloc, "fe80::1:4433")); |
| 147 | try std.testing.expectError(error.MalformedAddress, parseAddr(alloc, "127.0.0.1:")); | 147 | try std.testing.expectError(error.MalformedAddress, parseAddr(alloc, "127.0.0.1:")); |
| @@ -369,11 +369,11 @@ test "keyRefusalBody: the words three binaries print, byte for byte" { | |||
| 369 | // These bytes ARE the contract. Every key refusal any binary prints is | 369 | // These bytes ARE the contract. Every key refusal any binary prints is |
| 370 | // a prefix, this body, and at most a suffix: | 370 | // a prefix, this body, and at most a suffix: |
| 371 | // | 371 | // |
| 372 | // muxd: <body> (muxd run --quic) | 372 | // mux d: <body> (mux d run --quic) |
| 373 | // muxd endpoint: <body>; staying on ssh | 373 | // mux d endpoint: <body>; staying on ssh |
| 374 | // muxd: endpoint_req: <body> (the daemon's lazy bind) | 374 | // mux d: endpoint_req: <body> (the daemon's lazy bind) |
| 375 | // mux: <body> (the client's dial) | 375 | // mux: <body> (the client's dial) |
| 376 | // muxa: the JSON detail after "quic: unusable key" | 376 | // mux a: the JSON detail after "quic: unusable key" |
| 377 | // | 377 | // |
| 378 | // so a change here is a change to every caller at once — which is what | 378 | // so a change here is a change to every caller at once — which is what |
| 379 | // the four literal copies this replaced could never guarantee, and did | 379 | // the four literal copies this replaced could never guarantee, and did |
src/server/quic_server.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,4 +1,4 @@ | |||
| 1 | //! muxd's QUIC listener: one UDP socket, N authenticated connections, each | 1 | //! The daemon's QUIC listener: one UDP socket, N authenticated connections, each |
| 2 | //! carrying exactly one bidirectional stream of opaque bytes. The vocabulary | 2 | //! carrying exactly one bidirectional stream of opaque bytes. The vocabulary |
| 3 | //! both ends share — the C import, the key, the egress ring — is quic.zig's, | 3 | //! both ends share — the C import, the key, the egress ring — is quic.zig's, |
| 4 | //! and this file imports it. | 4 | //! and this file imports it. |
src/server/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,4 +1,4 @@ | |||
| 1 | //! muxd's daemon core: up to max_sessions sessions (engine + pty + command | 1 | //! The daemon core (`mux d run`): up to max_sessions sessions (engine + pty + command |
| 2 | //! tracker each); a connection is a session, named at attach. One listener | 2 | //! tracker each); a connection is a session, named at attach. One listener |
| 3 | //! on the unix socket, and a second on UDP when QUIC is configured. | 3 | //! on the unix socket, and a second on UDP when QUIC is configured. |
| 4 | //! Up to max_clients attached interactive clients plus a few one-shot | 4 | //! Up to max_clients attached interactive clients plus a few one-shot |
| @@ -163,7 +163,7 @@ fn onShutdownSignal(_: c_int) callconv(.c) void { | |||
| 163 | shutdown_flag.store(true, .release); | 163 | shutdown_flag.store(true, .release); |
| 164 | } | 164 | } |
| 165 | 165 | ||
| 166 | /// Install SIGINT/SIGTERM handlers so a foreground `muxd run` shuts down | 166 | /// Install SIGINT/SIGTERM handlers so a foreground `mux d run` shuts down |
| 167 | /// cleanly (socket file removed, shell reaped). Called by main; one test | 167 | /// cleanly (socket file removed, shell reaped). Called by main; one test |
| 168 | /// borrows it for the SIGPIPE ignore. | 168 | /// borrows it for the SIGPIPE ignore. |
| 169 | pub fn installSignalHandlers() void { | 169 | pub fn installSignalHandlers() void { |
| @@ -532,12 +532,12 @@ pub const Server = struct { | |||
| 532 | /// gives up on it. Overridden small in tests; 8 MiB is far more than a | 532 | /// gives up on it. Overridden small in tests; 8 MiB is far more than a |
| 533 | /// live session ever queues, so tripping it means the peer is gone. | 533 | /// live session ever queues, so tripping it means the peer is gone. |
| 534 | pending_cap: usize = 8 * 1024 * 1024, | 534 | pending_cap: usize = 8 * 1024 * 1024, |
| 535 | /// Connections that haven't attached (muxd dump, or a client waiting | 535 | /// Connections that haven't attached (mux d dump, or a client waiting |
| 536 | /// to attach). May send debug_dump; attach promotes into a client slot. | 536 | /// to attach). May send debug_dump; attach promotes into a client slot. |
| 537 | /// | 537 | /// |
| 538 | /// An fd and a buffer, never a Sink: QUIC serves clients, not | 538 | /// An fd and a buffer, never a Sink: QUIC serves clients, not |
| 539 | /// observers. An observer is a local one-shot tool (`muxd dump`, | 539 | /// observers. An observer is a local one-shot tool (`mux d dump`, |
| 540 | /// `muxd stats`) that connects over the unix socket, reads one answer | 540 | /// `mux d stats`) that connects over the unix socket, reads one answer |
| 541 | /// and exits — there is no remote story for it, so giving it a Sink | 541 | /// and exits — there is no remote story for it, so giving it a Sink |
| 542 | /// would be generality with no second case. If that ever changes, | 542 | /// would be generality with no second case. If that ever changes, |
| 543 | /// this is the comment that was wrong. | 543 | /// this is the comment that was wrong. |
| @@ -590,8 +590,8 @@ pub const Server = struct { | |||
| 590 | /// Inject the OSC 133 mark scripts into the session shell. OFF by | 590 | /// Inject the OSC 133 mark scripts into the session shell. OFF by |
| 591 | /// default: the shim is not free — under zsh it costs the user their | 591 | /// default: the shim is not free — under zsh it costs the user their |
| 592 | /// `~/.zshenv`, under bash it displaces their DEBUG trap — and what | 592 | /// `~/.zshenv`, under bash it displaces their DEBUG trap — and what |
| 593 | /// it buys, a knowable exit code, is read only by `muxa`. A caller | 593 | /// it buys, a knowable exit code, is read only by `mux a`. A caller |
| 594 | /// that wants marks says so; `muxd run` says so for | 594 | /// that wants marks says so; `mux d run` says so for |
| 595 | /// `MUX_SHELL_INTEGRATION=1`. A shell shellint has no scripts for is | 595 | /// `MUX_SHELL_INTEGRATION=1`. A shell shellint has no scripts for is |
| 596 | /// unaffected either way. | 596 | /// unaffected either way. |
| 597 | shell_integration: bool = false, | 597 | shell_integration: bool = false, |
| @@ -704,7 +704,7 @@ pub const Server = struct { | |||
| 704 | std.fs.cwd().deleteTree(dir) catch {}; | 704 | std.fs.cwd().deleteTree(dir) catch {}; |
| 705 | break :blk shellint.prepare(a, dir, shell_z) catch |err| { | 705 | break :blk shellint.prepare(a, dir, shell_z) catch |err| { |
| 706 | std.debug.print( | 706 | std.debug.print( |
| 707 | "muxd: resumed without shell integration ({s}: {t}); " ++ | 707 | "mux d: resumed without shell integration ({s}: {t}); " ++ |
| 708 | "sessions spawned from here run without command marks\n", | 708 | "sessions spawned from here run without command marks\n", |
| 709 | .{ dir, err }, | 709 | .{ dir, err }, |
| 710 | ); | 710 | ); |
| @@ -746,7 +746,7 @@ pub const Server = struct { | |||
| 746 | }; | 746 | }; |
| 747 | 747 | ||
| 748 | // Cumulative, so an upgrade is not mistaken for a restart by | 748 | // Cumulative, so an upgrade is not mistaken for a restart by |
| 749 | // anything sampling `muxd stats`. Saturating rather than @intCast | 749 | // anything sampling `mux d stats`. Saturating rather than @intCast |
| 750 | // below: a manifest is bytes, and a corrupt counter must not panic a | 750 | // below: a manifest is bytes, and a corrupt counter must not panic a |
| 751 | // daemon that is otherwise able to serve. | 751 | // daemon that is otherwise able to serve. |
| 752 | srv.stats = .{ | 752 | srv.stats = .{ |
| @@ -983,7 +983,7 @@ pub const Server = struct { | |||
| 983 | // `term_grace_ms`, not one each — a supervisor's stop timeout does | 983 | // `term_grace_ms`, not one each — a supervisor's stop timeout does |
| 984 | // not grow with `max_sessions`, and a daemon SIGKILLed halfway | 984 | // not grow with `max_sessions`, and a daemon SIGKILLed halfway |
| 985 | // through leaves its socket and every per-session agent socket for | 985 | // through leaves its socket and every per-session agent socket for |
| 986 | // the next `muxd run` to reason about. | 986 | // the next `mux d run` to reason about. |
| 987 | for (&self.sessions.table) |*slot| { | 987 | for (&self.sessions.table) |*slot| { |
| 988 | if (slot.* != null) slot.*.?.pty.requestExit(); | 988 | if (slot.* != null) slot.*.?.pty.requestExit(); |
| 989 | } | 989 | } |
| @@ -1217,8 +1217,8 @@ pub const Server = struct { | |||
| 1217 | } | 1217 | } |
| 1218 | 1218 | ||
| 1219 | /// Pumps until something asks the daemon to stop. The only nonzero exit | 1219 | /// Pumps until something asks the daemon to stop. The only nonzero exit |
| 1220 | /// `muxd run` has is a boot failure in main; getting here means the | 1220 | /// `mux d run` has is a boot failure in main; getting here means the |
| 1221 | /// daemon served, so the answer is 0 whether the ask was `muxd stop` or | 1221 | /// daemon served, so the answer is 0 whether the ask was `mux d stop` or |
| 1222 | /// a SIGTERM from a supervisor — which reads a nonzero exit on a clean | 1222 | /// a SIGTERM from a supervisor — which reads a nonzero exit on a clean |
| 1223 | /// shutdown as a crash. | 1223 | /// shutdown as a crash. |
| 1224 | pub fn run(self: *Server) !u8 { | 1224 | pub fn run(self: *Server) !u8 { |
| @@ -1600,7 +1600,7 @@ pub const Server = struct { | |||
| 1600 | ); | 1600 | ); |
| 1601 | } | 1601 | } |
| 1602 | 1602 | ||
| 1603 | /// Env handed in: tests cannot setenv. Refusals log as `muxd: <wire-verb>: | 1603 | /// Env handed in: tests cannot setenv. Refusals log as `mux d: <wire-verb>: |
| 1604 | /// <what>`, the arriving verb. | 1604 | /// <what>`, the arriving verb. |
| 1605 | pub fn endpointPortFrom( | 1605 | pub fn endpointPortFrom( |
| 1606 | self: *Server, | 1606 | self: *Server, |
| @@ -1629,12 +1629,12 @@ pub const Server = struct { | |||
| 1629 | // was no HOME to build a default under in the first place. | 1629 | // was no HOME to build a default under in the first place. |
| 1630 | if (owned) |dflt| { | 1630 | if (owned) |dflt| { |
| 1631 | std.debug.print( | 1631 | std.debug.print( |
| 1632 | "muxd: endpoint_req: no key at {s} (run `muxd keygen`)\n", | 1632 | "mux d: endpoint_req: no key at {s} (run `mux d keygen`)\n", |
| 1633 | .{dflt}, | 1633 | .{dflt}, |
| 1634 | ); | 1634 | ); |
| 1635 | } else { | 1635 | } else { |
| 1636 | std.debug.print( | 1636 | std.debug.print( |
| 1637 | "muxd: endpoint_req: no key to listen with and no HOME to find one under (run `muxd keygen`)\n", | 1637 | "mux d: endpoint_req: no key to listen with and no HOME to find one under (run `mux d keygen`)\n", |
| 1638 | .{}, | 1638 | .{}, |
| 1639 | ); | 1639 | ); |
| 1640 | } | 1640 | } |
| @@ -1654,13 +1654,13 @@ pub const Server = struct { | |||
| 1654 | const key = quic.Key.load(key_path) catch |err| { | 1654 | const key = quic.Key.load(key_path) catch |err| { |
| 1655 | var buf: [quic.key_refusal_len]u8 = undefined; | 1655 | var buf: [quic.key_refusal_len]u8 = undefined; |
| 1656 | std.debug.print( | 1656 | std.debug.print( |
| 1657 | "muxd: endpoint_req: {s}\n", | 1657 | "mux d: endpoint_req: {s}\n", |
| 1658 | .{quic.keyRefusalBody(&buf, err, key_path)}, | 1658 | .{quic.keyRefusalBody(&buf, err, key_path)}, |
| 1659 | ); | 1659 | ); |
| 1660 | return 0; | 1660 | return 0; |
| 1661 | }; | 1661 | }; |
| 1662 | return self.lazyBindQuic(key) catch |err| { | 1662 | return self.lazyBindQuic(key) catch |err| { |
| 1663 | std.debug.print("muxd: endpoint_req: cannot bind udp: {s}\n", .{@errorName(err)}); | 1663 | std.debug.print("mux d: endpoint_req: cannot bind udp: {s}\n", .{@errorName(err)}); |
| 1664 | return 0; | 1664 | return 0; |
| 1665 | }; | 1665 | }; |
| 1666 | } | 1666 | } |
| @@ -1938,8 +1938,8 @@ pub const Server = struct { | |||
| 1938 | // Shut down via the signal path, not a second one: this is the | 1938 | // Shut down via the signal path, not a second one: this is the |
| 1939 | // flag SIGTERM sets, so the poll loop, deinit's unlink and the | 1939 | // flag SIGTERM sets, so the poll loop, deinit's unlink and the |
| 1940 | // pty teardown are all already-tested code. No reply is sent — | 1940 | // pty teardown are all already-tested code. No reply is sent — |
| 1941 | // the ack is the socket dying, which is what `muxd stop` polls | 1941 | // the ack is the socket dying, which is what `mux d stop` polls |
| 1942 | // for. This arm has no product caller (`muxd stop` never | 1942 | // for. This arm has no product caller (`mux d stop` never |
| 1943 | // attaches, so it lands in serviceObserver); it is here so the | 1943 | // attaches, so it lands in serviceObserver); it is here so the |
| 1944 | // verb means the same thing on any connection, and it grants no | 1944 | // verb means the same thing on any connection, and it grants no |
| 1945 | // authority an attached client lacks — see the .input arm above, | 1945 | // authority an attached client lacks — see the .input arm above, |
| @@ -2180,10 +2180,10 @@ pub const Server = struct { | |||
| 2180 | // Same wording as the observer arm: one stderr line | 2180 | // Same wording as the observer arm: one stderr line |
| 2181 | // an operator can grep for, since status_reply is a | 2181 | // an operator can grep for, since status_reply is a |
| 2182 | // fixed binary layout with no room for words. | 2182 | // fixed binary layout with no room for words. |
| 2183 | std.debug.print("muxd: status_req for unknown session: {s}\n", .{name}); | 2183 | std.debug.print("mux d: status_req for unknown session: {s}\n", .{name}); |
| 2184 | // The one word the wire has for no, the same one a | 2184 | // The one word the wire has for no, the same one a |
| 2185 | // refused attach gets. Before the drop, not instead | 2185 | // refused attach gets. Before the drop, not instead |
| 2186 | // of it: silence made `muxa status --session nosuch` | 2186 | // of it: silence made `mux a status --session nosuch` |
| 2187 | // report a dead daemon. | 2187 | // report a dead daemon. |
| 2188 | // | 2188 | // |
| 2189 | // It outlives the drop only because this arm is | 2189 | // It outlives the drop only because this arm is |
| @@ -2539,10 +2539,10 @@ pub const Server = struct { | |||
| 2539 | .debug_dump => self.replyDumpObserver(fd, frame.payload) catch self.dropObserver(i), | 2539 | .debug_dump => self.replyDumpObserver(fd, frame.payload) catch self.dropObserver(i), |
| 2540 | .stats_req => self.replyStatsObserver(fd) catch self.dropObserver(i), | 2540 | .stats_req => self.replyStatsObserver(fd) catch self.dropObserver(i), |
| 2541 | .detach => self.dropObserver(i), | 2541 | .detach => self.dropObserver(i), |
| 2542 | // Where `muxd stop` actually lands, since it never attaches. | 2542 | // Where `mux d stop` actually lands, since it never attaches. |
| 2543 | // Same signal path as the client arm. | 2543 | // Same signal path as the client arm. |
| 2544 | .stop_req => shutdown_flag.store(true, .release), | 2544 | .stop_req => shutdown_flag.store(true, .release), |
| 2545 | // Where `muxd upgrade` lands: validate, reply, and let the run | 2545 | // Where `mux d upgrade` lands: validate, reply, and let the run |
| 2546 | // loop exec. The reply is blocking (observer has no send queue); | 2546 | // loop exec. The reply is blocking (observer has no send queue); |
| 2547 | // the exec is deferred to the run loop via pending_upgrade so | 2547 | // the exec is deferred to the run loop via pending_upgrade so |
| 2548 | // the frame handler returns cleanly and the close-all + execve | 2548 | // the frame handler returns cleanly and the close-all + execve |
| @@ -2589,12 +2589,12 @@ pub const Server = struct { | |||
| 2589 | self.dropObserver(i); | 2589 | self.dropObserver(i); |
| 2590 | self.pending_upgrade = .{ .path = path, .memfd = memfd }; | 2590 | self.pending_upgrade = .{ .path = path, .memfd = memfd }; |
| 2591 | }, | 2591 | }, |
| 2592 | // Where `muxd endpoint` actually lands, since it never attaches. | 2592 | // Where `mux d endpoint` actually lands, since it never attaches. |
| 2593 | .endpoint_req => { | 2593 | .endpoint_req => { |
| 2594 | const payload = proto.encodeEndpointReply(self.endpointPort()); | 2594 | const payload = proto.encodeEndpointReply(self.endpointPort()); |
| 2595 | proto.writeFrameBounded(fd, .endpoint_reply, &payload, proto.reply_budget_ms) catch self.dropObserver(i); | 2595 | proto.writeFrameBounded(fd, .endpoint_reply, &payload, proto.reply_budget_ms) catch self.dropObserver(i); |
| 2596 | }, | 2596 | }, |
| 2597 | // Where `muxa status` actually lands: it asks and exits without | 2597 | // Where `mux a status` actually lands: it asks and exits without |
| 2598 | // ever attaching. Blocking reply for the same reason the stats | 2598 | // ever attaching. Blocking reply for the same reason the stats |
| 2599 | // and endpoint arms use one — an observer has no send queue. | 2599 | // and endpoint arms use one — an observer has no send queue. |
| 2600 | .status_req => { | 2600 | .status_req => { |
| @@ -2612,7 +2612,7 @@ pub const Server = struct { | |||
| 2612 | // the wire) reads it. Bounded write like the attach | 2612 | // the wire) reads it. Bounded write like the attach |
| 2613 | // refusal above: the byte must be gone before the fd is, | 2613 | // refusal above: the byte must be gone before the fd is, |
| 2614 | // but not at the price of the whole pump. | 2614 | // but not at the price of the whole pump. |
| 2615 | std.debug.print("muxd: status_req for unknown session: {s}\n", .{name}); | 2615 | std.debug.print("mux d: status_req for unknown session: {s}\n", .{name}); |
| 2616 | proto.writeFrameBounded(fd, .exit_status, &.{1}, proto.reply_budget_ms) catch {}; | 2616 | proto.writeFrameBounded(fd, .exit_status, &.{1}, proto.reply_budget_ms) catch {}; |
| 2617 | self.dropObserver(i); | 2617 | self.dropObserver(i); |
| 2618 | return; | 2618 | return; |
| @@ -2636,7 +2636,7 @@ pub const Server = struct { | |||
| 2636 | /// `payload` is 1 byte (0 = plain, 1 = vt) ++ an optional session-name | 2636 | /// `payload` is 1 byte (0 = plain, 1 = vt) ++ an optional session-name |
| 2637 | /// tail, same pattern as attach/status_req/await_req — empty names the | 2637 | /// tail, same pattern as attach/status_req/await_req — empty names the |
| 2638 | /// default. Resolved via `findSession`, never `resolveSession`: a dump | 2638 | /// default. Resolved via `findSession`, never `resolveSession`: a dump |
| 2639 | /// is a read, and a read that could spawn a shell would make `muxd dump | 2639 | /// is a read, and a read that could spawn a shell would make `mux d dump |
| 2640 | /// --session typo` a way to accidentally stand one up. An unknown name | 2640 | /// --session typo` a way to accidentally stand one up. An unknown name |
| 2641 | /// answers IN WORDS rather than as a dropped connection — the wire | 2641 | /// answers IN WORDS rather than as a dropped connection — the wire |
| 2642 | /// carries no separate channel for "no", and `dump_reply` is the only | 2642 | /// carries no separate channel for "no", and `dump_reply` is the only |
| @@ -2650,12 +2650,12 @@ pub const Server = struct { | |||
| 2650 | const si = self.sessions.find(wire_name) orelse { | 2650 | const si = self.sessions.find(wire_name) orelse { |
| 2651 | // Through safeName like the two status_req arms. This reply goes | 2651 | // Through safeName like the two status_req arms. This reply goes |
| 2652 | // back to the peer that asked, so it is self-inflicted rather | 2652 | // back to the peer that asked, so it is self-inflicted rather |
| 2653 | // than an injection into someone else's terminal — but `muxd | 2653 | // than an injection into someone else's terminal — but `mux d |
| 2654 | // dump` prints it straight to a console, and one rule for | 2654 | // dump` prints it straight to a console, and one rule for |
| 2655 | // "a name in a message" is cheaper than remembering which of | 2655 | // "a name in a message" is cheaper than remembering which of |
| 2656 | // the three sites was the safe one. | 2656 | // the three sites was the safe one. |
| 2657 | const name = SessionTable.safeName(wire_name); | 2657 | const name = SessionTable.safeName(wire_name); |
| 2658 | return std.fmt.allocPrint(self.alloc, "muxd: no such session: {s}\n", .{name}); | 2658 | return std.fmt.allocPrint(self.alloc, "mux d: no such session: {s}\n", .{name}); |
| 2659 | }; | 2659 | }; |
| 2660 | return if (want_vt) | 2660 | return if (want_vt) |
| 2661 | try self.ses(si).eng.dumpVt(self.alloc) | 2661 | try self.ses(si).eng.dumpVt(self.alloc) |
| @@ -2664,8 +2664,8 @@ pub const Server = struct { | |||
| 2664 | } | 2664 | } |
| 2665 | 2665 | ||
| 2666 | // Observer replies go out unqueued, on `writeFrameBounded`. Observers | 2666 | // Observer replies go out unqueued, on `writeFrameBounded`. Observers |
| 2667 | // are local one-shot tools: `muxd dump`, `muxd stats` and `muxd | 2667 | // are local one-shot tools: `mux d dump`, `mux d stats` and `mux d |
| 2668 | // endpoint` read their one answer and exit, and `muxd stop` waits for | 2668 | // endpoint` read their one answer and exit, and `mux d stop` waits for |
| 2669 | // the socket to die rather than for a reply at all. So there is nothing | 2669 | // the socket to die rather than for a reply at all. So there is nothing |
| 2670 | // to queue into — an observer has no ClientSlot — but a dump can be | 2670 | // to queue into — an observer has no ClientSlot — but a dump can be |
| 2671 | // megabytes, so the write must tolerate a short one, and a peer that | 2671 | // megabytes, so the write must tolerate a short one, and a peer that |
| @@ -3416,7 +3416,10 @@ pub const Server = struct { | |||
| 3416 | return null; | 3416 | return null; |
| 3417 | } | 3417 | } |
| 3418 | 3418 | ||
| 3419 | // Child-run `path --version` must print exactly `muxd <version>\n`. | 3419 | // Child-run `path --version` must print exactly `mux <version>\n`. |
| 3420 | // A daemon of v0.0.1-15 or older expects `muxd <version>` here and so | ||
| 3421 | // refuses this binary as a candidate; `mux d upgrade` says what to do | ||
| 3422 | // about it when the refusal comes back. | ||
| 3420 | fn checkVersionOutput(self: *Server, path: []const u8, version: []const u8) ?[]const u8 { | 3423 | fn checkVersionOutput(self: *Server, path: []const u8, version: []const u8) ?[]const u8 { |
| 3421 | const a = self.alloc; | 3424 | const a = self.alloc; |
| 3422 | const result = std.process.Child.run(.{ | 3425 | const result = std.process.Child.run(.{ |
| @@ -3428,7 +3431,7 @@ pub const Server = struct { | |||
| 3428 | defer a.free(result.stderr); | 3431 | defer a.free(result.stderr); |
| 3429 | if (result.term != .Exited or result.term.Exited != 0) | 3432 | if (result.term != .Exited or result.term.Exited != 0) |
| 3430 | return a.dupe(u8, "version: nonzero exit") catch null; | 3433 | return a.dupe(u8, "version: nonzero exit") catch null; |
| 3431 | const expected = std.fmt.allocPrint(a, "muxd {s}\n", .{version}) catch | 3434 | const expected = std.fmt.allocPrint(a, "mux {s}\n", .{version}) catch |
| 3432 | return a.dupe(u8, "version: oom") catch null; | 3435 | return a.dupe(u8, "version: oom") catch null; |
| 3433 | defer a.free(expected); | 3436 | defer a.free(expected); |
| 3434 | if (!std.mem.eql(u8, result.stdout, expected)) | 3437 | if (!std.mem.eql(u8, result.stdout, expected)) |
| @@ -3454,7 +3457,7 @@ pub const Server = struct { | |||
| 3454 | return a.dupe(u8, "check: oom") catch null; | 3457 | return a.dupe(u8, "check: oom") catch null; |
| 3455 | const result = std.process.Child.run(.{ | 3458 | const result = std.process.Child.run(.{ |
| 3456 | .allocator = a, | 3459 | .allocator = a, |
| 3457 | .argv = &.{ path, "run", "--resume-fd", fd_str, "--check" }, | 3460 | .argv = &.{ path, "d", "run", "--resume-fd", fd_str, "--check" }, |
| 3458 | .max_output_bytes = 4096, | 3461 | .max_output_bytes = 4096, |
| 3459 | }) catch return a.dupe(u8, "check: cannot spawn candidate") catch null; | 3462 | }) catch return a.dupe(u8, "check: cannot spawn candidate") catch null; |
| 3460 | defer a.free(result.stdout); | 3463 | defer a.free(result.stdout); |
| @@ -3485,7 +3488,7 @@ pub const Server = struct { | |||
| 3485 | 3488 | ||
| 3486 | fn sealFd(fd: std.posix.fd_t) void { | 3489 | fn sealFd(fd: std.posix.fd_t) void { |
| 3487 | setCloexec(fd) catch |e| | 3490 | setCloexec(fd) catch |e| |
| 3488 | std.debug.print("muxd: seal fd {d}: {t}\n", .{ fd, e }); | 3491 | std.debug.print("mux d: seal fd {d}: {t}\n", .{ fd, e }); |
| 3489 | } | 3492 | } |
| 3490 | 3493 | ||
| 3491 | /// The flag back on every fd `execUpgrade` cleared; the memfd it also | 3494 | /// The flag back on every fd `execUpgrade` cleared; the memfd it also |
| @@ -3578,11 +3581,16 @@ pub const Server = struct { | |||
| 3578 | } | 3581 | } |
| 3579 | @memset(&self.observers, null); | 3582 | @memset(&self.observers, null); |
| 3580 | 3583 | ||
| 3581 | // Build argv: {"muxd", "run", "--resume-fd", "<n>"}. | 3584 | // Build argv: {"mux", "d", "run", "--resume-fd", "<n>"}. The |
| 3585 | // candidate is by definition NEWER than this binary, so it reads the | ||
| 3586 | // mode word; a v0.0.1-15 daemon exec'ing a newer one still sends the | ||
| 3587 | // bare `run` its own copy of this line was written with, which is | ||
| 3588 | // why mux.zig keeps that spelling alive. | ||
| 3582 | var fd_buf: [12]u8 = undefined; | 3589 | var fd_buf: [12]u8 = undefined; |
| 3583 | const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{memfd}) catch return; | 3590 | const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{memfd}) catch return; |
| 3584 | const argv = [_:null]?[*:0]const u8{ | 3591 | const argv = [_:null]?[*:0]const u8{ |
| 3585 | "muxd", | 3592 | "mux", |
| 3593 | "d", | ||
| 3586 | "run", | 3594 | "run", |
| 3587 | "--resume-fd", | 3595 | "--resume-fd", |
| 3588 | fd_str.ptr, | 3596 | fd_str.ptr, |
| @@ -3595,7 +3603,7 @@ pub const Server = struct { | |||
| 3595 | // execveZ returns a plain error set (not an error union): on success | 3603 | // execveZ returns a plain error set (not an error union): on success |
| 3596 | // it never returns, so any return is a failure. | 3604 | // it never returns, so any return is a failure. |
| 3597 | const exec_err = std.posix.execveZ(path_z.ptr, &argv, std.c.environ); | 3605 | const exec_err = std.posix.execveZ(path_z.ptr, &argv, std.c.environ); |
| 3598 | std.debug.print("muxd: upgrade exec failed: {s}\n", .{@errorName(exec_err)}); | 3606 | std.debug.print("mux d: upgrade exec failed: {s}\n", .{@errorName(exec_err)}); |
| 3599 | // execveZ only returns on failure (caught above); on success we | 3607 | // execveZ only returns on failure (caught above); on success we |
| 3600 | // never reach here. | 3608 | // never reach here. |
| 3601 | } | 3609 | } |
src/server/server_agent.zig
| Old | New | ||
|---|---|---|---|
| @@ -131,14 +131,14 @@ pub const AgentRelay = struct { | |||
| 131 | .{ parent, std.os.linux.getpid(), std.crypto.random.int(u48) }, | 131 | .{ parent, std.os.linux.getpid(), std.crypto.random.int(u48) }, |
| 132 | ) catch { | 132 | ) catch { |
| 133 | std.debug.print( | 133 | std.debug.print( |
| 134 | "muxd: agent forwarding unavailable (out of memory naming the " ++ | 134 | "mux d: agent forwarding unavailable (out of memory naming the " ++ |
| 135 | "socket directory under {s})\n", | 135 | "socket directory under {s})\n", |
| 136 | .{parent}, | 136 | .{parent}, |
| 137 | ); | 137 | ); |
| 138 | return null; | 138 | return null; |
| 139 | }; | 139 | }; |
| 140 | xdg.makeNewPrivateDir(dir) catch |err| { | 140 | xdg.makeNewPrivateDir(dir) catch |err| { |
| 141 | std.debug.print("muxd: agent forwarding unavailable ({s}: {t})\n", .{ dir, err }); | 141 | std.debug.print("mux d: agent forwarding unavailable ({s}: {t})\n", .{ dir, err }); |
| 142 | alloc.free(dir); | 142 | alloc.free(dir); |
| 143 | return null; | 143 | return null; |
| 144 | }; | 144 | }; |
| @@ -169,14 +169,14 @@ pub const AgentRelay = struct { | |||
| 169 | // is the same 107), so the length check is its error, not a second | 169 | // is the same 107), so the length check is its error, not a second |
| 170 | // rule stated here. | 170 | // rule stated here. |
| 171 | const addr = std.net.Address.initUnix(path) catch |err| { | 171 | const addr = std.net.Address.initUnix(path) catch |err| { |
| 172 | std.debug.print("muxd: no agent socket for session {s} ({t})\n", .{ name, err }); | 172 | std.debug.print("mux d: no agent socket for session {s} ({t})\n", .{ name, err }); |
| 173 | return null; | 173 | return null; |
| 174 | }; | 174 | }; |
| 175 | // Backlog 8, not the default 128: the only thing that ever dials | 175 | // Backlog 8, not the default 128: the only thing that ever dials |
| 176 | // this is one session's ssh clients, and a queue deeper than the | 176 | // this is one session's ssh clients, and a queue deeper than the |
| 177 | // hands typing into that shell is queueing for nobody. | 177 | // hands typing into that shell is queueing for nobody. |
| 178 | const listener = addr.listen(.{ .kernel_backlog = 8 }) catch |err| { | 178 | const listener = addr.listen(.{ .kernel_backlog = 8 }) catch |err| { |
| 179 | std.debug.print("muxd: no agent socket for session {s} ({t})\n", .{ name, err }); | 179 | std.debug.print("mux d: no agent socket for session {s} ({t})\n", .{ name, err }); |
| 180 | return null; | 180 | return null; |
| 181 | }; | 181 | }; |
| 182 | bound = true; | 182 | bound = true; |
| @@ -259,7 +259,7 @@ pub const AgentRelay = struct { | |||
| 259 | if (!self.full_said) { | 259 | if (!self.full_said) { |
| 260 | self.full_said = true; | 260 | self.full_said = true; |
| 261 | std.debug.print( | 261 | std.debug.print( |
| 262 | "muxd: agent channel table full ({d}); forwarding refused until one frees\n", | 262 | "mux d: agent channel table full ({d}); forwarding refused until one frees\n", |
| 263 | .{max_agent_chans}, | 263 | .{max_agent_chans}, |
| 264 | ); | 264 | ); |
| 265 | } | 265 | } |
src/server/server_sessions.zig
| Old | New | ||
|---|---|---|---|
| @@ -145,7 +145,7 @@ pub const SessionTable = struct { | |||
| 145 | 145 | ||
| 146 | /// Attach-or-create. Creation demands a size the session can live at, the | 146 | /// Attach-or-create. Creation demands a size the session can live at, the |
| 147 | /// SAME threshold `applySize` enforces. A 0x0 attach makes no size claim | 147 | /// SAME threshold `applySize` enforces. A 0x0 attach makes no size claim |
| 148 | /// at all — muxa, the wall's view stripes, an unzoomed browser tile — and a | 148 | /// at all — `mux a`, the wall's view stripes, an unzoomed browser tile — and a |
| 149 | /// client with no size must never be the reason a shell spawns. | 149 | /// client with no size must never be the reason a shell spawns. |
| 150 | /// | 150 | /// |
| 151 | /// 1x1 is a size a client genuinely sends, so gating on merely nonzero | 151 | /// 1x1 is a size a client genuinely sends, so gating on merely nonzero |
| @@ -177,7 +177,7 @@ pub const SessionTable = struct { | |||
| 177 | rows, | 177 | rows, |
| 178 | AgentRelay.bindSock(srv.alloc, srv.agents.dir, name), | 178 | AgentRelay.bindSock(srv.alloc, srv.agents.dir, name), |
| 179 | ) catch |err| { | 179 | ) catch |err| { |
| 180 | std.debug.print("muxd: session {s}: cannot spawn: {s}\n", .{ name, @errorName(err) }); | 180 | std.debug.print("mux d: session {s}: cannot spawn: {s}\n", .{ name, @errorName(err) }); |
| 181 | return null; | 181 | return null; |
| 182 | }; | 182 | }; |
| 183 | return si; | 183 | return si; |
| @@ -187,7 +187,7 @@ pub const SessionTable = struct { | |||
| 187 | /// dropped, its slot nulled, its name freed. Answers nothing, because a | 187 | /// dropped, its slot nulled, its name freed. Answers nothing, because a |
| 188 | /// shell's exit code is a fact about that shell and never about the | 188 | /// shell's exit code is a fact about that shell and never about the |
| 189 | /// daemon: an emptied table is a daemon with nothing on it, not a daemon | 189 | /// daemon: an emptied table is a daemon with nothing on it, not a daemon |
| 190 | /// that is leaving. `muxd stop` is the end (decisions.md). | 190 | /// that is leaving. `mux d stop` is the end (decisions.md). |
| 191 | pub fn reap(self: *SessionTable, srv: *Server) void { | 191 | pub fn reap(self: *SessionTable, srv: *Server) void { |
| 192 | for (&self.table, 0..) |*slot, si| { | 192 | for (&self.table, 0..) |*slot, si| { |
| 193 | const s = if (slot.*) |*sp| sp else continue; | 193 | const s = if (slot.*) |*sp| sp else continue; |
| @@ -210,7 +210,7 @@ pub const SessionTable = struct { | |||
| 210 | // ONE shell now. Queued then drained under the same 250ms | 210 | // ONE shell now. Queued then drained under the same 250ms |
| 211 | // deadline as ever, for the same reason as ever — a client | 211 | // deadline as ever, for the same reason as ever — a client |
| 212 | // that misses this frame reads EOF instead, reports | 212 | // that misses this frame reads EOF instead, reports |
| 213 | // "connection to muxd lost" and exits 1, so the shell's real | 213 | // "connection to the daemon lost" and exits 1, so the shell's real |
| 214 | // exit code would be lost rather than merely delayed. The | 214 | // exit code would be lost rather than merely delayed. The |
| 215 | // budget can stack — up to max_sessions × 250ms, if the | 215 | // budget can stack — up to max_sessions × 250ms, if the |
| 216 | // whole table dies into stalled peers in one pass — and a | 216 | // whole table dies into stalled peers in one pass — and a |
| @@ -220,7 +220,7 @@ pub const SessionTable = struct { | |||
| 220 | // constant of 32 (`max_sessions`) — and only ever paid in | 220 | // constant of 32 (`max_sessions`) — and only ever paid in |
| 221 | // full when the whole table dies into stalled peers in one | 221 | // full when the whole table dies into stalled peers in one |
| 222 | // pass. A dropped client's outstanding await needs | 222 | // pass. A dropped client's outstanding await needs |
| 223 | // nothing here: the connection dying IS muxa's answer | 223 | // nothing here: the connection dying IS `mux a`'s answer |
| 224 | // (decision 8), which is also why checkAwaits' session-less | 224 | // (decision 8), which is also why checkAwaits' session-less |
| 225 | // `continue` stays unreachable — no client survives its | 225 | // `continue` stays unreachable — no client survives its |
| 226 | // session. | 226 | // session. |
src/server/server_test_agent.zig
| Old | New | ||
|---|---|---|---|
| @@ -182,8 +182,8 @@ test "Server: a session with no agent socket does not inherit the daemon's" { | |||
| 182 | defer alloc.free(sock_path); | 182 | defer alloc.free(sock_path); |
| 183 | 183 | ||
| 184 | // The daemon is given an agent of its own, which is the whole hazard: | 184 | // The daemon is given an agent of its own, which is the whole hazard: |
| 185 | // muxd is often started from a desktop session that has one, and every | 185 | // the daemon is often started from a desktop session that has one, and every |
| 186 | // session it spawns inherits whatever muxd's own environment holds. | 186 | // session it spawns inherits whatever the daemon's own environment holds. |
| 187 | const inherited = try std.fmt.allocPrintSentinel(alloc, "{s}/daemons-own.sock", .{tmp.path()}, 0); | 187 | const inherited = try std.fmt.allocPrintSentinel(alloc, "{s}/daemons-own.sock", .{tmp.path()}, 0); |
| 188 | defer alloc.free(inherited); | 188 | defer alloc.free(inherited); |
| 189 | const libc = @cImport({ | 189 | const libc = @cImport({ |
| @@ -239,7 +239,7 @@ test "Server: a session with no agent socket does not inherit the daemon's" { | |||
| 239 | const got = std.fs.cwd().readFileAlloc(alloc, probe, 4096) catch | 239 | const got = std.fs.cwd().readFileAlloc(alloc, probe, 4096) catch |
| 240 | return error.ShellNeverAnswered; | 240 | return error.ShellNeverAnswered; |
| 241 | defer alloc.free(got); | 241 | defer alloc.free(got); |
| 242 | // Not the daemon's: a shell pointed at muxd's own ssh-agent reaches past | 242 | // Not the daemon's: a shell pointed at the daemon's own ssh-agent reaches past |
| 243 | // every client watching it, which is the one thing the socket exists to | 243 | // every client watching it, which is the one thing the socket exists to |
| 244 | // stop. "No socket" has to mean no agent, not somebody else's. | 244 | // stop. "No socket" has to mean no agent, not somebody else's. |
| 245 | try std.testing.expectEqualSlices(u8, "UNSET", got); | 245 | try std.testing.expectEqualSlices(u8, "UNSET", got); |
src/server/server_test_attach.zig
| Old | New | ||
|---|---|---|---|
| @@ -1440,7 +1440,7 @@ test "Server: a 0x0 attach joins but never creates" { | |||
| 1440 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); | 1440 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); |
| 1441 | defer srv.deinit(); | 1441 | defer srv.deinit(); |
| 1442 | 1442 | ||
| 1443 | // muxa send attaches at 0x0 — it makes no size claim. Against a name | 1443 | // mux a send attaches at 0x0 — it makes no size claim. Against a name |
| 1444 | // that does not exist, that must be a refusal, never a shell spawned | 1444 | // that does not exist, that must be a refusal, never a shell spawned |
| 1445 | // at a size nobody has. | 1445 | // at a size nobody has. |
| 1446 | const c1 = try std.net.connectUnixSocket(sock_path); | 1446 | const c1 = try std.net.connectUnixSocket(sock_path); |
| @@ -1643,7 +1643,7 @@ test "Server: an observer that sends one byte does not stall the pump, and finis | |||
| 1643 | defer srv.deinit(); | 1643 | defer srv.deinit(); |
| 1644 | 1644 | ||
| 1645 | // Through the real listener, so the accept path seats it as the daemon | 1645 | // Through the real listener, so the accept path seats it as the daemon |
| 1646 | // would seat `muxd stats`. | 1646 | // would seat `mux d stats`. |
| 1647 | const obs = try std.net.connectUnixSocket(sock_path); | 1647 | const obs = try std.net.connectUnixSocket(sock_path); |
| 1648 | defer obs.close(); | 1648 | defer obs.close(); |
| 1649 | try srv.pumpOnce(20); // accept | 1649 | try srv.pumpOnce(20); // accept |
| @@ -1866,7 +1866,7 @@ test "Server: the two bounded deadlines read a monotonic clock, not the calendar | |||
| 1866 | // machine that has been up less than a decade separates them by a | 1866 | // machine that has been up less than a decade separates them by a |
| 1867 | // margin no scheduling delay can close. That gap is the whole assertion: | 1867 | // margin no scheduling delay can close. That gap is the whole assertion: |
| 1868 | // a deadline stamped from the calendar is postponed by an NTP step | 1868 | // a deadline stamped from the calendar is postponed by an NTP step |
| 1869 | // backwards — during which `pty.master` is -1 and `muxd upgrade` is | 1869 | // backwards — during which `pty.master` is -1 and `mux d upgrade` is |
| 1870 | // refused — and an idle deadline stamped from it drops every healthy | 1870 | // refused — and an idle deadline stamped from it drops every healthy |
| 1871 | // observer on a step forwards. | 1871 | // observer on a step forwards. |
| 1872 | const wall_now = std.time.milliTimestamp(); | 1872 | const wall_now = std.time.milliTimestamp(); |
src/server/server_test_deliver.zig
| Old | New | ||
|---|---|---|---|
| @@ -289,7 +289,7 @@ test "Server: the shell's exit status reaches an attached client" { | |||
| 289 | 289 | ||
| 290 | // The exit code must survive the daemon's own shutdown. Without the | 290 | // The exit code must survive the daemon's own shutdown. Without the |
| 291 | // bounded drain this frame can be lost, and a client that misses it | 291 | // bounded drain this frame can be lost, and a client that misses it |
| 292 | // reports "connection to muxd lost" and exits 1 instead of 7. | 292 | // reports "connection to the daemon lost" and exits 1 instead of 7. |
| 293 | try proto.writeFrame(c.handle, .input, "exit 7\n"); | 293 | try proto.writeFrame(c.handle, .input, "exit 7\n"); |
| 294 | 294 | ||
| 295 | var status: ?u8 = null; | 295 | var status: ?u8 = null; |
src/server/server_test_session.zig
| Old | New | ||
|---|---|---|---|
| @@ -145,7 +145,7 @@ test "Server: a non-socket at the path is refused, not deleted" { | |||
| 145 | const file_path = try std.fmt.allocPrint(alloc, "{s}/notes.txt", .{dir_path}); | 145 | const file_path = try std.fmt.allocPrint(alloc, "{s}/notes.txt", .{dir_path}); |
| 146 | defer alloc.free(file_path); | 146 | defer alloc.free(file_path); |
| 147 | 147 | ||
| 148 | const contents = "muxd must not eat this"; | 148 | const contents = "mux must not eat this"; |
| 149 | try tmp.dir.writeFile(.{ .sub_path = "notes.txt", .data = contents }); | 149 | try tmp.dir.writeFile(.{ .sub_path = "notes.txt", .data = contents }); |
| 150 | 150 | ||
| 151 | // Connecting to a regular file fails with ECONNREFUSED — the very same | 151 | // Connecting to a regular file fails with ECONNREFUSED — the very same |
| @@ -359,7 +359,7 @@ test "Server: stats reports live client slots, and the number comes down again" | |||
| 359 | 359 | ||
| 360 | // A hand-counted bound is the failure mode this pins: `statsText` writes into | 360 | // A hand-counted bound is the failure mode this pins: `statsText` writes into |
| 361 | // a fixed buffer, so an under-count is `error.WriteFailed`, and the | 361 | // a fixed buffer, so an under-count is `error.WriteFailed`, and the |
| 362 | // `.stats_req` arm answers that by dropping the client — `muxd stats` reports | 362 | // `.stats_req` arm answers that by dropping the client — `mux d stats` reports |
| 363 | // the daemon gone rather than a short line. Nothing reaches these values | 363 | // the daemon gone rather than a short line. Nothing reaches these values |
| 364 | // today; a new counter in `stats_main_fmt` is what makes it reachable, and | 364 | // today; a new counter in `stats_main_fmt` is what makes it reachable, and |
| 365 | // that is exactly when this test has to be the thing that notices. | 365 | // that is exactly when this test has to be the thing that notices. |
| @@ -395,7 +395,7 @@ test "Server: stop_req from a bare connection requests shutdown; run returns 0" | |||
| 395 | const c = try std.net.connectUnixSocket(sock_path); | 395 | const c = try std.net.connectUnixSocket(sock_path); |
| 396 | defer c.close(); | 396 | defer c.close(); |
| 397 | // No attach first: the frame must be honored from the OBSERVER | 397 | // No attach first: the frame must be honored from the OBSERVER |
| 398 | // dispatch, which is where a bare `muxd stop` connection lives. | 398 | // dispatch, which is where a bare `mux d stop` connection lives. |
| 399 | try proto.writeFrame(c.handle, .stop_req, ""); | 399 | try proto.writeFrame(c.handle, .stop_req, ""); |
| 400 | 400 | ||
| 401 | // Bounded, so the mutation run FAILS here instead of hanging the | 401 | // Bounded, so the mutation run FAILS here instead of hanging the |
| @@ -478,7 +478,7 @@ test "Server: endpoint_req binds a listener lazily, answers the same port on bot | |||
| 478 | // Now the wire, on a listener that already exists — which is what | 478 | // Now the wire, on a listener that already exists — which is what |
| 479 | // keeps this half from depending on the test machine having a real | 479 | // keeps this half from depending on the test machine having a real |
| 480 | // ~/.config/mux/key. The observer arm is the load-bearing one: | 480 | // ~/.config/mux/key. The observer arm is the load-bearing one: |
| 481 | // `muxd endpoint` never attaches. | 481 | // `mux d endpoint` never attaches. |
| 482 | const obs = try std.net.connectUnixSocket(sock_path); | 482 | const obs = try std.net.connectUnixSocket(sock_path); |
| 483 | defer obs.close(); | 483 | defer obs.close(); |
| 484 | try proto.writeFrame(obs.handle, .endpoint_req, ""); | 484 | try proto.writeFrame(obs.handle, .endpoint_req, ""); |
| @@ -585,7 +585,7 @@ test "Server: status_req is answered on an attached client and on a bare observe | |||
| 585 | // which mechanism would decide, not a claim that one just did. | 585 | // which mechanism would decide, not a claim that one just did. |
| 586 | try std.testing.expectEqual(proto.Mechanism.pgid, st.cmd.mechanism); | 586 | try std.testing.expectEqual(proto.Mechanism.pgid, st.cmd.mechanism); |
| 587 | 587 | ||
| 588 | // `muxa status` never attaches, so the observer arm is the load-bearing | 588 | // `mux a status` never attaches, so the observer arm is the load-bearing |
| 589 | // one — same reasoning as endpoint_req's, and the same failure if it is | 589 | // one — same reasoning as endpoint_req's, and the same failure if it is |
| 590 | // missing: the frame falls into `else => {}` and the caller hangs. | 590 | // missing: the frame falls into `else => {}` and the caller hangs. |
| 591 | const obs = try std.net.connectUnixSocket(sock_path); | 591 | const obs = try std.net.connectUnixSocket(sock_path); |
| @@ -848,7 +848,7 @@ test "Server: an attach past max_sessions is refused with exit_status, sessions | |||
| 848 | // session cannot tell "one grace for the table" from "one grace each". A | 848 | // session cannot tell "one grace for the table" from "one grace each". A |
| 849 | // supervisor with a stop timeout kills a daemon that spends the second shape, | 849 | // supervisor with a stop timeout kills a daemon that spends the second shape, |
| 850 | // and a killed daemon leaves its socket and its agent sockets unlinked for | 850 | // and a killed daemon leaves its socket and its agent sockets unlinked for |
| 851 | // the next `muxd run` to reason about. | 851 | // the next `mux d run` to reason about. |
| 852 | test "Server: a table of TERM-ignoring shells costs one grace, not one each" { | 852 | test "Server: a table of TERM-ignoring shells costs one grace, not one each" { |
| 853 | const alloc = std.testing.allocator; | 853 | const alloc = std.testing.allocator; |
| 854 | 854 | ||
| @@ -1092,7 +1092,7 @@ fn awaitFrameThreaded( | |||
| 1092 | } | 1092 | } |
| 1093 | 1093 | ||
| 1094 | /// Ask the daemon for its stats until `want` appears in the text — what | 1094 | /// Ask the daemon for its stats until `want` appears in the text — what |
| 1095 | /// `muxd stats` shows an operator looking at a box with nothing on it. | 1095 | /// `mux d stats` shows an operator looking at a box with nothing on it. |
| 1096 | /// | 1096 | /// |
| 1097 | /// A connection of its OWN, per call, and that is the assertion's teeth: a | 1097 | /// A connection of its OWN, per call, and that is the assertion's teeth: a |
| 1098 | /// reply that misses its 200 ms stays buffered, so a shared socket would let | 1098 | /// reply that misses its 200 ms stays buffered, so a shared socket would let |
| @@ -1306,7 +1306,7 @@ test "Server: dump names a session; an unknown name answers in words" { | |||
| 1306 | const bad = (try awaitFrame(alloc, &srv, ca.handle, .dump_reply, 400)) orelse | 1306 | const bad = (try awaitFrame(alloc, &srv, ca.handle, .dump_reply, 400)) orelse |
| 1307 | return error.NoDumpReplyForBadName; | 1307 | return error.NoDumpReplyForBadName; |
| 1308 | defer bad.deinit(alloc); | 1308 | defer bad.deinit(alloc); |
| 1309 | try std.testing.expectEqualStrings("muxd: no such session: z\n", bad.payload); | 1309 | try std.testing.expectEqualStrings("mux d: no such session: z\n", bad.payload); |
| 1310 | } | 1310 | } |
| 1311 | 1311 | ||
| 1312 | test "Server: an observer's status_req names a session by tail" { | 1312 | test "Server: an observer's status_req names a session by tail" { |
| @@ -1372,7 +1372,7 @@ test "Server: an observer's status_req names a session by tail" { | |||
| 1372 | 1372 | ||
| 1373 | // An unknown name still ends the connection — status_reply is a fixed | 1373 | // An unknown name still ends the connection — status_reply is a fixed |
| 1374 | // binary layout with no room for words — but it says exit_status 1 | 1374 | // binary layout with no room for words — but it says exit_status 1 |
| 1375 | // first, the same no a refused attach gets. Silence made `muxa status | 1375 | // first, the same no a refused attach gets. Silence made `mux a status |
| 1376 | // --session nosuch` report `DaemonGone`, which reads as a dead daemon | 1376 | // --session nosuch` report `DaemonGone`, which reads as a dead daemon |
| 1377 | // rather than a name that was never there. | 1377 | // rather than a name that was never there. |
| 1378 | try proto.writeFrame(obs.handle, .status_req, "z"); | 1378 | try proto.writeFrame(obs.handle, .status_req, "z"); |
| @@ -1528,7 +1528,7 @@ test "Server: a session-less slot's status_req resolves the tail like an observe | |||
| 1528 | // pushInbound/handleFrame exactly as a QUIC datagram would be — not | 1528 | // pushInbound/handleFrame exactly as a QUIC datagram would be — not |
| 1529 | // serviceObserver, which only a unix connection can ever land in. This | 1529 | // serviceObserver, which only a unix connection can ever land in. This |
| 1530 | // is the gap the review found: handleFrame's status_req arm used to | 1530 | // is the gap the review found: handleFrame's status_req arm used to |
| 1531 | // `orelse return` a session-less slot outright, so `muxa status --quic` | 1531 | // `orelse return` a session-less slot outright, so `mux a status --quic` |
| 1532 | // against a live daemon just timed out. | 1532 | // against a live daemon just timed out. |
| 1533 | // Slot 1, not 0: cb's own attach already claimed slot 0 (freeClientSlot | 1533 | // Slot 1, not 0: cb's own attach already claimed slot 0 (freeClientSlot |
| 1534 | // hands out the first free index), and stomping it would leak its | 1534 | // hands out the first free index), and stomping it would leak its |
| @@ -1564,7 +1564,7 @@ test "Server: a session-less slot's status_req resolves the tail like an observe | |||
| 1564 | // An unknown name refuses in the daemon's one word for no — | 1564 | // An unknown name refuses in the daemon's one word for no — |
| 1565 | // exit_status 1 — and then drops the CLIENT, dropClient being the | 1565 | // exit_status 1 — and then drops the CLIENT, dropClient being the |
| 1566 | // QUIC-shaped analogue of dropObserver's drop. The frame has to | 1566 | // QUIC-shaped analogue of dropObserver's drop. The frame has to |
| 1567 | // precede the close: `muxa status --quic --session nosuch` reads | 1567 | // precede the close: `mux a status --quic --session nosuch` reads |
| 1568 | // silence as a dead daemon. | 1568 | // silence as a dead daemon. |
| 1569 | // | 1569 | // |
| 1570 | // The SOCKET sink only, which is what this pair is: it flushes as it | 1570 | // The SOCKET sink only, which is what this pair is: it flushes as it |
| @@ -1816,7 +1816,7 @@ test "Server: a keystroke into an ending session does not cost that client its e | |||
| 1816 | 1816 | ||
| 1817 | // The master is closed but the shell is still alive, so this keystroke | 1817 | // The master is closed but the shell is still alive, so this keystroke |
| 1818 | // reaches a session with nowhere to put it. Dropping the client for it | 1818 | // reaches a session with nowhere to put it. Dropping the client for it |
| 1819 | // would trade the exit code for "connection to muxd lost". | 1819 | // would trade the exit code for "connection to the daemon lost". |
| 1820 | try proto.writeFrame(c.handle, .input, "x"); | 1820 | try proto.writeFrame(c.handle, .input, "x"); |
| 1821 | const x = (try awaitFrame(alloc, &srv, c.handle, .exit_status, 600)) orelse return error.NoExitStatus; | 1821 | const x = (try awaitFrame(alloc, &srv, c.handle, .exit_status, 600)) orelse return error.NoExitStatus; |
| 1822 | x.deinit(alloc); | 1822 | x.deinit(alloc); |
src/server/server_test_upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -235,7 +235,7 @@ test "initFromManifest: the return watermark is re-stamped, never carried across | |||
| 235 | // The watermark cannot. The delta tracker is rebuilt from zero, so a | 235 | // The watermark cannot. The delta tracker is rebuilt from zero, so a |
| 236 | // seq from the old space is a watermark from the FUTURE, and no return | 236 | // seq from the old space is a watermark from the FUTURE, and no return |
| 237 | // after the upgrade can ever exceed it: measured on a live daemon, the | 237 | // after the upgrade can ever exceed it: measured on a live daemon, the |
| 238 | // first `muxa run` after an upgrade timed out and the second did not. | 238 | // first `mux a run` after an upgrade timed out and the second did not. |
| 239 | try std.testing.expectEqual(s2.tracker.seq, s2.last_return.?.seq); | 239 | try std.testing.expectEqual(s2.tracker.seq, s2.last_return.?.seq); |
| 240 | } | 240 | } |
| 241 | 241 | ||
| @@ -276,7 +276,7 @@ test "validateUpgrade: a relative path is refused" { | |||
| 276 | const reason = srv.validateUpgrade(.{ | 276 | const reason = srv.validateUpgrade(.{ |
| 277 | .allow_same_version = true, | 277 | .allow_same_version = true, |
| 278 | .version = "0.0.1-14", | 278 | .version = "0.0.1-14", |
| 279 | .path = "relative/muxd", | 279 | .path = "relative/mux", |
| 280 | }, "0.0.1-13"); | 280 | }, "0.0.1-13"); |
| 281 | try std.testing.expect(reason != null); | 281 | try std.testing.expect(reason != null); |
| 282 | if (reason) |r| alloc.free(r); | 282 | if (reason) |r| alloc.free(r); |
| @@ -417,18 +417,18 @@ test "Server: an upgrade asked for during a session's hangup is refused, not att | |||
| 417 | // the ending session: a wrong version or an unrunnable path would refuse | 417 | // the ending session: a wrong version or an unrunnable path would refuse |
| 418 | // this upgrade whether or not anything was hanging up. | 418 | // this upgrade whether or not anything was hanging up. |
| 419 | try tmp.dir.writeFile(.{ | 419 | try tmp.dir.writeFile(.{ |
| 420 | .sub_path = "fakemuxd.sh", | 420 | .sub_path = "fakemux.sh", |
| 421 | .data = | 421 | .data = |
| 422 | \\#!/bin/sh | 422 | \\#!/bin/sh |
| 423 | \\case "$1" in | 423 | \\case "$1" in |
| 424 | \\ --version) printf 'muxd 9.9.9\n' ;; | 424 | \\ --version) printf 'mux 9.9.9\n' ;; |
| 425 | \\esac | 425 | \\esac |
| 426 | \\exit 0 | 426 | \\exit 0 |
| 427 | \\ | 427 | \\ |
| 428 | , | 428 | , |
| 429 | .flags = .{ .mode = 0o755 }, | 429 | .flags = .{ .mode = 0o755 }, |
| 430 | }); | 430 | }); |
| 431 | const cand = try std.fmt.allocPrint(alloc, "{s}/fakemuxd.sh", .{tmp.path()}); | 431 | const cand = try std.fmt.allocPrint(alloc, "{s}/fakemux.sh", .{tmp.path()}); |
| 432 | defer alloc.free(cand); | 432 | defer alloc.free(cand); |
| 433 | 433 | ||
| 434 | const obs = try std.net.connectUnixSocket(sock_path); | 434 | const obs = try std.net.connectUnixSocket(sock_path); |
| @@ -485,18 +485,18 @@ test "Server: an EMPTY daemon upgrades — the manifest names no session and the | |||
| 485 | // nothing: emptiness must not be mistaken for the one state that DOES | 485 | // nothing: emptiness must not be mistaken for the one state that DOES |
| 486 | // refuse an upgrade, a session mid-hangup. | 486 | // refuse an upgrade, a session mid-hangup. |
| 487 | try tmp.dir.writeFile(.{ | 487 | try tmp.dir.writeFile(.{ |
| 488 | .sub_path = "fakemuxd.sh", | 488 | .sub_path = "fakemux.sh", |
| 489 | .data = | 489 | .data = |
| 490 | \\#!/bin/sh | 490 | \\#!/bin/sh |
| 491 | \\case "$1" in | 491 | \\case "$1" in |
| 492 | \\ --version) printf 'muxd 9.9.9\n' ;; | 492 | \\ --version) printf 'mux 9.9.9\n' ;; |
| 493 | \\esac | 493 | \\esac |
| 494 | \\exit 0 | 494 | \\exit 0 |
| 495 | \\ | 495 | \\ |
| 496 | , | 496 | , |
| 497 | .flags = .{ .mode = 0o755 }, | 497 | .flags = .{ .mode = 0o755 }, |
| 498 | }); | 498 | }); |
| 499 | const cand = try std.fmt.allocPrint(alloc, "{s}/fakemuxd.sh", .{dir_path}); | 499 | const cand = try std.fmt.allocPrint(alloc, "{s}/fakemux.sh", .{dir_path}); |
| 500 | defer alloc.free(cand); | 500 | defer alloc.free(cand); |
| 501 | if (srv.validateUpgrade(.{ | 501 | if (srv.validateUpgrade(.{ |
| 502 | .allow_same_version = false, | 502 | .allow_same_version = false, |
| @@ -563,18 +563,18 @@ test "Server: an accepted end cancels a pending upgrade — the exec never sees | |||
| 563 | (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); | 563 | (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); |
| 564 | 564 | ||
| 565 | try tmp.dir.writeFile(.{ | 565 | try tmp.dir.writeFile(.{ |
| 566 | .sub_path = "fakemuxd.sh", | 566 | .sub_path = "fakemux.sh", |
| 567 | .data = | 567 | .data = |
| 568 | \\#!/bin/sh | 568 | \\#!/bin/sh |
| 569 | \\case "$1" in | 569 | \\case "$1" in |
| 570 | \\ --version) printf 'muxd 9.9.9\n' ;; | 570 | \\ --version) printf 'mux 9.9.9\n' ;; |
| 571 | \\esac | 571 | \\esac |
| 572 | \\exit 0 | 572 | \\exit 0 |
| 573 | \\ | 573 | \\ |
| 574 | , | 574 | , |
| 575 | .flags = .{ .mode = 0o755 }, | 575 | .flags = .{ .mode = 0o755 }, |
| 576 | }); | 576 | }); |
| 577 | const cand = try std.fmt.allocPrint(alloc, "{s}/fakemuxd.sh", .{tmp.path()}); | 577 | const cand = try std.fmt.allocPrint(alloc, "{s}/fakemux.sh", .{tmp.path()}); |
| 578 | defer alloc.free(cand); | 578 | defer alloc.free(cand); |
| 579 | 579 | ||
| 580 | // Two observers, seated in slot order BEFORE either speaks: the hazard | 580 | // Two observers, seated in slot order BEFORE either speaks: the hazard |
src/server/shellint.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,4 +1,4 @@ | |||
| 1 | //! Shell integration: OSC 133 marks injected at spawn. muxd forks the | 1 | //! Shell integration: OSC 133 marks injected at spawn. The daemon forks the |
| 2 | //! session shell itself, so injection is env + argv at spawn time — no | 2 | //! session shell itself, so injection is env + argv at spawn time — no |
| 3 | //! rc-file edits, ever. Detection is by shell basename; unknown shells get | 3 | //! rc-file edits, ever. Detection is by shell basename; unknown shells get |
| 4 | //! nothing and the session runs on the pgid/settle fallbacks. | 4 | //! nothing and the session runs on the pgid/settle fallbacks. |
| @@ -84,7 +84,7 @@ pub const bash_init = | |||
| 84 | \\# | 84 | \\# |
| 85 | \\# Type-aware because bash 5.1 made PROMPT_COMMAND an array and the | 85 | \\# Type-aware because bash 5.1 made PROMPT_COMMAND an array and the |
| 86 | \\# distributions took it up — Arch's /etc/bash.bashrc appends one under | 86 | \\# distributions took it up — Arch's /etc/bash.bashrc appends one under |
| 87 | \\# any xterm* TERM, which is exactly what muxd sets. A string assignment | 87 | \\# any xterm* TERM, which is exactly what the daemon sets. A string assignment |
| 88 | \\# onto an array lands on element 0 and folds a member into a compound, | 88 | \\# onto an array lands on element 0 and folds a member into a compound, |
| 89 | \\# which is precisely the shape the membership check above cannot match. | 89 | \\# which is precisely the shape the membership check above cannot match. |
| 90 | \\if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then | 90 | \\if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then |
| @@ -173,7 +173,7 @@ pub fn install( | |||
| 173 | .{ parent_dir, std.os.linux.getpid(), std.crypto.random.int(u48) }, | 173 | .{ parent_dir, std.os.linux.getpid(), std.crypto.random.int(u48) }, |
| 174 | ) catch { | 174 | ) catch { |
| 175 | std.debug.print( | 175 | std.debug.print( |
| 176 | "muxd: shell integration unavailable (out of memory naming the shim " ++ | 176 | "mux d: shell integration unavailable (out of memory naming the shim " ++ |
| 177 | "directory under {s}); the session runs without command marks\n", | 177 | "directory under {s}); the session runs without command marks\n", |
| 178 | .{parent_dir}, | 178 | .{parent_dir}, |
| 179 | ); | 179 | ); |
| @@ -181,7 +181,7 @@ pub fn install( | |||
| 181 | }; | 181 | }; |
| 182 | return prepare(arena, dir, shell_path) catch |err| { | 182 | return prepare(arena, dir, shell_path) catch |err| { |
| 183 | std.debug.print( | 183 | std.debug.print( |
| 184 | "muxd: shell integration unavailable ({s}: {t}); " ++ | 184 | "mux d: shell integration unavailable ({s}: {t}); " ++ |
| 185 | "the session runs without command marks\n", | 185 | "the session runs without command marks\n", |
| 186 | .{ dir, err }, | 186 | .{ dir, err }, |
| 187 | ); | 187 | ); |
src/server/upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -66,7 +66,7 @@ pub const QuicState = struct { | |||
| 66 | }; | 66 | }; |
| 67 | 67 | ||
| 68 | /// Cumulative counters cross so an upgrade is not mistaken for a restart | 68 | /// Cumulative counters cross so an upgrade is not mistaken for a restart |
| 69 | /// by anything sampling `muxd stats`. | 69 | /// by anything sampling `mux d stats`. |
| 70 | pub const Counters = struct { | 70 | pub const Counters = struct { |
| 71 | snapshots: u64 = 0, | 71 | snapshots: u64 = 0, |
| 72 | snapshot_bytes: u64 = 0, | 72 | snapshot_bytes: u64 = 0, |
| @@ -117,7 +117,7 @@ pub const SessionRec = struct { | |||
| 117 | /// blank until the shell next sets one. | 117 | /// blank until the shell next sets one. |
| 118 | title: ?[]const u8, | 118 | title: ?[]const u8, |
| 119 | cmd: CmdRec, | 119 | cmd: CmdRec, |
| 120 | /// The return watermark `muxa await --since` answers from; losing it | 120 | /// The return watermark `mux a await --since` answers from; losing it |
| 121 | /// turns a satisfiable await into a timeout. | 121 | /// turns a satisfiable await into a timeout. |
| 122 | last_return: ?proto.CmdState, | 122 | last_return: ?proto.CmdState, |
| 123 | agent_fd: i32, | 123 | agent_fd: i32, |
| @@ -348,7 +348,7 @@ fn sampleDaemon() Daemon { | |||
| 348 | q.key[31] = 0xCD; | 348 | q.key[31] = 0xCD; |
| 349 | return .{ | 349 | return .{ |
| 350 | .writer_version = "0.0.1-13", | 350 | .writer_version = "0.0.1-13", |
| 351 | .writer_path = "/usr/bin/muxd.old", | 351 | .writer_path = "/usr/bin/mux.old", |
| 352 | .sock_path = "/run/user/1000/muxd.sock", | 352 | .sock_path = "/run/user/1000/muxd.sock", |
| 353 | .listener_fd = 3, | 353 | .listener_fd = 3, |
| 354 | .shellint_dir = "/tmp/mux-shim-1234", | 354 | .shellint_dir = "/tmp/mux-shim-1234", |
src/sockpath.zig
| Old | New | ||
|---|---|---|---|
| @@ -83,7 +83,7 @@ pub fn claim(path: []const u8) !void { | |||
| 83 | // Nobody is listening — but this is NOT yet proof of a stale | 83 | // Nobody is listening — but this is NOT yet proof of a stale |
| 84 | // socket: Linux answers ECONNREFUSED for a regular file at the | 84 | // socket: Linux answers ECONNREFUSED for a regular file at the |
| 85 | // path exactly as it does for a dead socket, so connect alone | 85 | // path exactly as it does for a dead socket, so connect alone |
| 86 | // cannot tell a dead daemon from `muxd run --sock notes.txt`. | 86 | // cannot tell a dead daemon from `mux d run --sock notes.txt`. |
| 87 | // The stat below is what separates them. | 87 | // The stat below is what separates them. |
| 88 | error.ConnectionRefused => {}, | 88 | error.ConnectionRefused => {}, |
| 89 | else => |e| return e, | 89 | else => |e| return e, |
src/tui/interact.zig
| Old | New | ||
|---|---|---|---|
| @@ -874,7 +874,7 @@ pub fn writeSelectionCopy( | |||
| 874 | } | 874 | } |
| 875 | 875 | ||
| 876 | /// An empty title CLEARS the host terminal's; the peer need not | 876 | /// An empty title CLEARS the host terminal's; the peer need not |
| 877 | /// be this version of muxd. | 877 | /// be this daemon version. |
| 878 | fn appendTermTitle( | 878 | fn appendTermTitle( |
| 879 | out: *std.ArrayList(u8), | 879 | out: *std.ArrayList(u8), |
| 880 | alloc: std.mem.Allocator, | 880 | alloc: std.mem.Allocator, |
| @@ -910,7 +910,7 @@ fn writeSideChannel( | |||
| 910 | // ---- prediction -------------------------------------------------------- | 910 | // ---- prediction -------------------------------------------------------- |
| 911 | // | 911 | // |
| 912 | // Nothing below writes to the replica — see the invariants at the top of the | 912 | // Nothing below writes to the replica — see the invariants at the top of the |
| 913 | // file. It is why the replica stays comparable to `muxd dump` at every | 913 | // file. It is why the replica stays comparable to `mux d dump` at every |
| 914 | // instant. | 914 | // instant. |
| 915 | 915 | ||
| 916 | /// What the replica shows at one cell — the `prev_ch` a prediction is | 916 | /// What the replica shows at one cell — the `prev_ch` a prediction is |
| @@ -1286,11 +1286,11 @@ pub const Core = struct { | |||
| 1286 | size: proto.Size, | 1286 | size: proto.Size, |
| 1287 | /// The terminal row this tile's rect starts at. A wall tile paints | 1287 | /// The terminal row this tile's rect starts at. A wall tile paints |
| 1288 | /// inside its own rect, not the screen's origin; the plain client and | 1288 | /// inside its own rect, not the screen's origin; the plain client and |
| 1289 | /// muxa leave it 0 and paint from row 1 as before. | 1289 | /// `mux a` leave it 0 and paint from row 1 as before. |
| 1290 | row_off: u16 = 0, | 1290 | row_off: u16 = 0, |
| 1291 | /// The terminal column this tile's rect starts at. A beside-neighbour | 1291 | /// The terminal column this tile's rect starts at. A beside-neighbour |
| 1292 | /// would be blanked by a whole-line clear, so every painter addresses | 1292 | /// would be blanked by a whole-line clear, so every painter addresses |
| 1293 | /// its own column origin. The plain client and muxa leave it 0. | 1293 | /// its own column origin. The plain client and `mux a` leave it 0. |
| 1294 | col_off: u16 = 0, | 1294 | col_off: u16 = 0, |
| 1295 | /// Whether this Core may clear the whole screen. A plain client or a | 1295 | /// Whether this Core may clear the whole screen. A plain client or a |
| 1296 | /// one-tile wall owns every row; a tile among neighbours does not. Set | 1296 | /// one-tile wall owns every row; a tile among neighbours does not. Set |
| @@ -1859,7 +1859,7 @@ pub const Core = struct { | |||
| 1859 | .agent_open, .agent_data, .agent_close => return .not_mine, | 1859 | .agent_open, .agent_data, .agent_close => return .not_mine, |
| 1860 | // Named rather than swept into the `else`, so that adding a | 1860 | // Named rather than swept into the `else`, so that adding a |
| 1861 | // meaning for one of them is an edit here and not a new switch | 1861 | // meaning for one of them is an edit here and not a new switch |
| 1862 | // somewhere else. Every one is either part of muxa's | 1862 | // somewhere else. Every one is either part of `mux a`'s |
| 1863 | // conversation with the daemon (its replies, and the | 1863 | // conversation with the daemon (its replies, and the |
| 1864 | // `cmd_state` pushes it subscribes to) or a frame that travels | 1864 | // `cmd_state` pushes it subscribes to) or a frame that travels |
| 1865 | // the other way. | 1865 | // the other way. |
src/tui/wall_picker.zig
| Old | New | ||
|---|---|---|---|
| @@ -201,7 +201,7 @@ pub fn pickBirth( | |||
| 201 | if (target == .hand) { | 201 | if (target == .hand) { |
| 202 | target.hand.asked = true; | 202 | target.hand.asked = true; |
| 203 | // ...and quietly. This dial happens on a tile thread, under the | 203 | // ...and quietly. This dial happens on a tile thread, under the |
| 204 | // wall's alternate screen: `muxd start`'s progress would land over | 204 | // wall's alternate screen: `mux d start`'s progress would land over |
| 205 | // tiles and rails, where the tile's own `connecting` label is | 205 | // tiles and rails, where the tile's own `connecting` label is |
| 206 | // already saying the only thing there is to say. | 206 | // already saying the only thing there is to say. |
| 207 | target.hand.quiet = true; | 207 | target.hand.quiet = true; |
src/tui/wall_pump.zig
| Old | New | ||
|---|---|---|---|
| @@ -245,7 +245,7 @@ fn dial(alloc: std.mem.Allocator, t: *Tile, target_in: client.Target) ?client.Tr | |||
| 245 | while (t.shared.running.load(.acquire) and !t.gone.load(.acquire)) { | 245 | while (t.shared.running.load(.acquire) and !t.gone.load(.acquire)) { |
| 246 | if (client.Transport.open(alloc, target, null, -1)) |tr| return tr else |_| {} | 246 | if (client.Transport.open(alloc, target, null, -1)) |tr| return tr else |_| {} |
| 247 | // An ask buys ONE attempt. Every retry below is the wall's own | 247 | // An ask buys ONE attempt. Every retry below is the wall's own |
| 248 | // idea: a `muxd start` per backoff would restart a daemon for as | 248 | // idea: a `mux d start` per backoff would restart a daemon for as |
| 249 | // long as the tile lives, and a fallback line per backoff would | 249 | // long as the tile lives, and a fallback line per backoff would |
| 250 | // scroll the alternate screen the tiles are painted on. | 250 | // scroll the alternate screen the tiles are painted on. |
| 251 | if (target == .hand) target.hand.asked = false; | 251 | if (target == .hand) target.hand.asked = false; |
| @@ -545,7 +545,7 @@ pub fn pumpTile(t: *Tile) void { | |||
| 545 | // The ask is SPENT, on whichever of the two branches above got the | 545 | // The ask is SPENT, on whichever of the two branches above got the |
| 546 | // link: the entry tile's dial happened on the main thread, a picker | 546 | // link: the entry tile's dial happened on the main thread, a picker |
| 547 | // birth's just happened here. Every `redial` below is handed this | 547 | // birth's just happened here. Every `redial` below is handed this |
| 548 | // copy, so a reconnect can neither start a daemon — `muxd stop` typed | 548 | // copy, so a reconnect can neither start a daemon — `mux d stop` typed |
| 549 | // on that box would otherwise be undone by the next backoff, the | 549 | // on that box would otherwise be undone by the next backoff, the |
| 550 | // poll's bug moved onto a tile — nor print the fallback line onto the | 550 | // poll's bug moved onto a tile — nor print the fallback line onto the |
| 551 | // alternate screen the tiles are painted on. | 551 | // alternate screen the tiles are painted on. |
| @@ -745,7 +745,7 @@ pub fn pumpTile(t: *Tile) void { | |||
| 745 | if (pending.expired(std.time.milliTimestamp())) | 745 | if (pending.expired(std.time.milliTimestamp())) |
| 746 | core.banner(switch (waiting) { | 746 | core.banner(switch (waiting) { |
| 747 | .end, .end_force => "[daemon too old to end a session]", | 747 | .end, .end_force => "[daemon too old to end a session]", |
| 748 | else => "[no session list: upgrade muxd]", | 748 | else => "[no session list: upgrade that daemon]", |
| 749 | }); | 749 | }); |
| 750 | 750 | ||
| 751 | // FRAMES BEFORE KEYS, and that order is load-bearing rather than | 751 | // FRAMES BEFORE KEYS, and that order is load-bearing rather than |
src/tui/wall_test_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -181,7 +181,7 @@ test "recordHost: a file it cannot write comes BACK — the wall may be on the a | |||
| 181 | try std.testing.expect(wall_host.recordHost(alloc, .{ .sock = "/a" }, "--sock /a", ok) == null); | 181 | try std.testing.expect(wall_host.recordHost(alloc, .{ .sock = "/a" }, "--sock /a", ok) == null); |
| 182 | // `--via` has no form in the host grammar, and writing nothing down for | 182 | // `--via` has no form in the host grammar, and writing nothing down for |
| 183 | // it is not a failure to report. | 183 | // it is not a failure to report. |
| 184 | try std.testing.expect(wall_host.recordHost(alloc, .{ .via = "ssh h muxd proxy" }, "x", bad) == null); | 184 | try std.testing.expect(wall_host.recordHost(alloc, .{ .via = "ssh h mux d proxy" }, "x", bad) == null); |
| 185 | } | 185 | } |
| 186 | 186 | ||
| 187 | test "applyHostList: a host with no live session gets no tile — the wall shows sessions only" { | 187 | test "applyHostList: a host with no live session gets no tile — the wall shows sessions only" { |
src/tui/wall_test_wall.zig
| Old | New | ||
|---|---|---|---|
| @@ -497,7 +497,7 @@ test "showsSelf: the wall never tiles the session the walling shell is standing | |||
| 497 | try std.testing.expect(!wv.showsSelf(target, "0", "/run/user/1000/other.sock", "0")); | 497 | try std.testing.expect(!wv.showsSelf(target, "0", "/run/user/1000/other.sock", "0")); |
| 498 | // Not a unix socket: a quic:// or --via target cannot be the daemon | 498 | // Not a unix socket: a quic:// or --via target cannot be the daemon |
| 499 | // whose shell planted these. | 499 | // whose shell planted these. |
| 500 | try std.testing.expect(!wv.showsSelf(.{ .via = "ssh host muxd proxy" }, "0", sock, "0")); | 500 | try std.testing.expect(!wv.showsSelf(.{ .via = "ssh host mux d proxy" }, "0", sock, "0")); |
| 501 | 501 | ||
| 502 | // Unset, and the emptied spelling of unset that the self-attach | 502 | // Unset, and the emptied spelling of unset that the self-attach |
| 503 | // refusal names as the way to override it. | 503 | // refusal names as the way to override it. |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -314,7 +314,7 @@ pub const Tile = struct { | |||
| 314 | /// | 314 | /// |
| 315 | /// False for the ENTRY tile, which keeps the plain client's rule | 315 | /// False for the ENTRY tile, which keeps the plain client's rule |
| 316 | /// verbatim: a transport that died before any session carried none, so | 316 | /// verbatim: a transport that died before any session carried none, so |
| 317 | /// it is a bad host, a typo'd command or a `muxd proxy` that exited, | 317 | /// it is a bad host, a typo'd command or a `mux d proxy` that exited, |
| 318 | /// and retrying any of those helps nobody (`client.lostMsg` is the | 318 | /// and retrying any of those helps nobody (`client.lostMsg` is the |
| 319 | /// sentence). True for wall tiles, which retry a dead host forever on | 319 | /// sentence). True for wall tiles, which retry a dead host forever on |
| 320 | /// purpose — a wall is a thing you leave up. | 320 | /// purpose — a wall is a thing you leave up. |
src/xdg.zig
| Old | New | ||
|---|---|---|---|
| @@ -7,7 +7,7 @@ | |||
| 7 | //! inspection. | 7 | //! inspection. |
| 8 | const std = @import("std"); | 8 | const std = @import("std"); |
| 9 | 9 | ||
| 10 | /// The one place the default key location is spelled; muxd keygen writes | 10 | /// The one place the default key location is spelled; mux d keygen writes |
| 11 | /// it and every binary's key resolution reads it. | 11 | /// it and every binary's key resolution reads it. |
| 12 | pub fn keyPath(alloc: std.mem.Allocator) ![]const u8 { | 12 | pub fn keyPath(alloc: std.mem.Allocator) ![]const u8 { |
| 13 | return keyPathFrom(alloc, std.posix.getenv("XDG_CONFIG_HOME"), std.posix.getenv("HOME")); | 13 | return keyPathFrom(alloc, std.posix.getenv("XDG_CONFIG_HOME"), std.posix.getenv("HOME")); |
| @@ -24,8 +24,8 @@ pub const KeyResolution = union(enum) { | |||
| 24 | /// The XDG default, which exists. Owned by the caller. | 24 | /// The XDG default, which exists. Owned by the caller. |
| 25 | default: []const u8, | 25 | default: []const u8, |
| 26 | /// The XDG default, which does not. Owned by the caller, and carried | 26 | /// The XDG default, which does not. Owned by the caller, and carried |
| 27 | /// out rather than printed: `mux` and `muxa` word this differently | 27 | /// out rather than printed: `mux` and `mux a` word this differently |
| 28 | /// (the agent answers in JSON) and both need the path; muxweb folds | 28 | /// (the agent answers in JSON) and both need the path; the hub folds |
| 29 | /// it into a bare MissingKey. | 29 | /// it into a bare MissingKey. |
| 30 | missing: []const u8, | 30 | missing: []const u8, |
| 31 | }; | 31 | }; |
| @@ -62,7 +62,7 @@ pub fn keyPathFrom( | |||
| 62 | return std.fmt.allocPrint(alloc, "{s}/.config/mux/key", .{h}); | 62 | return std.fmt.allocPrint(alloc, "{s}/.config/mux/key", .{h}); |
| 63 | } | 63 | } |
| 64 | 64 | ||
| 65 | /// The daemon's stdout+stderr. Truncated only by `muxd start`; the attach | 65 | /// The daemon's stdout+stderr. Truncated only by `mux d start`; the attach |
| 66 | /// auto-starts append (spawn.zig owns why an attach is not a restart). | 66 | /// auto-starts append (spawn.zig owns why an attach is not a restart). |
| 67 | pub fn logPath(alloc: std.mem.Allocator) ![]const u8 { | 67 | pub fn logPath(alloc: std.mem.Allocator) ![]const u8 { |
| 68 | return logPathFrom(alloc, std.posix.getenv("XDG_STATE_HOME"), std.posix.getenv("HOME")); | 68 | return logPathFrom(alloc, std.posix.getenv("XDG_STATE_HOME"), std.posix.getenv("HOME")); |