6c5f6a37
feat: one binary — `mux d` / `mux a` / `mux web`, and the client with no letter
a73x 2026-08-28 22:34
Commit message
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 |