0ba8f95d
feat: muxd's flags are read off the Opts struct
a73x 2026-08-27 05:49
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -151,6 +151,9 @@ const mod_table = [_]ModSpec{ | |||
| 151 | // the one exception and does not weaken that — it hands its tests a | 151 | // the one exception and does not weaken that — it hands its tests a |
| 152 | // short directory to put a socket in and knows nothing about the bytes. | 152 | // short directory to put a socket in and knows nothing about the bytes. |
| 153 | .{ .name = "proxy", .path = "src/proxy.zig", .layer = 0, .link_libc = true, .test_imports = &.{"testtmp"} }, | 153 | .{ .name = "proxy", .path = "src/proxy.zig", .layer = 0, .link_libc = true, .test_imports = &.{"testtmp"} }, |
| 154 | // Reflection over a caller's options struct, so it imports nothing: the | ||
| 155 | // struct is the flag table and the parser learns it at comptime. | ||
| 156 | .{ .name = "cliflags", .path = "src/cli/flags.zig", .layer = 0 }, | ||
| 154 | // ---- layer 1: single-hop over the leaves ---- | 157 | // ---- layer 1: single-hop over the leaves ---- |
| 155 | // Platform-neutral semantic decoding for terminal mode samples and | 158 | // Platform-neutral semantic decoding for terminal mode samples and |
| 156 | // side-channel events. The borrowed clipboard slice stays tied to the | 159 | // side-channel events. The borrowed clipboard slice stays tied to the |
| @@ -272,7 +275,7 @@ const mod_table = [_]ModSpec{ | |||
| 272 | // the sun_path bound, checked before any verb acts on the path; and the | 275 | // the sun_path bound, checked before any verb acts on the path; and the |
| 273 | // keygen round-trip test needs a directory to generate into, which the | 276 | // keygen round-trip test needs a directory to generate into, which the |
| 274 | // daemon itself never touches. | 277 | // daemon itself never touches. |
| 275 | .{ .name = "exe", .path = "src/main.zig", .layer = 3, .link_libc = true, .imports = &.{ "server", "protocol", "proxy", "quic", "quic_server", "xdg", "spawn", "handoff", "sockpath", "upgrade" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 278 | .{ .name = "exe", .path = "src/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 }, |
| 276 | // ---- layer 4 ---- | 279 | // ---- layer 4 ---- |
| 277 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route | 280 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route |
| 278 | // table, WS endpoint naming. Assets are injected (the exe root | 281 | // table, WS endpoint naming. Assets are injected (the exe root |
| @@ -557,9 +560,11 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui | |||
| 557 | // an argument, not a rule. | 560 | // an argument, not a rule. |
| 558 | var checked: std.ArrayList([]const u8) = .empty; | 561 | var checked: std.ArrayList([]const u8) = .empty; |
| 559 | zigFilesIn(b, "src", &checked); | 562 | zigFilesIn(b, "src", &checked); |
| 563 | zigFilesIn(b, "src/cli", &checked); | ||
| 560 | zigFilesIn(b, "tools", &checked); | 564 | zigFilesIn(b, "tools", &checked); |
| 561 | var indexed: std.ArrayList([]const u8) = .empty; | 565 | var indexed: std.ArrayList([]const u8) = .empty; |
| 562 | zigFilesIn(b, "src", &indexed); | 566 | zigFilesIn(b, "src", &indexed); |
| 567 | zigFilesIn(b, "src/cli", &indexed); | ||
| 563 | zigFilesIn(b, "test", &indexed); | 568 | zigFilesIn(b, "test", &indexed); |
| 564 | zigFilesIn(b, "tools", &indexed); | 569 | zigFilesIn(b, "tools", &indexed); |
| 565 | // build.zig is cited by name in src/main.zig's comments and is a real | 570 | // build.zig is cited by name in src/main.zig's comments and is a real |
| @@ -618,7 +623,7 @@ const test_order = [_][]const u8{ | |||
| 618 | "webhub", "wallview", "sockpath", "muxa", "server", "client", "proxy", | 623 | "webhub", "wallview", "sockpath", "muxa", "server", "client", "proxy", |
| 619 | "mux", "quic", "quic_server", "exe", "testtmp", "quic_client", "predict", | 624 | "mux", "quic", "quic_server", "exe", "testtmp", "quic_client", "predict", |
| 620 | "rawmode", "delaypipe", "xdg", "spawn", "handoff", "paint", "layout", | 625 | "rawmode", "delaypipe", "xdg", "spawn", "handoff", "paint", "layout", |
| 621 | "render", "ptyclient", "webhub_main", "wsclient", | 626 | "render", "ptyclient", "webhub_main", "wsclient", "cliflags", |
| 622 | }; | 627 | }; |
| 623 | 628 | ||
| 624 | comptime { | 629 | comptime { |
docscheck.budget
| Old | New | ||
|---|---|---|---|
| @@ -5,6 +5,7 @@ cmd.zig 0 | |||
| 5 | delta.zig 0 | 5 | delta.zig 0 |
| 6 | docscheck.zig 0 | 6 | docscheck.zig 0 |
| 7 | engine.zig 0 | 7 | engine.zig 0 |
| 8 | flags.zig 0 | ||
| 8 | handoff.zig 0 | 9 | handoff.zig 0 |
| 9 | interact.zig 0 | 10 | interact.zig 0 |
| 10 | keymap.zig 0 | 11 | keymap.zig 0 |
src/cli/flags.zig
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,213 @@ | |||
| 1 | //! A flag parser that reads its table off a struct: the field's TYPE is the | ||
| 2 | //! flag's arity and the field's NAME is its spelling, so adding a field adds | ||
| 3 | //! a flag and there is no second list to keep in step. The grammar is | ||
| 4 | //! `--flag VALUE`, space-separated. What a flag MEANS stays with the caller, | ||
| 5 | //! in post-checks over the parsed struct. | ||
| 6 | const std = @import("std"); | ||
| 7 | |||
| 8 | pub const Outcome = union(enum) { | ||
| 9 | ok, | ||
| 10 | /// `--help` or `-h`, wherever it sits: the caller prints its usage to | ||
| 11 | /// stdout and exits 0. | ||
| 12 | help, | ||
| 13 | unknown_arg: []const u8, | ||
| 14 | /// A value-taking flag at the end of argv, with nothing left to consume. | ||
| 15 | missing_value: []const u8, | ||
| 16 | /// An integer field whose value did not parse. | ||
| 17 | bad_number: []const u8, | ||
| 18 | }; | ||
| 19 | |||
| 20 | /// An optional field is its child type: null is a default, not an arity. | ||
| 21 | fn Bare(comptime F: type) type { | ||
| 22 | return switch (@typeInfo(F)) { | ||
| 23 | .optional => |o| o.child, | ||
| 24 | else => F, | ||
| 25 | }; | ||
| 26 | } | ||
| 27 | |||
| 28 | /// A flag given twice: the last wins. Nothing stops the scan — every word is | ||
| 29 | /// a flag or a mistake, because a positional here could only be a typo. | ||
| 30 | pub fn parse(comptime T: type, dst: *T, args: []const [:0]const u8) Outcome { | ||
| 31 | const fields = @typeInfo(T).@"struct".fields; | ||
| 32 | comptime for (fields) |f| { | ||
| 33 | if (f.name[0] == '_') continue; | ||
| 34 | const B = Bare(f.type); | ||
| 35 | if (B != bool and B != []const u8 and @typeInfo(B) != .int) | ||
| 36 | @compileError("cliflags: no arity for field `" ++ f.name ++ "`: " ++ @typeName(f.type)); | ||
| 37 | }; | ||
| 38 | |||
| 39 | // Asked for before anything is read, so that a `--help` sitting where a | ||
| 40 | // value belongs still answers with the usage instead of being eaten. | ||
| 41 | for (args) |a| { | ||
| 42 | if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) return .help; | ||
| 43 | } | ||
| 44 | |||
| 45 | var i: usize = 0; | ||
| 46 | while (i < args.len) : (i += 1) { | ||
| 47 | const a = args[i]; | ||
| 48 | var known = false; | ||
| 49 | inline for (fields) |f| { | ||
| 50 | if (f.name[0] != '_' and !known and std.mem.eql(u8, a, comptime flagName(f.name))) { | ||
| 51 | known = true; | ||
| 52 | const B = Bare(f.type); | ||
| 53 | if (B == bool) { | ||
| 54 | @field(dst, f.name) = true; | ||
| 55 | } else { | ||
| 56 | if (i + 1 >= args.len) return .{ .missing_value = a }; | ||
| 57 | i += 1; | ||
| 58 | @field(dst, f.name) = switch (@typeInfo(B)) { | ||
| 59 | .int => std.fmt.parseInt(B, args[i], 10) catch return .{ .bad_number = a }, | ||
| 60 | else => args[i], | ||
| 61 | }; | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | if (!known) return .{ .unknown_arg = a }; | ||
| 66 | } | ||
| 67 | return .ok; | ||
| 68 | } | ||
| 69 | |||
| 70 | pub fn flagName(comptime field: []const u8) []const u8 { | ||
| 71 | comptime var name: []const u8 = "--"; | ||
| 72 | inline for (field) |c| name = name ++ [_]u8{if (c == '_') '-' else c}; | ||
| 73 | return name; | ||
| 74 | } | ||
| 75 | |||
| 76 | /// A flag is named only where the word ENDS: `--sock` must not be answered | ||
| 77 | /// by prose that says `--socket`. | ||
| 78 | pub fn documented(name: []const u8, usage: []const u8) bool { | ||
| 79 | if (name.len == 0 or usage.len < name.len) return false; | ||
| 80 | var at: usize = 0; | ||
| 81 | while (at + name.len <= usage.len) : (at += 1) { | ||
| 82 | if (!std.mem.eql(u8, usage[at..][0..name.len], name)) continue; | ||
| 83 | const end = at + name.len; | ||
| 84 | if (end == usage.len) return true; | ||
| 85 | switch (usage[end]) { | ||
| 86 | ' ', '\n', ']' => return true, | ||
| 87 | else => {}, | ||
| 88 | } | ||
| 89 | } | ||
| 90 | return false; | ||
| 91 | } | ||
| 92 | |||
| 93 | /// Kills parser-to-prose drift at build time: a flag added to T and forgotten | ||
| 94 | /// in the text fails the compile. The mirror leg is NOT claimed — prose may | ||
| 95 | /// name a flag no field has, and only a reader will notice. | ||
| 96 | pub fn assertDocumented(comptime T: type, comptime usage: []const u8, comptime hidden: []const []const u8) void { | ||
| 97 | @setEvalBranchQuota(50_000); | ||
| 98 | comptime for (@typeInfo(T).@"struct".fields) |f| { | ||
| 99 | if (f.name[0] == '_') continue; | ||
| 100 | var skip = false; | ||
| 101 | for (hidden) |h| { | ||
| 102 | if (std.mem.eql(u8, h, f.name)) skip = true; | ||
| 103 | } | ||
| 104 | if (skip) continue; | ||
| 105 | if (!documented(flagName(f.name), usage)) | ||
| 106 | @compileError("cliflags: usage text never names " ++ flagName(f.name)); | ||
| 107 | }; | ||
| 108 | } | ||
| 109 | |||
| 110 | const Demo = struct { | ||
| 111 | _cmd: u8 = 0, | ||
| 112 | vt: bool = false, | ||
| 113 | sock: ?[]const u8 = null, | ||
| 114 | shell: []const u8 = "/bin/sh", | ||
| 115 | cols: u16 = 80, | ||
| 116 | quic_idle_ms: u32 = 15_000, | ||
| 117 | }; | ||
| 118 | |||
| 119 | test "parse: a bool field is a bare flag, a string field takes the next word, an integer field parses it" { | ||
| 120 | var o: Demo = .{}; | ||
| 121 | const args: []const [:0]const u8 = &.{ "--vt", "--sock", "/tmp/x.sock", "--shell", "/bin/dash", "--cols", "120" }; | ||
| 122 | try std.testing.expect(parse(Demo, &o, args) == .ok); | ||
| 123 | try std.testing.expect(o.vt); | ||
| 124 | try std.testing.expectEqualStrings("/tmp/x.sock", o.sock.?); | ||
| 125 | try std.testing.expectEqualStrings("/bin/dash", o.shell); | ||
| 126 | try std.testing.expectEqual(@as(u16, 120), o.cols); | ||
| 127 | |||
| 128 | // Untouched fields keep the struct's own defaults. | ||
| 129 | var d: Demo = .{}; | ||
| 130 | try std.testing.expect(parse(Demo, &d, &.{}) == .ok); | ||
| 131 | try std.testing.expect(!d.vt); | ||
| 132 | try std.testing.expect(d.sock == null); | ||
| 133 | try std.testing.expectEqual(@as(u32, 15_000), d.quic_idle_ms); | ||
| 134 | } | ||
| 135 | |||
| 136 | test "parse: a value flag at the end of argv is missing_value, not unknown_arg" { | ||
| 137 | var o: Demo = .{}; | ||
| 138 | const r = parse(Demo, &o, &.{"--sock"}); | ||
| 139 | try std.testing.expect(r == .missing_value); | ||
| 140 | try std.testing.expectEqualStrings("--sock", r.missing_value); | ||
| 141 | } | ||
| 142 | |||
| 143 | test "parse: an unknown flag names itself" { | ||
| 144 | var o: Demo = .{}; | ||
| 145 | const r = parse(Demo, &o, &.{ "--vt", "--wat" }); | ||
| 146 | try std.testing.expect(r == .unknown_arg); | ||
| 147 | try std.testing.expectEqualStrings("--wat", r.unknown_arg); | ||
| 148 | |||
| 149 | // A bare word is a mistake too: there are no positional arguments here. | ||
| 150 | var p: Demo = .{}; | ||
| 151 | try std.testing.expect(parse(Demo, &p, &.{"run"}) == .unknown_arg); | ||
| 152 | } | ||
| 153 | |||
| 154 | test "parse: --help and -h are the help outcome, before or after other flags" { | ||
| 155 | var o: Demo = .{}; | ||
| 156 | try std.testing.expect(parse(Demo, &o, &.{"--help"}) == .help); | ||
| 157 | try std.testing.expect(parse(Demo, &o, &.{"-h"}) == .help); | ||
| 158 | try std.testing.expect(parse(Demo, &o, &.{ "--vt", "--help" }) == .help); | ||
| 159 | try std.testing.expect(parse(Demo, &o, &.{ "--help", "--wat" }) == .help); | ||
| 160 | // Even where a value would be read: help outranks the grammar. | ||
| 161 | try std.testing.expect(parse(Demo, &o, &.{ "--sock", "--help" }) == .help); | ||
| 162 | } | ||
| 163 | |||
| 164 | test "parse: a leading-underscore field is not a flag" { | ||
| 165 | var o: Demo = .{}; | ||
| 166 | const r = parse(Demo, &o, &.{ "--cmd", "run" }); | ||
| 167 | try std.testing.expect(r == .unknown_arg); | ||
| 168 | try std.testing.expectEqualStrings("--cmd", r.unknown_arg); | ||
| 169 | try std.testing.expectEqual(@as(u8, 0), o._cmd); | ||
| 170 | } | ||
| 171 | |||
| 172 | test "parse: a flag given twice, the last wins" { | ||
| 173 | var o: Demo = .{}; | ||
| 174 | try std.testing.expect(parse(Demo, &o, &.{ "--cols", "100", "--cols", "42" }) == .ok); | ||
| 175 | try std.testing.expectEqual(@as(u16, 42), o.cols); | ||
| 176 | } | ||
| 177 | |||
| 178 | test "parse: a non-number for an integer field is bad_number naming the flag" { | ||
| 179 | var o: Demo = .{}; | ||
| 180 | const r = parse(Demo, &o, &.{ "--cols", "wide" }); | ||
| 181 | try std.testing.expect(r == .bad_number); | ||
| 182 | try std.testing.expectEqualStrings("--cols", r.bad_number); | ||
| 183 | |||
| 184 | // Out of the field's range, and negative into an unsigned, are the same | ||
| 185 | // mistake: the flag cannot hold what was typed. | ||
| 186 | var p: Demo = .{}; | ||
| 187 | try std.testing.expect(parse(Demo, &p, &.{ "--cols", "99999" }) == .bad_number); | ||
| 188 | try std.testing.expect(parse(Demo, &p, &.{ "--quic-idle-ms", "-5" }) == .bad_number); | ||
| 189 | } | ||
| 190 | |||
| 191 | test "flagName: underscores become dashes" { | ||
| 192 | try std.testing.expectEqualStrings("--quic-idle-ms", flagName("quic_idle_ms")); | ||
| 193 | try std.testing.expectEqualStrings("--vt", flagName("vt")); | ||
| 194 | } | ||
| 195 | |||
| 196 | test "documented: a prefix of a longer flag does not count" { | ||
| 197 | try std.testing.expect(!documented("--sock", " muxd run [--socket PATH]\n")); | ||
| 198 | try std.testing.expect(documented("--sock", " muxd run [--sock PATH]\n")); | ||
| 199 | try std.testing.expect(documented("--vt", " muxd dump [--vt]\n")); | ||
| 200 | try std.testing.expect(documented("--vt", " muxd dump --vt\n")); | ||
| 201 | try std.testing.expect(documented("--vt", " muxd dump --vt")); | ||
| 202 | try std.testing.expect(!documented("--rows", "nothing here\n")); | ||
| 203 | } | ||
| 204 | |||
| 205 | test "assertDocumented: every visible flag is named in the prose" { | ||
| 206 | const text = | ||
| 207 | \\ demo [--vt] [--sock PATH] [--cols N] [--quic-idle-ms N] | ||
| 208 | \\ | ||
| 209 | ; | ||
| 210 | // `shell` stands in for muxd's machine-written flags: hidden from the | ||
| 211 | // prose on purpose, so the assertion must not demand it. | ||
| 212 | comptime assertDocumented(Demo, text, &.{"shell"}); | ||
| 213 | } | ||
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -13,6 +13,7 @@ const spawn = @import("spawn"); | |||
| 13 | const handoff = @import("handoff"); | 13 | const handoff = @import("handoff"); |
| 14 | const sockpath = @import("sockpath"); | 14 | const sockpath = @import("sockpath"); |
| 15 | const upgrade = @import("upgrade"); | 15 | const upgrade = @import("upgrade"); |
| 16 | const cliflags = @import("cliflags"); | ||
| 16 | 17 | ||
| 17 | const usage = | 18 | const usage = |
| 18 | \\usage: | 19 | \\usage: |
| @@ -28,6 +29,7 @@ const usage = | |||
| 28 | \\ muxd upgrade [--sock PATH] (exec THIS binary over the daemon; sessions live) | 29 | \\ muxd upgrade [--sock PATH] (exec THIS binary over the daemon; sessions live) |
| 29 | \\ [--allow-same-version] (strictly newer, unless this; the e2e leg's) | 30 | \\ [--allow-same-version] (strictly newer, unless this; the e2e leg's) |
| 30 | \\ muxd --version | 31 | \\ muxd --version |
| 32 | \\ muxd --help | ||
| 31 | \\ | 33 | \\ |
| 32 | ; | 34 | ; |
| 33 | 35 | ||
| @@ -49,7 +51,7 @@ fn envKey() ?[]const u8 { | |||
| 49 | /// for what the number means and why it lives there. | 51 | /// for what the number means and why it lives there. |
| 50 | const default_quic_idle_ms: u32 = quic.default_idle_ms; | 52 | const default_quic_idle_ms: u32 = quic.default_idle_ms; |
| 51 | 53 | ||
| 52 | const Cmd = enum { run, dump, stats, proxy, endpoint, version, keygen, start, stop, upgrade }; | 54 | const Cmd = enum { run, dump, stats, proxy, endpoint, version, help, keygen, start, stop, upgrade }; |
| 53 | 55 | ||
| 54 | /// One row per verb. Adding a subcommand used to mean editing the usage | 56 | /// One row per verb. Adding a subcommand used to mean editing the usage |
| 55 | /// literal, the Cmd enum, a name→Cmd if/else chain, keygen's hand-rolled | 57 | /// literal, the Cmd enum, a name→Cmd if/else chain, keygen's hand-rolled |
| @@ -84,6 +86,9 @@ const specs = [_]Spec{ | |||
| 84 | // types, but it is a command: it names what the process does instead of | 86 | // types, but it is a command: it names what the process does instead of |
| 85 | // configuring one, and answers from the binary alone. | 87 | // configuring one, and answers from the binary alone. |
| 86 | .{ .name = "--version", .cmd = .version, .uses_socket = false, .flags = .ignored }, | 88 | .{ .name = "--version", .cmd = .version, .uses_socket = false, .flags = .ignored }, |
| 89 | // Spelled as a flag for the same reason, and `.ignored` for a second: | ||
| 90 | // asking for the usage must never be refused over the words next to it. | ||
| 91 | .{ .name = "--help", .cmd = .help, .uses_socket = false, .flags = .ignored }, | ||
| 87 | .{ .name = "run", .cmd = .run, .uses_socket = true, .flags = .all }, | 92 | .{ .name = "run", .cmd = .run, .uses_socket = true, .flags = .all }, |
| 88 | .{ .name = "dump", .cmd = .dump, .uses_socket = true, .flags = .all }, | 93 | .{ .name = "dump", .cmd = .dump, .uses_socket = true, .flags = .all }, |
| 89 | .{ .name = "stats", .cmd = .stats, .uses_socket = true, .flags = .all }, | 94 | .{ .name = "stats", .cmd = .stats, .uses_socket = true, .flags = .all }, |
| @@ -133,7 +138,9 @@ fn specForCmd(cmd: Cmd) Spec { | |||
| 133 | /// can be tested without a process to exit from — the same reason | 138 | /// can be tested without a process to exit from — the same reason |
| 134 | /// mux_main.zig's parser is its own function. | 139 | /// mux_main.zig's parser is its own function. |
| 135 | const Opts = struct { | 140 | const Opts = struct { |
| 136 | cmd: Cmd, | 141 | /// Not a flag, and the leading underscore is what says so: cliflags.parse |
| 142 | /// skips it, the verb having been settled by the row above. | ||
| 143 | _cmd: Cmd, | ||
| 137 | sock: ?[]const u8 = null, | 144 | sock: ?[]const u8 = null, |
| 138 | shell: ?[]const u8 = null, | 145 | shell: ?[]const u8 = null, |
| 139 | cols: u16 = 80, | 146 | cols: u16 = 80, |
| @@ -145,11 +152,13 @@ const Opts = struct { | |||
| 145 | /// `--key` without `--quic` is settled here, because that one has no | 152 | /// `--key` without `--quic` is settled here, because that one has no |
| 146 | /// reading that makes it sensible. | 153 | /// reading that makes it sensible. |
| 147 | key: ?[]const u8 = null, | 154 | key: ?[]const u8 = null, |
| 155 | /// u32 rather than u64 so that an absurd value is a parse failure rather | ||
| 156 | /// than an overflow where it is multiplied out to nanoseconds. | ||
| 148 | quic_idle_ms: u32 = default_quic_idle_ms, | 157 | quic_idle_ms: u32 = default_quic_idle_ms, |
| 149 | /// The session `dump` names. Empty is the wire's own default spelling | 158 | /// The session `dump` names. Optional so that only a name that was TYPED |
| 150 | /// — no tail at all — so a caller that never passes `--session` builds | 159 | /// is validated: `""` is the wire's own default spelling — no tail at |
| 151 | /// byte-identical payloads to before this flag existed. | 160 | /// all — and would pass a check written against the empty string. |
| 152 | session: []const u8 = "", | 161 | session: ?[]const u8 = null, |
| 153 | /// The inherited manifest descriptor an upgrade exec'd us with. Not a | 162 | /// The inherited manifest descriptor an upgrade exec'd us with. Not a |
| 154 | /// user flag: the old daemon writes it into our argv. Its presence is | 163 | /// user flag: the old daemon writes it into our argv. Its presence is |
| 155 | /// what makes `run` an ADOPTION rather than a start, and it is also | 164 | /// what makes `run` an ADOPTION rather than a start, and it is also |
| @@ -170,10 +179,19 @@ const Opts = struct { | |||
| 170 | allow_same_version: bool = false, | 179 | allow_same_version: bool = false, |
| 171 | }; | 180 | }; |
| 172 | 181 | ||
| 173 | /// A refusal, carrying whatever `main` needs to print one line about it. | 182 | // The three flags left out are written into argv by the OLD daemon on an |
| 174 | /// None of these is a daemon bug, so none of them gets a stack trace. | 183 | // upgrade and never typed by a hand, so the prose does not offer them. |
| 184 | comptime { | ||
| 185 | cliflags.assertDocumented(Opts, usage, &.{ "resume_fd", "check", "resume_fail_at" }); | ||
| 186 | } | ||
| 187 | |||
| 188 | /// What `main` prints instead of running the command. Every case but `help` | ||
| 189 | /// is a refusal; none of them is a daemon bug, so none gets a stack trace. | ||
| 175 | const Usage = union(enum) { | 190 | const Usage = union(enum) { |
| 176 | no_command, | 191 | no_command, |
| 192 | /// `--help` on a subcommand. The answer the user asked for, so it is the | ||
| 193 | /// one case `usageExit` exits 0 on. | ||
| 194 | help, | ||
| 177 | unknown_command: []const u8, | 195 | unknown_command: []const u8, |
| 178 | unknown_arg: []const u8, | 196 | unknown_arg: []const u8, |
| 179 | /// A flag at the end of argv with nothing left to consume. | 197 | /// A flag at the end of argv with nothing left to consume. |
| @@ -195,79 +213,31 @@ fn parseArgs(args: []const [:0]const u8) ParseResult { | |||
| 195 | // The one place a verb's flag class is enforced; which class each verb | 213 | // The one place a verb's flag class is enforced; which class each verb |
| 196 | // is in is stated once, in its row. | 214 | // is in is stated once, in its row. |
| 197 | switch (spec.flags) { | 215 | switch (spec.flags) { |
| 198 | .ignored => return .{ .ok = .{ .cmd = spec.cmd } }, | 216 | .ignored => return .{ .ok = .{ ._cmd = spec.cmd } }, |
| 199 | .none => if (args.len > 2) return .{ .err = .{ .unknown_arg = args[2] } }, | 217 | .none => if (args.len > 2) return .{ .err = .{ .unknown_arg = args[2] } }, |
| 200 | .all => {}, | 218 | .all => {}, |
| 201 | } | 219 | } |
| 202 | 220 | ||
| 203 | var o: Opts = .{ .cmd = spec.cmd }; | 221 | var o: Opts = .{ ._cmd = spec.cmd }; |
| 204 | var i: usize = 2; | 222 | switch (cliflags.parse(Opts, &o, args[2..])) { |
| 205 | while (i < args.len) : (i += 1) { | 223 | .ok => {}, |
| 206 | const a = args[i]; | 224 | .help => return .{ .err = .help }, |
| 207 | if (std.mem.eql(u8, a, "--vt")) { | 225 | .unknown_arg => |a| return .{ .err = .{ .unknown_arg = a } }, |
| 208 | o.vt = true; | 226 | .missing_value => |f| return .{ .err = .{ .missing_value = f } }, |
| 209 | continue; | 227 | .bad_number => |f| return .{ .err = .{ .bad_number = f } }, |
| 210 | } | 228 | } |
| 211 | if (std.mem.eql(u8, a, "--check")) { | 229 | |
| 212 | o.check = true; | 230 | // Refused here rather than carried to the wire as a payload nothing could |
| 213 | continue; | 231 | // ever look up — the same "check before it becomes a frame" the socket |
| 214 | } | 232 | // path length guard follows. |
| 215 | if (std.mem.eql(u8, a, "--allow-same-version")) { | 233 | if (o.session) |name| { |
| 216 | o.allow_same_version = true; | 234 | if (!proto.validSessionName(name)) return .{ .err = .{ .bad_session_name = name } }; |
| 217 | continue; | ||
| 218 | } | ||
| 219 | // Every remaining flag takes a value, so the missing-value case is | ||
| 220 | // answered once here rather than at each arm — a flag with nothing | ||
| 221 | // after it used to fall through to "unknown argument", which named | ||
| 222 | // the wrong mistake. | ||
| 223 | const takes_value = std.mem.eql(u8, a, "--sock") or | ||
| 224 | std.mem.eql(u8, a, "--shell") or | ||
| 225 | std.mem.eql(u8, a, "--cols") or | ||
| 226 | std.mem.eql(u8, a, "--rows") or | ||
| 227 | std.mem.eql(u8, a, "--quic") or | ||
| 228 | std.mem.eql(u8, a, "--key") or | ||
| 229 | std.mem.eql(u8, a, "--quic-idle-ms") or | ||
| 230 | std.mem.eql(u8, a, "--session") or | ||
| 231 | std.mem.eql(u8, a, "--resume-fd") or | ||
| 232 | std.mem.eql(u8, a, "--resume-fail-at"); | ||
| 233 | if (!takes_value) return .{ .err = .{ .unknown_arg = a } }; | ||
| 234 | if (i + 1 >= args.len) return .{ .err = .{ .missing_value = a } }; | ||
| 235 | i += 1; | ||
| 236 | const v = args[i]; | ||
| 237 | if (std.mem.eql(u8, a, "--sock")) { | ||
| 238 | o.sock = v; | ||
| 239 | } else if (std.mem.eql(u8, a, "--shell")) { | ||
| 240 | o.shell = v; | ||
| 241 | } else if (std.mem.eql(u8, a, "--quic")) { | ||
| 242 | o.quic = v; | ||
| 243 | } else if (std.mem.eql(u8, a, "--key")) { | ||
| 244 | o.key = v; | ||
| 245 | } else if (std.mem.eql(u8, a, "--session")) { | ||
| 246 | // Refused here rather than carried to the wire as a payload | ||
| 247 | // nothing could ever look up — the same "check before it | ||
| 248 | // becomes a frame" the socket-path length guard follows. | ||
| 249 | if (!proto.validSessionName(v)) return .{ .err = .{ .bad_session_name = v } }; | ||
| 250 | o.session = v; | ||
| 251 | } else if (std.mem.eql(u8, a, "--resume-fail-at")) { | ||
| 252 | o.resume_fail_at = v; | ||
| 253 | } else if (std.mem.eql(u8, a, "--resume-fd")) { | ||
| 254 | o.resume_fd = std.fmt.parseInt(std.posix.fd_t, v, 10) catch | ||
| 255 | return .{ .err = .{ .bad_number = a } }; | ||
| 256 | } else if (std.mem.eql(u8, a, "--cols")) { | ||
| 257 | o.cols = std.fmt.parseInt(u16, v, 10) catch return .{ .err = .{ .bad_number = a } }; | ||
| 258 | } else if (std.mem.eql(u8, a, "--rows")) { | ||
| 259 | o.rows = std.fmt.parseInt(u16, v, 10) catch return .{ .err = .{ .bad_number = a } }; | ||
| 260 | } else if (std.mem.eql(u8, a, "--quic-idle-ms")) { | ||
| 261 | // u32 rather than u64 so that an absurd value is a parse | ||
| 262 | // failure here instead of an overflow where it is multiplied | ||
| 263 | // out to nanoseconds. Zero is refused because ngtcp2 reads it | ||
| 264 | // as "no idle timeout", the opposite of what the flag says. | ||
| 265 | const n = std.fmt.parseInt(u32, v, 10) catch return .{ .err = .{ .bad_number = a } }; | ||
| 266 | if (n == 0) return .{ .err = .{ .bad_number = a } }; | ||
| 267 | o.quic_idle_ms = n; | ||
| 268 | } | ||
| 269 | } | 235 | } |
| 270 | 236 | ||
| 237 | // Zero is refused because ngtcp2 reads it as "no idle timeout", the | ||
| 238 | // opposite of what the flag says. | ||
| 239 | if (o.quic_idle_ms == 0) return .{ .err = .{ .bad_number = "--quic-idle-ms" } }; | ||
| 240 | |||
| 271 | // A key with nowhere to listen is a mistake parse can see the whole of. | 241 | // A key with nowhere to listen is a mistake parse can see the whole of. |
| 272 | // The mirror case is NOT one: `--quic` with no `--key` may still be | 242 | // The mirror case is NOT one: `--quic` with no `--key` may still be |
| 273 | // answered by MUX_KEY_FILE or the default key path, neither of which | 243 | // answered by MUX_KEY_FILE or the default key path, neither of which |
| @@ -277,8 +247,18 @@ fn parseArgs(args: []const [:0]const u8) ParseResult { | |||
| 277 | return .{ .ok = o }; | 247 | return .{ .ok = o }; |
| 278 | } | 248 | } |
| 279 | 249 | ||
| 250 | /// The code alone: a test can ask it without a process. | ||
| 251 | fn usageCode(u: Usage) u8 { | ||
| 252 | return if (u == .help) 0 else 2; | ||
| 253 | } | ||
| 254 | |||
| 280 | fn usageExit(u: Usage) u8 { | 255 | fn usageExit(u: Usage) u8 { |
| 281 | switch (u) { | 256 | switch (u) { |
| 257 | // stdout, unlike every refusal below: a usage someone asked for is | ||
| 258 | // output, and they may well have piped it into a pager. | ||
| 259 | .help => { | ||
| 260 | _ = std.posix.write(std.posix.STDOUT_FILENO, usage) catch {}; | ||
| 261 | }, | ||
| 282 | .no_command => std.debug.print("{s}", .{usage}), | 262 | .no_command => std.debug.print("{s}", .{usage}), |
| 283 | .unknown_command => std.debug.print("{s}", .{usage}), | 263 | .unknown_command => std.debug.print("{s}", .{usage}), |
| 284 | .unknown_arg => |a| std.debug.print("unknown argument: {s}\n{s}", .{ a, usage }), | 264 | .unknown_arg => |a| std.debug.print("unknown argument: {s}\n{s}", .{ a, usage }), |
| @@ -290,7 +270,7 @@ fn usageExit(u: Usage) u8 { | |||
| 290 | ), | 270 | ), |
| 291 | .bad_session_name => |n| std.debug.print("muxd: bad session name: {s}\n{s}", .{ n, usage }), | 271 | .bad_session_name => |n| std.debug.print("muxd: bad session name: {s}\n{s}", .{ n, usage }), |
| 292 | } | 272 | } |
| 293 | return 2; | 273 | return usageCode(u); |
| 294 | } | 274 | } |
| 295 | 275 | ||
| 296 | /// `HOST[:PORT]` where HOST is a literal address — `127.0.0.1:4433`, | 276 | /// `HOST[:PORT]` where HOST is a literal address — `127.0.0.1:4433`, |
| @@ -350,7 +330,7 @@ pub fn main() !u8 { | |||
| 350 | // is in the manifest, under a listener that is already bound to it. | 330 | // is in the manifest, under a listener that is already bound to it. |
| 351 | // Resolving the default here would refuse an upgrade on any daemon | 331 | // Resolving the default here would refuse an upgrade on any daemon |
| 352 | // started with `--sock` outside XDG_RUNTIME_DIR. | 332 | // started with `--sock` outside XDG_RUNTIME_DIR. |
| 353 | const uses_socket = specForCmd(o.cmd).uses_socket and o.resume_fd == null; | 333 | const uses_socket = specForCmd(o._cmd).uses_socket and o.resume_fd == null; |
| 354 | const sock_path = if (o.sock) |s| | 334 | const sock_path = if (o.sock) |s| |
| 355 | try alloc.dupe(u8, s) | 335 | try alloc.dupe(u8, s) |
| 356 | else if (!uses_socket) | 336 | else if (!uses_socket) |
| @@ -386,7 +366,7 @@ pub fn main() !u8 { | |||
| 386 | return 1; | 366 | return 1; |
| 387 | } | 367 | } |
| 388 | 368 | ||
| 389 | switch (o.cmd) { | 369 | switch (o._cmd) { |
| 390 | // The socket path resolved above is unused here and unchecked (see | 370 | // The socket path resolved above is unused here and unchecked (see |
| 391 | // the length guard above): asking a binary its version must work | 371 | // the length guard above): asking a binary its version must work |
| 392 | // with no daemon and no runtime dir. | 372 | // with no daemon and no runtime dir. |
| @@ -396,10 +376,11 @@ pub fn main() !u8 { | |||
| 396 | _ = std.posix.write(std.posix.STDOUT_FILENO, s) catch {}; | 376 | _ = std.posix.write(std.posix.STDOUT_FILENO, s) catch {}; |
| 397 | return 0; | 377 | return 0; |
| 398 | }, | 378 | }, |
| 379 | .help => return usageExit(.help), | ||
| 399 | .keygen => return keygen(alloc), | 380 | .keygen => return keygen(alloc), |
| 400 | .start => return startCmd(alloc, sock_path, args[2..]), | 381 | .start => return startCmd(alloc, sock_path, args[2..]), |
| 401 | .run => return if (o.resume_fd) |fd| resumeRun(alloc, o, fd) else run(alloc, o, sock_path), | 382 | .run => return if (o.resume_fd) |fd| resumeRun(alloc, o, fd) else run(alloc, o, sock_path), |
| 402 | .dump => return dump(alloc, sock_path, o.vt, o.session), | 383 | .dump => return dump(alloc, sock_path, o.vt, o.session orelse ""), |
| 403 | .stats => return stats(alloc, sock_path), | 384 | .stats => return stats(alloc, sock_path), |
| 404 | .stop => return stopCmd(alloc, sock_path), | 385 | .stop => return stopCmd(alloc, sock_path), |
| 405 | .upgrade => return upgradeCmd(alloc, sock_path, o.allow_same_version), | 386 | .upgrade => return upgradeCmd(alloc, sock_path, o.allow_same_version), |
| @@ -1297,17 +1278,18 @@ fn parse(comptime argv: []const [:0]const u8) ParseResult { | |||
| 1297 | test "parseArgs: subcommands and their existing flags" { | 1278 | test "parseArgs: subcommands and their existing flags" { |
| 1298 | const r = parse(&.{ "muxd", "run" }); | 1279 | const r = parse(&.{ "muxd", "run" }); |
| 1299 | try std.testing.expect(r == .ok); | 1280 | try std.testing.expect(r == .ok); |
| 1300 | try std.testing.expect(r.ok.cmd == .run); | 1281 | try std.testing.expect(r.ok._cmd == .run); |
| 1301 | try std.testing.expect(r.ok.sock == null); | 1282 | try std.testing.expect(r.ok.sock == null); |
| 1302 | try std.testing.expectEqual(@as(u16, 80), r.ok.cols); | 1283 | try std.testing.expectEqual(@as(u16, 80), r.ok.cols); |
| 1303 | try std.testing.expectEqual(@as(u16, 24), r.ok.rows); | 1284 | try std.testing.expectEqual(@as(u16, 24), r.ok.rows); |
| 1304 | 1285 | ||
| 1305 | const d = parse(&.{ "muxd", "dump", "--vt", "--sock", "/tmp/x.sock" }); | 1286 | const d = parse(&.{ "muxd", "dump", "--vt", "--sock", "/tmp/x.sock" }); |
| 1306 | try std.testing.expect(d.ok.cmd == .dump); | 1287 | try std.testing.expect(d.ok._cmd == .dump); |
| 1307 | try std.testing.expect(d.ok.vt); | 1288 | try std.testing.expect(d.ok.vt); |
| 1308 | try std.testing.expectEqualStrings("/tmp/x.sock", d.ok.sock.?); | 1289 | try std.testing.expectEqualStrings("/tmp/x.sock", d.ok.sock.?); |
| 1309 | // No --session named: the wire's own default spelling, empty. | 1290 | // No --session named: nothing to validate, and `dump` spells the absence |
| 1310 | try std.testing.expectEqualStrings("", d.ok.session); | 1291 | // on the wire as the empty tail. |
| 1292 | try std.testing.expect(d.ok.session == null); | ||
| 1311 | 1293 | ||
| 1312 | const g = parse(&.{ "muxd", "run", "--cols", "120", "--rows", "40", "--shell", "/bin/dash" }); | 1294 | const g = parse(&.{ "muxd", "run", "--cols", "120", "--rows", "40", "--shell", "/bin/dash" }); |
| 1313 | try std.testing.expectEqual(@as(u16, 120), g.ok.cols); | 1295 | try std.testing.expectEqual(@as(u16, 120), g.ok.cols); |
| @@ -1322,7 +1304,7 @@ test "parseArgs: subcommands and their existing flags" { | |||
| 1322 | test "parse: dump --session rides into the payload" { | 1304 | test "parse: dump --session rides into the payload" { |
| 1323 | const d = parse(&.{ "muxd", "dump", "--session", "b", "--sock", "/tmp/x.sock" }); | 1305 | const d = parse(&.{ "muxd", "dump", "--session", "b", "--sock", "/tmp/x.sock" }); |
| 1324 | try std.testing.expect(d == .ok); | 1306 | try std.testing.expect(d == .ok); |
| 1325 | try std.testing.expectEqualStrings("b", d.ok.session); | 1307 | try std.testing.expectEqualStrings("b", d.ok.session.?); |
| 1326 | 1308 | ||
| 1327 | // A name no tool could ever address is refused at parse — usage on | 1309 | // A name no tool could ever address is refused at parse — usage on |
| 1328 | // stderr, never carried to the wire as a payload nothing can look up. | 1310 | // stderr, never carried to the wire as a payload nothing can look up. |
| @@ -1478,13 +1460,34 @@ test "keygen: a generated key loads through quic.Key.load" { | |||
| 1478 | test "parseArgs: --version is a command, not a flag on one" { | 1460 | test "parseArgs: --version is a command, not a flag on one" { |
| 1479 | const r = parse(&.{ "muxd", "--version" }); | 1461 | const r = parse(&.{ "muxd", "--version" }); |
| 1480 | try std.testing.expect(r == .ok); | 1462 | try std.testing.expect(r == .ok); |
| 1481 | try std.testing.expect(r.ok.cmd == .version); | 1463 | try std.testing.expect(r.ok._cmd == .version); |
| 1464 | } | ||
| 1465 | |||
| 1466 | test "parseArgs: --help is a command, and a flag on one, and both exit 0 on stdout" { | ||
| 1467 | const bare = parse(&.{ "muxd", "--help" }); | ||
| 1468 | try std.testing.expect(bare == .ok); | ||
| 1469 | try std.testing.expect(bare.ok._cmd == .help); | ||
| 1470 | |||
| 1471 | // On a subcommand it is an outcome of the flag parse rather than a row, | ||
| 1472 | // and it must outrank the grammar: `--sock` here is still waiting for a | ||
| 1473 | // value, and asking for the usage is not a way to mistype one. | ||
| 1474 | try std.testing.expect(parse(&.{ "muxd", "run", "--help" }).err == .help); | ||
| 1475 | try std.testing.expect(parse(&.{ "muxd", "dump", "-h", "--sock", "/x" }).err == .help); | ||
| 1476 | try std.testing.expect(parse(&.{ "muxd", "run", "--sock", "--help" }).err == .help); | ||
| 1477 | |||
| 1478 | // The code, asked of `usageCode` rather than of `usageExit`: the latter | ||
| 1479 | // writes the usage to STDOUT, which under `zig build test` is the build | ||
| 1480 | // runner's own IPC channel, and the step hangs forever. 0 is the whole | ||
| 1481 | // difference between an answer and a refusal, so a refusal is asserted | ||
| 1482 | // beside it. | ||
| 1483 | try std.testing.expectEqual(@as(u8, 0), usageCode(.help)); | ||
| 1484 | try std.testing.expectEqual(@as(u8, 2), usageCode(.no_command)); | ||
| 1482 | } | 1485 | } |
| 1483 | 1486 | ||
| 1484 | test "parseArgs: keygen takes no flags" { | 1487 | test "parseArgs: keygen takes no flags" { |
| 1485 | const r = parse(&.{ "muxd", "keygen" }); | 1488 | const r = parse(&.{ "muxd", "keygen" }); |
| 1486 | try std.testing.expect(r == .ok); | 1489 | try std.testing.expect(r == .ok); |
| 1487 | try std.testing.expect(r.ok.cmd == .keygen); | 1490 | try std.testing.expect(r.ok._cmd == .keygen); |
| 1488 | try std.testing.expect(parse(&.{ "muxd", "keygen", "--sock", "/x" }).err == .unknown_arg); | 1491 | try std.testing.expect(parse(&.{ "muxd", "keygen", "--sock", "/x" }).err == .unknown_arg); |
| 1489 | } | 1492 | } |
| 1490 | 1493 | ||
| @@ -1499,7 +1502,7 @@ test "pickKey: --key beats MUX_KEY_FILE beats the default path" { | |||
| 1499 | test "parseArgs: start takes run's flags" { | 1502 | test "parseArgs: start takes run's flags" { |
| 1500 | const r = parse(&.{ "muxd", "start", "--sock", "/tmp/x.sock", "--cols", "100" }); | 1503 | const r = parse(&.{ "muxd", "start", "--sock", "/tmp/x.sock", "--cols", "100" }); |
| 1501 | try std.testing.expect(r == .ok); | 1504 | try std.testing.expect(r == .ok); |
| 1502 | try std.testing.expect(r.ok.cmd == .start); | 1505 | try std.testing.expect(r.ok._cmd == .start); |
| 1503 | try std.testing.expectEqualStrings("/tmp/x.sock", r.ok.sock.?); | 1506 | try std.testing.expectEqualStrings("/tmp/x.sock", r.ok.sock.?); |
| 1504 | try std.testing.expectEqual(@as(u16, 100), r.ok.cols); | 1507 | try std.testing.expectEqual(@as(u16, 100), r.ok.cols); |
| 1505 | } | 1508 | } |
| @@ -1507,22 +1510,22 @@ test "parseArgs: start takes run's flags" { | |||
| 1507 | test "parseArgs: stop is a command and takes --sock" { | 1510 | test "parseArgs: stop is a command and takes --sock" { |
| 1508 | const r = parse(&.{ "muxd", "stop" }); | 1511 | const r = parse(&.{ "muxd", "stop" }); |
| 1509 | try std.testing.expect(r == .ok); | 1512 | try std.testing.expect(r == .ok); |
| 1510 | try std.testing.expect(r.ok.cmd == .stop); | 1513 | try std.testing.expect(r.ok._cmd == .stop); |
| 1511 | try std.testing.expect(r.ok.sock == null); | 1514 | try std.testing.expect(r.ok.sock == null); |
| 1512 | 1515 | ||
| 1513 | const s = parse(&.{ "muxd", "stop", "--sock", "/tmp/x.sock" }); | 1516 | const s = parse(&.{ "muxd", "stop", "--sock", "/tmp/x.sock" }); |
| 1514 | try std.testing.expect(s.ok.cmd == .stop); | 1517 | try std.testing.expect(s.ok._cmd == .stop); |
| 1515 | try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); | 1518 | try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); |
| 1516 | } | 1519 | } |
| 1517 | 1520 | ||
| 1518 | test "parseArgs: endpoint is a command and takes --sock" { | 1521 | test "parseArgs: endpoint is a command and takes --sock" { |
| 1519 | const r = parse(&.{ "muxd", "endpoint" }); | 1522 | const r = parse(&.{ "muxd", "endpoint" }); |
| 1520 | try std.testing.expect(r == .ok); | 1523 | try std.testing.expect(r == .ok); |
| 1521 | try std.testing.expect(r.ok.cmd == .endpoint); | 1524 | try std.testing.expect(r.ok._cmd == .endpoint); |
| 1522 | try std.testing.expect(r.ok.sock == null); | 1525 | try std.testing.expect(r.ok.sock == null); |
| 1523 | 1526 | ||
| 1524 | const s = parse(&.{ "muxd", "endpoint", "--sock", "/tmp/x.sock" }); | 1527 | const s = parse(&.{ "muxd", "endpoint", "--sock", "/tmp/x.sock" }); |
| 1525 | try std.testing.expect(s.ok.cmd == .endpoint); | 1528 | try std.testing.expect(s.ok._cmd == .endpoint); |
| 1526 | try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); | 1529 | try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); |
| 1527 | 1530 | ||
| 1528 | // An unknown flag is refused, as it is for every command: a client of | 1531 | // An unknown flag is refused, as it is for every command: a client of |
| @@ -1544,7 +1547,7 @@ test "parseArgs: endpoint is a command and takes --sock" { | |||
| 1544 | test "parseArgs: run --resume-fd N --check is the old daemon's dry run" { | 1547 | test "parseArgs: run --resume-fd N --check is the old daemon's dry run" { |
| 1545 | const r = parse(&.{ "muxd", "run", "--resume-fd", "7", "--check" }); | 1548 | const r = parse(&.{ "muxd", "run", "--resume-fd", "7", "--check" }); |
| 1546 | try std.testing.expect(r == .ok); | 1549 | try std.testing.expect(r == .ok); |
| 1547 | try std.testing.expect(r.ok.cmd == .run); | 1550 | try std.testing.expect(r.ok._cmd == .run); |
| 1548 | try std.testing.expectEqual(@as(std.posix.fd_t, 7), r.ok.resume_fd.?); | 1551 | try std.testing.expectEqual(@as(std.posix.fd_t, 7), r.ok.resume_fd.?); |
| 1549 | try std.testing.expect(r.ok.check); | 1552 | try std.testing.expect(r.ok.check); |
| 1550 | 1553 | ||
| @@ -1584,13 +1587,13 @@ test "rollbackKeepsEnv: the rollback does not inherit the abort that caused it" | |||
| 1584 | test "parseArgs: upgrade is a command, and same-version is a flag it takes" { | 1587 | test "parseArgs: upgrade is a command, and same-version is a flag it takes" { |
| 1585 | const r = parse(&.{ "muxd", "upgrade" }); | 1588 | const r = parse(&.{ "muxd", "upgrade" }); |
| 1586 | try std.testing.expect(r == .ok); | 1589 | try std.testing.expect(r == .ok); |
| 1587 | try std.testing.expect(r.ok.cmd == .upgrade); | 1590 | try std.testing.expect(r.ok._cmd == .upgrade); |
| 1588 | // Off unless asked: the skew rule is strictly-newer, and an operator who | 1591 | // Off unless asked: the skew rule is strictly-newer, and an operator who |
| 1589 | // did not name the exception must not get it. | 1592 | // did not name the exception must not get it. |
| 1590 | try std.testing.expect(!r.ok.allow_same_version); | 1593 | try std.testing.expect(!r.ok.allow_same_version); |
| 1591 | 1594 | ||
| 1592 | const s = parse(&.{ "muxd", "upgrade", "--sock", "/tmp/x.sock", "--allow-same-version" }); | 1595 | const s = parse(&.{ "muxd", "upgrade", "--sock", "/tmp/x.sock", "--allow-same-version" }); |
| 1593 | try std.testing.expect(s.ok.cmd == .upgrade); | 1596 | try std.testing.expect(s.ok._cmd == .upgrade); |
| 1594 | try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); | 1597 | try std.testing.expectEqualStrings("/tmp/x.sock", s.ok.sock.?); |
| 1595 | try std.testing.expect(s.ok.allow_same_version); | 1598 | try std.testing.expect(s.ok.allow_same_version); |
| 1596 | } | 1599 | } |
| @@ -1624,7 +1627,7 @@ test "resumeRun: --check adopts nothing, so --resume-fail-at has nothing to abor | |||
| 1624 | try file.writeAll(buf.items); | 1627 | try file.writeAll(buf.items); |
| 1625 | 1628 | ||
| 1626 | const code = try resumeRun(alloc, .{ | 1629 | const code = try resumeRun(alloc, .{ |
| 1627 | .cmd = .run, | 1630 | ._cmd = .run, |
| 1628 | .check = true, | 1631 | .check = true, |
| 1629 | .resume_fd = memfd, | 1632 | .resume_fd = memfd, |
| 1630 | .resume_fail_at = "daemon", | 1633 | .resume_fail_at = "daemon", |