a73x

9ad104a9

refactor: the daemon is `mux d`, the agent `mux a`, the hub `mux web`

a73x   2026-08-28 22:34

Commit message
refactor: the daemon is `mux d`, the agent `mux a`, the hub `mux web`

Every printed and executed spelling, including the upgrade exec's own argv.
The rollback exec keeps bare `run`: the binary it rolls back TO is by
definition the older one, and only the newer spelling would be refused.

Socket, log, agent and shim paths are untouched — those name files, not
commands, and a running daemon owns them.

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"));