a73x

6c5f6a37

feat: one binary — `mux d` / `mux a` / `mux web`, and the client with no letter

a73x   2026-08-28 22:34

Commit message
feat: one binary — `mux d` / `mux a` / `mux web`, and the client with no letter

The four mains stay four files and become four modules of one root,
src/cli/mux.zig, which owns the mode word and nothing else. Each is handed
the argv slice it already read — args[1..], whose [0] is the mode word where
the program name used to be — so no parser changed a line.

`mux run …` is the one alias, and it is a bridge rather than a convenience:
a daemon of v0.0.1-15 or older execs its upgrade candidate as
`<binary> run --resume-fd N`.

Messages are prefixed with the words the user typed: `mux d:`, `mux a:`,
`mux web:`, plain `mux:`. `--version` answers `mux <version>` in every mode,
which is what the program is now called.

build.zig
Old New
@@ -261,7 +261,7 @@ const mod_table = [_]ModSpec{
261 // renders: `quic_client` for the remote arm and `xdg` for the one 261 // renders: `quic_client` for the remote arm and `xdg` for the one
262 // key-resolution rule all three binaries obey. Deliberately still no 262 // key-resolution rule all three binaries obey. Deliberately still no
263 // engine and no replica — muxa has nothing to draw. 263 // engine and no replica — muxa has nothing to draw.
264 .{ .name = "muxa", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic_client", "quic", "xdg", "cliflags" }, .quic_tests = true }, 264 .{ .name = "agent_main", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic_client", "quic", "xdg", "cliflags" }, .quic_tests = true },
265 .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } }, 265 .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } },
266 // ---- layer 3 ---- 266 // ---- layer 3 ----
267 // Dialling, and what a chord means. The client is the only thing that 267 // Dialling, and what a chord means. The client is the only thing that
@@ -280,7 +280,7 @@ const mod_table = [_]ModSpec{
280 // the sun_path bound, checked before any verb acts on the path; and the 280 // the sun_path bound, checked before any verb acts on the path; and the
281 // keygen round-trip test needs a directory to generate into, which the 281 // keygen round-trip test needs a directory to generate into, which the
282 // daemon itself never touches. 282 // daemon itself never touches.
283 .{ .name = "exe", .path = "src/cli/main.zig", .layer = 3, .link_libc = true, .imports = &.{ "server", "protocol", "proxy", "quic", "quic_server", "xdg", "spawn", "handoff", "sockpath", "upgrade", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, 283 .{ .name = "daemon_main", .path = "src/cli/main.zig", .layer = 3, .link_libc = true, .imports = &.{ "server", "protocol", "proxy", "quic", "quic_server", "xdg", "spawn", "handoff", "sockpath", "upgrade", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true },
284 // ---- layer 4 ---- 284 // ---- layer 4 ----
285 // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route 285 // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route
286 // table, WS endpoint naming. Assets are injected (the exe root 286 // table, WS endpoint naming. Assets are injected (the exe root
@@ -304,14 +304,18 @@ const mod_table = [_]ModSpec{
304 // one grammar, not three. Resolution itself now lives in the Hub, so 304 // one grammar, not three. Resolution itself now lives in the Hub, so
305 // handoff/protocol left with it; sockpath stays for the one startup 305 // handoff/protocol left with it; sockpath stays for the one startup
306 // message that names the sun_path bound. 306 // message that names the sun_path bound.
307 .{ .name = "webhub_main", .path = "src/cli/webhub_main.zig", .layer = 5, .link_libc = true, .imports = &.{ "client", "webhub", "wall", "xdg", "sockpath", "cliflags" }, .quic_tests = true }, 307 .{ .name = "hub_main", .path = "src/cli/webhub_main.zig", .layer = 5, .link_libc = true, .imports = &.{ "client", "webhub", "wall", "xdg", "sockpath", "cliflags" }, .quic_tests = true },
308 // sockpath is the sun_path bound only; the client binds no socket itself. 308 // sockpath is the sun_path bound only; the client binds no socket itself.
309 // protocol is the session-name validator alone (validSessionName): a bad 309 // protocol is the session-name validator alone (validSessionName): a bad
310 // --session has to be a usage error here, at parse, not bytes some 310 // --session has to be a usage error here, at parse, not bytes some
311 // daemon downstream has to notice and refuse. Layer 5 since `mux wall` 311 // daemon downstream has to notice and refuse. Layer 5 since `mux wall`
312 // pulled in wallview (layer 4); wall rides along for the no-arg wall 312 // pulled in wallview (layer 4); wall rides along for the no-arg wall
313 // (the state file the browser hub builds). 313 // (the state file the browser hub builds).
314 .{ .name = "mux", .path = "src/cli/mux_main.zig", .layer = 5, .link_libc = true, .imports = &.{ "client", "protocol", "xdg", "spawn", "handoff", "sockpath", "wallview", "wall", "hosts", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, 314 .{ .name = "client_main", .path = "src/cli/mux_main.zig", .layer = 5, .link_libc = true, .imports = &.{ "client", "protocol", "xdg", "spawn", "handoff", "sockpath", "wallview", "wall", "hosts", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true },
315 // ---- layer 6: the one binary ----
316 // Four words, one image. Nothing but the mode letter lives here, which
317 // is why it may sit above every other main without owning any of them.
318 .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 6, .link_libc = true, .imports = &.{ "daemon_main", "agent_main", "hub_main", "client_main" } },
315 }; 319 };
316 320
317 /// Comptime row lookup. Every hand-written module name in this file goes 321 /// Comptime row lookup. Every hand-written module name in this file goes
@@ -759,15 +763,16 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui
759 /// client forever — and a wedged step prints nothing at all. Failing 763 /// client forever — and a wedged step prints nothing at all. Failing
760 /// first is what makes the catch legible. script leads for the same 764 /// first is what makes the catch legible. script leads for the same
761 /// reason: instant, allocation-only, and both fixtures inherit its 765 /// reason: instant, allocation-only, and both fixtures inherit its
762 /// escape pins. mux and exe are executable roots but carry the argument 766 /// escape pins. The mains are roots of nothing but the one binary, and
763 /// parsers — a test that is never built is not a test (decisions.md). 767 /// they carry the argument parsers — a test that is never built is not a
768 /// test (decisions.md).
764 const test_order = [_][]const u8{ 769 const test_order = [_][]const u8{
765 "script", "select", "protocol", "client_core", "interact", "engine", "pty", 770 "script", "select", "protocol", "client_core", "interact", "engine", "pty",
766 "delta", "cmd", "wall", "hosts", "upgrade", "shellint", "replica", 771 "delta", "cmd", "wall", "hosts", "upgrade", "shellint", "replica",
767 "keymap", "webhub", "wallview", "sockpath", "muxa", "server", "client", 772 "keymap", "webhub", "wallview", "sockpath", "agent_main", "server", "client",
768 "proxy", "mux", "quic", "quic_server", "exe", "testtmp", "quic_client", 773 "proxy", "client_main", "quic", "quic_server", "daemon_main", "testtmp", "quic_client",
769 "predict", "rawmode", "delaypipe", "xdg", "spawn", "handoff", "paint", 774 "predict", "rawmode", "delaypipe", "xdg", "spawn", "handoff", "paint",
770 "layout", "render", "ptyclient", "webhub_main", "wsclient", "cliflags", 775 "layout", "render", "ptyclient", "hub_main", "wsclient", "cliflags", "mux",
771 }; 776 };
772 777
773 comptime { 778 comptime {
@@ -865,52 +870,46 @@ pub fn build(b: *std.Build) void {
865 // jurisdiction, explicit. 870 // jurisdiction, explicit.
866 const engine_mod = mods[comptime idxOf("engine")]; 871 const engine_mod = mods[comptime idxOf("engine")];
867 const mux_mod = mods[comptime idxOf("mux")]; 872 const mux_mod = mods[comptime idxOf("mux")];
868 const exe_mod = mods[comptime idxOf("exe")]; 873 const client_main_mod = mods[comptime idxOf("client_main")];
869 const muxa_mod = mods[comptime idxOf("muxa")]; 874 const daemon_main_mod = mods[comptime idxOf("daemon_main")];
875 const agent_main_mod = mods[comptime idxOf("agent_main")];
870 const rawmode_mod = mods[comptime idxOf("rawmode")]; 876 const rawmode_mod = mods[comptime idxOf("rawmode")];
871 const delaypipe_mod = mods[comptime idxOf("delaypipe")]; 877 const delaypipe_mod = mods[comptime idxOf("delaypipe")];
872 const render_mod = mods[comptime idxOf("render")]; 878 const render_mod = mods[comptime idxOf("render")];
873 const ptyclient_mod = mods[comptime idxOf("ptyclient")]; 879 const ptyclient_mod = mods[comptime idxOf("ptyclient")];
874 const wsclient_mod = mods[comptime idxOf("wsclient")]; 880 const wsclient_mod = mods[comptime idxOf("wsclient")];
875 const webhub_main_mod = mods[comptime idxOf("webhub_main")]; 881 const hub_main_mod = mods[comptime idxOf("hub_main")];
876 882
877 if (ghostty_dep) |dep| { 883 if (ghostty_dep) |dep| {
878 engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); 884 engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
879 } 885 }
880 mux_mod.addImport("build_options", version_opts.createModule()); 886 // ONE module object, shared: the four mains link into one binary now, and
881 exe_mod.addImport("build_options", version_opts.createModule()); 887 // a second instance of the same root file in one compilation is "file
882 webhub_main_mod.addImport("build_options", version_opts.createModule()); 888 // exists in modules 'build_options' and 'build_options0'".
883 muxa_mod.addImport("build_options", version_opts.createModule()); 889 const build_opts_mod = version_opts.createModule();
890 client_main_mod.addImport("build_options", build_opts_mod);
891 daemon_main_mod.addImport("build_options", build_opts_mod);
892 hub_main_mod.addImport("build_options", build_opts_mod);
893 agent_main_mod.addImport("build_options", build_opts_mod);
884 // A row with test_imports gets a SEPARATE module for its test twin, and 894 // A row with test_imports gets a SEPARATE module for its test twin, and
885 // build_options is outside the table's jurisdiction — so every such twin 895 // build_options is outside the table's jurisdiction — so every such twin
886 // needs it by hand, or the argument parsers lose the version they print. 896 // needs it by hand, or the argument parsers lose the version they print.
887 test_mods[comptime idxOf("exe")].addImport("build_options", version_opts.createModule()); 897 test_mods[comptime idxOf("daemon_main")].addImport("build_options", build_opts_mod);
888 test_mods[comptime idxOf("mux")].addImport("build_options", version_opts.createModule()); 898 test_mods[comptime idxOf("client_main")].addImport("build_options", build_opts_mod);
889 899
890 const exe = b.addExecutable(.{ .name = "muxd", .root_module = exe_mod }); 900 // ONE product binary. The four mains are modules under it, reached by
901 // the mode word rather than by four names on PATH — which is also what
902 // lets the client's auto-start fork into the daemon instead of hunting
903 // for a sibling. The fixtures below stay separate: they stand in for
904 // users, not for the product.
905 const mux_exe = b.addExecutable(.{ .name = "mux", .root_module = mux_mod });
891 // Zig 0.15's self-hosted x86_64 linker can't handle the .sframe 906 // Zig 0.15's self-hosted x86_64 linker can't handle the .sframe
892 // sections emitted by gcc >= 16 crt1.o on this system; LLD can. 907 // sections emitted by gcc >= 16 crt1.o on this system; LLD can.
893 exe.use_llvm = true;
894 exe.use_lld = true;
895 // The daemon is the side that will listen (Task 2c). Nothing references
896 // these symbols yet, so nothing is pulled in.
897 linkQuic(b, exe, quic);
898 b.installArtifact(exe);
899
900 const mux_exe = b.addExecutable(.{ .name = "mux", .root_module = mux_mod });
901 mux_exe.use_llvm = true; 908 mux_exe.use_llvm = true;
902 mux_exe.use_lld = true; 909 mux_exe.use_lld = true;
903 // The client speaks QUIC now, so it carries the stack too.
904 linkQuic(b, mux_exe, quic); 910 linkQuic(b, mux_exe, quic);
905 b.installArtifact(mux_exe); 911 b.installArtifact(mux_exe);
906 912
907 const muxa_exe = b.addExecutable(.{ .name = "muxa", .root_module = muxa_mod });
908 muxa_exe.use_llvm = true;
909 muxa_exe.use_lld = true;
910 // The agent client dials remote daemons now, so it carries the stack too.
911 linkQuic(b, muxa_exe, quic);
912 b.installArtifact(muxa_exe);
913
914 const rawmode_exe = b.addExecutable(.{ .name = "rawmode", .root_module = rawmode_mod }); 913 const rawmode_exe = b.addExecutable(.{ .name = "rawmode", .root_module = rawmode_mod });
915 rawmode_exe.use_llvm = true; 914 rawmode_exe.use_llvm = true;
916 rawmode_exe.use_lld = true; 915 rawmode_exe.use_lld = true;
@@ -1001,14 +1000,9 @@ pub fn build(b: *std.Build) void {
1001 // The page's three assets arrive as anonymous imports so @embedFile 1000 // The page's three assets arrive as anonymous imports so @embedFile
1002 // can name them; the wasm one is the artifact itself, which also 1001 // can name them; the wasm one is the artifact itself, which also
1003 // sequences the wasm build before the hub's. 1002 // sequences the wasm build before the hub's.
1004 webhub_main_mod.addAnonymousImport("index.html", .{ .root_source_file = b.path("web/index.html") }); 1003 hub_main_mod.addAnonymousImport("index.html", .{ .root_source_file = b.path("web/index.html") });
1005 webhub_main_mod.addAnonymousImport("mux.js", .{ .root_source_file = b.path("web/mux.js") }); 1004 hub_main_mod.addAnonymousImport("mux.js", .{ .root_source_file = b.path("web/mux.js") });
1006 webhub_main_mod.addAnonymousImport("mux_core.wasm", .{ .root_source_file = wasm_exe.getEmittedBin() }); 1005 hub_main_mod.addAnonymousImport("mux_core.wasm", .{ .root_source_file = wasm_exe.getEmittedBin() });
1007 const webhub_exe = b.addExecutable(.{ .name = "muxweb", .root_module = webhub_main_mod });
1008 webhub_exe.use_llvm = true;
1009 webhub_exe.use_lld = true;
1010 linkQuic(b, webhub_exe, quic);
1011 b.installArtifact(webhub_exe);
1012 1006
1013 const test_step = b.step("test", "Run unit tests"); 1007 const test_step = b.step("test", "Run unit tests");
1014 test_step.dependOn(&client_core_wasm_guard.step); 1008 test_step.dependOn(&client_core_wasm_guard.step);
@@ -1057,7 +1051,6 @@ pub fn build(b: *std.Build) void {
1057 } 1051 }
1058 1052
1059 const e2e = b.addSystemCommand(&.{"test/e2e.sh"}); 1053 const e2e = b.addSystemCommand(&.{"test/e2e.sh"});
1060 e2e.addArtifactArg(exe);
1061 e2e.addArtifactArg(mux_exe); 1054 e2e.addArtifactArg(mux_exe);
1062 // The prediction scenarios need a deterministic editor and a slow path; 1055 // The prediction scenarios need a deterministic editor and a slow path;
1063 // passed as artifacts so the suite runs against the binaries this build 1056 // passed as artifacts so the suite runs against the binaries this build
@@ -1067,14 +1060,6 @@ pub fn build(b: *std.Build) void {
1067 e2e.addArtifactArg(render_exe); 1060 e2e.addArtifactArg(render_exe);
1068 e2e.addArtifactArg(ptyclient_exe); 1061 e2e.addArtifactArg(ptyclient_exe);
1069 e2e.addArtifactArg(wsclient_exe); 1062 e2e.addArtifactArg(wsclient_exe);
1070 e2e.addArtifactArg(webhub_exe);
1071 // muxa, for the M18 block alone: it is the one verb family that can ask
1072 // about a session WITHOUT attaching to it, and the sessions it asks
1073 // about have to be created by `mux` first — which is why the question
1074 // is asked here rather than in the agent suite, where there is no `mux`
1075 // to create a named session with. test/agent.sh remains muxa's own home
1076 // for everything that does not need two sessions to exist.
1077 e2e.addArtifactArg(muxa_exe);
1078 const e2e_step = b.step("e2e", "Run end-to-end test"); 1063 const e2e_step = b.step("e2e", "Run end-to-end test");
1079 e2e_step.dependOn(&e2e.step); 1064 e2e_step.dependOn(&e2e.step);
1080 1065
@@ -1086,29 +1071,25 @@ pub fn build(b: *std.Build) void {
1086 // runs when somebody types the script is the shape a regression ships 1071 // runs when somebody types the script is the shape a regression ships
1087 // through. 1072 // through.
1088 const agent = b.addSystemCommand(&.{"test/agent.sh"}); 1073 const agent = b.addSystemCommand(&.{"test/agent.sh"});
1089 agent.addArtifactArg(exe); 1074 agent.addArtifactArg(mux_exe);
1090 agent.addArtifactArg(muxa_exe);
1091 const agent_step = b.step("agent", "Run the agent-surface end-to-end suite"); 1075 const agent_step = b.step("agent", "Run the agent-surface end-to-end suite");
1092 agent_step.dependOn(&agent.step); 1076 agent_step.dependOn(&agent.step);
1093 1077
1094 const soak = b.addSystemCommand(&.{"test/soak.sh"}); 1078 const soak = b.addSystemCommand(&.{"test/soak.sh"});
1095 soak.addArtifactArg(exe); 1079 // The same list the e2e step passes, in the same order: soak IS that
1080 // suite run N times, so an argument added to one and not the other
1081 // makes every soak run abort on an unbound variable before its first
1082 // scenario.
1096 soak.addArtifactArg(mux_exe); 1083 soak.addArtifactArg(mux_exe);
1097 soak.addArtifactArg(rawmode_exe); 1084 soak.addArtifactArg(rawmode_exe);
1098 soak.addArtifactArg(delaypipe_exe); 1085 soak.addArtifactArg(delaypipe_exe);
1099 soak.addArtifactArg(render_exe); 1086 soak.addArtifactArg(render_exe);
1100 soak.addArtifactArg(ptyclient_exe); 1087 soak.addArtifactArg(ptyclient_exe);
1101 soak.addArtifactArg(wsclient_exe); 1088 soak.addArtifactArg(wsclient_exe);
1102 soak.addArtifactArg(webhub_exe);
1103 // Same list as the e2e step, muxa included: soak IS the e2e suite run N
1104 // times, so an argument added to one and not the other makes every soak
1105 // run abort on an unbound variable before its first scenario.
1106 soak.addArtifactArg(muxa_exe);
1107 const soak_step = b.step("soak", "Run the e2e suite SOAK_N times (default 10)"); 1089 const soak_step = b.step("soak", "Run the e2e suite SOAK_N times (default 10)");
1108 soak_step.dependOn(&soak.step); 1090 soak_step.dependOn(&soak.step);
1109 1091
1110 const bench = b.addSystemCommand(&.{"test/bench.sh"}); 1092 const bench = b.addSystemCommand(&.{"test/bench.sh"});
1111 bench.addArtifactArg(exe);
1112 bench.addArtifactArg(mux_exe); 1093 bench.addArtifactArg(mux_exe);
1113 const bench_step = b.step("bench", "Measure delta vs snapshot bytes"); 1094 const bench_step = b.step("bench", "Measure delta vs snapshot bytes");
1114 bench_step.dependOn(&bench.step); 1095 bench_step.dependOn(&bench.step);
docscheck.budget
Old New
@@ -58,3 +58,4 @@ wall_test_picker.zig 0
58 wall_test_pump.zig 0 58 wall_test_pump.zig 0
59 wall_test_layout.zig 0 59 wall_test_layout.zig 0
60 wall_test_wall.zig 0 60 wall_test_wall.zig 0
61 mux.zig 0
src/cli/flags.zig
Old New
@@ -53,7 +53,7 @@ pub fn parse(comptime T: type, dst: *T, args: []const [:0]const u8) Outcome {
53 // `--version` shares the pass for the same reason: asking a binary its 53 // `--version` shares the pass for the same reason: asking a binary its
54 // version must answer a line that would otherwise be refused. 54 // version must answer a line that would otherwise be refused.
55 // It stops at `--` because past that point the words are payload: a 55 // It stops at `--` because past that point the words are payload: a
56 // `muxa send -- --help` types `--help` AT a session, and a usage page 56 // `mux a send -- --help` types `--help` AT a session, and a usage page
57 // instead of the keystrokes would be the parser answering for the user. 57 // instead of the keystrokes would be the parser answering for the user.
58 for (args) |a| { 58 for (args) |a| {
59 if (std.mem.eql(u8, a, "--")) break; 59 if (std.mem.eql(u8, a, "--")) break;
@@ -292,11 +292,11 @@ test "flagName: underscores become dashes" {
292 } 292 }
293 293
294 test "documented: a prefix of a longer flag does not count" { 294 test "documented: a prefix of a longer flag does not count" {
295 try std.testing.expect(!documented("--sock", " muxd run [--socket PATH]\n")); 295 try std.testing.expect(!documented("--sock", " mux d run [--socket PATH]\n"));
296 try std.testing.expect(documented("--sock", " muxd run [--sock PATH]\n")); 296 try std.testing.expect(documented("--sock", " mux d run [--sock PATH]\n"));
297 try std.testing.expect(documented("--vt", " muxd dump [--vt]\n")); 297 try std.testing.expect(documented("--vt", " mux d dump [--vt]\n"));
298 try std.testing.expect(documented("--vt", " muxd dump --vt\n")); 298 try std.testing.expect(documented("--vt", " mux d dump --vt\n"));
299 try std.testing.expect(documented("--vt", " muxd dump --vt")); 299 try std.testing.expect(documented("--vt", " mux d dump --vt"));
300 try std.testing.expect(!documented("--rows", "nothing here\n")); 300 try std.testing.expect(!documented("--rows", "nothing here\n"));
301 } 301 }
302 302
@@ -305,7 +305,7 @@ test "assertDocumented: every visible flag is named in the prose" {
305 \\ demo [--vt] [--sock PATH] [--cols N] [--quic-idle-ms N] 305 \\ demo [--vt] [--sock PATH] [--cols N] [--quic-idle-ms N]
306 \\ 306 \\
307 ; 307 ;
308 // `shell` stands in for muxd's machine-written flags: hidden from the 308 // `shell` stands in for the daemon's machine-written flags: hidden from the
309 // prose on purpose, so the assertion must not demand it. 309 // prose on purpose, so the assertion must not demand it.
310 comptime assertDocumented(Demo, text, &.{"shell"}); 310 comptime assertDocumented(Demo, text, &.{"shell"});
311 } 311 }
src/cli/main.zig
Old New
@@ -1,4 +1,4 @@
1 //! muxd — daemon entrypoint. `run` hosts the session; `dump` prints the 1 //! `mux d` — the daemon mode. `run` hosts the session; `dump` prints the
2 //! authoritative grid over the protocol (debug aid, also used by e2e); 2 //! authoritative grid over the protocol (debug aid, also used by e2e);
3 //! `proxy` exposes the session socket over stdio for `mux --via`. 3 //! `proxy` exposes the session socket over stdio for `mux --via`.
4 const std = @import("std"); 4 const std = @import("std");
@@ -17,19 +17,19 @@ const cliflags = @import("cliflags");
17 17
18 const usage = 18 const usage =
19 \\usage: 19 \\usage:
20 \\ muxd run [--sock PATH] [--shell PATH] [--cols N] [--rows N] 20 \\ mux d run [--sock PATH] [--shell PATH] [--cols N] [--rows N]
21 \\ [--quic HOST[:PORT] --key FILE] [--quic-idle-ms N] 21 \\ [--quic HOST[:PORT] --key FILE] [--quic-idle-ms N]
22 \\ muxd dump [--vt] [--session NAME] [--sock PATH] 22 \\ mux d dump [--vt] [--session NAME] [--sock PATH]
23 \\ muxd stats [--sock PATH] 23 \\ mux d stats [--sock PATH]
24 \\ muxd stop [--sock PATH] (ask the daemon on PATH to exit) 24 \\ mux d stop [--sock PATH] (ask the daemon on PATH to exit)
25 \\ muxd proxy [--sock PATH] (byte pump: stdio <-> session socket) 25 \\ mux d proxy [--sock PATH] (byte pump: stdio <-> session socket)
26 \\ muxd endpoint [--sock PATH] (proxy that first announces QUIC port+key) 26 \\ mux d endpoint [--sock PATH] (proxy that first announces QUIC port+key)
27 \\ muxd keygen (write a fresh key to ~/.config/mux/key) 27 \\ mux d keygen (write a fresh key to ~/.config/mux/key)
28 \\ muxd start [run's flags] (spawn a daemon detached; no-op if one is up) 28 \\ mux d start [run's flags] (spawn a daemon detached; no-op if one is up)
29 \\ muxd upgrade [--sock PATH] (exec THIS binary over the daemon; sessions live) 29 \\ mux d upgrade [--sock PATH] (exec THIS binary over the daemon; sessions live)
30 \\ [--allow-same-version] (strictly newer, unless this; the e2e leg's) 30 \\ [--allow-same-version] (strictly newer, unless this; the e2e leg's)
31 \\ muxd --version 31 \\ mux d --version
32 \\ muxd --help 32 \\ mux d --help
33 \\ 33 \\
34 ; 34 ;
35 35
@@ -62,7 +62,7 @@ const Spec = struct {
62 /// returns before the loop, so nothing after the name is read. 62 /// returns before the loop, so nothing after the name is read.
63 /// 63 ///
64 /// `ignored` is a contract, not an oversight: trailing arguments are 64 /// `ignored` is a contract, not an oversight: trailing arguments are
65 /// ACCEPTED and vanish silently. `muxd --version` with an over-long 65 /// ACCEPTED and vanish silently. `mux d --version` with an over-long
66 /// `--sock` exits 0 today, pinned in e2e.sh ("--version refused over 66 /// `--sock` exits 0 today, pinned in e2e.sh ("--version refused over
67 /// sock length"). Narrowing it to `none` would turn that into a 67 /// sock length"). Narrowing it to `none` would turn that into a
68 /// refusal. 68 /// refusal.
@@ -236,10 +236,10 @@ fn usageExit(u: Usage) u8 {
236 .no_command => std.debug.print("{s}", .{usage}), 236 .no_command => std.debug.print("{s}", .{usage}),
237 .unknown_command => std.debug.print("{s}", .{usage}), 237 .unknown_command => std.debug.print("{s}", .{usage}),
238 .unknown_arg => |a| std.debug.print("unknown argument: {s}\n{s}", .{ a, usage }), 238 .unknown_arg => |a| std.debug.print("unknown argument: {s}\n{s}", .{ a, usage }),
239 .missing_value => |f| std.debug.print("muxd: {s} needs a value\n{s}", .{ f, usage }), 239 .missing_value => |f| std.debug.print("mux d: {s} needs a value\n{s}", .{ f, usage }),
240 .bad_value => |f| std.debug.print("muxd: {s} was given a value it cannot hold\n{s}", .{ f, usage }), 240 .bad_value => |f| std.debug.print("mux d: {s} was given a value it cannot hold\n{s}", .{ f, usage }),
241 .key_without_quic => std.debug.print( 241 .key_without_quic => std.debug.print(
242 "muxd: --key without --quic has nothing to listen on; name both or neither\n", 242 "mux d: --key without --quic has nothing to listen on; name both or neither\n",
243 .{}, 243 .{},
244 ), 244 ),
245 } 245 }
@@ -279,15 +279,15 @@ fn parseBindAddr(s: []const u8) !std.net.Address {
279 return std.net.Address.parseIp(hp.host, hp.port); 279 return std.net.Address.parseIp(hp.host, hp.port);
280 } 280 }
281 281
282 pub fn main() !u8 { 282 /// `mux d`. argv arrives from the dispatcher rather than from
283 /// `argsAlloc`, because the local auto-start reaches this entry by fork
284 /// instead of exec (spawn.zig) and a forked child has no argv of its own.
285 pub fn main(args: []const [:0]const u8) !u8 {
283 var gpa: std.heap.DebugAllocator(.{}) = .init; 286 var gpa: std.heap.DebugAllocator(.{}) = .init;
284 defer if (gpa.deinit() == .leak) 287 defer if (gpa.deinit() == .leak)
285 std.debug.print("muxd: LEAK: allocations outlived deinit\n", .{}); 288 std.debug.print("mux d: LEAK: allocations outlived deinit\n", .{});
286 const alloc = gpa.allocator(); 289 const alloc = gpa.allocator();
287 290
288 const args = try std.process.argsAlloc(alloc);
289 defer std.process.argsFree(alloc, args);
290
291 const o = switch (parseArgs(args)) { 291 const o = switch (parseArgs(args)) {
292 .err => |u| return usageExit(u), 292 .err => |u| return usageExit(u),
293 .ok => |o| o, 293 .ok => |o| o,
@@ -314,7 +314,7 @@ pub fn main() !u8 {
314 // below: sockpath owns the rule, each binary owns the wording. 314 // below: sockpath owns the rule, each binary owns the wording.
315 error.NoRuntimeDir => { 315 error.NoRuntimeDir => {
316 std.debug.print( 316 std.debug.print(
317 "muxd: XDG_RUNTIME_DIR is unset, so there is no default socket path (name one with --sock)\n", 317 "mux d: XDG_RUNTIME_DIR is unset, so there is no default socket path (name one with --sock)\n",
318 .{}, 318 .{},
319 ); 319 );
320 return 1; 320 return 1;
@@ -333,7 +333,7 @@ pub fn main() !u8 {
333 // line's. 333 // line's.
334 if (uses_socket and sock_path.len > sockpath.max_sun_path) { 334 if (uses_socket and sock_path.len > sockpath.max_sun_path) {
335 std.debug.print( 335 std.debug.print(
336 "muxd: socket path too long ({d} bytes, max {d}): {s}\n", 336 "mux d: socket path too long ({d} bytes, max {d}): {s}\n",
337 .{ sock_path.len, sockpath.max_sun_path, sock_path }, 337 .{ sock_path.len, sockpath.max_sun_path, sock_path },
338 ); 338 );
339 return 1; 339 return 1;
@@ -343,7 +343,7 @@ pub fn main() !u8 {
343 // The socket path resolved above is unused here and unchecked (see 343 // The socket path resolved above is unused here and unchecked (see
344 // the length guard above): asking a binary its version must work 344 // the length guard above): asking a binary its version must work
345 // with no daemon and no runtime dir. 345 // with no daemon and no runtime dir.
346 .version => return cliflags.version("muxd", build_options.version), 346 .version => return cliflags.version("mux", build_options.version),
347 .help => return usageExit(.help), 347 .help => return usageExit(.help),
348 .keygen => return keygen(alloc), 348 .keygen => return keygen(alloc),
349 .start => return startCmd(alloc, sock_path, args[2..]), 349 .start => return startCmd(alloc, sock_path, args[2..]),
@@ -354,8 +354,8 @@ pub fn main() !u8 {
354 .upgrade => return upgradeCmd(alloc, sock_path, o.allow_same_version), 354 .upgrade => return upgradeCmd(alloc, sock_path, o.allow_same_version),
355 // A pump, and only a pump: a daemon starts when someone asks for 355 // A pump, and only a pump: a daemon starts when someone asks for
356 // one. `proxy.run` names the socket it could not reach, which is 356 // one. `proxy.run` names the socket it could not reach, which is
357 // what `mux --via 'ssh HOST muxd proxy'` shows a user whose remote 357 // what `mux --via 'ssh HOST mux d proxy'` shows a user whose remote
358 // has none — README's `ssh HOST 'muxd start'` is the answer. 358 // has none — README's `ssh HOST 'mux d start'` is the answer.
359 .proxy => return proxy.run(sock_path), 359 .proxy => return proxy.run(sock_path),
360 .endpoint => return endpointCmd(alloc, sock_path, std.posix.STDOUT_FILENO), 360 .endpoint => return endpointCmd(alloc, sock_path, std.posix.STDOUT_FILENO),
361 } 361 }
@@ -377,7 +377,7 @@ const manifest_read_max = 64 * 1024 * 1024;
377 /// otherwise trade the sessions forever, exec'ing each other at full speed. 377 /// otherwise trade the sessions forever, exec'ing each other at full speed.
378 /// A VARIABLE rather than a flag because the binary being exec'd back is by 378 /// A VARIABLE rather than a flag because the binary being exec'd back is by
379 /// definition older than this one: an unknown variable is ignored by any 379 /// definition older than this one: an unknown variable is ignored by any
380 /// muxd, an unknown flag is fatal usage. 380 /// binary, an unknown flag is fatal usage.
381 const rollback_marker = "MUX_UPGRADE_ROLLBACK"; 381 const rollback_marker = "MUX_UPGRADE_ROLLBACK";
382 382
383 /// The environment's spelling of `--resume-fail-at`. The flag alone cannot 383 /// The environment's spelling of `--resume-fail-at`. The flag alone cannot
@@ -417,12 +417,12 @@ fn rollback(
417 // every descriptor is still open and still inherited, and the manifest 417 // every descriptor is still open and still inherited, and the manifest
418 // names the binary that opened them. 418 // names the binary that opened them.
419 std.debug.print( 419 std.debug.print(
420 "muxd run: adoption failed at {s}; exec'ing {s} back\n", 420 "mux d run: adoption failed at {s}; exec'ing {s} back\n",
421 .{ section, writer_path }, 421 .{ section, writer_path },
422 ); 422 );
423 if (std.posix.getenv(rollback_marker) != null) { 423 if (std.posix.getenv(rollback_marker) != null) {
424 std.debug.print( 424 std.debug.print(
425 "muxd run: this IS the rollback ({s} refused the manifest it wrote); giving up\n", 425 "mux d run: this IS the rollback ({s} refused the manifest it wrote); giving up\n",
426 .{writer_path}, 426 .{writer_path},
427 ); 427 );
428 return 1; 428 return 1;
@@ -435,7 +435,11 @@ fn rollback(
435 435
436 var fd_buf: [12]u8 = undefined; 436 var fd_buf: [12]u8 = undefined;
437 const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{resume_fd}) catch return 1; 437 const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{resume_fd}) catch return 1;
438 const argv = [_:null]?[*:0]const u8{ "muxd", "run", "--resume-fd", fd_str.ptr }; 438 // Bare `run`, not `d run`: the binary being exec'd back is by
439 // definition OLDER than this one, and v0.0.1-15 and earlier know only
440 // the bare spelling. This binary accepts both (mux.zig's bridge), so
441 // the older word is the one that works either way.
442 const argv = [_:null]?[*:0]const u8{ "mux", "run", "--resume-fd", fd_str.ptr };
439 const path_z = alloc.dupeZ(u8, writer_path) catch return 1; 443 const path_z = alloc.dupeZ(u8, writer_path) catch return 1;
440 const envp = rollbackEnvp(alloc) catch return 1; 444 const envp = rollbackEnvp(alloc) catch return 1;
441 445
@@ -446,7 +450,7 @@ fn rollback(
446 alloc.free(path_z); 450 alloc.free(path_z);
447 alloc.free(std.mem.span(envp)); 451 alloc.free(std.mem.span(envp));
448 std.debug.print( 452 std.debug.print(
449 "muxd run: rollback exec of {s} failed: {s}\n", 453 "mux d run: rollback exec of {s} failed: {s}\n",
450 .{ writer_path, @errorName(exec_err) }, 454 .{ writer_path, @errorName(exec_err) },
451 ); 455 );
452 return 1; 456 return 1;
@@ -473,7 +477,7 @@ fn rollbackEnvp(alloc: std.mem.Allocator) ![*:null]const ?[*:0]const u8 {
473 return envp.ptr; 477 return envp.ptr;
474 } 478 }
475 479
476 /// `muxd run --resume-fd N`: the argv an upgrading daemon exec'd this binary 480 /// `mux d run --resume-fd N`: the argv an upgrading daemon exec'd this binary
477 /// with. Same pid, same children, same descriptors — the manifest names 481 /// with. Same pid, same children, same descriptors — the manifest names
478 /// which ones. It is read from the descriptor and never from a path: the 482 /// which ones. It is read from the descriptor and never from a path: the
479 /// memfd is anonymous memory, and the QUIC key inside it must not touch disk. 483 /// memfd is anonymous memory, and the QUIC key inside it must not touch disk.
@@ -482,17 +486,17 @@ fn resumeRun(alloc: std.mem.Allocator, o: Opts, resume_fd: std.posix.fd_t) !u8 {
482 // shares the file description with it, so the rewind is ours to do. 486 // shares the file description with it, so the rewind is ours to do.
483 var file = std.fs.File{ .handle = resume_fd }; 487 var file = std.fs.File{ .handle = resume_fd };
484 file.seekTo(0) catch |err| { 488 file.seekTo(0) catch |err| {
485 std.debug.print("muxd run: --resume-fd {d} does not seek ({t})\n", .{ resume_fd, err }); 489 std.debug.print("mux d run: --resume-fd {d} does not seek ({t})\n", .{ resume_fd, err });
486 return 1; 490 return 1;
487 }; 491 };
488 const bytes = file.readToEndAlloc(alloc, manifest_read_max) catch |err| { 492 const bytes = file.readToEndAlloc(alloc, manifest_read_max) catch |err| {
489 std.debug.print("muxd run: cannot read the manifest on fd {d} ({t})\n", .{ resume_fd, err }); 493 std.debug.print("mux d run: cannot read the manifest on fd {d} ({t})\n", .{ resume_fd, err });
490 return 1; 494 return 1;
491 }; 495 };
492 defer alloc.free(bytes); 496 defer alloc.free(bytes);
493 497
494 var parsed = upgrade.parseManifest(alloc, bytes) catch |err| { 498 var parsed = upgrade.parseManifest(alloc, bytes) catch |err| {
495 std.debug.print("muxd run: manifest on fd {d} is not one ({t})\n", .{ resume_fd, err }); 499 std.debug.print("mux d run: manifest on fd {d} is not one ({t})\n", .{ resume_fd, err });
496 return 1; 500 return 1;
497 }; 501 };
498 defer parsed.deinit(); 502 defer parsed.deinit();
@@ -535,7 +539,7 @@ fn resumeRun(alloc: std.mem.Allocator, o: Opts, resume_fd: std.posix.fd_t) !u8 {
535 return try srv.run(); 539 return try srv.run();
536 } 540 }
537 541
538 /// `muxd run`: the daemon, in the foreground. Every code this function picks 542 /// `mux d run`: the daemon, in the foreground. Every code this function picks
539 /// is a boot failure — an operator mistake caught before anything bound. 543 /// is a boot failure — an operator mistake caught before anything bound.
540 /// Reaching `srv.run()` means the daemon served, and it answers 0 whenever 544 /// Reaching `srv.run()` means the daemon served, and it answers 0 whenever
541 /// something asks it to stop. No session's exit is ever reported here: a 545 /// something asks it to stop. No session's exit is ever reported here: a
@@ -550,7 +554,7 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
550 if (o.quic) |hostport| { 554 if (o.quic) |hostport| {
551 quic_bind = parseBindAddr(hostport) catch { 555 quic_bind = parseBindAddr(hostport) catch {
552 std.debug.print( 556 std.debug.print(
553 "muxd: --quic wants HOST:PORT with a literal address, got {s}\n", 557 "mux d: --quic wants HOST:PORT with a literal address, got {s}\n",
554 .{hostport}, 558 .{hostport},
555 ); 559 );
556 return 1; 560 return 1;
@@ -566,14 +570,14 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
566 if (std.fs.cwd().access(dflt, .{})) |_| dflt else |_| null; 570 if (std.fs.cwd().access(dflt, .{})) |_| dflt else |_| null;
567 const key_path = pickKey(o.key, std.posix.getenv(xdg.key_env), dflt_if_present) orelse { 571 const key_path = pickKey(o.key, std.posix.getenv(xdg.key_env), dflt_if_present) orelse {
568 std.debug.print( 572 std.debug.print(
569 "muxd: no key: pass --key, set MUX_KEY_FILE, or run `muxd keygen` (default {s})\n", 573 "mux d: no key: pass --key, set MUX_KEY_FILE, or run `mux d keygen` (default {s})\n",
570 .{dflt}, 574 .{dflt},
571 ); 575 );
572 return 2; 576 return 2;
573 }; 577 };
574 quic_key = quic.Key.load(key_path) catch |err| switch (err) { 578 quic_key = quic.Key.load(key_path) catch |err| switch (err) {
575 // The three the user can act on, in quic.zig's words — the one 579 // The three the user can act on, in quic.zig's words — the one
576 // owner of them, because `muxd endpoint`, the daemon's 580 // owner of them, because `mux d endpoint`, the daemon's
577 // `endpoint_req` and the client print the same sentences. 581 // `endpoint_req` and the client print the same sentences.
578 // 582 //
579 // Anything else still propagates rather than being flattened 583 // Anything else still propagates rather than being flattened
@@ -586,7 +590,7 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
586 error.KeyFileMalformed, 590 error.KeyFileMalformed,
587 => { 591 => {
588 var buf: [quic.key_refusal_len]u8 = undefined; 592 var buf: [quic.key_refusal_len]u8 = undefined;
589 std.debug.print("muxd: {s}\n", .{quic.keyRefusalBody(&buf, err, key_path)}); 593 std.debug.print("mux d: {s}\n", .{quic.keyRefusalBody(&buf, err, key_path)});
590 return 1; 594 return 1;
591 }, 595 },
592 else => return err, 596 else => return err,
@@ -606,14 +610,14 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
606 // already there, the second one says so and stops. 610 // already there, the second one says so and stops.
607 error.AddressInUse => { 611 error.AddressInUse => {
608 std.debug.print( 612 std.debug.print(
609 "muxd: a daemon is already listening on udp {s}\n", 613 "mux d: a daemon is already listening on udp {s}\n",
610 .{o.quic.?}, 614 .{o.quic.?},
611 ); 615 );
612 return 1; 616 return 1;
613 }, 617 },
614 else => { 618 else => {
615 std.debug.print( 619 std.debug.print(
616 "muxd: cannot listen on udp {s}: {s}\n", 620 "mux d: cannot listen on udp {s}: {s}\n",
617 .{ o.quic.?, @errorName(err) }, 621 .{ o.quic.?, @errorName(err) },
618 ); 622 );
619 return 1; 623 return 1;
@@ -631,7 +635,7 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
631 try alloc.dupeZ(u8, std.posix.getenv("SHELL") orelse "/bin/sh"); 635 try alloc.dupeZ(u8, std.posix.getenv("SHELL") orelse "/bin/sh");
632 defer alloc.free(shell_z); 636 defer alloc.free(shell_z);
633 637
634 // Read from the DAEMON's environment, necessarily: muxd forks the 638 // Read from the DAEMON's environment, necessarily: the daemon forks the
635 // session shell, so by the time anyone could pass a flag through a 639 // session shell, so by the time anyone could pass a flag through a
636 // client the shell has been running for a while. 640 // client the shell has been running for a while.
637 const shell_integration = shellIntegrationEnabled( 641 const shell_integration = shellIntegrationEnabled(
@@ -656,12 +660,12 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
656 // and telling it "a daemon is already running" is exactly 660 // and telling it "a daemon is already running" is exactly
657 // right — by the time it reads the message, one is. 661 // right — by the time it reads the message, one is.
658 error.DaemonAlreadyRunning, error.AddressInUse => { 662 error.DaemonAlreadyRunning, error.AddressInUse => {
659 std.debug.print("muxd: a daemon is already running on {s}\n", .{sock_path}); 663 std.debug.print("mux d: a daemon is already running on {s}\n", .{sock_path});
660 return 1; 664 return 1;
661 }, 665 },
662 error.SockPathNotASocket => { 666 error.SockPathNotASocket => {
663 std.debug.print( 667 std.debug.print(
664 "muxd: {s} exists and is not a socket (move it, or name another with --sock)\n", 668 "mux d: {s} exists and is not a socket (move it, or name another with --sock)\n",
665 .{sock_path}, 669 .{sock_path},
666 ); 670 );
667 return 1; 671 return 1;
@@ -693,7 +697,7 @@ fn oneShotQuery(
693 ) !u8 { 697 ) !u8 {
694 const stream = std.net.connectUnixSocket(sock_path) catch { 698 const stream = std.net.connectUnixSocket(sock_path) catch {
695 std.debug.print( 699 std.debug.print(
696 "muxd {s}: nothing listening on {s} (`muxd start` starts a daemon)\n", 700 "mux d {s}: nothing listening on {s} (`mux d start` starts a daemon)\n",
697 .{ verb, sock_path }, 701 .{ verb, sock_path },
698 ); 702 );
699 return 1; 703 return 1;
@@ -713,7 +717,7 @@ fn oneShotQuery(
713 717
714 fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool, session: []const u8) !u8 { 718 fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool, session: []const u8) !u8 {
715 // vt byte ++ session-name tail, built by the wire module — empty is the 719 // vt byte ++ session-name tail, built by the wire module — empty is the
716 // wire's own default spelling, so a bare `muxd dump` sends exactly the 720 // wire's own default spelling, so a bare `mux d dump` sends exactly the
717 // one-byte payload that predates session names. 721 // one-byte payload that predates session names.
718 var buf: [proto.debug_dump_max_len]u8 = undefined; 722 var buf: [proto.debug_dump_max_len]u8 = undefined;
719 const payload = proto.encodeDebugDumpNamed(&buf, vt_mode, session); 723 const payload = proto.encodeDebugDumpNamed(&buf, vt_mode, session);
@@ -727,14 +731,14 @@ fn stats(alloc: std.mem.Allocator, sock_path: []const u8) !u8 {
727 /// Ask the daemon on `sock_path` to exit, then wait until the PROCESS is 731 /// Ask the daemon on `sock_path` to exit, then wait until the PROCESS is
728 /// gone, not just the path. Exit 0 covers both "stopped" and "nothing 732 /// gone, not just the path. Exit 0 covers both "stopped" and "nothing
729 /// there" — the state the user asked for is the state they got, which is 733 /// there" — the state the user asked for is the state they got, which is
730 /// what makes the verb safe to script (`muxd start`'s re-runnability, 734 /// what makes the verb safe to script (`mux d start`'s re-runnability,
731 /// mirrored). 735 /// mirrored).
732 /// 736 ///
733 /// Prefixes split the way `start`'s do: `muxd stop:` for a refusal or a 737 /// Prefixes split the way `start`'s do: `mux d stop:` for a refusal or a
734 /// report about this command, plain `muxd:` for a lifecycle verdict. 738 /// report about this command, plain `mux d:` for a lifecycle verdict.
735 fn stopCmd(alloc: std.mem.Allocator, sock_path: []const u8) !u8 { 739 fn stopCmd(alloc: std.mem.Allocator, sock_path: []const u8) !u8 {
736 const stream = std.net.connectUnixSocket(sock_path) catch { 740 const stream = std.net.connectUnixSocket(sock_path) catch {
737 std.debug.print("muxd stop: nothing listening on {s}\n", .{sock_path}); 741 std.debug.print("mux d stop: nothing listening on {s}\n", .{sock_path});
738 return 0; 742 return 0;
739 }; 743 };
740 // A daemon that dies between connect and write reached the asked-for 744 // A daemon that dies between connect and write reached the asked-for
@@ -765,14 +769,14 @@ fn stopCmd(alloc: std.mem.Allocator, sock_path: []const u8) !u8 {
765 const secs = @divTrunc(stop_deadline_ms, 1000); 769 const secs = @divTrunc(stop_deadline_ms, 1000);
766 if (!asked) { 770 if (!asked) {
767 std.debug.print( 771 std.debug.print(
768 "muxd stop: could not deliver the stop request to {s}, and it is still answering after {d}s\n", 772 "mux d stop: could not deliver the stop request to {s}, and it is still answering after {d}s\n",
769 .{ sock_path, secs }, 773 .{ sock_path, secs },
770 ); 774 );
771 return 1; 775 return 1;
772 } 776 }
773 var hint: [log_hint_len]u8 = undefined; 777 var hint: [log_hint_len]u8 = undefined;
774 std.debug.print( 778 std.debug.print(
775 "muxd stop: {s} still answering after {d}s{s}\n", 779 "mux d stop: {s} still answering after {d}s{s}\n",
776 .{ sock_path, secs, logHint(alloc, &hint) }, 780 .{ sock_path, secs, logHint(alloc, &hint) },
777 ); 781 );
778 return 1; 782 return 1;
@@ -792,13 +796,13 @@ fn peerPid(fd: std.posix.socket_t) ?std.posix.pid_t {
792 /// A socket gone quiet is the unlink, and the unlink is the FIRST thing a 796 /// A socket gone quiet is the unlink, and the unlink is the FIRST thing a
793 /// stopping daemon does; reaping its shells (one TERM grace when one 797 /// stopping daemon does; reaping its shells (one TERM grace when one
794 /// ignores it) and deleting its dirs come after. "stopped" said at the 798 /// ignores it) and deleting its dirs come after. "stopped" said at the
795 /// unlink handed a scripted `muxd start`, or a supervisor's "is it down", 799 /// unlink handed a scripted `mux d start`, or a supervisor's "is it down",
796 /// a daemon still running. The bound is the reap's own grace with room to 800 /// a daemon still running. The bound is the reap's own grace with room to
797 /// spare — a daemon still here after it is wedged in teardown, and that 801 /// spare — a daemon still here after it is wedged in teardown, and that
798 /// is a report, not a wait. 802 /// is a report, not a wait.
799 fn waitPidGone(peer: ?std.posix.pid_t, sock_path: []const u8) u8 { 803 fn waitPidGone(peer: ?std.posix.pid_t, sock_path: []const u8) u8 {
800 const pid = peer orelse { 804 const pid = peer orelse {
801 std.debug.print("muxd: stopped\n", .{}); 805 std.debug.print("mux d: stopped\n", .{});
802 return 0; 806 return 0;
803 }; 807 };
804 const gone_deadline_ms: i64 = 3000; 808 const gone_deadline_ms: i64 = 3000;
@@ -809,14 +813,14 @@ fn waitPidGone(peer: ?std.posix.pid_t, sock_path: []const u8) u8 {
809 while (std.posix.kill(pid, 0)) |_| { 813 while (std.posix.kill(pid, 0)) |_| {
810 if (std.time.milliTimestamp() - t0 >= gone_deadline_ms) { 814 if (std.time.milliTimestamp() - t0 >= gone_deadline_ms) {
811 std.debug.print( 815 std.debug.print(
812 "muxd stop: {s} is closed, but pid {d} is still running {d}s later\n", 816 "mux d stop: {s} is closed, but pid {d} is still running {d}s later\n",
813 .{ sock_path, pid, @divTrunc(gone_deadline_ms, 1000) }, 817 .{ sock_path, pid, @divTrunc(gone_deadline_ms, 1000) },
814 ); 818 );
815 return 1; 819 return 1;
816 } 820 }
817 std.Thread.sleep(20 * std.time.ns_per_ms); 821 std.Thread.sleep(20 * std.time.ns_per_ms);
818 } else |_| {} 822 } else |_| {}
819 std.debug.print("muxd: stopped\n", .{}); 823 std.debug.print("mux d: stopped\n", .{});
820 return 0; 824 return 0;
821 } 825 }
822 826
@@ -824,17 +828,17 @@ fn waitPidGone(peer: ?std.posix.pid_t, sock_path: []const u8) u8 {
824 /// the one that asks: it knows its own version and its own path, and the 828 /// the one that asks: it knows its own version and its own path, and the
825 /// daemon is the one that decides. 829 /// daemon is the one that decides.
826 /// 830 ///
827 /// Prefixes split as `stop`'s do: `muxd upgrade:` for a refusal or a report 831 /// Prefixes split as `stop`'s do: `mux d upgrade:` for a refusal or a report
828 /// about this command, plain `muxd:` for the lifecycle verdict. 832 /// about this command, plain `mux d:` for the lifecycle verdict.
829 fn upgradeCmd(alloc: std.mem.Allocator, sock_path: []const u8, allow_same: bool) !u8 { 833 fn upgradeCmd(alloc: std.mem.Allocator, sock_path: []const u8, allow_same: bool) !u8 {
830 var exe_buf: [std.fs.max_path_bytes]u8 = undefined; 834 var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
831 const exe = std.fs.selfExePath(&exe_buf) catch { 835 const exe = std.fs.selfExePath(&exe_buf) catch {
832 std.debug.print("muxd upgrade: cannot find own binary via /proc/self/exe\n", .{}); 836 std.debug.print("mux d upgrade: cannot find own binary via /proc/self/exe\n", .{});
833 return 1; 837 return 1;
834 }; 838 };
835 839
836 const stream = std.net.connectUnixSocket(sock_path) catch { 840 const stream = std.net.connectUnixSocket(sock_path) catch {
837 std.debug.print("muxd upgrade: nothing listening on {s}\n", .{sock_path}); 841 std.debug.print("mux d upgrade: nothing listening on {s}\n", .{sock_path});
838 return 1; 842 return 1;
839 }; 843 };
840 defer stream.close(); 844 defer stream.close();
@@ -845,11 +849,11 @@ fn upgradeCmd(alloc: std.mem.Allocator, sock_path: []const u8, allow_same: bool)
845 .version = build_options.version, 849 .version = build_options.version,
846 .path = exe, 850 .path = exe,
847 }) catch { 851 }) catch {
848 std.debug.print("muxd upgrade: cannot name {s} in a request\n", .{exe}); 852 std.debug.print("mux d upgrade: cannot name {s} in a request\n", .{exe});
849 return 1; 853 return 1;
850 }; 854 };
851 proto.writeFrame(stream.handle, .upgrade_req, payload) catch { 855 proto.writeFrame(stream.handle, .upgrade_req, payload) catch {
852 std.debug.print("muxd upgrade: {s} closed before the request landed\n", .{sock_path}); 856 std.debug.print("mux d upgrade: {s} closed before the request landed\n", .{sock_path});
853 return 1; 857 return 1;
854 }; 858 };
855 859
@@ -872,14 +876,27 @@ fn upgradeCmd(alloc: std.mem.Allocator, sock_path: []const u8, allow_same: bool)
872 if (frame.payload[0] != 0) { 876 if (frame.payload[0] != 0) {
873 // The daemon's words, verbatim: it is the side that knows which 877 // The daemon's words, verbatim: it is the side that knows which
874 // check failed, and paraphrasing here would lose the versions. 878 // check failed, and paraphrasing here would lose the versions.
875 std.debug.print("muxd upgrade: refused: {s}\n", .{frame.payload[1..]}); 879 std.debug.print("mux d upgrade: refused: {s}\n", .{frame.payload[1..]});
880 // One refusal gets a translation, because the daemon saying it
881 // cannot know why. v0.0.1-15 and older probe the candidate for
882 // `muxd <version>` and this binary answers `mux <version>` —
883 // the four binaries became one. If that daemon's wording ever
884 // changes the hint simply stops appearing; the verbatim line
885 // above is what the user acts on either way.
886 if (std.mem.eql(u8, frame.payload[1..], "version: output mismatch"))
887 std.debug.print(
888 "mux d upgrade: a daemon of v0.0.1-15 or older wants a candidate that " ++
889 "prints `muxd <version>`; this one is `mux`. There is no in-place " ++
890 "path across that: `mux d stop` then `mux d start`, once.\n",
891 .{},
892 );
876 return 1; 893 return 1;
877 } 894 }
878 std.debug.print("muxd: upgraded to {s}\n", .{build_options.version}); 895 std.debug.print("mux d: upgraded to {s}\n", .{build_options.version});
879 return confirmServing(alloc, sock_path); 896 return confirmServing(alloc, sock_path);
880 } 897 }
881 std.debug.print( 898 std.debug.print(
882 "muxd upgrade: no reply: this daemon predates upgrade — stop and run\n", 899 "mux d upgrade: no reply: this daemon predates upgrade — stop and run\n",
883 .{}, 900 .{},
884 ); 901 );
885 return 1; 902 return 1;
@@ -892,7 +909,7 @@ fn confirmServing(alloc: std.mem.Allocator, sock_path: []const u8) u8 {
892 // whichever image accepts it. Only an ANSWERED frame says the new one 909 // whichever image accepts it. Only an ANSWERED frame says the new one
893 // is pumping. 910 // is pumping.
894 const stream = std.net.connectUnixSocket(sock_path) catch { 911 const stream = std.net.connectUnixSocket(sock_path) catch {
895 std.debug.print("muxd upgrade: {s} stopped answering after the exec\n", .{sock_path}); 912 std.debug.print("mux d upgrade: {s} stopped answering after the exec\n", .{sock_path});
896 return 1; 913 return 1;
897 }; 914 };
898 defer stream.close(); 915 defer stream.close();
@@ -911,7 +928,7 @@ fn confirmServing(alloc: std.mem.Allocator, sock_path: []const u8) u8 {
911 const secs = @divTrunc(deadline_ms, 1000); 928 const secs = @divTrunc(deadline_ms, 1000);
912 var hint: [log_hint_len]u8 = undefined; 929 var hint: [log_hint_len]u8 = undefined;
913 std.debug.print( 930 std.debug.print(
914 "muxd upgrade: exec'd, but {s} has not answered in {d}s{s}\n", 931 "mux d upgrade: exec'd, but {s} has not answered in {d}s{s}\n",
915 .{ sock_path, secs, logHint(alloc, &hint) }, 932 .{ sock_path, secs, logHint(alloc, &hint) },
916 ); 933 );
917 return 1; 934 return 1;
@@ -925,7 +942,7 @@ const log_hint_len = std.fs.max_path_bytes + 64;
925 /// 942 ///
926 /// Only when the path resolves: an absent HOME (a container, a systemd 943 /// Only when the path resolves: an absent HOME (a container, a systemd
927 /// unit) must not replace the finding that matters with an error trace. 944 /// unit) must not replace the finding that matters with an error trace.
928 /// And the hedge stays in the words: a foreground `muxd run` logs to 945 /// And the hedge stays in the words: a foreground `mux d run` logs to
929 /// its own stderr, so naming the xdg path unconditionally would guess. 946 /// its own stderr, so naming the xdg path unconditionally would guess.
930 fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 { 947 fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 {
931 const log = xdg.logPath(alloc) catch return ""; 948 const log = xdg.logPath(alloc) catch return "";
@@ -940,7 +957,7 @@ fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 {
940 ) catch ""; 957 ) catch "";
941 } 958 }
942 959
943 /// `muxd proxy` with a one-line preamble: ensure a key, ask the daemon for 960 /// `mux d proxy` with a one-line preamble: ensure a key, ask the daemon for
944 /// its QUIC port, print `endpoint <port> <hex-key>` (or `endpoint none`) as 961 /// its QUIC port, print `endpoint <port> <hex-key>` (or `endpoint none`) as
945 /// the FIRST bytes on `out_fd`, then become exactly the proxy byte pump. 962 /// the FIRST bytes on `out_fd`, then become exactly the proxy byte pump.
946 /// 963 ///
@@ -973,14 +990,14 @@ fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 {
973 /// 990 ///
974 /// The pump that follows keeps speaking in its own name: a socket that 991 /// The pump that follows keeps speaking in its own name: a socket that
975 /// disappears between the ask and the attach is reported by proxy.zig as 992 /// disappears between the ask and the attach is reported by proxy.zig as
976 /// `muxd proxy: cannot connect to …`. That is deliberate — reusing 993 /// `mux d proxy: cannot connect to …`. That is deliberate — reusing
977 /// `proxy.run` is the whole design, and the line names the code that 994 /// `proxy.run` is the whole design, and the line names the code that
978 /// failed rather than the verb that was typed. 995 /// failed rather than the verb that was typed.
979 fn endpointCmd(alloc: std.mem.Allocator, sock_path: []const u8, out_fd: std.posix.fd_t) !u8 { 996 fn endpointCmd(alloc: std.mem.Allocator, sock_path: []const u8, out_fd: std.posix.fd_t) !u8 {
980 // Silent, and that is the whole of the verb's new manners: the wall 997 // Silent, and that is the whole of the verb's new manners: the wall
981 // runs this over ssh once a second per listed host, and a word on 998 // runs this over ssh once a second per listed host, and a word on
982 // stderr here is a word on the wall's alternate screen. The client 999 // stderr here is a word on the wall's alternate screen. The client
983 // that ASKED for the session narrates instead, and runs `muxd start`. 1000 // that ASKED for the session narrates instead, and runs `mux d start`.
984 if (!spawn.probe(sock_path)) return 1; 1001 if (!spawn.probe(sock_path)) return 1;
985 1002
986 // The announce goes out on the same stdout the pump is about to use, 1003 // The announce goes out on the same stdout the pump is about to use,
@@ -1027,7 +1044,7 @@ fn endpointCmd(alloc: std.mem.Allocator, sock_path: []const u8, out_fd: std.posi
1027 // first) is the likely one, but a full disk under a redirect and a 1044 // first) is the likely one, but a full disk under a redirect and a
1028 // closed fd reach here too, and they want different reactions. 1045 // closed fd reach here too, and they want different reactions.
1029 std.debug.print( 1046 std.debug.print(
1030 "muxd endpoint: cannot write the announce to stdout: {s}\n", 1047 "mux d endpoint: cannot write the announce to stdout: {s}\n",
1031 .{@errorName(err)}, 1048 .{@errorName(err)},
1032 ); 1049 );
1033 return 1; 1050 return 1;
@@ -1043,12 +1060,12 @@ fn endpointCmd(alloc: std.mem.Allocator, sock_path: []const u8, out_fd: std.posi
1043 fn reportNoListener(alloc: std.mem.Allocator, sock_path: []const u8) void { 1060 fn reportNoListener(alloc: std.mem.Allocator, sock_path: []const u8) void {
1044 var hint: [log_hint_len]u8 = undefined; 1061 var hint: [log_hint_len]u8 = undefined;
1045 std.debug.print( 1062 std.debug.print(
1046 "muxd endpoint: the daemon on {s} produced no QUIC listener; staying on ssh{s}\n", 1063 "mux d endpoint: the daemon on {s} produced no QUIC listener; staying on ssh{s}\n",
1047 .{ sock_path, logHint(alloc, &hint) }, 1064 .{ sock_path, logHint(alloc, &hint) },
1048 ); 1065 );
1049 } 1066 }
1050 1067
1051 /// The key `muxd endpoint` announces, or null with exactly one stderr line 1068 /// The key `mux d endpoint` announces, or null with exactly one stderr line
1052 /// saying why not. The reading of the environment and the deciding live in 1069 /// saying why not. The reading of the environment and the deciding live in
1053 /// `announceKeyFrom` below; this half owns the words. 1070 /// `announceKeyFrom` below; this half owns the words.
1054 fn announceKey(alloc: std.mem.Allocator) ?quic.Key { 1071 fn announceKey(alloc: std.mem.Allocator) ?quic.Key {
@@ -1060,11 +1077,11 @@ fn announceKey(alloc: std.mem.Allocator) ?quic.Key {
1060 switch (announceKeyFrom(xdg.pickKey(null, std.posix.getenv(xdg.key_env)), dflt)) { 1077 switch (announceKeyFrom(xdg.pickKey(null, std.posix.getenv(xdg.key_env)), dflt)) {
1061 .key => |k| return k, 1078 .key => |k| return k,
1062 .no_path => std.debug.print( 1079 .no_path => std.debug.print(
1063 "muxd endpoint: no HOME to resolve a key path; staying on ssh\n", 1080 "mux d endpoint: no HOME to resolve a key path; staying on ssh\n",
1064 .{}, 1081 .{},
1065 ), 1082 ),
1066 .create_failed => |f| std.debug.print( 1083 .create_failed => |f| std.debug.print(
1067 "muxd endpoint: cannot create {s}: {s}; staying on ssh\n", 1084 "mux d endpoint: cannot create {s}: {s}; staying on ssh\n",
1068 .{ f.path, @errorName(f.err) }, 1085 .{ f.path, @errorName(f.err) },
1069 ), 1086 ),
1070 .load_failed => |f| reportKeyRefusal(f.path, f.err), 1087 .load_failed => |f| reportKeyRefusal(f.path, f.err),
@@ -1133,7 +1150,7 @@ fn announceKeyFrom(env: ?[]const u8, dflt: ?[]const u8) KeyResult {
1133 fn reportKeyRefusal(path: []const u8, err: anyerror) void { 1150 fn reportKeyRefusal(path: []const u8, err: anyerror) void {
1134 var buf: [quic.key_refusal_len]u8 = undefined; 1151 var buf: [quic.key_refusal_len]u8 = undefined;
1135 std.debug.print( 1152 std.debug.print(
1136 "muxd endpoint: {s}; staying on ssh\n", 1153 "mux d endpoint: {s}; staying on ssh\n",
1137 .{quic.keyRefusalBody(&buf, err, path)}, 1154 .{quic.keyRefusalBody(&buf, err, path)},
1138 ); 1155 );
1139 } 1156 }
@@ -1179,41 +1196,36 @@ fn askEndpointPort(alloc: std.mem.Allocator, sock_path: []const u8) u16 {
1179 return 0; 1196 return 0;
1180 } 1197 }
1181 1198
1182 /// `muxd start` = ensureDaemon under an explicit flag. Everything after 1199 /// `mux d start` = ensureDaemon under an explicit flag. Everything after
1183 /// `start` is forwarded to `run` verbatim — no re-serialization, so a flag 1200 /// `start` is forwarded to `run` verbatim — no re-serialization, so a flag
1184 /// that parses here behaves identically there. parseArgs has already 1201 /// that parses here behaves identically there. parseArgs has already
1185 /// validated the flags in THIS process; what it cannot validate (a bad 1202 /// validated the flags in THIS process; what it cannot validate (a bad
1186 /// bind address, a missing key file) surfaces in the daemon's log, which 1203 /// bind address, a missing key file) surfaces in the daemon's log, which
1187 /// the failure path names. 1204 /// the failure path names.
1188 fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const [:0]const u8) !u8 { 1205 fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const [:0]const u8) !u8 {
1189 var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
1190 const exe = std.fs.selfExePath(&exe_buf) catch {
1191 std.debug.print("muxd: cannot find own binary via /proc/self/exe\n", .{});
1192 return 1;
1193 };
1194 const progress: spawn.Progress = .{ 1206 const progress: spawn.Progress = .{
1195 .fd = std.posix.STDERR_FILENO, 1207 .fd = std.posix.STDERR_FILENO,
1196 .prefix = "muxd", 1208 .prefix = "mux d",
1197 .tty = std.posix.isatty(std.posix.STDERR_FILENO), 1209 .tty = std.posix.isatty(std.posix.STDERR_FILENO),
1198 }; 1210 };
1199 // Default log path: the xdg one is the whole point for a real daemon. 1211 // Default log path: the xdg one is the whole point for a real daemon.
1200 // Truncating, and this is the only caller that does: `start` is the one 1212 // Truncating, and this is the only caller that does: `start` is the one
1201 // verb whose user asked for a (re)start, so the log they go on to read 1213 // verb whose user asked for a (re)start, so the log they go on to read
1202 // must be about the daemon they just started. 1214 // must be about the daemon they just started.
1203 const r = spawn.ensureDaemon(alloc, exe, forwarded, sock_path, progress, spawn.start_deadline_ms, .{ 1215 const r = spawn.ensureDaemon(alloc, spawn.self_exe, forwarded, sock_path, progress, spawn.start_deadline_ms, .{
1204 .truncate = true, 1216 .truncate = true,
1205 }) catch |err| switch (err) { 1217 }) catch |err| switch (err) {
1206 // The failure line, with the log path, was already printed by 1218 // The failure line, with the log path, was already printed by
1207 // Progress — a second line here would say the same thing worse. 1219 // Progress — a second line here would say the same thing worse.
1208 error.NeverAnswered => return 1, 1220 error.NeverAnswered => return 1,
1209 error.BinaryNotFound, error.SpawnFailed => { 1221 error.BinaryNotFound, error.SpawnFailed => {
1210 std.debug.print("muxd: could not spawn {s}: {s}\n", .{ exe, @errorName(err) }); 1222 std.debug.print("mux d: could not spawn {s}: {s}\n", .{ spawn.self_exe, @errorName(err) });
1211 return 1; 1223 return 1;
1212 }, 1224 },
1213 }; 1225 };
1214 if (r == .already_running) { 1226 if (r == .already_running) {
1215 std.debug.print( 1227 std.debug.print(
1216 "muxd: already running on {s} (stop it first with `muxd stop --sock {s}` if you meant different flags)\n", 1228 "mux d: already running on {s} (stop it first with `mux d stop --sock {s}` if you meant different flags)\n",
1217 .{ sock_path, sock_path }, 1229 .{ sock_path, sock_path },
1218 ); 1230 );
1219 } 1231 }
@@ -1226,7 +1238,7 @@ fn keygen(alloc: std.mem.Allocator) !u8 {
1226 xdg.writeNewKey(path) catch |err| switch (err) { 1238 xdg.writeNewKey(path) catch |err| switch (err) {
1227 error.KeyExists => { 1239 error.KeyExists => {
1228 std.debug.print( 1240 std.debug.print(
1229 "muxd keygen: {s} already exists; rotation is `rm` + `keygen`, deliberately\n", 1241 "mux d keygen: {s} already exists; rotation is `rm` + `keygen`, deliberately\n",
1230 .{path}, 1242 .{path},
1231 ); 1243 );
1232 return 1; 1244 return 1;
@@ -1253,14 +1265,14 @@ fn parse(comptime argv: []const [:0]const u8) ParseResult {
1253 } 1265 }
1254 1266
1255 test "parseArgs: subcommands and their existing flags" { 1267 test "parseArgs: subcommands and their existing flags" {
1256 const r = parse(&.{ "muxd", "run" }); 1268 const r = parse(&.{ "d", "run" });
1257 try std.testing.expect(r == .ok); 1269 try std.testing.expect(r == .ok);
1258 try std.testing.expect(r.ok._cmd == .run); 1270 try std.testing.expect(r.ok._cmd == .run);
1259 try std.testing.expect(r.ok.sock == null); 1271 try std.testing.expect(r.ok.sock == null);
1260 try std.testing.expectEqual(@as(u16, 80), r.ok.cols); 1272 try std.testing.expectEqual(@as(u16, 80), r.ok.cols);
1261 try std.testing.expectEqual(@as(u16, 24), r.ok.rows); 1273 try std.testing.expectEqual(@as(u16, 24), r.ok.rows);
1262 1274
1263 const d = parse(&.{ "muxd", "dump", "--vt", "--sock", "/tmp/x.sock" }); 1275 const d = parse(&.{ "d", "dump", "--vt", "--sock", "/tmp/x.sock" });
1264 try std.testing.expect(d.ok._cmd == .dump); 1276 try std.testing.expect(d.ok._cmd == .dump);
1265 try std.testing.expect(d.ok.vt); 1277 try std.testing.expect(d.ok.vt);
1266 try std.testing.expectEqualStrings("/tmp/x.sock", d.ok.sock.?); 1278 try std.testing.expectEqualStrings("/tmp/x.sock", d.ok.sock.?);
@@ -1268,18 +1280,18 @@ test "parseArgs: subcommands and their existing flags" {
1268 // on the wire as the empty tail. 1280 // on the wire as the empty tail.
1269 try std.testing.expect(d.ok.session == null); 1281 try std.testing.expect(d.ok.session == null);
1270 1282
1271 const g = parse(&.{ "muxd", "run", "--cols", "120", "--rows", "40", "--shell", "/bin/dash" }); 1283 const g = parse(&.{ "d", "run", "--cols", "120", "--rows", "40", "--shell", "/bin/dash" });
1272 try std.testing.expectEqual(@as(u16, 120), g.ok.cols); 1284 try std.testing.expectEqual(@as(u16, 120), g.ok.cols);
1273 try std.testing.expectEqual(@as(u16, 40), g.ok.rows); 1285 try std.testing.expectEqual(@as(u16, 40), g.ok.rows);
1274 try std.testing.expectEqualStrings("/bin/dash", g.ok.shell.?); 1286 try std.testing.expectEqualStrings("/bin/dash", g.ok.shell.?);
1275 1287
1276 try std.testing.expect(parse(&.{"muxd"}).err == .no_command); 1288 try std.testing.expect(parse(&.{"d"}).err == .no_command);
1277 try std.testing.expect(parse(&.{ "muxd", "wat" }).err == .unknown_command); 1289 try std.testing.expect(parse(&.{ "d", "wat" }).err == .unknown_command);
1278 try std.testing.expect(parse(&.{ "muxd", "run", "--wat" }).err == .unknown_arg); 1290 try std.testing.expect(parse(&.{ "d", "run", "--wat" }).err == .unknown_arg);
1279 } 1291 }
1280 1292
1281 test "parse: dump --session rides into the payload" { 1293 test "parse: dump --session rides into the payload" {
1282 const d = parse(&.{ "muxd", "dump", "--session", "b", "--sock", "/tmp/x.sock" }); 1294 const d = parse(&.{ "d", "dump", "--session", "b", "--sock", "/tmp/x.sock" });
1283 try std.testing.expect(d == .ok); 1295 try std.testing.expect(d == .ok);
1284 try std.testing.expectEqualStrings("b", d.ok.session.?.name); 1296 try std.testing.expectEqualStrings("b", d.ok.session.?.name);
1285 1297
@@ -1287,66 +1299,66 @@ test "parse: dump --session rides into the payload" {
1287 // stderr, never carried to the wire as a payload nothing can look up. 1299 // stderr, never carried to the wire as a payload nothing can look up.
1288 // The flag is what the refusal names now, so a user is told which of 1300 // The flag is what the refusal names now, so a user is told which of
1289 // several values on the line was the one refused. 1301 // several values on the line was the one refused.
1290 const bad = parse(&.{ "muxd", "dump", "--session", "has space" }); 1302 const bad = parse(&.{ "d", "dump", "--session", "has space" });
1291 try std.testing.expect(bad.err == .bad_value); 1303 try std.testing.expect(bad.err == .bad_value);
1292 try std.testing.expectEqualStrings("--session", bad.err.bad_value); 1304 try std.testing.expectEqualStrings("--session", bad.err.bad_value);
1293 } 1305 }
1294 1306
1295 test "parseArgs: --key without --quic is refused; --quic alone defers to main" { 1307 test "parseArgs: --key without --quic is refused; --quic alone defers to main" {
1296 const both = parse(&.{ "muxd", "run", "--quic", "0.0.0.0:4433", "--key", "/k" }); 1308 const both = parse(&.{ "d", "run", "--quic", "0.0.0.0:4433", "--key", "/k" });
1297 try std.testing.expect(both == .ok); 1309 try std.testing.expect(both == .ok);
1298 try std.testing.expectEqualStrings("0.0.0.0:4433", both.ok.quic.?); 1310 try std.testing.expectEqualStrings("0.0.0.0:4433", both.ok.quic.?);
1299 try std.testing.expectEqualStrings("/k", both.ok.key.?); 1311 try std.testing.expectEqualStrings("/k", both.ok.key.?);
1300 1312
1301 // --quic without --key is no longer a parse error: main resolves 1313 // --quic without --key is no longer a parse error: main resolves
1302 // MUX_KEY_FILE and the default path, and parse cannot see either. 1314 // MUX_KEY_FILE and the default path, and parse cannot see either.
1303 const deferred = parse(&.{ "muxd", "run", "--quic", "0.0.0.0:4433" }); 1315 const deferred = parse(&.{ "d", "run", "--quic", "0.0.0.0:4433" });
1304 try std.testing.expect(deferred == .ok); 1316 try std.testing.expect(deferred == .ok);
1305 try std.testing.expect(deferred.ok.key == null); 1317 try std.testing.expect(deferred.ok.key == null);
1306 1318
1307 // A key with nowhere to listen is still a mistake with no reading that 1319 // A key with nowhere to listen is still a mistake with no reading that
1308 // makes it sensible, and parse can see the whole of it. 1320 // makes it sensible, and parse can see the whole of it.
1309 try std.testing.expect(parse(&.{ "muxd", "run", "--key", "/k" }).err == .key_without_quic); 1321 try std.testing.expect(parse(&.{ "d", "run", "--key", "/k" }).err == .key_without_quic);
1310 1322
1311 // Neither is the ordinary case and must stay silent. 1323 // Neither is the ordinary case and must stay silent.
1312 const neither = parse(&.{ "muxd", "run" }); 1324 const neither = parse(&.{ "d", "run" });
1313 try std.testing.expect(neither.ok.quic == null); 1325 try std.testing.expect(neither.ok.quic == null);
1314 try std.testing.expect(neither.ok.key == null); 1326 try std.testing.expect(neither.ok.key == null);
1315 } 1327 }
1316 1328
1317 test "parseArgs: --quic-idle-ms defaults, parses, and refuses nonsense" { 1329 test "parseArgs: --quic-idle-ms defaults, parses, and refuses nonsense" {
1318 const dflt = parse(&.{ "muxd", "run", "--quic", "127.0.0.1:1", "--key", "/k" }); 1330 const dflt = parse(&.{ "d", "run", "--quic", "127.0.0.1:1", "--key", "/k" });
1319 // Spelled out rather than written `quic.default_idle_ms`: asserting 1331 // Spelled out rather than written `quic.default_idle_ms`: asserting
1320 // against the same constant the parser reads would hold for any value, 1332 // against the same constant the parser reads would hold for any value,
1321 // so it could never catch the number changing. 1333 // so it could never catch the number changing.
1322 try std.testing.expectEqual(@as(u32, 15_000), dflt.ok.quic_idle_ms.ms); 1334 try std.testing.expectEqual(@as(u32, 15_000), dflt.ok.quic_idle_ms.ms);
1323 1335
1324 const set = parse(&.{ "muxd", "run", "--quic", "127.0.0.1:1", "--key", "/k", "--quic-idle-ms", "2500" }); 1336 const set = parse(&.{ "d", "run", "--quic", "127.0.0.1:1", "--key", "/k", "--quic-idle-ms", "2500" });
1325 try std.testing.expectEqual(@as(u32, 2500), set.ok.quic_idle_ms.ms); 1337 try std.testing.expectEqual(@as(u32, 2500), set.ok.quic_idle_ms.ms);
1326 1338
1327 // Zero means "no idle timeout" to ngtcp2 — the opposite of what anyone 1339 // Zero means "no idle timeout" to ngtcp2 — the opposite of what anyone
1328 // typing a timeout of zero is asking for, so it is refused rather than 1340 // typing a timeout of zero is asking for, so it is refused rather than
1329 // silently inverted. 1341 // silently inverted.
1330 try std.testing.expect(parse(&.{ "muxd", "run", "--quic-idle-ms", "0" }).err == .bad_value); 1342 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "0" }).err == .bad_value);
1331 try std.testing.expect(parse(&.{ "muxd", "run", "--quic-idle-ms", "soon" }).err == .bad_value); 1343 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "soon" }).err == .bad_value);
1332 try std.testing.expect(parse(&.{ "muxd", "run", "--quic-idle-ms", "-5" }).err == .bad_value); 1344 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "-5" }).err == .bad_value);
1333 // Wider than u32: refused at the parse rather than overflowing where it 1345 // Wider than u32: refused at the parse rather than overflowing where it
1334 // is multiplied out to nanoseconds. 1346 // is multiplied out to nanoseconds.
1335 try std.testing.expect(parse(&.{ "muxd", "run", "--quic-idle-ms", "99999999999" }).err == .bad_value); 1347 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "99999999999" }).err == .bad_value);
1336 // The idle flag alone does not turn QUIC on, and must not smuggle the 1348 // The idle flag alone does not turn QUIC on, and must not smuggle the
1337 // both-or-neither rule past the check. 1349 // both-or-neither rule past the check.
1338 try std.testing.expect(parse(&.{ "muxd", "run", "--quic-idle-ms", "2500" }) == .ok); 1350 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "2500" }) == .ok);
1339 1351
1340 // Same treatment for the numbers that were already here. 1352 // Same treatment for the numbers that were already here.
1341 try std.testing.expect(parse(&.{ "muxd", "run", "--cols", "wide" }).err == .bad_value); 1353 try std.testing.expect(parse(&.{ "d", "run", "--cols", "wide" }).err == .bad_value);
1342 try std.testing.expect(parse(&.{ "muxd", "run", "--rows", "99999" }).err == .bad_value); 1354 try std.testing.expect(parse(&.{ "d", "run", "--rows", "99999" }).err == .bad_value);
1343 } 1355 }
1344 1356
1345 test "parseArgs: a value-taking flag at the end of argv names itself" { 1357 test "parseArgs: a value-taking flag at the end of argv names itself" {
1346 // This used to report "unknown argument: --quic", which blames the flag 1358 // This used to report "unknown argument: --quic", which blames the flag
1347 // rather than the missing value. 1359 // rather than the missing value.
1348 inline for (.{ "--sock", "--shell", "--cols", "--rows", "--quic", "--key", "--quic-idle-ms", "--session", "--resume-fd", "--resume-fail-at" }) |flag| { 1360 inline for (.{ "--sock", "--shell", "--cols", "--rows", "--quic", "--key", "--quic-idle-ms", "--session", "--resume-fd", "--resume-fail-at" }) |flag| {
1349 const r = parse(&.{ "muxd", "run", flag }); 1361 const r = parse(&.{ "d", "run", flag });
1350 try std.testing.expect(r.err == .missing_value); 1362 try std.testing.expect(r.err == .missing_value);
1351 try std.testing.expectEqualStrings(flag, r.err.missing_value); 1363 try std.testing.expectEqualStrings(flag, r.err.missing_value);
1352 } 1364 }
@@ -1359,13 +1371,13 @@ test "parseArgs: a value-taking flag at the end of argv names itself" {
1359 // 1371 //
1360 // Anchored to the command position rather than to a bare name, because the 1372 // Anchored to the command position rather than to a bare name, because the
1361 // prose says these words in passing: `endpoint`'s parenthetical contains 1373 // prose says these words in passing: `endpoint`'s parenthetical contains
1362 // "proxy", so deleting the whole `muxd proxy` line still leaves the word in 1374 // "proxy", so deleting the whole `mux d proxy` line still leaves the word in
1363 // the text, and an unanchored search would call that documented. 1375 // the text, and an unanchored search would call that documented.
1364 // `inline for` is what makes `s.name` comptime, so the anchor can be built 1376 // `inline for` is what makes `s.name` comptime, so the anchor can be built
1365 // with `++`. 1377 // with `++`.
1366 test "usage names every subcommand" { 1378 test "usage names every subcommand" {
1367 inline for (specs) |s| { 1379 inline for (specs) |s| {
1368 const named = std.mem.indexOf(u8, usage, "\n muxd " ++ s.name) != null; 1380 const named = std.mem.indexOf(u8, usage, "\n mux d " ++ s.name) != null;
1369 // expect() alone would print only "expected true", which does not 1381 // expect() alone would print only "expected true", which does not
1370 // say which verb went missing. 1382 // say which verb went missing.
1371 if (!named) std.debug.print("usage never names the subcommand `{s}`\n", .{s.name}); 1383 if (!named) std.debug.print("usage never names the subcommand `{s}`\n", .{s.name});
@@ -1437,30 +1449,30 @@ test "keygen: a generated key loads through quic.Key.load" {
1437 } 1449 }
1438 1450
1439 test "parseArgs: --version is a command, not a flag on one" { 1451 test "parseArgs: --version is a command, not a flag on one" {
1440 const r = parse(&.{ "muxd", "--version" }); 1452 const r = parse(&.{ "d", "--version" });
1441 try std.testing.expect(r == .ok); 1453 try std.testing.expect(r == .ok);
1442 try std.testing.expect(r.ok._cmd == .version); 1454 try std.testing.expect(r.ok._cmd == .version);
1443 1455
1444 // Typed onto a verb it becomes that same command, so `muxd run 1456 // Typed onto a verb it becomes that same command, so `mux d run
1445 // --version` answers instead of refusing an unknown flag. Asserted on 1457 // --version` answers instead of refusing an unknown flag. Asserted on
1446 // the parse rather than on `main`, which writes the version to STDOUT 1458 // the parse rather than on `main`, which writes the version to STDOUT
1447 // and would hang the build runner's IPC. 1459 // and would hang the build runner's IPC.
1448 const on_run = parse(&.{ "muxd", "run", "--sock", "/x", "--version" }); 1460 const on_run = parse(&.{ "d", "run", "--sock", "/x", "--version" });
1449 try std.testing.expect(on_run == .ok); 1461 try std.testing.expect(on_run == .ok);
1450 try std.testing.expect(on_run.ok._cmd == .version); 1462 try std.testing.expect(on_run.ok._cmd == .version);
1451 } 1463 }
1452 1464
1453 test "parseArgs: --help is a command, and a flag on one, and both exit 0 on stdout" { 1465 test "parseArgs: --help is a command, and a flag on one, and both exit 0 on stdout" {
1454 const bare = parse(&.{ "muxd", "--help" }); 1466 const bare = parse(&.{ "d", "--help" });
1455 try std.testing.expect(bare == .ok); 1467 try std.testing.expect(bare == .ok);
1456 try std.testing.expect(bare.ok._cmd == .help); 1468 try std.testing.expect(bare.ok._cmd == .help);
1457 1469
1458 // On a subcommand it is an outcome of the flag parse rather than a row, 1470 // On a subcommand it is an outcome of the flag parse rather than a row,
1459 // and it must outrank the grammar: `--sock` here is still waiting for a 1471 // and it must outrank the grammar: `--sock` here is still waiting for a
1460 // value, and asking for the usage is not a way to mistype one. 1472 // value, and asking for the usage is not a way to mistype one.
1461 try std.testing.expect(parse(&.{ "muxd", "run", "--help" }).err == .help); 1473 try std.testing.expect(parse(&.{ "d", "run", "--help" }).err == .help);
1462 try std.testing.expect(parse(&.{ "muxd", "dump", "-h", "--sock", "/x" }).err == .help); 1474 try std.testing.expect(parse(&.{ "d", "dump", "-h", "--sock", "/x" }).err == .help);
1463 try std.testing.expect(parse(&.{ "muxd", "run", "--sock", "--help" }).err == .help); 1475 try std.testing.expect(parse(&.{ "d", "run", "--sock", "--help" }).err == .help);
1464 1476
1465 // The code, asked of `usageCode` rather than of `usageExit`: the latter 1477 // The code, asked of `usageCode` rather than of `usageExit`: the latter
1466 // writes the usage to STDOUT, which under `zig build test` is the build 1478 // writes the usage to STDOUT, which under `zig build test` is the build
@@ -1472,10 +1484,10 @@ test "parseArgs: --help is a command, and a flag on one, and both exit 0 on stdo
1472 } 1484 }
1473 1485
1474 test "parseArgs: keygen takes no flags" { 1486 test "parseArgs: keygen takes no flags" {
1475 const r = parse(&.{ "muxd", "keygen" }); 1487 const r = parse(&.{ "d", "keygen" });
1476 try std.testing.expect(r == .ok); 1488 try std.testing.expect(r == .ok);
1477 try std.testing.expect(r.ok._cmd == .keygen); 1489 try std.testing.expect(r.ok._cmd == .keygen);
1478 try std.testing.expect(parse(&.{ "muxd", "keygen", "--sock", "/x" }).err == .unknown_arg); 1490 try std.testing.expect(parse(&.{ "d", "keygen", "--sock", "/x" }).err == .unknown_arg);
1479 } 1491 }
1480 1492
1481 test "pickKey: --key beats MUX_KEY_FILE beats the default path" { 1493 test "pickKey: --key beats MUX_KEY_FILE beats the default path" {
@@ -1490,7 +1502,7 @@ test "pickKey: --key beats MUX_KEY_FILE beats the default path" {
1490 } 1502 }
1491 1503
1492 test "parseArgs: start takes run's flags" { 1504 test "parseArgs: start takes run's flags" {
1493 const r = parse(&.{ "muxd", "start", "--sock", "/tmp/x.sock", "--cols", "100" }); 1505 const r = parse(&.{ "d", "start", "--sock", "/tmp/x.sock", "--cols", "100" });
1494 try std.testing.expect(r == .ok); 1506 try std.testing.expect(r == .ok);
1495 try std.testing.expect(r.ok._cmd == .start); 1507 try std.testing.expect(r.ok._cmd == .start);
1496 try std.testing.expectEqualStrings("/tmp/x.sock", r.ok.sock.?); 1508 try std.testing.expectEqualStrings("/tmp/x.sock", r.ok.sock.?);
@@ -1498,23 +1510,23 @@ test "parseArgs: start takes run's flags" {
1498 } 1510 }
1499 1511
1500 test "parseArgs: stop is a command and takes --sock" { 1512 test "parseArgs: stop is a command and takes --sock" {
1501 const r = parse(&.{ "muxd", "stop" }); 1513 const r = parse(&.{ "d", "stop" });
1502 try std.testing.expect(r == .ok); 1514 try std.testing.expect(r == .ok);
1503 try std.testing.expect(r.ok._cmd == .stop); 1515 try std.testing.expect(r.ok._cmd == .stop);
1504 try std.testing.expect(r.ok.sock == null); 1516 try std.testing.expect(r.ok.sock == null);
1505 1517
1506 const s = parse(&.{ "muxd", "stop", "--sock", "/tmp/x.sock" }); 1518 const s = parse(&.{ "d", "stop", "--sock", "/tmp/x.sock" });
1507 try std.testing.expect(s.ok._cmd == .stop); 1519 try std.testing.expect(s.ok._cmd == .stop);
1508 try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); 1520 try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?);
1509 } 1521 }
1510 1522
1511 test "parseArgs: endpoint is a command and takes --sock" { 1523 test "parseArgs: endpoint is a command and takes --sock" {
1512 const r = parse(&.{ "muxd", "endpoint" }); 1524 const r = parse(&.{ "d", "endpoint" });
1513 try std.testing.expect(r == .ok); 1525 try std.testing.expect(r == .ok);
1514 try std.testing.expect(r.ok._cmd == .endpoint); 1526 try std.testing.expect(r.ok._cmd == .endpoint);
1515 try std.testing.expect(r.ok.sock == null); 1527 try std.testing.expect(r.ok.sock == null);
1516 1528
1517 const s = parse(&.{ "muxd", "endpoint", "--sock", "/tmp/x.sock" }); 1529 const s = parse(&.{ "d", "endpoint", "--sock", "/tmp/x.sock" });
1518 try std.testing.expect(s.ok._cmd == .endpoint); 1530 try std.testing.expect(s.ok._cmd == .endpoint);
1519 try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); 1531 try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?);
1520 1532
@@ -1527,15 +1539,15 @@ test "parseArgs: endpoint is a command and takes --sock" {
1527 // only one this command reads. `keygen` is the sole verb that narrows 1539 // only one this command reads. `keygen` is the sole verb that narrows
1528 // its own surface, and widening that rule to `proxy` and `endpoint` 1540 // its own surface, and widening that rule to `proxy` and `endpoint`
1529 // together is its own change, not this one's. 1541 // together is its own change, not this one's.
1530 try std.testing.expect(parse(&.{ "muxd", "endpoint", "--quiet" }).err == .unknown_arg); 1542 try std.testing.expect(parse(&.{ "d", "endpoint", "--quiet" }).err == .unknown_arg);
1531 try std.testing.expect(parse(&.{ "muxd", "endpoint", "--cols", "100" }) == .ok); 1543 try std.testing.expect(parse(&.{ "d", "endpoint", "--cols", "100" }) == .ok);
1532 const missing = parse(&.{ "muxd", "endpoint", "--sock" }); 1544 const missing = parse(&.{ "d", "endpoint", "--sock" });
1533 try std.testing.expect(missing.err == .missing_value); 1545 try std.testing.expect(missing.err == .missing_value);
1534 try std.testing.expectEqualStrings("--sock", missing.err.missing_value); 1546 try std.testing.expectEqualStrings("--sock", missing.err.missing_value);
1535 } 1547 }
1536 1548
1537 test "parseArgs: run --resume-fd N --check is the old daemon's dry run" { 1549 test "parseArgs: run --resume-fd N --check is the old daemon's dry run" {
1538 const r = parse(&.{ "muxd", "run", "--resume-fd", "7", "--check" }); 1550 const r = parse(&.{ "d", "run", "--resume-fd", "7", "--check" });
1539 try std.testing.expect(r == .ok); 1551 try std.testing.expect(r == .ok);
1540 try std.testing.expect(r.ok._cmd == .run); 1552 try std.testing.expect(r.ok._cmd == .run);
1541 try std.testing.expectEqual(@as(std.posix.fd_t, 7), r.ok.resume_fd.?); 1553 try std.testing.expectEqual(@as(std.posix.fd_t, 7), r.ok.resume_fd.?);
@@ -1543,15 +1555,15 @@ test "parseArgs: run --resume-fd N --check is the old daemon's dry run" {
1543 1555
1544 // A number, like --cols: an fd that is not one would be read as a 1556 // A number, like --cols: an fd that is not one would be read as a
1545 // descriptor the daemon never passed. 1557 // descriptor the daemon never passed.
1546 try std.testing.expect(parse(&.{ "muxd", "run", "--resume-fd", "x" }).err == .bad_value); 1558 try std.testing.expect(parse(&.{ "d", "run", "--resume-fd", "x" }).err == .bad_value);
1547 1559
1548 const f = parse(&.{ "muxd", "run", "--resume-fd", "3", "--resume-fail-at", "session" }); 1560 const f = parse(&.{ "d", "run", "--resume-fd", "3", "--resume-fail-at", "session" });
1549 try std.testing.expectEqualStrings("session", f.ok.resume_fail_at.?); 1561 try std.testing.expectEqualStrings("session", f.ok.resume_fail_at.?);
1550 1562
1551 // Neither flag is the ordinary start, and both must stay off there — 1563 // Neither flag is the ordinary start, and both must stay off there —
1552 // a `run` that thought it was resuming would adopt nothing and serve 1564 // a `run` that thought it was resuming would adopt nothing and serve
1553 // nothing. 1565 // nothing.
1554 const plain = parse(&.{ "muxd", "run" }); 1566 const plain = parse(&.{ "d", "run" });
1555 try std.testing.expect(plain.ok.resume_fd == null); 1567 try std.testing.expect(plain.ok.resume_fd == null);
1556 try std.testing.expect(!plain.ok.check); 1568 try std.testing.expect(!plain.ok.check);
1557 } 1569 }
@@ -1575,14 +1587,14 @@ test "rollbackKeepsEnv: the rollback does not inherit the abort that caused it"
1575 } 1587 }
1576 1588
1577 test "parseArgs: upgrade is a command, and same-version is a flag it takes" { 1589 test "parseArgs: upgrade is a command, and same-version is a flag it takes" {
1578 const r = parse(&.{ "muxd", "upgrade" }); 1590 const r = parse(&.{ "d", "upgrade" });
1579 try std.testing.expect(r == .ok); 1591 try std.testing.expect(r == .ok);
1580 try std.testing.expect(r.ok._cmd == .upgrade); 1592 try std.testing.expect(r.ok._cmd == .upgrade);
1581 // Off unless asked: the skew rule is strictly-newer, and an operator who 1593 // Off unless asked: the skew rule is strictly-newer, and an operator who
1582 // did not name the exception must not get it. 1594 // did not name the exception must not get it.
1583 try std.testing.expect(!r.ok.allow_same_version); 1595 try std.testing.expect(!r.ok.allow_same_version);
1584 1596
1585 const s = parse(&.{ "muxd", "upgrade", "--sock", "/tmp/x.sock", "--allow-same-version" }); 1597 const s = parse(&.{ "d", "upgrade", "--sock", "/tmp/x.sock", "--allow-same-version" });
1586 try std.testing.expect(s.ok._cmd == .upgrade); 1598 try std.testing.expect(s.ok._cmd == .upgrade);
1587 try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); 1599 try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?);
1588 try std.testing.expect(s.ok.allow_same_version); 1600 try std.testing.expect(s.ok.allow_same_version);
@@ -1602,7 +1614,7 @@ test "resumeRun: --check adopts nothing, so --resume-fail-at has nothing to abor
1602 // would exec the OLD binary out of a probe the old daemon runs as a 1614 // would exec the OLD binary out of a probe the old daemon runs as a
1603 // CHILD, and a rollback target that cannot be exec'd fails this 1615 // CHILD, and a rollback target that cannot be exec'd fails this
1604 // test instead of replacing the test runner with it. 1616 // test instead of replacing the test runner with it.
1605 .writer_path = "/nonexistent/muxd", 1617 .writer_path = "/nonexistent/mux",
1606 .sock_path = "/tmp/mux-resume-check-test.sock", 1618 .sock_path = "/tmp/mux-resume-check-test.sock",
1607 .listener_fd = -1, 1619 .listener_fd = -1,
1608 .shellint_dir = null, 1620 .shellint_dir = null,
@@ -1717,7 +1729,7 @@ test "askEndpointPort: a socket nobody serves answers 0, quickly" {
1717 // never entered. An implementation that reached for the deadline 1729 // never entered. An implementation that reached for the deadline
1718 // first — polling for a reply that cannot come, retrying the connect 1730 // first — polling for a reply that cannot come, retrying the connect
1719 // until the budget ran out — would answer the same 0 two seconds 1731 // until the budget ran out — would answer the same 0 two seconds
1720 // later, and every `muxd endpoint` against a daemon that died between 1732 // later, and every `mux d endpoint` against a daemon that died between
1721 // the ensure and the ask would pay it. 1733 // the ensure and the ask would pay it.
1722 const t0 = std.time.milliTimestamp(); 1734 const t0 = std.time.milliTimestamp();
1723 try std.testing.expectEqual(@as(u16, 0), askEndpointPort(std.testing.allocator, sock)); 1735 try std.testing.expectEqual(@as(u16, 0), askEndpointPort(std.testing.allocator, sock));
@@ -1743,7 +1755,7 @@ test "endpointCmd: a box with no daemon is refused, never started — the wall p
1743 try endpointCmd(std.testing.allocator, sock, out.handle), 1755 try endpointCmd(std.testing.allocator, sock, out.handle),
1744 ); 1756 );
1745 // The verb READS a box. Starting a daemon here gave a listed machine 1757 // The verb READS a box. Starting a daemon here gave a listed machine
1746 // one (and a shell in session 0) from a poll, and undid a `muxd stop` 1758 // one (and a shell in session 0) from a poll, and undid a `mux d stop`
1747 // on the next cycle a second later. 1759 // on the next cycle a second later.
1748 try std.testing.expect(!spawn.probe(sock)); 1760 try std.testing.expect(!spawn.probe(sock));
1749 try std.testing.expectEqual(@as(u64, 0), (try out.stat()).size); 1761 try std.testing.expectEqual(@as(u64, 0), (try out.stat()).size);
@@ -1816,7 +1828,7 @@ test "shellIntegrationEnabled: an unset environment means off" {
1816 // The daily-driver default. The injection is not free — under zsh the 1828 // The daily-driver default. The injection is not free — under zsh the
1817 // ZDOTDIR shim costs the user their ~/.zshenv, and under bash the DEBUG 1829 // ZDOTDIR shim costs the user their ~/.zshenv, and under bash the DEBUG
1818 // trap displaces theirs (atuin, bash-preexec) — and what it buys is a 1830 // trap displaces theirs (atuin, bash-preexec) — and what it buys is a
1819 // knowable exit code, which only `muxa` reads. A user who never asked 1831 // knowable exit code, which only `mux a` reads. A user who never asked
1820 // for it must not pay for it. 1832 // for it must not pay for it.
1821 try std.testing.expect(!shellIntegrationEnabled(null)); 1833 try std.testing.expect(!shellIntegrationEnabled(null));
1822 } 1834 }
src/cli/mux.zig
Old New
@@ -0,0 +1,43 @@
1 //! mux — the one binary. The first word picks a mode: `d` is the daemon,
2 //! `a` the JSON agent surface, `web` the browser hub, and anything else
3 //! (including nothing) is the client. Each mode's own parser then reads the
4 //! rest, so this file owns the mode letter and nothing else: there is no
5 //! second flag grammar here, and no argv[0] dispatch — the four programs
6 //! became four words, not four names for one file.
7 const std = @import("std");
8 const daemon = @import("daemon_main");
9 const agent = @import("agent_main");
10 const hub = @import("hub_main");
11 const client = @import("client_main");
12
13 pub fn main() !u8 {
14 var gpa: std.heap.DebugAllocator(.{}) = .init;
15 defer if (gpa.deinit() == .leak)
16 std.debug.print("mux: LEAK: allocations outlived deinit\n", .{});
17 const alloc = gpa.allocator();
18
19 const args = try std.process.argsAlloc(alloc);
20 defer std.process.argsFree(alloc, args);
21
22 if (args.len > 1) {
23 // Each mode is handed a slice whose [0] is the word the user typed
24 // and whose [1..] is its own line — the shape every one of these
25 // parsers already reads, from back when [0] was the program name.
26 const rest = args[1..];
27 if (std.mem.eql(u8, args[1], "d")) return daemon.main(rest);
28 if (std.mem.eql(u8, args[1], "a")) return agent.main(rest);
29 if (std.mem.eql(u8, args[1], "web")) return hub.main(rest);
30 // The bridge for daemons of v0.0.1-15 and older, and the only word
31 // that means a mode without naming one. Their upgrade execs the
32 // candidate as `<binary> run --resume-fd N` (the argv built in
33 // server.zig's upgradeExec), so a v15 daemon can only exec its way
34 // into this binary if bare `run` still reaches the daemon. Nothing
35 // else is aliased: `mux stop` is a host called "stop".
36 if (std.mem.eql(u8, args[1], "run")) return daemon.main(args);
37 }
38 return client.main(args);
39 }
40
41 test {
42 std.testing.refAllDeclsRecursive(@This());
43 }
src/cli/mux_main.zig
Old New
@@ -1,6 +1,7 @@
1 //! mux — client binary. `mux [--sock PATH]` attaches to the local muxd; 1 //! `mux` with no mode letter — the client. `mux [--sock PATH]` attaches
2 //! to the local daemon;
2 //! `mux --via CMD` attaches over CMD's stdio instead (any command that 3 //! `mux --via CMD` attaches over CMD's stdio instead (any command that
3 //! exposes a session socket as a byte pipe, e.g. `ssh host muxd proxy`); 4 //! exposes a session socket as a byte pipe, e.g. `ssh host mux d proxy`);
4 //! `mux HOST` runs the ssh→QUIC handoff — ssh fetches the daemon's QUIC 5 //! `mux HOST` runs the ssh→QUIC handoff — ssh fetches the daemon's QUIC
5 //! coordinates and carries the session only if the QUIC dial does not. 6 //! coordinates and carries the session only if the QUIC dial does not.
6 //! 7 //!
@@ -25,13 +26,24 @@ const wall = @import("wall");
25 const cliflags = @import("cliflags"); 26 const cliflags = @import("cliflags");
26 const TmpDir = @import("testtmp").TmpDir; 27 const TmpDir = @import("testtmp").TmpDir;
27 28
29 /// The whole tree's root page. The four modes come first because the mode
30 /// word is the first thing typed; the rest of the page is the client's,
31 /// because the client is what `mux` with no mode word runs. Each other mode
32 /// documents itself under its own `--help`.
28 const usage = 33 const usage =
29 \\usage: mux [HOST | --sock PATH | --via CMD | quic://HOST[:PORT]] 34 \\usage: mux [TARGET ...] attach, or the wall (this page)
35 \\ mux d VERB ... the daemon: run start stop stats dump proxy
36 \\ endpoint keygen upgrade (`mux d --help`)
37 \\ mux a VERB ... the agent surface, one JSON object per verb
38 \\ (`mux a --help`)
39 \\ mux web [TARGET ...] the browser hub (`mux web --help`)
40 \\
41 \\ mux [HOST | --sock PATH | --via CMD | quic://HOST[:PORT]]
30 \\ HOST attaches over ssh and hands off to QUIC when the daemon offers it 42 \\ HOST attaches over ssh and hands off to QUIC when the daemon offers it
31 \\ (muxd must be on HOST's PATH; cached coordinates make later attaches 43 \\ (`mux` must be on HOST's PATH; cached coordinates make later attaches
32 \\ skip ssh entirely) 44 \\ skip ssh entirely)
33 \\ quic://HOST[:PORT] (PORT defaults to 4433) uses --key FILE, 45 \\ quic://HOST[:PORT] (PORT defaults to 4433) uses --key FILE,
34 \\ MUX_KEY_FILE, or ~/.config/mux/key; muxd must be running with a 46 \\ MUX_KEY_FILE, or ~/.config/mux/key; the daemon must be running with a
35 \\ matching --quic and key 47 \\ matching --quic and key
36 \\ [--quic-idle-ms N] tunes how fast a dead link is noticed 48 \\ [--quic-idle-ms N] tunes how fast a dead link is noticed
37 \\ [--session NAME] attaches to (or creates) a named session instead of 49 \\ [--session NAME] attaches to (or creates) a named session instead of
@@ -48,7 +60,7 @@ const usage =
48 \\ mux hosts rm SPELLING take one off (its sessions keep running) 60 \\ mux hosts rm SPELLING take one off (its sessions keep running)
49 \\ 61 \\
50 \\ SPELLING names a DAEMON — HOST, quic://HOST[:PORT], or --sock PATH 62 \\ SPELLING names a DAEMON — HOST, quic://HOST[:PORT], or --sock PATH
51 \\ (one argument, or two as in muxweb) — and never a session: `#NAME` 63 \\ (one argument, or two as in `mux web`) — and never a session: `#NAME`
52 \\ is refused, because the wall shows every session a daemon has. 64 \\ is refused, because the wall shows every session a daemon has.
53 \\ 65 \\
54 \\ On the wall, `Ctrl-\ 1-9` focuses a tile and types into it, 66 \\ On the wall, `Ctrl-\ 1-9` focuses a tile and types into it,
@@ -76,7 +88,7 @@ const ParseResult = union(enum) {
76 attach: struct { sock: ?[]const u8 = null, via: ?[]const u8 = null, session: []const u8 = "", agent: bool = false }, 88 attach: struct { sock: ?[]const u8 = null, via: ?[]const u8 = null, session: []const u8 = "", agent: bool = false },
77 /// A bare hostname: the ssh recipe is built from it in main, where there 89 /// A bare hostname: the ssh recipe is built from it in main, where there
78 /// is an allocator to build it with. `idle_ms` rides along because the 90 /// is an allocator to build it with. `idle_ms` rides along because the
79 /// handoff ends in a QUIC link like any other — muxweb's HOST tiles 91 /// handoff ends in a QUIC link like any other — the hub's HOST tiles
80 /// have always carried it, and mux dropping it on the floor made 92 /// have always carried it, and mux dropping it on the floor made
81 /// `--quic-idle-ms` silently do nothing on exactly the spelling most 93 /// `--quic-idle-ms` silently do nothing on exactly the spelling most
82 /// people use. 94 /// people use.
@@ -251,7 +263,7 @@ fn parseArgs(args: []const [:0]const u8, env_key: ?[]const u8) ParseError!ParseR
251 } }; 263 } };
252 } 264 }
253 // A key with no quic:// has nothing to authenticate and is ignored 265 // A key with no quic:// has nothing to authenticate and is ignored
254 // rather than refused: unlike muxd, where --key without --quic means a 266 // rather than refused: unlike the daemon, where --key without --quic means a
255 // listener was meant, here it is one env var away from being set for 267 // listener was meant, here it is one env var away from being set for
256 // every invocation in a shell, and refusing `mux --sock ...` because 268 // every invocation in a shell, and refusing `mux --sock ...` because
257 // MUX_KEY_FILE happens to be exported would be absurd. 269 // MUX_KEY_FILE happens to be exported would be absurd.
@@ -259,15 +271,14 @@ fn parseArgs(args: []const [:0]const u8, env_key: ?[]const u8) ParseError!ParseR
259 return .{ .attach = .{ .sock = o.sock, .via = o.via, .session = session, .agent = o.agent } }; 271 return .{ .attach = .{ .sock = o.sock, .via = o.via, .session = session, .agent = o.agent } };
260 } 272 }
261 273
262 pub fn main() !u8 { 274 /// The bare `mux`: no mode letter, so the words are a transport or
275 /// nothing. argv is the dispatcher's, program name and all.
276 pub fn main(args: []const [:0]const u8) !u8 {
263 var gpa: std.heap.DebugAllocator(.{}) = .init; 277 var gpa: std.heap.DebugAllocator(.{}) = .init;
264 defer if (gpa.deinit() == .leak) 278 defer if (gpa.deinit() == .leak)
265 std.debug.print("mux: LEAK: allocations outlived deinit\n", .{}); 279 std.debug.print("mux: LEAK: allocations outlived deinit\n", .{});
266 const alloc = gpa.allocator(); 280 const alloc = gpa.allocator();
267 281
268 const args = try std.process.argsAlloc(alloc);
269 defer std.process.argsFree(alloc, args);
270
271 // A subcommand, checked before the flag parse: `hosts` edits or reads a 282 // A subcommand, checked before the flag parse: `hosts` edits or reads a
272 // file and dials no session, so it is not a transport spelling. 283 // file and dials no session, so it is not a transport spelling.
273 if (args.len > 1 and std.mem.eql(u8, args[1], "hosts")) 284 if (args.len > 1 and std.mem.eql(u8, args[1], "hosts"))
@@ -336,7 +347,7 @@ pub fn main() !u8 {
336 .given, .default => |p| p, 347 .given, .default => |p| p,
337 .missing => |p| { 348 .missing => |p| {
338 std.debug.print( 349 std.debug.print(
339 "mux: no key: pass --key, set MUX_KEY_FILE, or run `muxd keygen` (default {s})\n", 350 "mux: no key: pass --key, set MUX_KEY_FILE, or run `mux d keygen` (default {s})\n",
340 .{p}, 351 .{p},
341 ); 352 );
342 return 2; 353 return 2;
@@ -352,7 +363,7 @@ pub fn main() !u8 {
352 // The handoff recipe: ssh fetches the coordinates (and, on a 363 // The handoff recipe: ssh fetches the coordinates (and, on a
353 // cold attach, carries the session if QUIC cannot), while a 364 // cold attach, carries the session if QUIC cannot), while a
354 // warm attach dials from the cache and never spawns ssh at all. 365 // warm attach dials from the cache and never spawns ssh at all.
355 // handoff.recipeFor owns both pieces; muxweb builds its HOST 366 // handoff.recipeFor owns both pieces; the hub builds its HOST
356 // tiles from the same call. 367 // tiles from the same call.
357 const r = try handoff.recipeFor(alloc, h.name, false); 368 const r = try handoff.recipeFor(alloc, h.name, false);
358 defer r.deinit(alloc); 369 defer r.deinit(alloc);
@@ -404,25 +415,10 @@ fn defaultSock(alloc: std.mem.Allocator) !?[]const u8 {
404 }; 415 };
405 } 416 }
406 417
407 /// Auto-start: give a local attach a daemon to land on, and the ONLY 418 /// The ONLY auto-start left, and it starts this image by its own /proc
408 /// auto-start left. False when it cannot — the line saying why is already 419 /// link — no name to look up, so none to get wrong.
409 /// printed. Unix-socket transport only: quic:// has nothing local to spawn,
410 /// and `--via` starts nothing at all, because the remote proxy reads a box
411 /// (README's `ssh HOST 'muxd start'` is the answer there).
412 fn ensureLocalDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !bool { 420 fn ensureLocalDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !bool {
413 const muxd_path = try spawn.findInPath( 421 return spawn.ensureForAttach(alloc, sock_path, "mux");
414 alloc,
415 std.posix.getenv("PATH") orelse "",
416 "muxd",
417 );
418 defer if (muxd_path) |p| alloc.free(p);
419 if (muxd_path) |exe| return spawn.ensureForAttach(alloc, exe, sock_path, "mux");
420 // No muxd anywhere AND nothing serving: only now is the missing binary
421 // the user's problem, and both facts fit in one honest line. A live
422 // daemon needs no binary on PATH.
423 if (spawn.probe(sock_path)) return true;
424 std.debug.print("mux: no daemon on {s} and no muxd in PATH to start one\n", .{sock_path});
425 return false;
426 } 422 }
427 423
428 /// Whether the wall has to start the local daemon itself. Asked of the OS, 424 /// Whether the wall has to start the local daemon itself. Asked of the OS,
@@ -459,10 +455,10 @@ fn attachLocal(
459 return 2; 455 return 2;
460 } 456 }
461 457
462 // The same `sun_path` guard muxd applies (main.zig), for the same 458 // The same `sun_path` guard the daemon applies (main.zig), for the same
463 // reason and off the same constant. It sits before the PATH search 459 // reason and off the same constant. It sits before the PATH search
464 // rather than at the connect because auto-start would otherwise reach 460 // rather than at the connect because auto-start would otherwise reach
465 // it first: mux finds muxd, spawns a child that refuses the path 461 // it first: mux forks a daemon that refuses the path
466 // instantly, and polls the full 2s into "daemon did not answer" — a 462 // instantly, and polls the full 2s into "daemon did not answer" — a
467 // timeout story about a path that was doomed at parse. Refusing here 463 // timeout story about a path that was doomed at parse. Refusing here
468 // costs nothing and says the real thing. 464 // costs nothing and says the real thing.
@@ -813,9 +809,9 @@ test "parseArgs: --sock and --via each name their transport" {
813 try std.testing.expectEqualStrings("/tmp/x.sock", s.attach.sock.?); 809 try std.testing.expectEqualStrings("/tmp/x.sock", s.attach.sock.?);
814 try std.testing.expect(s.attach.via == null); 810 try std.testing.expect(s.attach.via == null);
815 811
816 const v = try parse(&.{ "mux", "--via", "ssh box muxd proxy" }); 812 const v = try parse(&.{ "mux", "--via", "ssh box mux d proxy" });
817 try std.testing.expect(v == .attach); 813 try std.testing.expect(v == .attach);
818 try std.testing.expectEqualStrings("ssh box muxd proxy", v.attach.via.?); 814 try std.testing.expectEqualStrings("ssh box mux d proxy", v.attach.via.?);
819 try std.testing.expect(v.attach.sock == null); 815 try std.testing.expect(v.attach.sock == null);
820 } 816 }
821 817
@@ -839,7 +835,7 @@ test "parseArgs: a bare word is a host to hop to" {
839 test "parseArgs: naming two transports is a conflict, however it is spelled" { 835 test "parseArgs: naming two transports is a conflict, however it is spelled" {
840 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "vm1", "--sock", "/tmp/x.sock" })); 836 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "vm1", "--sock", "/tmp/x.sock" }));
841 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "--sock", "/tmp/x.sock", "vm1" })); 837 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "--sock", "/tmp/x.sock", "vm1" }));
842 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "vm1", "--via", "ssh box muxd proxy" })); 838 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "vm1", "--via", "ssh box mux d proxy" }));
843 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "--sock", "/a", "--via", "c" })); 839 try std.testing.expectError(error.Conflict, parse(&.{ "mux", "--sock", "/a", "--via", "c" }));
844 // Two of the same kind is the same ambiguity as two different kinds — 840 // Two of the same kind is the same ambiguity as two different kinds —
845 // for the two spellings that carry no flag. A flag repeated is not 841 // for the two spellings that carry no flag. A flag repeated is not
@@ -1133,7 +1129,7 @@ test "parseArgs: --quic-idle-ms parses, and refuses what ngtcp2 would invert" {
1133 1129
1134 // A bare HOST ends in a QUIC link too, so the flag has to reach it — 1130 // A bare HOST ends in a QUIC link too, so the flag has to reach it —
1135 // the .host result carried no idle_ms at all and the flag was accepted 1131 // the .host result carried no idle_ms at all and the flag was accepted
1136 // and then dropped, which is worse than refusing it. muxweb's HOST 1132 // and then dropped, which is worse than refusing it. The hub's HOST
1137 // tiles were already right; this is mux catching up. 1133 // tiles were already right; this is mux catching up.
1138 const h = try parse(&.{ "mux", "vm1", "--quic-idle-ms", "1500" }); 1134 const h = try parse(&.{ "mux", "vm1", "--quic-idle-ms", "1500" });
1139 try std.testing.expect(h == .host); 1135 try std.testing.expect(h == .host);
src/cli/muxa.zig
Old New
@@ -1,4 +1,4 @@
1 //! muxa: the agent-facing mux client. Every verb prints one JSON object on 1 //! `mux a`: the agent-facing mode. Every verb prints one JSON object on
2 //! stdout and exits 0 on success; failures print {"error": "..."} and exit 2 //! stdout and exits 0 on success; failures print {"error": "..."} and exit
3 //! nonzero. Attaches at 0x0 always — an agent must never claim the grid 3 //! nonzero. Attaches at 0x0 always — an agent must never claim the grid
4 //! out from under the human's size (load-bearing spec rule). 4 //! out from under the human's size (load-bearing spec rule).
@@ -11,7 +11,7 @@
11 //! 3 the wait timed out; the object says `"reason":"timeout"` 11 //! 3 the wait timed out; the object says `"reason":"timeout"`
12 //! 4 the object could not be written at all (see `emit`) 12 //! 4 the object could not be written at all (see `emit`)
13 //! 13 //!
14 //! A command's own exit code is never muxa's: it is the `exit_code` field, 14 //! A command's own exit code is never this mode's: it is the `exit_code` field,
15 //! and a `run` whose command failed still exits 0 because the question was 15 //! and a `run` whose command failed still exits 0 because the question was
16 //! answered. 16 //! answered.
17 const std = @import("std"); 17 const std = @import("std");
@@ -24,10 +24,10 @@ const cliflags = @import("cliflags");
24 const build_options = @import("build_options"); 24 const build_options = @import("build_options");
25 25
26 const usage = 26 const usage =
27 \\usage: muxa <verb> [--sock PATH | --quic HOST[:PORT] [--key PATH]] 27 \\usage: mux a <verb> [--sock PATH | --quic HOST[:PORT] [--key PATH]]
28 \\ [--settle MS] [--timeout MS] [--vt] [--session NAME] [args] 28 \\ [--settle MS] [--timeout MS] [--vt] [--session NAME] [args]
29 \\ muxa --help | --version 29 \\ mux a --help | --version
30 \\NAME must already exist: muxa attaches at 0x0 and never creates a session 30 \\NAME must already exist: `mux a` attaches at 0x0 and never creates a session
31 \\(`capture` is the exception that stays quiet: it answers in the grid). 31 \\(`capture` is the exception that stays quiet: it answers in the grid).
32 \\verbs: 32 \\verbs:
33 \\ status session snapshot as JSON 33 \\ status session snapshot as JSON
@@ -53,7 +53,7 @@ const Opts = struct {
53 key: ?[]const u8 = null, 53 key: ?[]const u8 = null,
54 settle: u32 = 0, 54 settle: u32 = 0,
55 // Never 0 by default: the daemon reads a 0 timeout on await_req as "no 55 // Never 0 by default: the daemon reads a 0 timeout on await_req as "no
56 // bound at all" (documented on AwaitReq), so a muxa that defaulted to 0 56 // bound at all" (documented on AwaitReq), so an agent client that defaulted to 0
57 // would turn every await into an unbounded wait. 57 // would turn every await into an unbounded wait.
58 timeout: u32 = 30_000, 58 timeout: u32 = 30_000,
59 vt: bool = false, 59 vt: bool = false,
@@ -69,7 +69,7 @@ const Opts = struct {
69 // ONE name for the attach AND every ask after it, so the daemon's 69 // ONE name for the attach AND every ask after it, so the daemon's
70 // attached-tail equality rule (server.zig) never sees a mismatch 70 // attached-tail equality rule (server.zig) never sees a mismatch
71 // out of this binary. Empty is the wire's own default spelling, so 71 // out of this binary. Empty is the wire's own default spelling, so
72 // a bare `muxa status` builds the frames it always did. 72 // a bare `mux a status` builds the frames it always did.
73 return if (o.session) |n| n.name else ""; 73 return if (o.session) |n| n.name else "";
74 } 74 }
75 75
@@ -108,7 +108,7 @@ fn parseArgs(args: []const [:0]const u8) ParseError!Opts {
108 // named, and the two answers differ — this is the mistake `mux` 108 // named, and the two answers differ — this is the mistake `mux`
109 // refuses as `.conflict` for the same reason. 109 // refuses as `.conflict` for the same reason.
110 if (o.quic != null and o.sock != null) return error.Usage; 110 if (o.quic != null and o.sock != null) return error.Usage;
111 // A key with nothing to authenticate to, refused exactly where muxd 111 // A key with nothing to authenticate to, refused exactly where the daemon
112 // refuses it: there is no reading of `--key` without `--quic` that 112 // refuses it: there is no reading of `--key` without `--quic` that
113 // makes it sensible, and the unix socket has no key at all. 113 // makes it sensible, and the unix socket has no key at all.
114 if (o.key != null and o.quic == null) return error.Usage; 114 if (o.key != null and o.quic == null) return error.Usage;
@@ -182,32 +182,32 @@ test "decodeEscapes covers the sequences send needs" {
182 } 182 }
183 183
184 test "parseArgs verbs and flags" { 184 test "parseArgs verbs and flags" {
185 const a1 = [_][:0]const u8{ "muxa", "status" }; 185 const a1 = [_][:0]const u8{ "a", "status" };
186 try std.testing.expectEqual(Verb.status, (try parseArgs(&a1))._verb.?); 186 try std.testing.expectEqual(Verb.status, (try parseArgs(&a1))._verb.?);
187 const a2 = [_][:0]const u8{ "muxa", "run", "--timeout", "5000", "make test" }; 187 const a2 = [_][:0]const u8{ "a", "run", "--timeout", "5000", "make test" };
188 const o2 = try parseArgs(&a2); 188 const o2 = try parseArgs(&a2);
189 try std.testing.expectEqual(@as(u32, 5000), o2.timeout); 189 try std.testing.expectEqual(@as(u32, 5000), o2.timeout);
190 try std.testing.expectEqualStrings("make test", o2._arg.?); 190 try std.testing.expectEqualStrings("make test", o2._arg.?);
191 const a3 = [_][:0]const u8{ "muxa", "bogus" }; 191 const a3 = [_][:0]const u8{ "a", "bogus" };
192 try std.testing.expectError(error.Usage, parseArgs(&a3)); 192 try std.testing.expectError(error.Usage, parseArgs(&a3));
193 193
194 // The verb is a positional, so a flag may sit before it — one grammar, 194 // The verb is a positional, so a flag may sit before it — one grammar,
195 // not a verb slot with rules of its own. 195 // not a verb slot with rules of its own.
196 const early = [_][:0]const u8{ "muxa", "--vt", "capture" }; 196 const early = [_][:0]const u8{ "a", "--vt", "capture" };
197 const oe = try parseArgs(&early); 197 const oe = try parseArgs(&early);
198 try std.testing.expectEqual(Verb.capture, oe._verb.?); 198 try std.testing.expectEqual(Verb.capture, oe._verb.?);
199 try std.testing.expect(oe.vt); 199 try std.testing.expect(oe.vt);
200 200
201 // Only the FIRST bare word is read as a verb: a verb-shaped argument is 201 // Only the FIRST bare word is read as a verb: a verb-shaped argument is
202 // the verb's argument, which is what `muxa send status` has to mean. 202 // the verb's argument, which is what `mux a send status` has to mean.
203 const shadow = [_][:0]const u8{ "muxa", "send", "status" }; 203 const shadow = [_][:0]const u8{ "a", "send", "status" };
204 const os = try parseArgs(&shadow); 204 const os = try parseArgs(&shadow);
205 try std.testing.expectEqual(Verb.send, os._verb.?); 205 try std.testing.expectEqual(Verb.send, os._verb.?);
206 try std.testing.expectEqualStrings("status", os._arg.?); 206 try std.testing.expectEqualStrings("status", os._arg.?);
207 207
208 // A line whose every word is a flag names no verb, and is the same 208 // A line whose every word is a flag names no verb, and is the same
209 // usage mistake a bare `muxa` is. 209 // usage mistake a bare `mux a` is.
210 const verbless = [_][:0]const u8{ "muxa", "--vt" }; 210 const verbless = [_][:0]const u8{ "a", "--vt" };
211 try std.testing.expectError(error.Usage, parseArgs(&verbless)); 211 try std.testing.expectError(error.Usage, parseArgs(&verbless));
212 } 212 }
213 213
@@ -215,95 +215,95 @@ test "parseArgs: -- hands the rest to the verb, flags and all" {
215 // Without the end-of-flags marker this is an unknown flag and the whole 215 // Without the end-of-flags marker this is an unknown flag and the whole
216 // invocation is refused — the exact shape an agent sends when a key 216 // invocation is refused — the exact shape an agent sends when a key
217 // sequence starts with a dash. 217 // sequence starts with a dash.
218 const dashed = [_][:0]const u8{ "muxa", "send", "-n foo" }; 218 const dashed = [_][:0]const u8{ "a", "send", "-n foo" };
219 try std.testing.expectError(error.Usage, parseArgs(&dashed)); 219 try std.testing.expectError(error.Usage, parseArgs(&dashed));
220 220
221 const a = [_][:0]const u8{ "muxa", "send", "--settle", "50", "--", "-n foo" }; 221 const a = [_][:0]const u8{ "a", "send", "--settle", "50", "--", "-n foo" };
222 const o = try parseArgs(&a); 222 const o = try parseArgs(&a);
223 try std.testing.expectEqual(@as(u32, 50), o.settle); 223 try std.testing.expectEqual(@as(u32, 50), o.settle);
224 try std.testing.expectEqualStrings("-n foo", o._arg.?); 224 try std.testing.expectEqualStrings("-n foo", o._arg.?);
225 225
226 // Past the marker, a flag spelling is just text — and a second 226 // Past the marker, a flag spelling is just text — and a second
227 // positional is still one too many. 227 // positional is still one too many.
228 const flagish = [_][:0]const u8{ "muxa", "run", "--", "--timeout" }; 228 const flagish = [_][:0]const u8{ "a", "run", "--", "--timeout" };
229 try std.testing.expectEqualStrings("--timeout", (try parseArgs(&flagish))._arg.?); 229 try std.testing.expectEqualStrings("--timeout", (try parseArgs(&flagish))._arg.?);
230 const two = [_][:0]const u8{ "muxa", "run", "--", "a", "b" }; 230 const two = [_][:0]const u8{ "a", "run", "--", "a", "b" };
231 try std.testing.expectError(error.Usage, parseArgs(&two)); 231 try std.testing.expectError(error.Usage, parseArgs(&two));
232 232
233 // The marker outranks the help scan too: an agent typing `--help` AT a 233 // The marker outranks the help scan too: an agent typing `--help` AT a
234 // session must reach the pty, not this binary's usage page. 234 // session must reach the pty, not this binary's usage page.
235 const help_payload = [_][:0]const u8{ "muxa", "send", "--", "--help" }; 235 const help_payload = [_][:0]const u8{ "a", "send", "--", "--help" };
236 try std.testing.expectEqualStrings("--help", (try parseArgs(&help_payload))._arg.?); 236 try std.testing.expectEqualStrings("--help", (try parseArgs(&help_payload))._arg.?);
237 } 237 }
238 238
239 test "muxa: --help and --version are answered wherever they can be typed" { 239 test "mux a: --help and --version are answered wherever they can be typed" {
240 // A bare `muxa --help` types it where a verb would go, and the same 240 // A bare `mux a --help` types it where a verb would go, and the same
241 // scan answers it after a verb: ONE table, so the two cannot drift. 241 // scan answers it after a verb: ONE table, so the two cannot drift.
242 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "--help" })); 242 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "a", "--help" }));
243 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "-h" })); 243 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "a", "-h" }));
244 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "status", "--help" })); 244 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "a", "status", "--help" }));
245 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "run", "--timeout", "--help" })); 245 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "a", "run", "--timeout", "--help" }));
246 try std.testing.expectError(error.Version, parseArgs(&[_][:0]const u8{ "muxa", "--version" })); 246 try std.testing.expectError(error.Version, parseArgs(&[_][:0]const u8{ "a", "--version" }));
247 try std.testing.expectError(error.Version, parseArgs(&[_][:0]const u8{ "muxa", "status", "--version" })); 247 try std.testing.expectError(error.Version, parseArgs(&[_][:0]const u8{ "a", "status", "--version" }));
248 248
249 // Neither is a verb, so a word that is neither and is no verb either is 249 // Neither is a verb, so a word that is neither and is no verb either is
250 // still the usage error it always was. 250 // still the usage error it always was.
251 try std.testing.expectError(error.Usage, parseArgs(&[_][:0]const u8{ "muxa", "--wat" })); 251 try std.testing.expectError(error.Usage, parseArgs(&[_][:0]const u8{ "a", "--wat" }));
252 try std.testing.expectError(error.Usage, parseArgs(&[_][:0]const u8{"muxa"})); 252 try std.testing.expectError(error.Usage, parseArgs(&[_][:0]const u8{"a"}));
253 } 253 }
254 254
255 test "muxa: --session rides every verb; a bad name is usage, not wire bytes" { 255 test "mux a: --session rides every verb; a bad name is usage, not wire bytes" {
256 const a = [_][:0]const u8{ "muxa", "status", "--session", "b" }; 256 const a = [_][:0]const u8{ "a", "status", "--session", "b" };
257 const o = try parseArgs(&a); 257 const o = try parseArgs(&a);
258 try std.testing.expectEqualStrings("b", o.sessionName()); 258 try std.testing.expectEqualStrings("b", o.sessionName());
259 259
260 // No --session named: the wire's own default spelling, empty. 260 // No --session named: the wire's own default spelling, empty.
261 const bare = [_][:0]const u8{ "muxa", "status" }; 261 const bare = [_][:0]const u8{ "a", "status" };
262 try std.testing.expectEqualStrings("", (try parseArgs(&bare)).sessionName()); 262 try std.testing.expectEqualStrings("", (try parseArgs(&bare)).sessionName());
263 263
264 // A name no tool could ever address is refused at parse (the usage 264 // A name no tool could ever address is refused at parse (the usage
265 // exit, 2) rather than reaching a daemon as a payload nothing can 265 // exit, 2) rather than reaching a daemon as a payload nothing can
266 // look up. 266 // look up.
267 const bad = [_][:0]const u8{ "muxa", "status", "--session", "has space" }; 267 const bad = [_][:0]const u8{ "a", "status", "--session", "has space" };
268 try std.testing.expectError(error.Usage, parseArgs(&bad)); 268 try std.testing.expectError(error.Usage, parseArgs(&bad));
269 269
270 // Empty is the wire's default, not a name anyone can mean by typing it. 270 // Empty is the wire's default, not a name anyone can mean by typing it.
271 const empty = [_][:0]const u8{ "muxa", "status", "--session", "" }; 271 const empty = [_][:0]const u8{ "a", "status", "--session", "" };
272 try std.testing.expectError(error.Usage, parseArgs(&empty)); 272 try std.testing.expectError(error.Usage, parseArgs(&empty));
273 } 273 }
274 274
275 test "parseArgs: --quic and --key, and the pairs that make no sense" { 275 test "parseArgs: --quic and --key, and the pairs that make no sense" {
276 const q = [_][:0]const u8{ "muxa", "status", "--quic", "10.0.0.2:4433" }; 276 const q = [_][:0]const u8{ "a", "status", "--quic", "10.0.0.2:4433" };
277 const oq = try parseArgs(&q); 277 const oq = try parseArgs(&q);
278 try std.testing.expectEqualStrings("10.0.0.2:4433", oq.quic.?); 278 try std.testing.expectEqualStrings("10.0.0.2:4433", oq.quic.?);
279 // Not naming a key is not an error here: MUX_KEY_FILE and the XDG 279 // Not naming a key is not an error here: MUX_KEY_FILE and the XDG
280 // default are still to be tried, and parse may look at neither. 280 // default are still to be tried, and parse may look at neither.
281 try std.testing.expectEqual(@as(?[]const u8, null), oq.key); 281 try std.testing.expectEqual(@as(?[]const u8, null), oq.key);
282 282
283 const k = [_][:0]const u8{ "muxa", "run", "--quic", "box:4433", "--key", "/k", "make test" }; 283 const k = [_][:0]const u8{ "a", "run", "--quic", "box:4433", "--key", "/k", "make test" };
284 const ok = try parseArgs(&k); 284 const ok = try parseArgs(&k);
285 try std.testing.expectEqualStrings("box:4433", ok.quic.?); 285 try std.testing.expectEqualStrings("box:4433", ok.quic.?);
286 try std.testing.expectEqualStrings("/k", ok.key.?); 286 try std.testing.expectEqualStrings("/k", ok.key.?);
287 try std.testing.expectEqualStrings("make test", ok._arg.?); 287 try std.testing.expectEqualStrings("make test", ok._arg.?);
288 288
289 // cliflags owns the arity; what is muxa's is that its refusal becomes 289 // cliflags owns the arity; what is this mode's is that its refusal becomes
290 // the usage exit and not a dial with a flag's name for a host. 290 // the usage exit and not a dial with a flag's name for a host.
291 const dangling_q = [_][:0]const u8{ "muxa", "status", "--quic" }; 291 const dangling_q = [_][:0]const u8{ "a", "status", "--quic" };
292 try std.testing.expectError(error.Usage, parseArgs(&dangling_q)); 292 try std.testing.expectError(error.Usage, parseArgs(&dangling_q));
293 293
294 // Two transports named at once: which one an agent's frames went to 294 // Two transports named at once: which one an agent's frames went to
295 // would be this parser's private business, and it is not entitled to 295 // would be this parser's private business, and it is not entitled to
296 // one — the same refusal `mux` spells as `.conflict`. 296 // one — the same refusal `mux` spells as `.conflict`.
297 const both = [_][:0]const u8{ "muxa", "status", "--sock", "/tmp/s", "--quic", "b:1" }; 297 const both = [_][:0]const u8{ "a", "status", "--sock", "/tmp/s", "--quic", "b:1" };
298 try std.testing.expectError(error.Usage, parseArgs(&both)); 298 try std.testing.expectError(error.Usage, parseArgs(&both));
299 299
300 // A key with nothing to authenticate to, refused exactly where muxd 300 // A key with nothing to authenticate to, refused exactly where the daemon
301 // refuses it. 301 // refuses it.
302 const lonely_key = [_][:0]const u8{ "muxa", "status", "--key", "/k" }; 302 const lonely_key = [_][:0]const u8{ "a", "status", "--key", "/k" };
303 try std.testing.expectError(error.Usage, parseArgs(&lonely_key)); 303 try std.testing.expectError(error.Usage, parseArgs(&lonely_key));
304 304
305 // Neither named is the ordinary local case and stays silent. 305 // Neither named is the ordinary local case and stays silent.
306 const neither = [_][:0]const u8{ "muxa", "status" }; 306 const neither = [_][:0]const u8{ "a", "status" };
307 try std.testing.expectEqual(@as(?[]const u8, null), (try parseArgs(&neither)).quic); 307 try std.testing.expectEqual(@as(?[]const u8, null), (try parseArgs(&neither)).quic);
308 } 308 }
309 309
@@ -993,7 +993,7 @@ test "a refusal a full client table cannot have caused does not blame one" {
993 // `status` never attaches, so `exit_status 1` here has one producer: 993 // `status` never attaches, so `exit_status 1` here has one producer:
994 // findSession missed. An observer with no slot is closed frameless, so 994 // findSession missed. An observer with no slot is closed frameless, so
995 // no fullness of any table can reach this reply — naming a constant 995 // no fullness of any table can reach this reply — naming a constant
996 // that cannot be involved sends an agent to read `muxd stats`. 996 // that cannot be involved sends an agent to read `mux d stats`.
997 try writeAttachRefusedError(fbs.writer(), "nosuch", .query); 997 try writeAttachRefusedError(fbs.writer(), "nosuch", .query);
998 try std.testing.expect(std.mem.indexOf(u8, fbs.getWritten(), "so nosuch must already exist") != null); 998 try std.testing.expect(std.mem.indexOf(u8, fbs.getWritten(), "so nosuch must already exist") != null);
999 try std.testing.expect(std.mem.indexOf(u8, fbs.getWritten(), "max_clients") == null); 999 try std.testing.expect(std.mem.indexOf(u8, fbs.getWritten(), "max_clients") == null);
@@ -1022,8 +1022,8 @@ fn emitTo(fd: std.posix.fd_t, json: []const u8, ok: u8) u8 {
1022 // often taken stderr with it, and the exit code is the half that 1022 // often taken stderr with it, and the exit code is the half that
1023 // survives either way. This line is for the human reading the log. 1023 // survives either way. This line is for the human reading the log.
1024 var buf: [128]u8 = undefined; 1024 var buf: [128]u8 = undefined;
1025 const msg = std.fmt.bufPrint(&buf, "muxa: cannot write the reply: {s}\n", .{@errorName(e)}) catch 1025 const msg = std.fmt.bufPrint(&buf, "mux a: cannot write the reply: {s}\n", .{@errorName(e)}) catch
1026 "muxa: cannot write the reply\n"; 1026 "mux a: cannot write the reply\n";
1027 proto.writeAllFd(std.posix.STDERR_FILENO, msg) catch {}; 1027 proto.writeAllFd(std.posix.STDERR_FILENO, msg) catch {};
1028 return write_failed_code; 1028 return write_failed_code;
1029 }; 1029 };
@@ -1044,7 +1044,7 @@ test "an unwritable stdout is a distinct exit code, never a silent 0" {
1044 try std.posix.dup2(devnull, std.posix.STDERR_FILENO); 1044 try std.posix.dup2(devnull, std.posix.STDERR_FILENO);
1045 1045
1046 // A pipe whose reader is gone: the next write is EPIPE, which is the 1046 // A pipe whose reader is gone: the next write is EPIPE, which is the
1047 // field case — an agent harness that stopped reading muxa's stdout. 1047 // field case — an agent harness that stopped reading this mode's stdout.
1048 const gone = try std.posix.pipe(); 1048 const gone = try std.posix.pipe();
1049 std.posix.close(gone[0]); 1049 std.posix.close(gone[0]);
1050 defer std.posix.close(gone[1]); 1050 defer std.posix.close(gone[1]);
@@ -1160,11 +1160,11 @@ fn failSend(e: anyerror, name: []const u8, ask: Refused, who: []const u8, msg: [
1160 fn writeAttachRefusedError(writer: anytype, name: []const u8, ask: Refused) !void { 1160 fn writeAttachRefusedError(writer: anytype, name: []const u8, ask: Refused) !void {
1161 const why, const need = switch (ask) { 1161 const why, const need = switch (ask) {
1162 .attach => .{ 1162 .attach => .{
1163 "the daemon refused this attach: muxa joins at 0x0 and never creates, so ", 1163 "the daemon refused this attach: `mux a` joins at 0x0 and never creates, so ",
1164 " must already exist and the daemon must have room for one more client (max_clients)", 1164 " must already exist and the daemon must have room for one more client (max_clients)",
1165 }, 1165 },
1166 .query => .{ 1166 .query => .{
1167 "the daemon refused this query: muxa never creates a session, so ", 1167 "the daemon refused this query: `mux a` never creates a session, so ",
1168 " must already exist", 1168 " must already exist",
1169 }, 1169 },
1170 }; 1170 };
@@ -1197,12 +1197,12 @@ test "the session-ended failure keeps the error+detail shape every failure has"
1197 try std.testing.expect(std.mem.indexOf(u8, none.getWritten(), "\"exit_code\":null") != null); 1197 try std.testing.expect(std.mem.indexOf(u8, none.getWritten(), "\"exit_code\":null") != null);
1198 } 1198 }
1199 1199
1200 pub fn main() !u8 { 1200 /// `mux a`. argv is the dispatcher's, minus the program name.
1201 pub fn main(args: []const [:0]const u8) !u8 {
1201 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); 1202 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
1202 defer arena_state.deinit(); 1203 defer arena_state.deinit();
1203 const alloc = arena_state.allocator(); 1204 const alloc = arena_state.allocator();
1204 1205
1205 const args = try std.process.argsAlloc(alloc);
1206 const o = parseArgs(args) catch |e| switch (e) { 1206 const o = parseArgs(args) catch |e| switch (e) {
1207 // Usage is diagnostic, so it goes to stderr: stdout stays strictly 1207 // Usage is diagnostic, so it goes to stderr: stdout stays strictly
1208 // one JSON object per invocation, even on the argument-error path. 1208 // one JSON object per invocation, even on the argument-error path.
@@ -1210,10 +1210,10 @@ pub fn main() !u8 {
1210 proto.writeAllFd(std.posix.STDERR_FILENO, usage) catch {}; 1210 proto.writeAllFd(std.posix.STDERR_FILENO, usage) catch {};
1211 return 2; 1211 return 2;
1212 }, 1212 },
1213 // Help and version go to stdout — the one place muxa otherwise 1213 // Help and version go to stdout — the one place this mode otherwise
1214 // reserves for JSON, and the only two verbs allowed to spend it. 1214 // reserves for JSON, and the only two verbs allowed to spend it.
1215 error.Help => return cliflags.help(usage), 1215 error.Help => return cliflags.help(usage),
1216 error.Version => return cliflags.version("muxa", build_options.version), 1216 error.Version => return cliflags.version("mux", build_options.version),
1217 }; 1217 };
1218 1218
1219 // Started BEFORE the connect, not after: over QUIC the handshake is 1219 // Started BEFORE the connect, not after: over QUIC the handshake is
@@ -1234,7 +1234,7 @@ pub fn main() !u8 {
1234 1234
1235 const sock_path = if (o.sock) |s| s else sockpath.defaultSockPath(alloc) catch |err| switch (err) { 1235 const sock_path = if (o.sock) |s| s else sockpath.defaultSockPath(alloc) catch |err| switch (err) {
1236 // An agent reads replies, not stderr, so this one refuses through 1236 // An agent reads replies, not stderr, so this one refuses through
1237 // the same JSON shape as every other muxa failure. 1237 // the same JSON shape as every other failure here.
1238 error.NoRuntimeDir => return fail( 1238 error.NoRuntimeDir => return fail(
1239 "no default socket path", 1239 "no default socket path",
1240 "XDG_RUNTIME_DIR is unset; name the socket with --sock", 1240 "XDG_RUNTIME_DIR is unset; name the socket with --sock",
@@ -1256,7 +1256,7 @@ pub fn main() !u8 {
1256 } 1256 }
1257 1257
1258 var conn = Conn.open(alloc, sock_path) catch |e| { 1258 var conn = Conn.open(alloc, sock_path) catch |e| {
1259 // The path goes in the detail: a muxa pointed at the wrong socket 1259 // The path goes in the detail: an agent client pointed at the wrong socket
1260 // is this binary's likeliest field failure, and an agent reading 1260 // is this binary's likeliest field failure, and an agent reading
1261 // "FileNotFound" alone cannot tell which path it was that missed. 1261 // "FileNotFound" alone cannot tell which path it was that missed.
1262 var buf: [256]u8 = undefined; 1262 var buf: [256]u8 = undefined;
@@ -1302,7 +1302,7 @@ fn openQuicConn(
1302 // `--key`, then `$MUX_KEY_FILE`, then the XDG default if it exists. 1302 // `--key`, then `$MUX_KEY_FILE`, then the XDG default if it exists.
1303 // The order is not spelled here on purpose: xdg owns it, mux reads the 1303 // The order is not spelled here on purpose: xdg owns it, mux reads the
1304 // same two functions, and a copy that drifted is how two binaries end 1304 // same two functions, and a copy that drifted is how two binaries end
1305 // up authenticating with different keys. (muxd's pickKey stands apart 1305 // up authenticating with different keys. (the daemon's pickKey stands apart
1306 // deliberately: it takes the default only when the file exists.) 1306 // deliberately: it takes the default only when the file exists.)
1307 const res = xdg.resolveKeyPath(alloc, xdg.pickKey(o.key, std.posix.getenv(xdg.key_env))) catch |e| 1307 const res = xdg.resolveKeyPath(alloc, xdg.pickKey(o.key, std.posix.getenv(xdg.key_env))) catch |e|
1308 return .{ .exit = fail("quic: cannot resolve a key path", @errorName(e)) }; 1308 return .{ .exit = fail("quic: cannot resolve a key path", @errorName(e)) };
@@ -1310,9 +1310,9 @@ fn openQuicConn(
1310 .given, .default => |p| p, 1310 .given, .default => |p| p,
1311 // The path is the detail because it is the actionable half: the 1311 // The path is the detail because it is the actionable half: the
1312 // agent (or the human reading its log) needs to know which file 1312 // agent (or the human reading its log) needs to know which file
1313 // `muxd keygen` was supposed to have written. 1313 // `mux d keygen` was supposed to have written.
1314 .missing => |p| return .{ .exit = fail( 1314 .missing => |p| return .{ .exit = fail(
1315 "quic: no key: pass --key, set MUX_KEY_FILE, or run `muxd keygen`", 1315 "quic: no key: pass --key, set MUX_KEY_FILE, or run `mux d keygen`",
1316 p, 1316 p,
1317 ) }, 1317 ) },
1318 }; 1318 };
@@ -1413,7 +1413,7 @@ test "printStatus spells a pending exit code as JSON null" {
1413 .cmd = .{ .phase = .running, .mechanism = .marks, .exit_code = null, .start_row = 3, .end_row = 4, .seq = 9 }, 1413 .cmd = .{ .phase = .running, .mechanism = .marks, .exit_code = null, .start_row = 3, .end_row = 4, .seq = 9 },
1414 }); 1414 });
1415 // The whole object, byte for byte, not a handful of substrings: this is 1415 // The whole object, byte for byte, not a handful of substrings: this is
1416 // muxa's published contract with an agent's JSON parser, and the fields 1416 // this mode's published contract with an agent's JSON parser, and the fields
1417 // it shares with `await` are written by a helper both verbs call — a 1417 // it shares with `await` are written by a helper both verbs call — a
1418 // pin on the parts cannot see a comma or a nesting level move. 1418 // pin on the parts cannot see a comma or a nesting level move.
1419 try std.testing.expectEqualStrings( 1419 try std.testing.expectEqualStrings(
@@ -1426,7 +1426,7 @@ test "printStatus spells a pending exit code as JSON null" {
1426 } 1426 }
1427 1427
1428 fn verbCapture(alloc: std.mem.Allocator, conn: *Conn, vt: bool, session: []const u8, deadline: i64) !u8 { 1428 fn verbCapture(alloc: std.mem.Allocator, conn: *Conn, vt: bool, session: []const u8, deadline: i64) !u8 {
1429 // vt byte ++ session-name tail, the same shape muxd's own `dump` sends 1429 // vt byte ++ session-name tail, the same shape the daemon's own `dump` sends
1430 // — and built by the same encoder, so it cannot drift from it. 1430 // — and built by the same encoder, so it cannot drift from it.
1431 var buf: [proto.debug_dump_max_len]u8 = undefined; 1431 var buf: [proto.debug_dump_max_len]u8 = undefined;
1432 const payload = proto.encodeDebugDumpNamed(&buf, vt, session); 1432 const payload = proto.encodeDebugDumpNamed(&buf, vt, session);
src/cli/webhub_main.zig
Old New
@@ -1,4 +1,4 @@
1 //! muxweb — the hub binary. `muxweb [TARGET ...] 1 //! `mux web` — the hub mode. `mux web [TARGET ...]
2 //! [--port N]`: serves the wall page on 127.0.0.1 and pumps one 2 //! [--port N]`: serves the wall page on 127.0.0.1 and pumps one
3 //! WebSocket per tile, dialing each TARGET the way the mux CLI does. 3 //! WebSocket per tile, dialing each TARGET the way the mux CLI does.
4 //! TARGET spellings are mux's own: bare HOST (ssh→QUIC handoff), 4 //! TARGET spellings are mux's own: bare HOST (ssh→QUIC handoff),
@@ -24,7 +24,7 @@ const sockpath = @import("sockpath");
24 const cliflags = @import("cliflags"); 24 const cliflags = @import("cliflags");
25 25
26 const usage = 26 const usage =
27 \\usage: muxweb [TARGET[#SESSION] ...] [--port N] 27 \\usage: mux web [TARGET[#SESSION] ...] [--port N]
28 \\ each TARGET is a tile: HOST | --sock PATH | quic://HOST[:PORT] 28 \\ each TARGET is a tile: HOST | --sock PATH | quic://HOST[:PORT]
29 \\ `--sock PATH` may be two arguments or one quoted '--sock PATH', the 29 \\ `--sock PATH` may be two arguments or one quoted '--sock PATH', the
30 \\ spelling the wall file holds; `mux wall` takes both too 30 \\ spelling the wall file holds; `mux wall` takes both too
@@ -89,7 +89,7 @@ fn parseArgs(
89 // targets on the line, `usage` alone would not say which. 89 // targets on the line, `usage` alone would not say which.
90 if (p._argv.err) |e| { 90 if (p._argv.err) |e| {
91 if (e.err == error.OutOfMemory) return error.OutOfMemory; 91 if (e.err == error.OutOfMemory) return error.OutOfMemory;
92 std.debug.print("muxweb: tile {s}: {s}\n", .{ e.word, wall.reason(e.err) }); 92 std.debug.print("mux web: tile {s}: {s}\n", .{ e.word, wall.reason(e.err) });
93 return error.Usage; 93 return error.Usage;
94 } 94 }
95 switch (outcome) { 95 switch (outcome) {
@@ -111,22 +111,20 @@ fn parseArgs(
111 return p; 111 return p;
112 } 112 }
113 113
114 pub fn main() !u8 { 114 /// `mux web`. argv is the dispatcher's, minus the program name.
115 pub fn main(args: []const [:0]const u8) !u8 {
115 var gpa: std.heap.DebugAllocator(.{}) = .init; 116 var gpa: std.heap.DebugAllocator(.{}) = .init;
116 defer if (gpa.deinit() == .leak) 117 defer if (gpa.deinit() == .leak)
117 std.debug.print("muxweb: LEAK: allocations outlived deinit\n", .{}); 118 std.debug.print("mux web: LEAK: allocations outlived deinit\n", .{});
118 const alloc = gpa.allocator(); 119 const alloc = gpa.allocator();
119 120
120 const args = try std.process.argsAlloc(alloc);
121 defer std.process.argsFree(alloc, args);
122
123 var parsed = parseArgs(alloc, args, std.posix.getenv(xdg.key_env)) catch |err| switch (err) { 121 var parsed = parseArgs(alloc, args, std.posix.getenv(xdg.key_env)) catch |err| switch (err) {
124 error.Usage => { 122 error.Usage => {
125 std.debug.print("{s}", .{usage}); 123 std.debug.print("{s}", .{usage});
126 return 2; 124 return 2;
127 }, 125 },
128 error.Help => return cliflags.help(usage), 126 error.Help => return cliflags.help(usage),
129 error.Version => return cliflags.version("muxweb", build_options.version), 127 error.Version => return cliflags.version("mux", build_options.version),
130 else => |e| return e, 128 else => |e| return e,
131 }; 129 };
132 defer parsed.deinit(); 130 defer parsed.deinit();
@@ -148,7 +146,7 @@ pub fn main() !u8 {
148 w = wall.load(arena, state_path) catch |err| { 146 w = wall.load(arena, state_path) catch |err| {
149 // The file may have been hand-edited into a line that no longer 147 // The file may have been hand-edited into a line that no longer
150 // parses. Naming it beats a stack trace: the fix is in the file. 148 // parses. Naming it beats a stack trace: the fix is in the file.
151 std.debug.print("muxweb: cannot read wall {s}: {s}\n", .{ state_path, @errorName(err) }); 149 std.debug.print("mux web: cannot read wall {s}: {s}\n", .{ state_path, @errorName(err) });
152 return 2; 150 return 2;
153 }; 151 };
154 } else { 152 } else {
@@ -165,14 +163,14 @@ pub fn main() !u8 {
165 var hub = webhub.Hub.init(arena, w, state_path, parsed.key, parsed.quic_idle_ms.ms) catch |err| switch (err) { 163 var hub = webhub.Hub.init(arena, w, state_path, parsed.key, parsed.quic_idle_ms.ms) catch |err| switch (err) {
166 error.MissingKey => { 164 error.MissingKey => {
167 std.debug.print( 165 std.debug.print(
168 "muxweb: no key for a quic:// tile: pass --key, set MUX_KEY_FILE, or run `muxd keygen`\n", 166 "mux web: no key for a quic:// tile: pass --key, set MUX_KEY_FILE, or run `mux d keygen`\n",
169 .{}, 167 .{},
170 ); 168 );
171 return 2; 169 return 2;
172 }, 170 },
173 error.SockPathTooLong => { 171 error.SockPathTooLong => {
174 std.debug.print( 172 std.debug.print(
175 "muxweb: socket path too long (max {d} bytes)\n", 173 "mux web: socket path too long (max {d} bytes)\n",
176 .{sockpath.max_sun_path}, 174 .{sockpath.max_sun_path},
177 ); 175 );
178 return 2; 176 return 2;
@@ -182,9 +180,9 @@ pub fn main() !u8 {
182 defer hub.deinit(); 180 defer hub.deinit();
183 // Argv overrides the VIEW — this run shows the tiles it named, and only 181 // Argv overrides the VIEW — this run shows the tiles it named, and only
184 // those — but it no longer overwrites the FILE. That file stopped being 182 // those — but it no longer overwrites the FILE. That file stopped being
185 // "the last wall muxweb was told to show" when attaches started writing 183 // "the last wall the hub was told to show" when attaches started writing
186 // to it: it is the user's attach history now, and one 184 // to it: it is the user's attach history now, and one
187 // `muxweb HOST` would have silently erased every tile every `mux` had 185 // `mux web HOST` would have silently erased every tile every `mux` had
188 // recorded. So each argv tile is ADDED (deduped by spelling, wall.zig) 186 // recorded. So each argv tile is ADDED (deduped by spelling, wall.zig)
189 // and nothing is removed. Forgetting stays explicit, which is the whole 187 // and nothing is removed. Forgetting stays explicit, which is the whole
190 // "remove is detach" doctrine: the page's `×`, the wall's `x`, 188 // "remove is detach" doctrine: the page's `×`, the wall's `x`,
@@ -194,14 +192,14 @@ pub fn main() !u8 {
194 // it just read would turn a read failure into a lost wall. 192 // it just read would turn a read failure into a lost wall.
195 if (parsed._argv.tiles.items.len != 0) for (hub.wall_state.targets.items) |spelling| { 193 if (parsed._argv.tiles.items.len != 0) for (hub.wall_state.targets.items) |spelling| {
196 _ = wall.record(alloc, state_path, spelling) catch |err| { 194 _ = wall.record(alloc, state_path, spelling) catch |err| {
197 std.debug.print("muxweb: cannot save wall {s}: {s}\n", .{ state_path, @errorName(err) }); 195 std.debug.print("mux web: cannot save wall {s}: {s}\n", .{ state_path, @errorName(err) });
198 return 2; 196 return 2;
199 }; 197 };
200 }; 198 };
201 199
202 const addr = std.net.Address.parseIp("127.0.0.1", parsed.port) catch unreachable; 200 const addr = std.net.Address.parseIp("127.0.0.1", parsed.port) catch unreachable;
203 var listener = addr.listen(.{ .reuse_address = true }) catch |err| { 201 var listener = addr.listen(.{ .reuse_address = true }) catch |err| {
204 std.debug.print("muxweb: cannot bind 127.0.0.1:{d}: {s}\n", .{ parsed.port, @errorName(err) }); 202 std.debug.print("mux web: cannot bind 127.0.0.1:{d}: {s}\n", .{ parsed.port, @errorName(err) });
205 return 1; 203 return 1;
206 }; 204 };
207 defer listener.deinit(); 205 defer listener.deinit();
@@ -211,9 +209,9 @@ pub fn main() !u8 {
211 // is the hub's, not a position — it is what `/ws/<id>` names, and the 209 // is the hub's, not a position — it is what `/ws/<id>` names, and the
212 // page can add and remove tiles under it while this line stays true. 210 // page can add and remove tiles under it while this line stays true.
213 for (hub.tiles.items) |t| { 211 for (hub.tiles.items) |t| {
214 std.debug.print("muxweb: tile {d}: {s}\n", .{ t.id, t.label }); 212 std.debug.print("mux web: tile {d}: {s}\n", .{ t.id, t.label });
215 } 213 }
216 std.debug.print("muxweb: serving http://127.0.0.1:{d} pid={d}\n", .{ 214 std.debug.print("mux web: serving http://127.0.0.1:{d} pid={d}\n", .{
217 parsed.port, 215 parsed.port,
218 std.os.linux.getpid(), 216 std.os.linux.getpid(),
219 }); 217 });
@@ -239,7 +237,7 @@ pub fn main() !u8 {
239 test "parse: three spellings become three tiles in argv order, port and key bind" { 237 test "parse: three spellings become three tiles in argv order, port and key bind" {
240 const alloc = std.testing.allocator; 238 const alloc = std.testing.allocator;
241 const args = [_][:0]const u8{ 239 const args = [_][:0]const u8{
242 "muxweb", "box1", "--sock", "/tmp/a.sock", "quic://h:4433", "--key", "/k", "--port", "8000", 240 "web", "box1", "--sock", "/tmp/a.sock", "quic://h:4433", "--key", "/k", "--port", "8000",
243 }; 241 };
244 var r = try parseArgs(alloc, &args, null); 242 var r = try parseArgs(alloc, &args, null);
245 defer r.deinit(); 243 defer r.deinit();
@@ -256,8 +254,8 @@ test "parse: three spellings become three tiles in argv order, port and key bind
256 test "parse: a quoted '--sock PATH#SESSION' is the same tile as the two-argument form" { 254 test "parse: a quoted '--sock PATH#SESSION' is the same tile as the two-argument form" {
257 const alloc = std.testing.allocator; 255 const alloc = std.testing.allocator;
258 // The wall file's own spelling, pasted straight onto the command line: 256 // The wall file's own spelling, pasted straight onto the command line:
259 // muxweb used to refuse it while `mux wall` required it. 257 // The hub used to refuse it while `mux wall` required it.
260 var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--sock /tmp/a.sock#b" }, null); 258 var r = try parseArgs(alloc, &[_][:0]const u8{ "web", "--sock /tmp/a.sock#b" }, null);
261 defer r.deinit(); 259 defer r.deinit();
262 try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len); 260 try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len);
263 try std.testing.expectEqualStrings("--sock /tmp/a.sock#b", r._argv.tiles.items[0]); 261 try std.testing.expectEqualStrings("--sock /tmp/a.sock#b", r._argv.tiles.items[0]);
@@ -268,7 +266,7 @@ test "parse: zero targets, bad flags, and flag-beats-env" {
268 // No targets is an empty argv wall, not a refusal: restore-from-file 266 // No targets is an empty argv wall, not a refusal: restore-from-file
269 // semantics live in main, which is the only place that can read a file. 267 // semantics live in main, which is the only place that can read a file.
270 { 268 {
271 var r = try parseArgs(alloc, &[_][:0]const u8{"muxweb"}, null); 269 var r = try parseArgs(alloc, &[_][:0]const u8{"web"}, null);
272 defer r.deinit(); 270 defer r.deinit();
273 try std.testing.expectEqual(@as(usize, 0), r._argv.tiles.items.len); 271 try std.testing.expectEqual(@as(usize, 0), r._argv.tiles.items.len);
274 } 272 }
@@ -278,41 +276,41 @@ test "parse: zero targets, bad flags, and flag-beats-env" {
278 // provoked it, which is what the single errdefer guarantees. 276 // provoked it, which is what the single errdefer guarantees.
279 // 277 //
280 // A flag with no value is a usage mistake, not a transport. 278 // A flag with no value is a usage mistake, not a transport.
281 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--sock" }, null)); 279 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "web", "--sock" }, null));
282 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--port", "x" }, null)); 280 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--port", "x" }, null));
283 // The refusals that had a tile on the list already, so the cleanup is 281 // The refusals that had a tile on the list already, so the cleanup is
284 // load-bearing rather than theoretical. 282 // load-bearing rather than theoretical.
285 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--wat" }, null)); 283 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--wat" }, null));
286 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "quic://" }, null)); 284 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "quic://" }, null));
287 // A `#NAME` that ate the whole target: all three transport spellings 285 // A `#NAME` that ate the whole target: all three transport spellings
288 // refuse an empty spec, so `--sock '#b'` fails at usage altitude rather 286 // refuse an empty spec, so `--sock '#b'` fails at usage altitude rather
289 // than at a connect to the empty path, far from the typo. 287 // than at a connect to the empty path, far from the typo.
290 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--sock", "#b" }, null)); 288 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--sock", "#b" }, null));
291 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--quic-idle-ms", "0" }, null)); 289 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--quic-idle-ms", "0" }, null));
292 // Port 0 means "kernel, you pick" — but the hub announces the port it 290 // Port 0 means "kernel, you pick" — but the hub announces the port it
293 // was asked for, so the door it prints is not the door it opened. 291 // was asked for, so the door it prints is not the door it opened.
294 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--port", "0" }, null)); 292 try std.testing.expectError(error.Usage, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--port", "0" }, null));
295 // ...and an ordinary port still binds, so the refusal is the zero and 293 // ...and an ordinary port still binds, so the refusal is the zero and
296 // not the flag. 294 // not the flag.
297 { 295 {
298 var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--port", "1" }, null); 296 var r = try parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--port", "1" }, null);
299 defer r.deinit(); 297 defer r.deinit();
300 try std.testing.expectEqual(@as(u16, 1), r.port); 298 try std.testing.expectEqual(@as(u16, 1), r.port);
301 } 299 }
302 // Env fills in when --key is absent; --key wins when both are set. 300 // Env fills in when --key is absent; --key wins when both are set.
303 { 301 {
304 var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h" }, "/env-key"); 302 var r = try parseArgs(alloc, &[_][:0]const u8{ "web", "h" }, "/env-key");
305 defer r.deinit(); 303 defer r.deinit();
306 try std.testing.expectEqualStrings("/env-key", r.key.?); 304 try std.testing.expectEqualStrings("/env-key", r.key.?);
307 } 305 }
308 { 306 {
309 var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--key", "/flag-key" }, "/env-key"); 307 var r = try parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--key", "/flag-key" }, "/env-key");
310 defer r.deinit(); 308 defer r.deinit();
311 try std.testing.expectEqualStrings("/flag-key", r.key.?); 309 try std.testing.expectEqualStrings("/flag-key", r.key.?);
312 } 310 }
313 // Empty either way means unset. 311 // Empty either way means unset.
314 { 312 {
315 var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h" }, ""); 313 var r = try parseArgs(alloc, &[_][:0]const u8{ "web", "h" }, "");
316 defer r.deinit(); 314 defer r.deinit();
317 try std.testing.expectEqual(@as(?[]const u8, null), r.key); 315 try std.testing.expectEqual(@as(?[]const u8, null), r.key);
318 } 316 }
@@ -321,7 +319,7 @@ test "parse: zero targets, bad flags, and flag-beats-env" {
321 test "tiles: the spelling reaches the wall verbatim, `#NAME` and all" { 319 test "tiles: the spelling reaches the wall verbatim, `#NAME` and all" {
322 const alloc = std.testing.allocator; 320 const alloc = std.testing.allocator;
323 const args = [_][:0]const u8{ 321 const args = [_][:0]const u8{
324 "muxweb", "host#b", "quic://h:1#b", "--sock", "/tmp/x#b", "plainhost", "a#b#c", 322 "web", "host#b", "quic://h:1#b", "--sock", "/tmp/x#b", "plainhost", "a#b#c",
325 }; 323 };
326 var r = try parseArgs(alloc, &args, null); 324 var r = try parseArgs(alloc, &args, null);
327 defer r.deinit(); 325 defer r.deinit();
@@ -344,38 +342,38 @@ test "tiles: a bad session name after # is still a usage error at parse" {
344 const alloc = std.testing.allocator; 342 const alloc = std.testing.allocator;
345 // The refusal stays HERE, at argv altitude, rather than surfacing later 343 // The refusal stays HERE, at argv altitude, rather than surfacing later
346 // as one tile that will not attach. These print a line naming the tile 344 // as one tile that will not attach. These print a line naming the tile
347 // before returning, so the muxweb: lines in this test's output are the 345 // before returning, so the `mux web:` lines in this test's output are the
348 // point, not noise: with several tiles on the line, `usage` alone would 346 // point, not noise: with several tiles on the line, `usage` alone would
349 // not say which one. 347 // not say which one.
350 try std.testing.expectError( 348 try std.testing.expectError(
351 error.Usage, 349 error.Usage,
352 parseArgs(alloc, &[_][:0]const u8{ "muxweb", "host#has space" }, null), 350 parseArgs(alloc, &[_][:0]const u8{ "web", "host#has space" }, null),
353 ); 351 );
354 // A bare trailing '#' asks for the empty name. It is the default ON THE 352 // A bare trailing '#' asks for the empty name. It is the default ON THE
355 // WIRE but not a name a user may spell, so typing it is a mistake. 353 // WIRE but not a name a user may spell, so typing it is a mistake.
356 try std.testing.expectError( 354 try std.testing.expectError(
357 error.Usage, 355 error.Usage,
358 parseArgs(alloc, &[_][:0]const u8{ "muxweb", "host#" }, null), 356 parseArgs(alloc, &[_][:0]const u8{ "web", "host#" }, null),
359 ); 357 );
360 // Same rule through --sock's value and through quic://. 358 // Same rule through --sock's value and through quic://.
361 try std.testing.expectError( 359 try std.testing.expectError(
362 error.Usage, 360 error.Usage,
363 parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--sock", "/tmp/x#bad name" }, null), 361 parseArgs(alloc, &[_][:0]const u8{ "web", "--sock", "/tmp/x#bad name" }, null),
364 ); 362 );
365 try std.testing.expectError( 363 try std.testing.expectError(
366 error.Usage, 364 error.Usage,
367 parseArgs(alloc, &[_][:0]const u8{ "muxweb", "quic://h:1#a/b" }, null), 365 parseArgs(alloc, &[_][:0]const u8{ "web", "quic://h:1#a/b" }, null),
368 ); 366 );
369 } 367 }
370 368
371 test "help is an answer, not a refusal, and -- fences the tiles from the flags" { 369 test "help is an answer, not a refusal, and -- fences the tiles from the flags" {
372 const alloc = std.testing.allocator; 370 const alloc = std.testing.allocator;
373 try std.testing.expectError(error.Help, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--help" }, null)); 371 try std.testing.expectError(error.Help, parseArgs(alloc, &[_][:0]const u8{ "web", "--help" }, null));
374 try std.testing.expectError(error.Help, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "-h" }, null)); 372 try std.testing.expectError(error.Help, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "-h" }, null));
375 373
376 // Past `--` a word is a tile whatever it is spelled like: the escape a 374 // Past `--` a word is a tile whatever it is spelled like: the escape a
377 // host whose name reads as a flag would otherwise have none of. 375 // host whose name reads as a flag would otherwise have none of.
378 var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--", "host" }, null); 376 var r = try parseArgs(alloc, &[_][:0]const u8{ "web", "--", "host" }, null);
379 defer r.deinit(); 377 defer r.deinit();
380 try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len); 378 try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len);
381 try std.testing.expectEqualStrings("host", r._argv.tiles.items[0]); 379 try std.testing.expectEqualStrings("host", r._argv.tiles.items[0]);
@@ -383,7 +381,7 @@ test "help is an answer, not a refusal, and -- fences the tiles from the flags"
383 381
384 test "version short-circuits everything else on the line" { 382 test "version short-circuits everything else on the line" {
385 const alloc = std.testing.allocator; 383 const alloc = std.testing.allocator;
386 try std.testing.expectError(error.Version, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--version", "--bogus" }, null)); 384 try std.testing.expectError(error.Version, parseArgs(alloc, &[_][:0]const u8{ "web", "h", "--version", "--bogus" }, null));
387 } 385 }
388 386
389 // Forces semantic analysis of every pub decl under `zig build test`, so an 387 // Forces semantic analysis of every pub decl under `zig build test`, so an