a73x

2c7f1c38

refactor: muxa parses its flags with cliflags

a73x   2026-08-27 06:51

Commit message
refactor: muxa parses its flags with cliflags

build.zig
Old New
@@ -256,7 +256,7 @@ const mod_table = [_]ModSpec{
256 // renders: `quic_client` for the remote arm and `xdg` for the one 256 // renders: `quic_client` for the remote arm and `xdg` for the one
257 // key-resolution rule all three binaries obey. Deliberately still no 257 // key-resolution rule all three binaries obey. Deliberately still no
258 // engine and no replica — muxa has nothing to draw. 258 // engine and no replica — muxa has nothing to draw.
259 .{ .name = "muxa", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic_client", "quic", "xdg" }, .quic_tests = true }, 259 .{ .name = "muxa", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic_client", "quic", "xdg", "cliflags" }, .quic_tests = true },
260 .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } }, 260 .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } },
261 // ---- layer 3 ---- 261 // ---- layer 3 ----
262 // Dialling, and what a chord means. The client is the only thing that 262 // Dialling, and what a chord means. The client is the only thing that
@@ -734,6 +734,7 @@ pub fn build(b: *std.Build) void {
734 mux_mod.addImport("build_options", version_opts.createModule()); 734 mux_mod.addImport("build_options", version_opts.createModule());
735 exe_mod.addImport("build_options", version_opts.createModule()); 735 exe_mod.addImport("build_options", version_opts.createModule());
736 webhub_main_mod.addImport("build_options", version_opts.createModule()); 736 webhub_main_mod.addImport("build_options", version_opts.createModule());
737 muxa_mod.addImport("build_options", version_opts.createModule());
737 // A row with test_imports gets a SEPARATE module for its test twin, and 738 // A row with test_imports gets a SEPARATE module for its test twin, and
738 // build_options is outside the table's jurisdiction — so every such twin 739 // build_options is outside the table's jurisdiction — so every such twin
739 // needs it by hand, or the argument parsers lose the version they print. 740 // needs it by hand, or the argument parsers lose the version they print.
src/cli/muxa.zig
Old New
@@ -20,10 +20,13 @@ const sockpath = @import("sockpath");
20 const quic_client = @import("quic_client"); 20 const quic_client = @import("quic_client");
21 const quic = @import("quic"); 21 const quic = @import("quic");
22 const xdg = @import("xdg"); 22 const xdg = @import("xdg");
23 const cliflags = @import("cliflags");
24 const build_options = @import("build_options");
23 25
24 const usage = 26 const usage =
25 \\usage: muxa <verb> [--sock PATH | --quic HOST[:PORT] [--key PATH]] 27 \\usage: muxa <verb> [--sock PATH | --quic HOST[:PORT] [--key PATH]]
26 \\ [--settle MS] [--timeout MS] [--vt] [--session NAME] [args] 28 \\ [--settle MS] [--timeout MS] [--vt] [--session NAME] [args]
29 \\ muxa --help | --version
27 \\NAME must already exist: muxa attaches at 0x0 and never creates a session 30 \\NAME must already exist: muxa attaches at 0x0 and never creates a session
28 \\(`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).
29 \\verbs: 32 \\verbs:
@@ -35,8 +38,9 @@ const usage =
35 \\ 38 \\
36 ; 39 ;
37 40
41 const Verb = enum { status, capture, send, run, await };
42
38 const Opts = struct { 43 const Opts = struct {
39 verb: enum { status, capture, send, run, await },
40 sock: ?[]const u8 = null, 44 sock: ?[]const u8 = null,
41 /// `HOST[:PORT]` of a remote daemon's QUIC listener. The verbs are 45 /// `HOST[:PORT]` of a remote daemon's QUIC listener. The verbs are
42 /// identical over it — same frames, same JSON — which is the whole 46 /// identical over it — same frames, same JSON — which is the whole
@@ -47,81 +51,79 @@ const Opts = struct {
47 /// still to be tried, and neither is parse's to look at (xdg.pickKey 51 /// still to be tried, and neither is parse's to look at (xdg.pickKey
48 /// and xdg.resolveKeyPath own that order here as they do for mux). 52 /// and xdg.resolveKeyPath own that order here as they do for mux).
49 key: ?[]const u8 = null, 53 key: ?[]const u8 = null,
50 settle_ms: u32 = 0, 54 settle: u32 = 0,
51 // 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
52 // bound at all" (documented on AwaitReq), so a muxa that defaulted to 0 56 // bound at all" (documented on AwaitReq), so a muxa that defaulted to 0
53 // would turn every await into an unbounded wait. 57 // would turn every await into an unbounded wait.
54 timeout_ms: u32 = 30_000, 58 timeout: u32 = 30_000,
55 vt: bool = false, 59 vt: bool = false,
60 /// Optional so that only a name that was TYPED is validated: `""` is
61 /// the wire's own default spelling and would fail a check written for
62 /// a name. `_session` below is what the frames actually carry.
63 session: ?[]const u8 = null,
56 /// Which session every verb this invocation makes asks about — the 64 /// Which session every verb this invocation makes asks about — the
57 /// attach it opens with AND every ask that follows carry the same 65 /// attach it opens with AND every ask that follows carry the same
58 /// name, which is what keeps the daemon's attached-tail equality rule 66 /// name, which is what keeps the daemon's attached-tail equality rule
59 /// (server.zig) from ever seeing a mismatch out of this binary. Empty 67 /// (server.zig) from ever seeing a mismatch out of this binary. Empty
60 /// is the wire's own default spelling, so a bare `muxa status` builds 68 /// is the wire's own default spelling, so a bare `muxa status` builds
61 /// byte-identical frames to before this flag existed. 69 /// byte-identical frames to before this flag existed.
62 session: []const u8 = "", 70 _session: []const u8 = "",
63 arg: ?[]const u8 = null, 71 /// Null until `parseArgs` reads argv[1]; a parse that returned an Opts
72 /// has one.
73 _verb: ?Verb = null,
74 _arg: ?[]const u8 = null,
75
76 /// The verb's argument. A second is a mistake: no verb here takes two.
77 pub fn positional(self: *Opts, word: []const u8) bool {
78 if (self._arg != null) return false;
79 self._arg = word;
80 return true;
81 }
64 }; 82 };
65 83
66 fn parseArgs(args: []const [:0]const u8) ?Opts { 84 comptime {
67 if (args.len < 2) return null; 85 cliflags.assertDocumented(Opts, usage, &.{});
68 const verb = std.meta.stringToEnum(@FieldType(Opts, "verb"), args[1]) orelse return null; 86 }
69 var o: Opts = .{ .verb = verb }; 87
70 var i: usize = 2; 88 const ParseError = error{ Usage, Help, Version };
71 // Everything after a bare `--` is the positional argument, whatever it 89
72 // looks like. Agents send byte-strings for their own reasons, and 90 fn parseArgs(args: []const [:0]const u8) ParseError!Opts {
73 // `muxa send -- '-n foo\n'` must reach the pty rather than be read as 91 if (args.len < 2) return error.Usage;
74 // a flag this binary does not have. 92 const verb = std.meta.stringToEnum(Verb, args[1]) orelse {
75 var end_of_flags = false; 93 // The verb slot is also the only slot `muxa --help` has. Asked of
76 while (i < args.len) : (i += 1) { 94 // the same table the verbs' flags go through, so the two spellings
77 const a = args[i]; 95 // of asking cannot drift apart; anything else there is a bad verb.
78 if (end_of_flags) { 96 var probe: Opts = .{};
79 if (o.arg != null) return null; 97 return switch (cliflags.parse(Opts, &probe, args[1..2])) {
80 o.arg = a; 98 .help => error.Help,
81 } else if (std.mem.eql(u8, a, "--")) { 99 .version => error.Version,
82 end_of_flags = true; 100 else => error.Usage,
83 } else if (std.mem.eql(u8, a, "--sock")) { 101 };
84 i += 1; 102 };
85 if (i >= args.len) return null; 103 var o: Opts = .{ ._verb = verb };
86 o.sock = args[i]; 104 switch (cliflags.parse(Opts, &o, args[2..])) {
87 } else if (std.mem.eql(u8, a, "--quic")) { 105 .ok => {},
88 i += 1; 106 .help => return error.Help,
89 if (i >= args.len) return null; 107 .version => return error.Version,
90 o.quic = args[i]; 108 .unknown_arg, .missing_value, .bad_number => return error.Usage,
91 } else if (std.mem.eql(u8, a, "--key")) { 109 }
92 i += 1; 110
93 if (i >= args.len) return null; 111 // Refused here rather than carried to the wire as a payload nothing
94 o.key = args[i]; 112 // could ever look up: usage exit (2), not a frame.
95 } else if (std.mem.eql(u8, a, "--settle")) { 113 if (o.session) |name| {
96 i += 1; 114 if (!proto.validSessionName(name)) return error.Usage;
97 if (i >= args.len) return null;
98 o.settle_ms = std.fmt.parseInt(u32, args[i], 10) catch return null;
99 } else if (std.mem.eql(u8, a, "--timeout")) {
100 i += 1;
101 if (i >= args.len) return null;
102 o.timeout_ms = std.fmt.parseInt(u32, args[i], 10) catch return null;
103 } else if (std.mem.eql(u8, a, "--vt")) {
104 o.vt = true;
105 } else if (std.mem.eql(u8, a, "--session")) {
106 i += 1;
107 if (i >= args.len) return null;
108 // Refused here rather than carried to the wire as a payload
109 // nothing could ever look up: usage exit (2), not a frame.
110 if (!proto.validSessionName(args[i])) return null;
111 o.session = args[i];
112 } else if (o.arg == null and a.len > 0 and a[0] != '-') {
113 o.arg = a;
114 } else return null;
115 } 115 }
116 o._session = o.session orelse "";
117
116 // Name ONE transport. A `--sock` silently ignored beside a `--quic` 118 // Name ONE transport. A `--sock` silently ignored beside a `--quic`
117 // would send an agent's frames somewhere other than the socket it 119 // would send an agent's frames somewhere other than the socket it
118 // named, and the two answers differ — this is the mistake `mux` 120 // named, and the two answers differ — this is the mistake `mux`
119 // refuses as `.conflict` for the same reason. 121 // refuses as `.conflict` for the same reason.
120 if (o.quic != null and o.sock != null) return null; 122 if (o.quic != null and o.sock != null) return error.Usage;
121 // A key with nothing to authenticate to, refused exactly where muxd 123 // A key with nothing to authenticate to, refused exactly where muxd
122 // refuses it: there is no reading of `--key` without `--quic` that 124 // refuses it: there is no reading of `--key` without `--quic` that
123 // makes it sensible, and the unix socket has no key at all. 125 // makes it sensible, and the unix socket has no key at all.
124 if (o.key != null and o.quic == null) return null; 126 if (o.key != null and o.quic == null) return error.Usage;
125 return o; 127 return o;
126 } 128 }
127 129
@@ -193,13 +195,13 @@ test "decodeEscapes covers the sequences send needs" {
193 195
194 test "parseArgs verbs and flags" { 196 test "parseArgs verbs and flags" {
195 const a1 = [_][:0]const u8{ "muxa", "status" }; 197 const a1 = [_][:0]const u8{ "muxa", "status" };
196 try std.testing.expectEqual(@FieldType(Opts, "verb").status, parseArgs(&a1).?.verb); 198 try std.testing.expectEqual(Verb.status, (try parseArgs(&a1))._verb.?);
197 const a2 = [_][:0]const u8{ "muxa", "run", "--timeout", "5000", "make test" }; 199 const a2 = [_][:0]const u8{ "muxa", "run", "--timeout", "5000", "make test" };
198 const o2 = parseArgs(&a2).?; 200 const o2 = try parseArgs(&a2);
199 try std.testing.expectEqual(@as(u32, 5000), o2.timeout_ms); 201 try std.testing.expectEqual(@as(u32, 5000), o2.timeout);
200 try std.testing.expectEqualStrings("make test", o2.arg.?); 202 try std.testing.expectEqualStrings("make test", o2._arg.?);
201 const a3 = [_][:0]const u8{ "muxa", "bogus" }; 203 const a3 = [_][:0]const u8{ "muxa", "bogus" };
202 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&a3)); 204 try std.testing.expectError(error.Usage, parseArgs(&a3));
203 } 205 }
204 206
205 test "parseArgs: -- hands the rest to the verb, flags and all" { 207 test "parseArgs: -- hands the rest to the verb, flags and all" {
@@ -207,76 +209,101 @@ test "parseArgs: -- hands the rest to the verb, flags and all" {
207 // invocation is refused — the exact shape an agent sends when a key 209 // invocation is refused — the exact shape an agent sends when a key
208 // sequence starts with a dash. 210 // sequence starts with a dash.
209 const dashed = [_][:0]const u8{ "muxa", "send", "-n foo" }; 211 const dashed = [_][:0]const u8{ "muxa", "send", "-n foo" };
210 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&dashed)); 212 try std.testing.expectError(error.Usage, parseArgs(&dashed));
211 213
212 const a = [_][:0]const u8{ "muxa", "send", "--settle", "50", "--", "-n foo" }; 214 const a = [_][:0]const u8{ "muxa", "send", "--settle", "50", "--", "-n foo" };
213 const o = parseArgs(&a).?; 215 const o = try parseArgs(&a);
214 try std.testing.expectEqual(@as(u32, 50), o.settle_ms); 216 try std.testing.expectEqual(@as(u32, 50), o.settle);
215 try std.testing.expectEqualStrings("-n foo", o.arg.?); 217 try std.testing.expectEqualStrings("-n foo", o._arg.?);
216 218
217 // Past the marker, a flag spelling is just text — and a second 219 // Past the marker, a flag spelling is just text — and a second
218 // positional is still one too many. 220 // positional is still one too many.
219 const flagish = [_][:0]const u8{ "muxa", "run", "--", "--timeout" }; 221 const flagish = [_][:0]const u8{ "muxa", "run", "--", "--timeout" };
220 try std.testing.expectEqualStrings("--timeout", parseArgs(&flagish).?.arg.?); 222 try std.testing.expectEqualStrings("--timeout", (try parseArgs(&flagish))._arg.?);
221 const two = [_][:0]const u8{ "muxa", "run", "--", "a", "b" }; 223 const two = [_][:0]const u8{ "muxa", "run", "--", "a", "b" };
222 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&two)); 224 try std.testing.expectError(error.Usage, parseArgs(&two));
225
226 // The marker outranks the help scan too: an agent typing `--help` AT a
227 // session must reach the pty, not this binary's usage page.
228 const help_payload = [_][:0]const u8{ "muxa", "send", "--", "--help" };
229 try std.testing.expectEqualStrings("--help", (try parseArgs(&help_payload))._arg.?);
230 }
231
232 test "muxa: --help and --version are answered wherever they can be typed" {
233 // The verb slot is the only slot a bare `muxa --help` has, and the flag
234 // position is where anyone types it after finding a verb first.
235 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "--help" }));
236 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "-h" }));
237 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "status", "--help" }));
238 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "run", "--timeout", "--help" }));
239 try std.testing.expectError(error.Version, parseArgs(&[_][:0]const u8{ "muxa", "--version" }));
240 try std.testing.expectError(error.Version, parseArgs(&[_][:0]const u8{ "muxa", "status", "--version" }));
241
242 // Neither is a verb, so a word that is neither and is no verb either is
243 // still the usage error it always was.
244 try std.testing.expectError(error.Usage, parseArgs(&[_][:0]const u8{ "muxa", "--wat" }));
245 try std.testing.expectError(error.Usage, parseArgs(&[_][:0]const u8{"muxa"}));
223 } 246 }
224 247
225 test "muxa: --session rides every verb; a bad name is usage, not wire bytes" { 248 test "muxa: --session rides every verb; a bad name is usage, not wire bytes" {
226 const a = [_][:0]const u8{ "muxa", "status", "--session", "b" }; 249 const a = [_][:0]const u8{ "muxa", "status", "--session", "b" };
227 const o = parseArgs(&a).?; 250 const o = try parseArgs(&a);
228 try std.testing.expectEqualStrings("b", o.session); 251 try std.testing.expectEqualStrings("b", o._session);
229 252
230 // No --session named: the wire's own default spelling, empty. 253 // No --session named: the wire's own default spelling, empty.
231 const bare = [_][:0]const u8{ "muxa", "status" }; 254 const bare = [_][:0]const u8{ "muxa", "status" };
232 try std.testing.expectEqualStrings("", parseArgs(&bare).?.session); 255 try std.testing.expectEqualStrings("", (try parseArgs(&bare))._session);
233 256
234 // A name no tool could ever address is refused at parse (the usage 257 // A name no tool could ever address is refused at parse (the usage
235 // exit, 2) rather than reaching a daemon as a payload nothing can 258 // exit, 2) rather than reaching a daemon as a payload nothing can
236 // look up. 259 // look up.
237 const bad = [_][:0]const u8{ "muxa", "status", "--session", "has space" }; 260 const bad = [_][:0]const u8{ "muxa", "status", "--session", "has space" };
238 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&bad)); 261 try std.testing.expectError(error.Usage, parseArgs(&bad));
262
263 // Empty is the wire's default, not a name anyone can mean by typing it.
264 const empty = [_][:0]const u8{ "muxa", "status", "--session", "" };
265 try std.testing.expectError(error.Usage, parseArgs(&empty));
239 266
240 // Dangling like every other value-taking flag. 267 // Dangling like every other value-taking flag.
241 const dangling = [_][:0]const u8{ "muxa", "status", "--session" }; 268 const dangling = [_][:0]const u8{ "muxa", "status", "--session" };
242 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&dangling)); 269 try std.testing.expectError(error.Usage, parseArgs(&dangling));
243 } 270 }
244 271
245 test "parseArgs: --quic and --key, and the pairs that make no sense" { 272 test "parseArgs: --quic and --key, and the pairs that make no sense" {
246 const q = [_][:0]const u8{ "muxa", "status", "--quic", "10.0.0.2:4433" }; 273 const q = [_][:0]const u8{ "muxa", "status", "--quic", "10.0.0.2:4433" };
247 const oq = parseArgs(&q).?; 274 const oq = try parseArgs(&q);
248 try std.testing.expectEqualStrings("10.0.0.2:4433", oq.quic.?); 275 try std.testing.expectEqualStrings("10.0.0.2:4433", oq.quic.?);
249 // Not naming a key is not an error here: MUX_KEY_FILE and the XDG 276 // Not naming a key is not an error here: MUX_KEY_FILE and the XDG
250 // default are still to be tried, and parse may look at neither. 277 // default are still to be tried, and parse may look at neither.
251 try std.testing.expectEqual(@as(?[]const u8, null), oq.key); 278 try std.testing.expectEqual(@as(?[]const u8, null), oq.key);
252 279
253 const k = [_][:0]const u8{ "muxa", "run", "--quic", "box:4433", "--key", "/k", "make test" }; 280 const k = [_][:0]const u8{ "muxa", "run", "--quic", "box:4433", "--key", "/k", "make test" };
254 const ok = parseArgs(&k).?; 281 const ok = try parseArgs(&k);
255 try std.testing.expectEqualStrings("box:4433", ok.quic.?); 282 try std.testing.expectEqualStrings("box:4433", ok.quic.?);
256 try std.testing.expectEqualStrings("/k", ok.key.?); 283 try std.testing.expectEqualStrings("/k", ok.key.?);
257 try std.testing.expectEqualStrings("make test", ok.arg.?); 284 try std.testing.expectEqualStrings("make test", ok._arg.?);
258 285
259 // A flag at the end of argv with no value is refused, like every other 286 // A flag at the end of argv with no value is refused, like every other
260 // value-taking flag this parser has. 287 // value-taking flag this parser has.
261 const dangling_q = [_][:0]const u8{ "muxa", "status", "--quic" }; 288 const dangling_q = [_][:0]const u8{ "muxa", "status", "--quic" };
262 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&dangling_q)); 289 try std.testing.expectError(error.Usage, parseArgs(&dangling_q));
263 const dangling_k = [_][:0]const u8{ "muxa", "status", "--quic", "b:1", "--key" }; 290 const dangling_k = [_][:0]const u8{ "muxa", "status", "--quic", "b:1", "--key" };
264 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&dangling_k)); 291 try std.testing.expectError(error.Usage, parseArgs(&dangling_k));
265 292
266 // Two transports named at once: which one an agent's frames went to 293 // Two transports named at once: which one an agent's frames went to
267 // would be this parser's private business, and it is not entitled to 294 // would be this parser's private business, and it is not entitled to
268 // one — the same refusal `mux` spells as `.conflict`. 295 // one — the same refusal `mux` spells as `.conflict`.
269 const both = [_][:0]const u8{ "muxa", "status", "--sock", "/tmp/s", "--quic", "b:1" }; 296 const both = [_][:0]const u8{ "muxa", "status", "--sock", "/tmp/s", "--quic", "b:1" };
270 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&both)); 297 try std.testing.expectError(error.Usage, parseArgs(&both));
271 298
272 // A key with nothing to authenticate to, refused exactly where muxd 299 // A key with nothing to authenticate to, refused exactly where muxd
273 // refuses it. 300 // refuses it.
274 const lonely_key = [_][:0]const u8{ "muxa", "status", "--key", "/k" }; 301 const lonely_key = [_][:0]const u8{ "muxa", "status", "--key", "/k" };
275 try std.testing.expectEqual(@as(?Opts, null), parseArgs(&lonely_key)); 302 try std.testing.expectError(error.Usage, parseArgs(&lonely_key));
276 303
277 // Neither named is the ordinary local case and stays silent. 304 // Neither named is the ordinary local case and stays silent.
278 const neither = [_][:0]const u8{ "muxa", "status" }; 305 const neither = [_][:0]const u8{ "muxa", "status" };
279 try std.testing.expectEqual(@as(?[]const u8, null), parseArgs(&neither).?.quic); 306 try std.testing.expectEqual(@as(?[]const u8, null), (try parseArgs(&neither)).quic);
280 } 307 }
281 308
282 /// A live QUIC connection plus everything a REDIAL of it needs. The dial 309 /// A live QUIC connection plus everything a REDIAL of it needs. The dial
@@ -1175,11 +1202,26 @@ pub fn main() !u8 {
1175 const alloc = arena_state.allocator(); 1202 const alloc = arena_state.allocator();
1176 1203
1177 const args = try std.process.argsAlloc(alloc); 1204 const args = try std.process.argsAlloc(alloc);
1178 const o = parseArgs(args) orelse { 1205 const o = parseArgs(args) catch |e| switch (e) {
1179 // Usage is diagnostic, so it goes to stderr: stdout stays strictly 1206 // Usage is diagnostic, so it goes to stderr: stdout stays strictly
1180 // one JSON object per invocation, even on the argument-error path. 1207 // one JSON object per invocation, even on the argument-error path.
1181 proto.writeAllFd(std.posix.STDERR_FILENO, usage) catch {}; 1208 error.Usage => {
1182 return 2; 1209 proto.writeAllFd(std.posix.STDERR_FILENO, usage) catch {};
1210 return 2;
1211 },
1212 // A usage someone ASKED for is the answer, not a diagnostic, so it
1213 // goes to stdout — the one place muxa otherwise reserves for JSON,
1214 // and the only two verbs that are allowed to spend it.
1215 error.Help => {
1216 proto.writeAllFd(std.posix.STDOUT_FILENO, usage) catch {};
1217 return 0;
1218 },
1219 error.Version => {
1220 var vbuf: [64]u8 = undefined;
1221 const s = std.fmt.bufPrint(&vbuf, "muxa {s}\n", .{build_options.version}) catch unreachable;
1222 proto.writeAllFd(std.posix.STDOUT_FILENO, s) catch {};
1223 return 0;
1224 },
1183 }; 1225 };
1184 1226
1185 // Started BEFORE the connect, not after: over QUIC the handshake is 1227 // Started BEFORE the connect, not after: over QUIC the handshake is
@@ -1187,7 +1229,7 @@ pub fn main() !u8 {
1187 // began counting only once the connection was up would promise 1229 // began counting only once the connection was up would promise
1188 // something different on the two transports. Over a unix socket the 1230 // something different on the two transports. Over a unix socket the
1189 // connect is a syscall, so this moves the instant by microseconds. 1231 // connect is a syscall, so this moves the instant by microseconds.
1190 const deadline = deadlineFor(o.timeout_ms); 1232 const deadline = deadlineFor(o.timeout);
1191 1233
1192 if (o.quic) |host_port| { 1234 if (o.quic) |host_port| {
1193 var conn = switch (openQuicConn(alloc, o, host_port, deadline)) { 1235 var conn = switch (openQuicConn(alloc, o, host_port, deadline)) {
@@ -1238,14 +1280,14 @@ pub fn main() !u8 {
1238 /// The verbs, once. Both transports arrive here with a Conn and nothing 1280 /// The verbs, once. Both transports arrive here with a Conn and nothing
1239 /// else that distinguishes them, which is the property `--quic` is selling. 1281 /// else that distinguishes them, which is the property `--quic` is selling.
1240 fn dispatch(alloc: std.mem.Allocator, conn: *Conn, o: Opts, deadline: i64) !u8 { 1282 fn dispatch(alloc: std.mem.Allocator, conn: *Conn, o: Opts, deadline: i64) !u8 {
1241 return switch (o.verb) { 1283 return switch (o._verb.?) {
1242 .status => verbStatus(alloc, conn, o.session, deadline), 1284 .status => verbStatus(alloc, conn, o._session, deadline),
1243 .capture => verbCapture(alloc, conn, o.vt, o.session, deadline), 1285 .capture => verbCapture(alloc, conn, o.vt, o._session, deadline),
1244 .send => verbSend(alloc, conn, o.arg, o.session, deadline), 1286 .send => verbSend(alloc, conn, o._arg, o._session, deadline),
1245 // The one thing `run` needs that `await` does not, checked here so 1287 // The one thing `run` needs that `await` does not, checked here so
1246 // the shared pipeline below can read `cmdline == null` as "this is 1288 // the shared pipeline below can read `cmdline == null` as "this is
1247 // an await" rather than as "a run that was spelled wrong". 1289 // an await" rather than as "a run that was spelled wrong".
1248 .run => if (o.arg) |cmdline| 1290 .run => if (o._arg) |cmdline|
1249 awaitVerb(alloc, conn, o, deadline, cmdline) 1291 awaitVerb(alloc, conn, o, deadline, cmdline)
1250 else 1292 else
1251 fail("run: needs CMDLINE", ""), 1293 fail("run: needs CMDLINE", ""),
@@ -1533,9 +1575,9 @@ fn doAwait(
1533 var buf: [proto.await_req_max_len]u8 = undefined; 1575 var buf: [proto.await_req_max_len]u8 = undefined;
1534 const payload = proto.encodeAwaitReqNamed(&buf, .{ 1576 const payload = proto.encodeAwaitReqNamed(&buf, .{
1535 .since_seq = since_seq, 1577 .since_seq = since_seq,
1536 .settle_ms = o.settle_ms, 1578 .settle_ms = o.settle,
1537 .timeout_ms = o.timeout_ms, 1579 .timeout_ms = o.timeout,
1538 }, o.session); 1580 }, o._session);
1539 try conn.sendFrame(.await_req, payload, deadline); 1581 try conn.sendFrame(.await_req, payload, deadline);
1540 const frame = try conn.awaitFrame(.await_reply, deadline); 1582 const frame = try conn.awaitFrame(.await_reply, deadline);
1541 defer frame.deinit(alloc); 1583 defer frame.deinit(alloc);
@@ -1570,7 +1612,7 @@ fn awaitReissuing(
1570 // daemon that lost our connection lost the client slot with 1612 // daemon that lost our connection lost the client slot with
1571 // it, so an await_req arriving unattached asks about nothing. 1613 // it, so an await_req arriving unattached asks about nothing.
1572 // A failure here is still the reconnect failing. 1614 // A failure here is still the reconnect failing.
1573 attachZero(conn, o.session, deadline) catch |reattach| { 1615 attachZero(conn, o._session, deadline) catch |reattach| {
1574 // A refused re-attach is the daemon's answer, not the tear 1616 // A refused re-attach is the daemon's answer, not the tear
1575 // that got us here: `ConnectionLost` would send an agent 1617 // that got us here: `ConnectionLost` would send an agent
1576 // to check the network for a session that is gone. 1618 // to check the network for a session that is gone.
@@ -1789,16 +1831,16 @@ fn awaitVerb(
1789 const who = if (cmdline == null) "await" else "run"; 1831 const who = if (cmdline == null) "await" else "run";
1790 const started = std.time.milliTimestamp(); 1832 const started = std.time.milliTimestamp();
1791 1833
1792 attachZero(conn, o.session, deadline) catch |e| 1834 attachZero(conn, o._session, deadline) catch |e|
1793 return failSend(e, o.session, .attach, who, "attach failed"); 1835 return failSend(e, o._session, .attach, who, "attach failed");
1794 1836
1795 // BEFORE the input, not after: the watermark has to be the one this 1837 // BEFORE the input, not after: the watermark has to be the one this
1796 // command must beat. Read afterwards, a command fast enough to return 1838 // command must beat. Read afterwards, a command fast enough to return
1797 // between the two would have already moved the seq past a value we 1839 // between the two would have already moved the seq past a value we
1798 // never recorded, and the await would sit waiting for a return that 1840 // never recorded, and the await would sit waiting for a return that
1799 // had happened. 1841 // had happened.
1800 const since = currentSeq(alloc, conn, o.session, deadline) catch |e| switch (e) { 1842 const since = currentSeq(alloc, conn, o._session, deadline) catch |e| switch (e) {
1801 error.AttachRefused => return failAttachRefused(o.session, .attach), 1843 error.AttachRefused => return failAttachRefused(o._session, .attach),
1802 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)), 1844 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)),
1803 else => return failAs(who, "status failed", @errorName(e)), 1845 else => return failAs(who, "status failed", @errorName(e)),
1804 }; 1846 };
@@ -1813,11 +1855,11 @@ fn awaitVerb(
1813 return failAs(who, "cannot build the command line", @errorName(e)); 1855 return failAs(who, "cannot build the command line", @errorName(e));
1814 defer alloc.free(line); 1856 defer alloc.free(line);
1815 conn.sendFrame(.input, line, deadline) catch |e| 1857 conn.sendFrame(.input, line, deadline) catch |e|
1816 return failSend(e, o.session, .attach, who, "input failed"); 1858 return failSend(e, o._session, .attach, who, "input failed");
1817 } 1859 }
1818 1860
1819 const r = awaitReissuing(alloc, conn, o, since, awaitDeadline(o, conn)) catch |e| switch (e) { 1861 const r = awaitReissuing(alloc, conn, o, since, awaitDeadline(o, conn)) catch |e| switch (e) {
1820 error.AttachRefused => return failAttachRefused(o.session, .attach), 1862 error.AttachRefused => return failAttachRefused(o._session, .attach),
1821 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)), 1863 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)),
1822 else => { 1864 else => {
1823 var detail: [128]u8 = undefined; 1865 var detail: [128]u8 = undefined;
@@ -1848,12 +1890,12 @@ fn elapsed(started: i64) i64 {
1848 return std.time.milliTimestamp() - started; 1890 return std.time.milliTimestamp() - started;
1849 } 1891 }
1850 1892
1851 /// This client's deadline: the daemon's own bound plus the grace 1893 /// This client's deadline: the daemon's own bound plus the grace window
1852 /// window (await_grace_ms, widened per transport by `Conn.graceMs`). 1894 /// (await_grace_ms, widened per transport by `Conn.graceMs`). Unbounded
1853 /// An unbounded request stays unbounded. 1895 /// stays unbounded.
1854 fn awaitDeadline(o: Opts, conn: *const Conn) i64 { 1896 fn awaitDeadline(o: Opts, conn: *const Conn) i64 {
1855 if (o.timeout_ms == 0) return std.math.maxInt(i64); 1897 if (o.timeout == 0) return std.math.maxInt(i64);
1856 return std.time.milliTimestamp() + o.timeout_ms + conn.graceMs(); 1898 return std.time.milliTimestamp() + o.timeout + conn.graceMs();
1857 } 1899 }
1858 1900
1859 // Forces semantic analysis of every pub decl under `zig build test`, so an 1901 // Forces semantic analysis of every pub decl under `zig build test`, so an