a73x

502e12d7

refactor: the daemon's foreground is `start`, and `-d` is what forks it

a73x   2026-08-30 07:39

Commit message
refactor: the daemon's foreground is `start`, and `-d` is what forks it

`mux d run` and `mux d start` were two verbs for one daemon, and the
second one's whole content was a flag: fork, poll, report. One verb now,
one flag, and the child is handed the same line minus the `-d` that asked
for the fork.

The candidate argv an upgrade execs, and the rollback's, spell `start`
with it. There is no `run` row left, hidden or otherwise: a v0.0.1-16
daemon execs its successor as `d run`, so 16 to the next release is a
`mux d stop` then a `mux d start -d`, as 15 to 16 already was.

README.md
Old New
@@ -38,7 +38,7 @@ ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < dist/mux-vVERSIO
38 ## Quick start, local 38 ## Quick start, local
39 39
40 ```sh 40 ```sh
41 mux d run & # the daemon: hosts your shell and the authoritative screen 41 mux d start & # the daemon: hosts your shell and the authoritative screen
42 mux # attach 42 mux # attach
43 ``` 43 ```
44 44
@@ -182,18 +182,18 @@ Works anywhere ssh works. On the remote host: put `mux` on PATH (`make
182 install` puts it in `~/.local/bin`), then: 182 install` puts it in `~/.local/bin`), then:
183 183
184 ```sh 184 ```sh
185 ssh HOST 'mux d start' 185 ssh HOST 'mux d start -d'
186 mux HOST # attach; Ctrl-\ d detaches, running it again reattaches 186 mux HOST # attach; Ctrl-\ d detaches, running it again reattaches
187 ``` 187 ```
188 188
189 `mux d start` spawns the daemon detached and waits until it answers, so it 189 `mux d start -d` spawns the daemon detached and waits until it answers, so it
190 is safe to run every time — if one is already up it says so and exits 0. 190 is safe to run every time — if one is already up it says so and exits 0.
191 191
192 `mux HOST` runs `mux d endpoint --start` on HOST over ssh, ONE run 192 `mux HOST` runs `mux d endpoint --start` on HOST over ssh, ONE run
193 (finding a `mux` in `~/.local/bin` even though ssh's non-login shell leaves 193 (finding a `mux` in `~/.local/bin` even though ssh's non-login shell leaves
194 it off PATH). That verb ensures a daemon and then announces the 194 it off PATH). That verb ensures a daemon and then announces the
195 coordinates on the same stdout, so a cold attach costs one ssh login and 195 coordinates on the same stdout, so a cold attach costs one ssh login and
196 the `ssh HOST 'mux d start'` line above is optional after the first time. 196 the `ssh HOST 'mux d start -d'` line above is optional after the first time.
197 Nothing else starts a daemon, because nothing else spells the flag: the 197 Nothing else starts a daemon, because nothing else spells the flag: the
198 wall's per-host poll, `mux hosts`, every reconnect and `mux --via 'ssh HOST 198 wall's per-host poll, `mux hosts`, every reconnect and `mux --via 'ssh HOST
199 mux d proxy'` run the bare `mux d endpoint`, report the box unreachable and 199 mux d proxy'` run the bare `mux d endpoint`, report the box unreachable and
@@ -287,7 +287,7 @@ Put `mux` on the host (`scp` a static binary), then:
287 mux d keygen # once 287 mux d keygen # once
288 ssh HOST 'mkdir -p -m 700 ~/.config/mux && cat > ~/.config/mux/key \ 288 ssh HOST 'mkdir -p -m 700 ~/.config/mux && cat > ~/.config/mux/key \
289 && chmod 600 ~/.config/mux/key' < ~/.config/mux/key # once per host 289 && chmod 600 ~/.config/mux/key' < ~/.config/mux/key # once per host
290 ssh HOST 'mux d start --quic 0.0.0.0' # once per host boot 290 ssh HOST 'mux d start -d --quic 0.0.0.0' # once per host boot
291 mux quic://HOST # every attach 291 mux quic://HOST # every attach
292 ``` 292 ```
293 293
@@ -532,7 +532,7 @@ recently active client — typing, attaching, or resizing claims it (latest
532 wins). A session survives logout (this assumes systemd-logind's default 532 wins). A session survives logout (this assumes systemd-logind's default
533 `KillUserProcesses=no`; a box configured to kill user processes at logout 533 `KillUserProcesses=no`; a box configured to kill user processes at logout
534 kills the daemon with them), though not a reboot. 534 kills the daemon with them), though not a reboot.
535 `mux d run` refuses a socket another daemon already owns; there is no 535 `mux d start` refuses a socket another daemon already owns; there is no
536 socket-stealing. It runs until something stops it — no session's exit ends 536 socket-stealing. It runs until something stops it — no session's exit ends
537 it — and exits 0 when `mux d stop` or a supervisor's SIGTERM asks; a nonzero 537 it — and exits 0 when `mux d stop` or a supervisor's SIGTERM asks; a nonzero
538 exit means it never got as far as serving. 538 exit means it never got as far as serving.
@@ -556,7 +556,7 @@ ending, retry` rather than carrying a shell with no master across the exec.
556 556
557 Coming from v0.0.1-15 or older is the one upgrade `mux d upgrade` cannot 557 Coming from v0.0.1-15 or older is the one upgrade `mux d upgrade` cannot
558 do: that daemon probes a candidate for `muxd <version>` and this binary 558 do: that daemon probes a candidate for `muxd <version>` and this binary
559 prints `mux <version>`, so it refuses. `mux d stop` then `mux d start`, 559 prints `mux <version>`, so it refuses. `mux d stop` then `mux d start -d`,
560 once per box. `make install` deletes `muxd`, `muxa` and `muxweb` from the 560 once per box. `make install` deletes `muxd`, `muxa` and `muxweb` from the
561 install directory on its way in — they are modes of `mux` now, and a 561 install directory on its way in — they are modes of `mux` now, and a
562 `muxd` left on PATH is a second daemon waiting for someone to start it. 562 `muxd` left on PATH is a second daemon waiting for someone to start it.
src/cli/flags.zig
Old New
@@ -407,8 +407,8 @@ test "flagName: underscores become dashes" {
407 } 407 }
408 408
409 test "documented: a prefix of a longer flag does not count" { 409 test "documented: a prefix of a longer flag does not count" {
410 try std.testing.expect(!documented("--sock", " mux d run [--socket PATH]\n")); 410 try std.testing.expect(!documented("--sock", " mux d start [--socket PATH]\n"));
411 try std.testing.expect(documented("--sock", " mux d run [--sock PATH]\n")); 411 try std.testing.expect(documented("--sock", " mux d start [--sock PATH]\n"));
412 try std.testing.expect(documented("--vt", " mux d dump [--vt]\n")); 412 try std.testing.expect(documented("--vt", " mux d dump [--vt]\n"));
413 try std.testing.expect(documented("--vt", " mux d dump --vt\n")); 413 try std.testing.expect(documented("--vt", " mux d dump --vt\n"));
414 try std.testing.expect(documented("--vt", " mux d dump --vt")); 414 try std.testing.expect(documented("--vt", " mux d dump --vt"));
src/cli/main.zig
Old New
@@ -1,4 +1,4 @@
1 //! `mux d` — the daemon mode. `run` hosts the session; `dump` prints the 1 //! `mux d` — the daemon mode. `start` 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 // folder rule 5 exemption: the daemon's whole job is to spawn the user's 4 // folder rule 5 exemption: the daemon's whole job is to spawn the user's
@@ -20,15 +20,15 @@ const cliflags = @import("cliflags");
20 20
21 const usage = 21 const usage =
22 \\usage: 22 \\usage:
23 \\ mux d run [--sock PATH] [--shell PATH] [--cols N] [--rows N] 23 \\ mux d start [-d] [--sock PATH] [--shell PATH] [--cols N] [--rows N]
24 \\ [--quic HOST[:PORT] --key FILE] [--quic-idle-ms N] 24 \\ [--quic HOST[:PORT] --key FILE] [--quic-idle-ms N]
25 \\ (-d forks it off and waits; no-op if one is up)
25 \\ mux d dump [--vt] [--session NAME] [--sock PATH] 26 \\ mux d dump [--vt] [--session NAME] [--sock PATH]
26 \\ mux d stats [--sock PATH] 27 \\ mux d stats [--sock PATH]
27 \\ mux d stop [--sock PATH] (ask the daemon on PATH to exit) 28 \\ mux d stop [--sock PATH] (ask the daemon on PATH to exit)
28 \\ mux d proxy [--sock PATH] (byte pump: stdio <-> session socket) 29 \\ mux d proxy [--sock PATH] (byte pump: stdio <-> session socket)
29 \\ mux d endpoint [--sock PATH] [--start] (announce QUIC port+key, then proxy) 30 \\ mux d endpoint [--sock PATH] [--start] (announce QUIC port+key, then proxy)
30 \\ mux d keygen (write a fresh key to ~/.config/mux/key) 31 \\ mux d keygen (write a fresh key to ~/.config/mux/key)
31 \\ mux d start [run's flags] (spawn a daemon detached; no-op if one is up)
32 \\ mux d upgrade [--sock PATH] (exec THIS binary over the daemon; sessions live) 32 \\ mux d upgrade [--sock PATH] (exec THIS binary over the daemon; sessions live)
33 \\ [--allow-same-version] (strictly newer, unless this; the e2e leg's) 33 \\ [--allow-same-version] (strictly newer, unless this; the e2e leg's)
34 \\ mux d --version 34 \\ mux d --version
@@ -36,7 +36,7 @@ const usage =
36 \\ 36 \\
37 ; 37 ;
38 38
39 const Cmd = enum { run, dump, stats, proxy, endpoint, version, help, keygen, start, stop, upgrade }; 39 const Cmd = enum { dump, stats, proxy, endpoint, version, help, keygen, start, stop, upgrade };
40 40
41 /// One row per verb. Adding a subcommand used to mean editing the usage 41 /// One row per verb. Adding a subcommand used to mean editing the usage
42 /// literal, the Cmd enum, a name→Cmd if/else chain, keygen's hand-rolled 42 /// literal, the Cmd enum, a name→Cmd if/else chain, keygen's hand-rolled
@@ -74,7 +74,7 @@ const specs = [_]Spec{
74 // Spelled as a flag for the same reason, and `.ignored` for a second: 74 // Spelled as a flag for the same reason, and `.ignored` for a second:
75 // asking for the usage must never be refused over the words next to it. 75 // asking for the usage must never be refused over the words next to it.
76 .{ .name = "--help", .cmd = .help, .uses_socket = false, .flags = .ignored }, 76 .{ .name = "--help", .cmd = .help, .uses_socket = false, .flags = .ignored },
77 .{ .name = "run", .cmd = .run, .uses_socket = true, .flags = .all }, 77 .{ .name = "start", .cmd = .start, .uses_socket = true, .flags = .all },
78 .{ .name = "dump", .cmd = .dump, .uses_socket = true, .flags = .all }, 78 .{ .name = "dump", .cmd = .dump, .uses_socket = true, .flags = .all },
79 .{ .name = "stats", .cmd = .stats, .uses_socket = true, .flags = .all }, 79 .{ .name = "stats", .cmd = .stats, .uses_socket = true, .flags = .all },
80 .{ .name = "proxy", .cmd = .proxy, .uses_socket = true, .flags = .all }, 80 .{ .name = "proxy", .cmd = .proxy, .uses_socket = true, .flags = .all },
@@ -82,7 +82,6 @@ const specs = [_]Spec{
82 // keygen configures nothing: its one output is the default path, and a 82 // keygen configures nothing: its one output is the default path, and a
83 // flag here would be a request this command cannot honor. 83 // flag here would be a request this command cannot honor.
84 .{ .name = "keygen", .cmd = .keygen, .uses_socket = false, .flags = .none }, 84 .{ .name = "keygen", .cmd = .keygen, .uses_socket = false, .flags = .none },
85 .{ .name = "start", .cmd = .start, .uses_socket = true, .flags = .all },
86 .{ .name = "stop", .cmd = .stop, .uses_socket = true, .flags = .all }, 85 .{ .name = "stop", .cmd = .stop, .uses_socket = true, .flags = .all },
87 .{ .name = "upgrade", .cmd = .upgrade, .uses_socket = true, .flags = .all }, 86 .{ .name = "upgrade", .cmd = .upgrade, .uses_socket = true, .flags = .all },
88 }; 87 };
@@ -133,7 +132,7 @@ const Opts = struct {
133 vt: bool = false, 132 vt: bool = false,
134 quic: ?[]const u8 = null, 133 quic: ?[]const u8 = null,
135 /// Null does NOT mean "no key": it means the command line named none, 134 /// Null does NOT mean "no key": it means the command line named none,
136 /// and `run` still has MUX_KEY_FILE and the default path to try. Only 135 /// and `start` still has MUX_KEY_FILE and the default path to try. Only
137 /// `--key` without `--quic` is settled here, because that one has no 136 /// `--key` without `--quic` is settled here, because that one has no
138 /// reading that makes it sensible. 137 /// reading that makes it sensible.
139 key: ?[]const u8 = null, 138 key: ?[]const u8 = null,
@@ -143,9 +142,13 @@ const Opts = struct {
143 /// is offered to the type: `""` is the wire's own default spelling — no 142 /// is offered to the type: `""` is the wire's own default spelling — no
144 /// tail at all — and would fail a rule written for a name a user typed. 143 /// tail at all — and would fail a rule written for a name a user typed.
145 session: ?proto.SessionName = null, 144 session: ?proto.SessionName = null,
145 /// `start`'s alone: fork, hand the child the same line without this
146 /// flag, and wait until the socket answers. Off is the foreground
147 /// daemon — the process that binds is the one that was typed.
148 detach: bool = false,
146 /// The inherited manifest descriptor an upgrade exec'd us with. Not a 149 /// The inherited manifest descriptor an upgrade exec'd us with. Not a
147 /// user flag: the old daemon writes it into our argv. Its presence is 150 /// user flag: the old daemon writes it into our argv. Its presence is
148 /// what makes `run` an ADOPTION rather than a start, and it is also 151 /// what makes `start` an ADOPTION rather than a start, and it is also
149 /// what excuses this process from resolving a socket path — the 152 /// what excuses this process from resolving a socket path — the
150 /// manifest names the socket the inherited listener is already bound to. 153 /// manifest names the socket the inherited listener is already bound to.
151 resume_fd: ?std.posix.fd_t = null, 154 resume_fd: ?std.posix.fd_t = null,
@@ -164,11 +167,16 @@ const Opts = struct {
164 /// `endpoint`'s alone, and `parseArgs` refuses it on every other verb: 167 /// `endpoint`'s alone, and `parseArgs` refuses it on every other verb:
165 /// ensure a daemon on the socket, then announce as usual. It is what 168 /// ensure a daemon on the socket, then announce as usual. It is what
166 /// makes a cold `mux HOST` ONE ssh run — the client used to read the 169 /// makes a cold `mux HOST` ONE ssh run — the client used to read the
167 /// refusal, run `mux d start` over a second ssh, and ask again over a 170 /// refusal, run `mux d start -d` over a second ssh, and ask again over a
168 /// third. The decision is the REMOTE's now, and "a read never starts a 171 /// third. The decision is the REMOTE's now, and "a read never starts a
169 /// daemon" holds by argv: a poll spells `mux d endpoint` and starts 172 /// daemon" holds by argv: a poll spells `mux d endpoint` and starts
170 /// nothing, an ask spells this. 173 /// nothing, an ask spells this.
171 start: bool = false, 174 start: bool = false,
175
176 /// `-d` is what everyone types and the only spelling the prose offers;
177 /// `--detach` is the field's own name and works because cliflags reads
178 /// the struct.
179 pub const aliases = .{.{ "-d", "detach" }};
172 }; 180 };
173 181
174 // The three flags left out are written into argv by the OLD daemon on an 182 // The three flags left out are written into argv by the OLD daemon on an
@@ -316,8 +324,10 @@ pub fn main(args: []const [:0]const u8) !u8 {
316 .version => return cliflags.version("mux", build_options.version), 324 .version => return cliflags.version("mux", build_options.version),
317 .help => return usageExit(.help), 325 .help => return usageExit(.help),
318 .keygen => return keygen(alloc), 326 .keygen => return keygen(alloc),
319 .start => return startCmd(alloc, sock_path, args[2..]), 327 .start => return if (o.resume_fd) |fd|
320 .run => return if (o.resume_fd) |fd| resumeRun(alloc, o, fd) else run(alloc, o, sock_path), 328 resumeRun(alloc, o, fd)
329 else
330 startCmd(alloc, o, sock_path, args[2..]),
321 .dump => return dump(alloc, sock_path, o.vt, if (o.session) |n| n.name else ""), 331 .dump => return dump(alloc, sock_path, o.vt, if (o.session) |n| n.name else ""),
322 .stats => return stats(alloc, sock_path), 332 .stats => return stats(alloc, sock_path),
323 .stop => return stopCmd(alloc, sock_path), 333 .stop => return stopCmd(alloc, sock_path),
@@ -325,7 +335,7 @@ pub fn main(args: []const [:0]const u8) !u8 {
325 // A pump, and only a pump: a daemon starts when someone asks for 335 // A pump, and only a pump: a daemon starts when someone asks for
326 // one. `proxy.run` names the socket it could not reach, which is 336 // one. `proxy.run` names the socket it could not reach, which is
327 // what `mux --via 'ssh HOST mux d proxy'` shows a user whose remote 337 // what `mux --via 'ssh HOST mux d proxy'` shows a user whose remote
328 // has none — README's `ssh HOST 'mux d start'` is the answer. 338 // has none — README's `ssh HOST 'mux d start -d'` is the answer.
329 .proxy => return proxy.run(sock_path), 339 .proxy => return proxy.run(sock_path),
330 .endpoint => return endpointCmd(alloc, sock_path, std.posix.STDOUT_FILENO, o.start), 340 .endpoint => return endpointCmd(alloc, sock_path, std.posix.STDOUT_FILENO, o.start),
331 } 341 }
@@ -387,12 +397,12 @@ fn rollback(
387 // every descriptor is still open and still inherited, and the manifest 397 // every descriptor is still open and still inherited, and the manifest
388 // names the binary that opened them. 398 // names the binary that opened them.
389 std.debug.print( 399 std.debug.print(
390 "mux d run: adoption failed at {s}; exec'ing {s} back\n", 400 "mux d start: adoption failed at {s}; exec'ing {s} back\n",
391 .{ section, writer_path }, 401 .{ section, writer_path },
392 ); 402 );
393 if (std.posix.getenv(rollback_marker) != null) { 403 if (std.posix.getenv(rollback_marker) != null) {
394 std.debug.print( 404 std.debug.print(
395 "mux d run: this IS the rollback ({s} refused the manifest it wrote); giving up\n", 405 "mux d start: this IS the rollback ({s} refused the manifest it wrote); giving up\n",
396 .{writer_path}, 406 .{writer_path},
397 ); 407 );
398 return 1; 408 return 1;
@@ -405,11 +415,11 @@ fn rollback(
405 415
406 var fd_buf: [12]u8 = undefined; 416 var fd_buf: [12]u8 = undefined;
407 const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{resume_fd}) catch return 1; 417 const fd_str = std.fmt.bufPrintZ(&fd_buf, "{d}", .{resume_fd}) catch return 1;
408 // `d run`, the only spelling there is. The binary being exec'd back is 418 // `d start`, the only spelling there is. The binary being exec'd back is
409 // older than this one, but not by more than the rename: a daemon of 419 // older than this one, but not by more than the rename: a daemon of
410 // v0.0.1-15 or earlier refuses this binary as a candidate at its 420 // v0.0.1-15 or earlier refuses this binary as a candidate at its
411 // version probe, so it can never have been the writer here. 421 // version probe, so it can never have been the writer here.
412 const argv = [_:null]?[*:0]const u8{ "mux", "d", "run", "--resume-fd", fd_str.ptr }; 422 const argv = [_:null]?[*:0]const u8{ "mux", "d", "start", "--resume-fd", fd_str.ptr };
413 const path_z = alloc.dupeZ(u8, writer_path) catch return 1; 423 const path_z = alloc.dupeZ(u8, writer_path) catch return 1;
414 const envp = rollbackEnvp(alloc) catch return 1; 424 const envp = rollbackEnvp(alloc) catch return 1;
415 425
@@ -420,7 +430,7 @@ fn rollback(
420 alloc.free(path_z); 430 alloc.free(path_z);
421 alloc.free(std.mem.span(envp)); 431 alloc.free(std.mem.span(envp));
422 std.debug.print( 432 std.debug.print(
423 "mux d run: rollback exec of {s} failed: {s}\n", 433 "mux d start: rollback exec of {s} failed: {s}\n",
424 .{ writer_path, @errorName(exec_err) }, 434 .{ writer_path, @errorName(exec_err) },
425 ); 435 );
426 return 1; 436 return 1;
@@ -447,7 +457,7 @@ fn rollbackEnvp(alloc: std.mem.Allocator) ![*:null]const ?[*:0]const u8 {
447 return envp.ptr; 457 return envp.ptr;
448 } 458 }
449 459
450 /// `mux d run --resume-fd N`: the argv an upgrading daemon exec'd this binary 460 /// `mux d start --resume-fd N`: the argv an upgrading daemon exec'd this binary
451 /// with. Same pid, same children, same descriptors — the manifest names 461 /// with. Same pid, same children, same descriptors — the manifest names
452 /// which ones. It is read from the descriptor and never from a path: the 462 /// which ones. It is read from the descriptor and never from a path: the
453 /// memfd is anonymous memory, and the QUIC key inside it must not touch disk. 463 /// memfd is anonymous memory, and the QUIC key inside it must not touch disk.
@@ -456,17 +466,17 @@ fn resumeRun(alloc: std.mem.Allocator, o: Opts, resume_fd: std.posix.fd_t) !u8 {
456 // shares the file description with it, so the rewind is ours to do. 466 // shares the file description with it, so the rewind is ours to do.
457 var file = std.fs.File{ .handle = resume_fd }; 467 var file = std.fs.File{ .handle = resume_fd };
458 file.seekTo(0) catch |err| { 468 file.seekTo(0) catch |err| {
459 std.debug.print("mux d run: --resume-fd {d} does not seek ({t})\n", .{ resume_fd, err }); 469 std.debug.print("mux d start: --resume-fd {d} does not seek ({t})\n", .{ resume_fd, err });
460 return 1; 470 return 1;
461 }; 471 };
462 const bytes = file.readToEndAlloc(alloc, manifest_read_max) catch |err| { 472 const bytes = file.readToEndAlloc(alloc, manifest_read_max) catch |err| {
463 std.debug.print("mux d run: cannot read the manifest on fd {d} ({t})\n", .{ resume_fd, err }); 473 std.debug.print("mux d start: cannot read the manifest on fd {d} ({t})\n", .{ resume_fd, err });
464 return 1; 474 return 1;
465 }; 475 };
466 defer alloc.free(bytes); 476 defer alloc.free(bytes);
467 477
468 var parsed = upgrade.parseManifest(alloc, bytes) catch |err| { 478 var parsed = upgrade.parseManifest(alloc, bytes) catch |err| {
469 std.debug.print("mux d run: manifest on fd {d} is not one ({t})\n", .{ resume_fd, err }); 479 std.debug.print("mux d start: manifest on fd {d} is not one ({t})\n", .{ resume_fd, err });
470 return 1; 480 return 1;
471 }; 481 };
472 defer parsed.deinit(); 482 defer parsed.deinit();
@@ -509,7 +519,7 @@ fn resumeRun(alloc: std.mem.Allocator, o: Opts, resume_fd: std.posix.fd_t) !u8 {
509 return try srv.run(); 519 return try srv.run();
510 } 520 }
511 521
512 /// `mux d run`: the daemon, in the foreground. Every code this function picks 522 /// `mux d start`: the daemon, in the foreground. Every code this function picks
513 /// is a boot failure — an operator mistake caught before anything bound. 523 /// is a boot failure — an operator mistake caught before anything bound.
514 /// Reaching `srv.run()` means the daemon served, and it answers 0 whenever 524 /// Reaching `srv.run()` means the daemon served, and it answers 0 whenever
515 /// something asks it to stop. No session's exit is ever reported here: a 525 /// something asks it to stop. No session's exit is ever reported here: a
@@ -719,7 +729,7 @@ fn oneShotQuery(
719 ) !u8 { 729 ) !u8 {
720 const stream = std.net.connectUnixSocket(sock_path) catch { 730 const stream = std.net.connectUnixSocket(sock_path) catch {
721 std.debug.print( 731 std.debug.print(
722 "mux d {s}: nothing listening on {s} (`mux d start` starts a daemon)\n", 732 "mux d {s}: nothing listening on {s} (`mux d start -d` starts one)\n",
723 .{ verb, sock_path }, 733 .{ verb, sock_path },
724 ); 734 );
725 return 1; 735 return 1;
@@ -749,7 +759,7 @@ fn stats(alloc: std.mem.Allocator, sock_path: []const u8) !u8 {
749 /// Ask the daemon on `sock_path` to exit, then wait until the PROCESS is 759 /// Ask the daemon on `sock_path` to exit, then wait until the PROCESS is
750 /// gone, not just the path. Exit 0 covers both "stopped" and "nothing 760 /// gone, not just the path. Exit 0 covers both "stopped" and "nothing
751 /// there" — the state the user asked for is the state they got, which is 761 /// there" — the state the user asked for is the state they got, which is
752 /// what makes the verb safe to script (`mux d start`'s re-runnability, 762 /// what makes the verb safe to script (`mux d start -d`'s re-runnability,
753 /// mirrored). 763 /// mirrored).
754 /// 764 ///
755 /// Prefixes split the way `start`'s do: `mux d stop:` for a refusal or a 765 /// Prefixes split the way `start`'s do: `mux d stop:` for a refusal or a
@@ -814,7 +824,7 @@ fn peerPid(fd: std.posix.socket_t) ?std.posix.pid_t {
814 /// A socket gone quiet is the unlink, and the unlink is the FIRST thing a 824 /// A socket gone quiet is the unlink, and the unlink is the FIRST thing a
815 /// stopping daemon does; reaping its shells (one TERM grace when one 825 /// stopping daemon does; reaping its shells (one TERM grace when one
816 /// ignores it) and deleting its dirs come after. "stopped" said at the 826 /// ignores it) and deleting its dirs come after. "stopped" said at the
817 /// unlink handed a scripted `mux d start`, or a supervisor's "is it down", 827 /// unlink handed a scripted `mux d start -d`, or a supervisor's "is it down",
818 /// a daemon still running. The bound is the reap's own grace with room to 828 /// a daemon still running. The bound is the reap's own grace with room to
819 /// spare — a daemon still here after it is wedged in teardown, and that 829 /// spare — a daemon still here after it is wedged in teardown, and that
820 /// is a report, not a wait. 830 /// is a report, not a wait.
@@ -908,7 +918,7 @@ fn upgradeCmd(alloc: std.mem.Allocator, sock_path: []const u8, allow_same: bool)
908 "mux d upgrade: if that daemon is v0.0.1-15 or older, it wants a " ++ 918 "mux d upgrade: if that daemon is v0.0.1-15 or older, it wants a " ++
909 "candidate that prints `muxd <version>` and this one is `mux`. " ++ 919 "candidate that prints `muxd <version>` and this one is `mux`. " ++
910 "There is no in-place path across that rename: `mux d stop` then " ++ 920 "There is no in-place path across that rename: `mux d stop` then " ++
911 "`mux d start`, once.\n", 921 "`mux d start -d`, once.\n",
912 .{}, 922 .{},
913 ); 923 );
914 return 1; 924 return 1;
@@ -957,7 +967,7 @@ const log_hint_len = std.fs.max_path_bytes + 64;
957 /// 967 ///
958 /// Only when the path resolves: an absent HOME (a container, a systemd 968 /// Only when the path resolves: an absent HOME (a container, a systemd
959 /// unit) must not replace the finding that matters with an error trace. 969 /// unit) must not replace the finding that matters with an error trace.
960 /// And the hedge stays in the words: a foreground `mux d run` logs to 970 /// And the hedge stays in the words: a foreground `mux d start` logs to
961 /// its own stderr, so naming the xdg path unconditionally would guess. 971 /// its own stderr, so naming the xdg path unconditionally would guess.
962 fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 { 972 fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 {
963 const log = xdg.logPath(alloc) catch return ""; 973 const log = xdg.logPath(alloc) catch return "";
@@ -1021,9 +1031,9 @@ fn endpointCmd(alloc: std.mem.Allocator, sock_path: []const u8, out_fd: std.posi
1021 // — a stray daemon nothing reports. `spawn.ensureForAttach` forwards it 1031 // — a stray daemon nothing reports. `spawn.ensureForAttach` forwards it
1022 // for the same reason. Nothing else is: a bare daemon binds its QUIC 1032 // for the same reason. Nothing else is: a bare daemon binds its QUIC
1023 // listener when the announce below asks it to, which is how a plain 1033 // listener when the announce below asks it to, which is how a plain
1024 // `mux d start` already produces a port. 1034 // `mux d start -d` already produces a port.
1025 // 1035 //
1026 // Truncating, like `mux d start`: the user asked for a daemon, so the 1036 // Truncating, like `mux d start -d`: the user asked for a daemon, so the
1027 // log they go on to read must be about the one they just got. 1037 // log they go on to read must be about the one they just got.
1028 // 1038 //
1029 // A spawn that failed has already said so on stderr, which ssh carries 1039 // A spawn that failed has already said so on stderr, which ssh carries
@@ -1223,18 +1233,21 @@ fn askEndpointPort(alloc: std.mem.Allocator, sock_path: []const u8) u16 {
1223 return proto.decodeEndpointReply(frame.payload) catch 0; 1233 return proto.decodeEndpointReply(frame.payload) catch 0;
1224 } 1234 }
1225 1235
1226 /// `mux d start` = ensureDaemon under an explicit flag. Everything after 1236 /// `mux d start`: the daemon, in this process, or — under `-d` — in a
1227 /// `start` is forwarded to `run` verbatim — no re-serialization, so a flag 1237 /// child of it. Everything after `start` is forwarded to the child
1228 /// that parses here behaves identically there. parseArgs has already 1238 /// verbatim minus the `-d` itself, so a flag that parses here behaves
1229 /// validated the flags in THIS process; what it cannot validate (a bad 1239 /// identically there. parseArgs has already validated the flags in THIS
1230 /// bind address, a missing key file) surfaces in the daemon's log, which 1240 /// process; what it cannot validate (a bad bind address, a missing key
1231 /// the failure path names. 1241 /// file) surfaces in the daemon's log, which the failure path names.
1232 fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const [:0]const u8) !u8 { 1242 fn startCmd(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8, forwarded: []const [:0]const u8) !u8 {
1243 if (!o.detach) return run(alloc, o, sock_path);
1244 const child_args = try withoutDetach(alloc, forwarded);
1245 defer alloc.free(child_args);
1233 // Default log path: the xdg one is the whole point for a real daemon. 1246 // Default log path: the xdg one is the whole point for a real daemon.
1234 // Truncating, and this is the only caller that does: `start` is the one 1247 // Truncating, and this is the only caller that does: `-d` is the one
1235 // verb whose user asked for a (re)start, so the log they go on to read 1248 // flag whose user asked for a (re)start, so the log they go on to read
1236 // must be about the daemon they just started. 1249 // must be about the daemon they just started.
1237 const r = spawn.ensure(alloc, forwarded, sock_path, "mux d", .{ .truncate = true }) orelse return 1; 1250 const r = spawn.ensure(alloc, child_args, sock_path, "mux d", .{ .truncate = true }) orelse return 1;
1238 if (r == .already_running) { 1251 if (r == .already_running) {
1239 std.debug.print( 1252 std.debug.print(
1240 "mux d: already running on {s} (stop it first with `mux d stop --sock {s}` if you meant different flags)\n", 1253 "mux d: already running on {s} (stop it first with `mux d stop --sock {s}` if you meant different flags)\n",
@@ -1244,6 +1257,46 @@ fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const
1244 return 0; 1257 return 0;
1245 } 1258 }
1246 1259
1260 /// `start`'s own words back, minus the flag that asked for the fork. The
1261 /// child is the FOREGROUND daemon: a `-d` still in its line would fork
1262 /// again, and its child again, for as long as the machine lasted.
1263 ///
1264 /// A word is only a flag where a flag can stand — `--shell -d` names a
1265 /// shell — so the walk skips each value-taking flag's value, reading the
1266 /// arity off `Opts` exactly as cliflags does.
1267 fn withoutDetach(alloc: std.mem.Allocator, args: []const [:0]const u8) ![]const [:0]const u8 {
1268 var kept: std.ArrayList([:0]const u8) = .empty;
1269 errdefer kept.deinit(alloc);
1270 var i: usize = 0;
1271 while (i < args.len) : (i += 1) {
1272 const a = args[i];
1273 if (std.mem.eql(u8, a, "-d") or std.mem.eql(u8, a, "--detach")) continue;
1274 try kept.append(alloc, a);
1275 if (takesValue(a) and i + 1 < args.len) {
1276 i += 1;
1277 try kept.append(alloc, args[i]);
1278 }
1279 }
1280 return kept.toOwnedSlice(alloc);
1281 }
1282
1283 /// Whether `word` is one of `Opts`' flags that eats the word after it.
1284 /// Read off the struct, so a field added later is covered without a second
1285 /// list to keep in step.
1286 fn takesValue(word: []const u8) bool {
1287 inline for (@typeInfo(Opts).@"struct".fields) |f| {
1288 if (f.name[0] != '_') {
1289 const B = if (@typeInfo(f.type) == .optional)
1290 @typeInfo(f.type).optional.child
1291 else
1292 f.type;
1293 if (B != bool and std.mem.eql(u8, word, comptime cliflags.flagName(f.name)))
1294 return true;
1295 }
1296 }
1297 return false;
1298 }
1299
1247 fn keygen(alloc: std.mem.Allocator) !u8 { 1300 fn keygen(alloc: std.mem.Allocator) !u8 {
1248 const path = try xdg.keyPath(alloc); 1301 const path = try xdg.keyPath(alloc);
1249 defer alloc.free(path); 1302 defer alloc.free(path);
@@ -1283,9 +1336,9 @@ fn nameZ(comptime name: []const u8) [:0]const u8 {
1283 } 1336 }
1284 1337
1285 test "parseArgs: subcommands and their existing flags" { 1338 test "parseArgs: subcommands and their existing flags" {
1286 const r = parse(&.{ "d", "run" }); 1339 const r = parse(&.{ "d", "start" });
1287 try std.testing.expect(r == .ok); 1340 try std.testing.expect(r == .ok);
1288 try std.testing.expect(r.ok._cmd == .run); 1341 try std.testing.expect(r.ok._cmd == .start);
1289 try std.testing.expect(r.ok.sock == null); 1342 try std.testing.expect(r.ok.sock == null);
1290 try std.testing.expectEqual(@as(u16, 80), r.ok.cols); 1343 try std.testing.expectEqual(@as(u16, 80), r.ok.cols);
1291 try std.testing.expectEqual(@as(u16, 24), r.ok.rows); 1344 try std.testing.expectEqual(@as(u16, 24), r.ok.rows);
@@ -1298,14 +1351,14 @@ test "parseArgs: subcommands and their existing flags" {
1298 // on the wire as the empty tail. 1351 // on the wire as the empty tail.
1299 try std.testing.expect(d.ok.session == null); 1352 try std.testing.expect(d.ok.session == null);
1300 1353
1301 const g = parse(&.{ "d", "run", "--cols", "120", "--rows", "40", "--shell", "/bin/dash" }); 1354 const g = parse(&.{ "d", "start", "--cols", "120", "--rows", "40", "--shell", "/bin/dash" });
1302 try std.testing.expectEqual(@as(u16, 120), g.ok.cols); 1355 try std.testing.expectEqual(@as(u16, 120), g.ok.cols);
1303 try std.testing.expectEqual(@as(u16, 40), g.ok.rows); 1356 try std.testing.expectEqual(@as(u16, 40), g.ok.rows);
1304 try std.testing.expectEqualStrings("/bin/dash", g.ok.shell.?); 1357 try std.testing.expectEqualStrings("/bin/dash", g.ok.shell.?);
1305 1358
1306 try std.testing.expect(parse(&.{"d"}).err == .no_command); 1359 try std.testing.expect(parse(&.{"d"}).err == .no_command);
1307 try std.testing.expect(parse(&.{ "d", "wat" }).err == .unknown_command); 1360 try std.testing.expect(parse(&.{ "d", "wat" }).err == .unknown_command);
1308 try std.testing.expect(parse(&.{ "d", "run", "--wat" }).err == .unknown_arg); 1361 try std.testing.expect(parse(&.{ "d", "start", "--wat" }).err == .unknown_arg);
1309 } 1362 }
1310 1363
1311 test "parse: dump --session rides into the payload" { 1364 test "parse: dump --session rides into the payload" {
@@ -1323,60 +1376,60 @@ test "parse: dump --session rides into the payload" {
1323 } 1376 }
1324 1377
1325 test "parseArgs: --key without --quic is refused; --quic alone defers to main" { 1378 test "parseArgs: --key without --quic is refused; --quic alone defers to main" {
1326 const both = parse(&.{ "d", "run", "--quic", "0.0.0.0:4433", "--key", "/k" }); 1379 const both = parse(&.{ "d", "start", "--quic", "0.0.0.0:4433", "--key", "/k" });
1327 try std.testing.expect(both == .ok); 1380 try std.testing.expect(both == .ok);
1328 try std.testing.expectEqualStrings("0.0.0.0:4433", both.ok.quic.?); 1381 try std.testing.expectEqualStrings("0.0.0.0:4433", both.ok.quic.?);
1329 try std.testing.expectEqualStrings("/k", both.ok.key.?); 1382 try std.testing.expectEqualStrings("/k", both.ok.key.?);
1330 1383
1331 // --quic without --key is no longer a parse error: main resolves 1384 // --quic without --key is no longer a parse error: main resolves
1332 // MUX_KEY_FILE and the default path, and parse cannot see either. 1385 // MUX_KEY_FILE and the default path, and parse cannot see either.
1333 const deferred = parse(&.{ "d", "run", "--quic", "0.0.0.0:4433" }); 1386 const deferred = parse(&.{ "d", "start", "--quic", "0.0.0.0:4433" });
1334 try std.testing.expect(deferred == .ok); 1387 try std.testing.expect(deferred == .ok);
1335 try std.testing.expect(deferred.ok.key == null); 1388 try std.testing.expect(deferred.ok.key == null);
1336 1389
1337 // A key with nowhere to listen is still a mistake with no reading that 1390 // A key with nowhere to listen is still a mistake with no reading that
1338 // makes it sensible, and parse can see the whole of it. 1391 // makes it sensible, and parse can see the whole of it.
1339 try std.testing.expect(parse(&.{ "d", "run", "--key", "/k" }).err == .key_without_quic); 1392 try std.testing.expect(parse(&.{ "d", "start", "--key", "/k" }).err == .key_without_quic);
1340 1393
1341 // Neither is the ordinary case and must stay silent. 1394 // Neither is the ordinary case and must stay silent.
1342 const neither = parse(&.{ "d", "run" }); 1395 const neither = parse(&.{ "d", "start" });
1343 try std.testing.expect(neither.ok.quic == null); 1396 try std.testing.expect(neither.ok.quic == null);
1344 try std.testing.expect(neither.ok.key == null); 1397 try std.testing.expect(neither.ok.key == null);
1345 } 1398 }
1346 1399
1347 test "parseArgs: --quic-idle-ms defaults, parses, and refuses nonsense" { 1400 test "parseArgs: --quic-idle-ms defaults, parses, and refuses nonsense" {
1348 const dflt = parse(&.{ "d", "run", "--quic", "127.0.0.1:1", "--key", "/k" }); 1401 const dflt = parse(&.{ "d", "start", "--quic", "127.0.0.1:1", "--key", "/k" });
1349 // Spelled out rather than written `quic.default_idle_ms`: asserting 1402 // Spelled out rather than written `quic.default_idle_ms`: asserting
1350 // against the same constant the parser reads would hold for any value, 1403 // against the same constant the parser reads would hold for any value,
1351 // so it could never catch the number changing. 1404 // so it could never catch the number changing.
1352 try std.testing.expectEqual(@as(u32, 15_000), dflt.ok.quic_idle_ms.ms); 1405 try std.testing.expectEqual(@as(u32, 15_000), dflt.ok.quic_idle_ms.ms);
1353 1406
1354 const set = parse(&.{ "d", "run", "--quic", "127.0.0.1:1", "--key", "/k", "--quic-idle-ms", "2500" }); 1407 const set = parse(&.{ "d", "start", "--quic", "127.0.0.1:1", "--key", "/k", "--quic-idle-ms", "2500" });
1355 try std.testing.expectEqual(@as(u32, 2500), set.ok.quic_idle_ms.ms); 1408 try std.testing.expectEqual(@as(u32, 2500), set.ok.quic_idle_ms.ms);
1356 1409
1357 // Zero means "no idle timeout" to ngtcp2 — the opposite of what anyone 1410 // Zero means "no idle timeout" to ngtcp2 — the opposite of what anyone
1358 // typing a timeout of zero is asking for, so it is refused rather than 1411 // typing a timeout of zero is asking for, so it is refused rather than
1359 // silently inverted. 1412 // silently inverted.
1360 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "0" }).err == .bad_value); 1413 try std.testing.expect(parse(&.{ "d", "start", "--quic-idle-ms", "0" }).err == .bad_value);
1361 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "soon" }).err == .bad_value); 1414 try std.testing.expect(parse(&.{ "d", "start", "--quic-idle-ms", "soon" }).err == .bad_value);
1362 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "-5" }).err == .bad_value); 1415 try std.testing.expect(parse(&.{ "d", "start", "--quic-idle-ms", "-5" }).err == .bad_value);
1363 // Wider than u32: refused at the parse rather than overflowing where it 1416 // Wider than u32: refused at the parse rather than overflowing where it
1364 // is multiplied out to nanoseconds. 1417 // is multiplied out to nanoseconds.
1365 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "99999999999" }).err == .bad_value); 1418 try std.testing.expect(parse(&.{ "d", "start", "--quic-idle-ms", "99999999999" }).err == .bad_value);
1366 // The idle flag alone does not turn QUIC on, and must not smuggle the 1419 // The idle flag alone does not turn QUIC on, and must not smuggle the
1367 // both-or-neither rule past the check. 1420 // both-or-neither rule past the check.
1368 try std.testing.expect(parse(&.{ "d", "run", "--quic-idle-ms", "2500" }) == .ok); 1421 try std.testing.expect(parse(&.{ "d", "start", "--quic-idle-ms", "2500" }) == .ok);
1369 1422
1370 // Same treatment for the numbers that were already here. 1423 // Same treatment for the numbers that were already here.
1371 try std.testing.expect(parse(&.{ "d", "run", "--cols", "wide" }).err == .bad_value); 1424 try std.testing.expect(parse(&.{ "d", "start", "--cols", "wide" }).err == .bad_value);
1372 try std.testing.expect(parse(&.{ "d", "run", "--rows", "99999" }).err == .bad_value); 1425 try std.testing.expect(parse(&.{ "d", "start", "--rows", "99999" }).err == .bad_value);
1373 } 1426 }
1374 1427
1375 test "parseArgs: a value-taking flag at the end of argv names itself" { 1428 test "parseArgs: a value-taking flag at the end of argv names itself" {
1376 // This used to report "unknown argument: --quic", which blames the flag 1429 // This used to report "unknown argument: --quic", which blames the flag
1377 // rather than the missing value. 1430 // rather than the missing value.
1378 inline for (.{ "--sock", "--shell", "--cols", "--rows", "--quic", "--key", "--quic-idle-ms", "--session", "--resume-fd", "--resume-fail-at" }) |flag| { 1431 inline for (.{ "--sock", "--shell", "--cols", "--rows", "--quic", "--key", "--quic-idle-ms", "--session", "--resume-fd", "--resume-fail-at" }) |flag| {
1379 const r = parse(&.{ "d", "run", flag }); 1432 const r = parse(&.{ "d", "start", flag });
1380 try std.testing.expect(r.err == .missing_value); 1433 try std.testing.expect(r.err == .missing_value);
1381 try std.testing.expectEqualStrings(flag, r.err.missing_value); 1434 try std.testing.expectEqualStrings(flag, r.err.missing_value);
1382 } 1435 }
@@ -1432,13 +1485,13 @@ test "parseArgs: --version is a command, not a flag on one" {
1432 try std.testing.expect(r == .ok); 1485 try std.testing.expect(r == .ok);
1433 try std.testing.expect(r.ok._cmd == .version); 1486 try std.testing.expect(r.ok._cmd == .version);
1434 1487
1435 // Typed onto a verb it becomes that same command, so `mux d run 1488 // Typed onto a verb it becomes that same command, so `mux d start
1436 // --version` answers instead of refusing an unknown flag. Asserted on 1489 // --version` answers instead of refusing an unknown flag. Asserted on
1437 // the parse rather than on `main`, which writes the version to STDOUT 1490 // the parse rather than on `main`, which writes the version to STDOUT
1438 // and would hang the build runner's IPC. 1491 // and would hang the build runner's IPC.
1439 const on_run = parse(&.{ "d", "run", "--sock", "/x", "--version" }); 1492 const on_start = parse(&.{ "d", "start", "--sock", "/x", "--version" });
1440 try std.testing.expect(on_run == .ok); 1493 try std.testing.expect(on_start == .ok);
1441 try std.testing.expect(on_run.ok._cmd == .version); 1494 try std.testing.expect(on_start.ok._cmd == .version);
1442 } 1495 }
1443 1496
1444 test "parseArgs: --help is a command, and a flag on one, and both exit 0 on stdout" { 1497 test "parseArgs: --help is a command, and a flag on one, and both exit 0 on stdout" {
@@ -1449,9 +1502,9 @@ test "parseArgs: --help is a command, and a flag on one, and both exit 0 on stdo
1449 // On a subcommand it is an outcome of the flag parse rather than a row, 1502 // On a subcommand it is an outcome of the flag parse rather than a row,
1450 // and it must outrank the grammar: `--sock` here is still waiting for a 1503 // and it must outrank the grammar: `--sock` here is still waiting for a
1451 // value, and asking for the usage is not a way to mistype one. 1504 // value, and asking for the usage is not a way to mistype one.
1452 try std.testing.expect(parse(&.{ "d", "run", "--help" }).err == .help); 1505 try std.testing.expect(parse(&.{ "d", "start", "--help" }).err == .help);
1453 try std.testing.expect(parse(&.{ "d", "dump", "-h", "--sock", "/x" }).err == .help); 1506 try std.testing.expect(parse(&.{ "d", "dump", "-h", "--sock", "/x" }).err == .help);
1454 try std.testing.expect(parse(&.{ "d", "run", "--sock", "--help" }).err == .help); 1507 try std.testing.expect(parse(&.{ "d", "start", "--sock", "--help" }).err == .help);
1455 1508
1456 // The code, asked of `usageCode` rather than of `usageExit`: the latter 1509 // The code, asked of `usageCode` rather than of `usageExit`: the latter
1457 // writes the usage to STDOUT, which under `zig build test` is the build 1510 // writes the usage to STDOUT, which under `zig build test` is the build
@@ -1469,12 +1522,39 @@ test "parseArgs: keygen takes no flags" {
1469 try std.testing.expect(parse(&.{ "d", "keygen", "--sock", "/x" }).err == .unknown_arg); 1522 try std.testing.expect(parse(&.{ "d", "keygen", "--sock", "/x" }).err == .unknown_arg);
1470 } 1523 }
1471 1524
1472 test "parseArgs: start takes run's flags" { 1525 test "parseArgs: -d is start's own flag, and the rest are the daemon's" {
1473 const r = parse(&.{ "d", "start", "--sock", "/tmp/x.sock", "--cols", "100" }); 1526 const r = parse(&.{ "d", "start", "--sock", "/tmp/x.sock", "--cols", "100" });
1474 try std.testing.expect(r == .ok); 1527 try std.testing.expect(r == .ok);
1475 try std.testing.expect(r.ok._cmd == .start); 1528 try std.testing.expect(r.ok._cmd == .start);
1476 try std.testing.expectEqualStrings("/tmp/x.sock", r.ok.sock.?); 1529 try std.testing.expectEqualStrings("/tmp/x.sock", r.ok.sock.?);
1477 try std.testing.expectEqual(@as(u16, 100), r.ok.cols); 1530 try std.testing.expectEqual(@as(u16, 100), r.ok.cols);
1531 // Off unless typed: the process that binds is the one that was typed,
1532 // and a `start` that forked by default would put the daemon somewhere
1533 // the user's shell cannot see it fail.
1534 try std.testing.expect(!r.ok.detach);
1535
1536 const d = parse(&.{ "d", "start", "-d", "--sock", "/tmp/x.sock" });
1537 try std.testing.expect(d.ok.detach);
1538 }
1539
1540 test "withoutDetach: the child is handed the line minus the fork, values and all" {
1541 const alloc = std.testing.allocator;
1542
1543 // The whole point: a `-d` still in the child's argv forks again, and
1544 // its child again, for as long as the machine lasts.
1545 const stripped = try withoutDetach(alloc, &.{ "-d", "--sock", "/s", "--cols", "100" });
1546 defer alloc.free(stripped);
1547 try std.testing.expectEqual(@as(usize, 4), stripped.len);
1548 try std.testing.expectEqualStrings("--sock", stripped[0]);
1549 try std.testing.expectEqualStrings("/s", stripped[1]);
1550
1551 // A word is only a flag where a flag can stand. `--shell -d` names a
1552 // shell called `-d`, and a blind filter would hand the daemon
1553 // `--shell --cols` instead.
1554 const value = try withoutDetach(alloc, &.{ "--shell", "-d", "--cols", "100" });
1555 defer alloc.free(value);
1556 try std.testing.expectEqual(@as(usize, 4), value.len);
1557 try std.testing.expectEqualStrings("-d", value[1]);
1478 } 1558 }
1479 1559
1480 test "parseArgs: stop is a command and takes --sock" { 1560 test "parseArgs: stop is a command and takes --sock" {
@@ -1551,24 +1631,24 @@ test "parseArgs: --start belongs to endpoint alone, and every other verb refuses
1551 } 1631 }
1552 } 1632 }
1553 1633
1554 test "parseArgs: run --resume-fd N --check is the old daemon's dry run" { 1634 test "parseArgs: start --resume-fd N --check is the old daemon's dry run" {
1555 const r = parse(&.{ "d", "run", "--resume-fd", "7", "--check" }); 1635 const r = parse(&.{ "d", "start", "--resume-fd", "7", "--check" });
1556 try std.testing.expect(r == .ok); 1636 try std.testing.expect(r == .ok);
1557 try std.testing.expect(r.ok._cmd == .run); 1637 try std.testing.expect(r.ok._cmd == .start);
1558 try std.testing.expectEqual(@as(std.posix.fd_t, 7), r.ok.resume_fd.?); 1638 try std.testing.expectEqual(@as(std.posix.fd_t, 7), r.ok.resume_fd.?);
1559 try std.testing.expect(r.ok.check); 1639 try std.testing.expect(r.ok.check);
1560 1640
1561 // A number, like --cols: an fd that is not one would be read as a 1641 // A number, like --cols: an fd that is not one would be read as a
1562 // descriptor the daemon never passed. 1642 // descriptor the daemon never passed.
1563 try std.testing.expect(parse(&.{ "d", "run", "--resume-fd", "x" }).err == .bad_value); 1643 try std.testing.expect(parse(&.{ "d", "start", "--resume-fd", "x" }).err == .bad_value);
1564 1644
1565 const f = parse(&.{ "d", "run", "--resume-fd", "3", "--resume-fail-at", "session" }); 1645 const f = parse(&.{ "d", "start", "--resume-fd", "3", "--resume-fail-at", "session" });
1566 try std.testing.expectEqualStrings("session", f.ok.resume_fail_at.?); 1646 try std.testing.expectEqualStrings("session", f.ok.resume_fail_at.?);
1567 1647
1568 // Neither flag is the ordinary start, and both must stay off there — 1648 // Neither flag is the ordinary start, and both must stay off there —
1569 // a `run` that thought it was resuming would adopt nothing and serve 1649 // a `start` that thought it was resuming would adopt nothing and serve
1570 // nothing. 1650 // nothing.
1571 const plain = parse(&.{ "d", "run" }); 1651 const plain = parse(&.{ "d", "start" });
1572 try std.testing.expect(plain.ok.resume_fd == null); 1652 try std.testing.expect(plain.ok.resume_fd == null);
1573 try std.testing.expect(!plain.ok.check); 1653 try std.testing.expect(!plain.ok.check);
1574 } 1654 }
@@ -1634,7 +1714,7 @@ test "resumeRun: --check adopts nothing, so --resume-fail-at has nothing to abor
1634 try file.writeAll(buf.items); 1714 try file.writeAll(buf.items);
1635 1715
1636 const code = try resumeRun(alloc, .{ 1716 const code = try resumeRun(alloc, .{
1637 ._cmd = .run, 1717 ._cmd = .start,
1638 .check = true, 1718 .check = true,
1639 .resume_fd = memfd, 1719 .resume_fd = memfd,
1640 .resume_fail_at = "daemon", 1720 .resume_fail_at = "daemon",
src/cli/mux.zig
Old New
@@ -45,14 +45,14 @@ pub fn main() !u8 {
45 } 45 }
46 46
47 test "modeOf: the three mode words, and nothing else" { 47 test "modeOf: the three mode words, and nothing else" {
48 try std.testing.expectEqual(Mode.daemon, modeOf(&.{ "mux", "d", "run" })); 48 try std.testing.expectEqual(Mode.daemon, modeOf(&.{ "mux", "d", "start" }));
49 try std.testing.expectEqual(Mode.agent, modeOf(&.{ "mux", "a", "status" })); 49 try std.testing.expectEqual(Mode.agent, modeOf(&.{ "mux", "a", "status" }));
50 try std.testing.expectEqual(Mode.hub, modeOf(&.{ "mux", "web" })); 50 try std.testing.expectEqual(Mode.hub, modeOf(&.{ "mux", "web" }));
51 try std.testing.expectEqual(Mode.client, modeOf(&.{"mux"})); 51 try std.testing.expectEqual(Mode.client, modeOf(&.{"mux"}));
52 try std.testing.expectEqual(Mode.client, modeOf(&.{ "mux", "box" })); 52 try std.testing.expectEqual(Mode.client, modeOf(&.{ "mux", "box" }));
53 } 53 }
54 54
55 test "modeOf: `run` is a host, not an alias for `d run`" { 55 test "modeOf: `run` is a host, and no daemon verb has a top-level alias" {
56 // There WAS a bare `run` here, as the bridge for a daemon of v0.0.1-15 56 // There WAS a bare `run` here, as the bridge for a daemon of v0.0.1-15
57 // or older exec'ing its upgrade candidate as `<binary> run --resume-fd 57 // or older exec'ing its upgrade candidate as `<binary> run --resume-fd
58 // N`. It is gone because it can never be reached: that daemon runs 58 // N`. It is gone because it can never be reached: that daemon runs
src/cli/spawn.zig
Old New
@@ -1,5 +1,5 @@
1 //! Get a daemon onto a socket path: probe, spawn detached, poll until it 1 //! Get a daemon onto a socket path: probe, spawn detached, poll until it
2 //! answers. `mux d start` is explicit and spelled out; the LOCAL client's 2 //! answers. `mux d start -d` is explicit and spelled out; the LOCAL client's
3 //! own entry (`mux` with no host) is the one attach that may still start 3 //! own entry (`mux` with no host) is the one attach that may still start
4 //! one, through `ensureForAttach` below. The remote verbs a client reaches 4 //! one, through `ensureForAttach` below. The remote verbs a client reaches
5 //! over ssh do not: reading a box must never create a session there. 5 //! over ssh do not: reading a box must never create a session there.
@@ -26,7 +26,7 @@ pub fn selfExe(buf: *[std.fs.max_path_bytes]u8) []const u8 {
26 // from the basename of the FILENAME handed to execve, so exec'ing the 26 // from the basename of the FILENAME handed to execve, so exec'ing the
27 // link itself leaves every daemon called `exe` — nothing `pgrep mux`, 27 // link itself leaves every daemon called `exe` — nothing `pgrep mux`,
28 // `killall mux`, `ps -o comm` or systemd's MainPID name can find — 28 // `killall mux`, `ps -o comm` or systemd's MainPID name can find —
29 // while only the args still read `mux d run`. The e2e that reads a 29 // while only the args still read `mux d start`. The e2e that reads a
30 // spawned daemon's `/proc/PID/comm` is what says so. 30 // spawned daemon's `/proc/PID/comm` is what says so.
31 return execOrLink(std.fs.selfExePath(buf) catch return self_exe); 31 return execOrLink(std.fs.selfExePath(buf) catch return self_exe);
32 } 32 }
@@ -47,7 +47,7 @@ fn execOrLink(resolved: []const u8) []const u8 {
47 pub const Ensured = enum { already_running, started }; 47 pub const Ensured = enum { already_running, started };
48 48
49 /// How long a spawn gets to answer, for every caller. One number because a 49 /// How long a spawn gets to answer, for every caller. One number because a
50 /// user who waited two seconds for `muxd start` must not wait a different 50 /// user who waited two seconds for `mux d start -d` must not wait a different
51 /// two seconds for an attach that starts the same daemon the same way: the 51 /// two seconds for an attach that starts the same daemon the same way: the
52 /// deadline describes how long muxd takes to bind, which is not a fact 52 /// deadline describes how long muxd takes to bind, which is not a fact
53 /// about which verb asked for it. 53 /// about which verb asked for it.
@@ -63,7 +63,7 @@ pub const LogSpec = struct {
63 /// machine, and writing over it from a unit test would punch a hole in 63 /// machine, and writing over it from a unit test would punch a hole in
64 /// a live daemon's log. 64 /// a live daemon's log.
65 path: ?[]const u8 = null, 65 path: ?[]const u8 = null,
66 /// True for the two verbs whose user asked for a daemon — `mux d start` 66 /// True for the two verbs whose user asked for a daemon — `mux d start -d`
67 /// and `mux d endpoint --start`; both probe first, so a daemon already 67 /// and `mux d endpoint --start`; both probe first, so a daemon already
68 /// writing its log is never truncated under it. Auto-start appends 68 /// writing its log is never truncated under it. Auto-start appends
69 /// instead: an attach is not a restart. 69 /// instead: an attach is not a restart.
@@ -145,10 +145,10 @@ pub fn ensureDaemon(
145 const devnull = std.fs.cwd().openFile("/dev/null", .{}) catch return error.SpawnFailed; 145 const devnull = std.fs.cwd().openFile("/dev/null", .{}) catch return error.SpawnFailed;
146 defer devnull.close(); 146 defer devnull.close();
147 147
148 // argv for the child: mux d run <forwarded...>, all null-terminated. 148 // argv for the child: mux d start <forwarded...>, all null-terminated.
149 // The mode word is spelled out so `ps` shows a daemon as a daemon — 149 // The mode word is spelled out so `ps` shows a daemon as a daemon —
150 // it is the only thing separating the long-lived process from the 150 // it is the only thing separating the long-lived process from the
151 // `mux d start` that spawned it. 151 // `mux d start -d` that spawned it.
152 const exe_z = alloc.dupeZ(u8, exe_path) catch return error.SpawnFailed; 152 const exe_z = alloc.dupeZ(u8, exe_path) catch return error.SpawnFailed;
153 defer alloc.free(exe_z); 153 defer alloc.free(exe_z);
154 const argv = alloc.allocSentinel(?[*:0]const u8, run_args.len + 3, null) catch 154 const argv = alloc.allocSentinel(?[*:0]const u8, run_args.len + 3, null) catch
@@ -156,7 +156,7 @@ pub fn ensureDaemon(
156 defer alloc.free(argv); 156 defer alloc.free(argv);
157 argv[0] = "mux"; 157 argv[0] = "mux";
158 argv[1] = "d"; 158 argv[1] = "d";
159 argv[2] = "run"; 159 argv[2] = "start";
160 for (run_args, 0..) |a, i| argv[i + 3] = a.ptr; 160 for (run_args, 0..) |a, i| argv[i + 3] = a.ptr;
161 161
162 progress.emitFmt("{s}: starting\u{2026}", .{progress.prefix}); 162 progress.emitFmt("{s}: starting\u{2026}", .{progress.prefix});
@@ -495,7 +495,7 @@ test "ensureDaemon: the child execs the path it was HANDED, never a name off PAT
495 // shebang script it is the script, whatever argv[0] the caller wrote. 495 // shebang script it is the script, whatever argv[0] the caller wrote.
496 // Nothing on this box resolves by NAME to a file in a fresh tmp dir, so 496 // Nothing on this box resolves by NAME to a file in a fresh tmp dir, so
497 // a spawn that searched PATH cannot pass this. `$*` pins the other half 497 // a spawn that searched PATH cannot pass this. `$*` pins the other half
498 // — the daemon is asked for `d run`, which is also what makes a daemon 498 // — the daemon is asked for `d start`, which is also what makes a daemon
499 // legible in `ps`. Production hands `selfExe` and nothing else, so the 499 // legible in `ps`. Production hands `selfExe` and nothing else, so the
500 // same mechanism cannot reach a sibling: `mux` used to hunt PATH for a 500 // same mechanism cannot reach a sibling: `mux` used to hunt PATH for a
501 // `muxd`, and an e2e leg graded an installed v0.0.1-10 that way. 501 // `muxd`, and an e2e leg graded an installed v0.0.1-10 that way.
@@ -533,11 +533,11 @@ test "ensureDaemon: the child execs the path it was HANDED, never a name off PAT
533 // not as a FileNotFound three frames inside std. 533 // not as a FileNotFound three frames inside std.
534 const got = std.fs.cwd().readFile(seen, &got_buf) catch "<the child ran something else>"; 534 const got = std.fs.cwd().readFile(seen, &got_buf) catch "<the child ran something else>";
535 var want_buf: [200]u8 = undefined; 535 var want_buf: [200]u8 = undefined;
536 const want = try std.fmt.bufPrint(&want_buf, "{s}|d run", .{stub}); 536 const want = try std.fmt.bufPrint(&want_buf, "{s}|d start", .{stub});
537 try std.testing.expectEqualStrings(want, std.mem.trimRight(u8, got, "\n")); 537 try std.testing.expectEqualStrings(want, std.mem.trimRight(u8, got, "\n"));
538 } 538 }
539 539
540 test "ensureDaemon: an attach appends to the log, `muxd start` truncates it" { 540 test "ensureDaemon: an attach appends to the log, `mux d start -d` truncates it" {
541 var tmp = try testtmp.TmpDir.make(); 541 var tmp = try testtmp.TmpDir.make();
542 defer tmp.cleanup(); 542 defer tmp.cleanup();
543 var pbuf: [128]u8 = undefined; 543 var pbuf: [128]u8 = undefined;
@@ -580,7 +580,7 @@ test "ensureDaemon: an attach appends to the log, `muxd start` truncates it" {
580 // that wrote over the front and happened to be shorter still fails. 580 // that wrote over the front and happened to be shorter still fails.
581 try std.testing.expectEqualStrings(seed, try std.fs.cwd().readFile(log, &rbuf)); 581 try std.testing.expectEqualStrings(seed, try std.fs.cwd().readFile(log, &rbuf));
582 582
583 // `muxd start` still truncates, and that half is asserted here rather 583 // `mux d start -d` still truncates, and that half is asserted here rather
584 // than assumed: the two verbs differ only in this flag, so a change that 584 // than assumed: the two verbs differ only in this flag, so a change that
585 // made everything append would otherwise pass unnoticed until an 585 // made everything append would otherwise pass unnoticed until an
586 // operator read a restarted daemon's log and found the old one's. 586 // operator read a restarted daemon's log and found the old one's.
src/client/client.zig
Old New
@@ -1050,7 +1050,7 @@ pub fn spellingCap(target: Target) usize {
1050 } 1050 }
1051 1051
1052 // The grid a birth asks for. It is `main.Opts`'s own default — the size 1052 // The grid a birth asks for. It is `main.Opts`'s own default — the size
1053 // `mux d run` gives session 0 — because a session created for a client 1053 // `mux d start` gives session 0 — because a session created for a client
1054 // that claims no size has to be born at SOMETHING, and the daemon's own 1054 // that claims no size has to be born at SOMETHING, and the daemon's own
1055 // answer to that question is the one nobody has to explain. Wrong, and a 1055 // answer to that question is the one nobody has to explain. Wrong, and a
1056 // restored browser tile comes back at a shape no client ever asked for. 1056 // restored browser tile comes back at a shape no client ever asked for.
src/quic.zig
Old New
@@ -420,7 +420,7 @@ test "keyRefusalBody: the words three binaries print, byte for byte" {
420 // These bytes ARE the contract. Every key refusal any binary prints is 420 // These bytes ARE the contract. Every key refusal any binary prints is
421 // a prefix, this body, and at most a suffix: 421 // a prefix, this body, and at most a suffix:
422 // 422 //
423 // mux d: <body> (mux d run --quic) 423 // mux d: <body> (mux d start --quic)
424 // mux d endpoint: <body>; staying on ssh 424 // mux d endpoint: <body>; staying on ssh
425 // mux d: endpoint_req: <body> (the daemon's lazy bind) 425 // mux d: endpoint_req: <body> (the daemon's lazy bind)
426 // mux: <body> (the client's dial) 426 // mux: <body> (the client's dial)
src/server/server.zig
Old New
@@ -1,4 +1,4 @@
1 //! The daemon core (`mux d run`): up to max_sessions sessions (engine + pty + command 1 //! The daemon core (`mux d start`): up to max_sessions sessions (engine + pty + command
2 //! tracker each); a connection is a session, named at attach. One listener 2 //! tracker each); a connection is a session, named at attach. One listener
3 //! on the unix socket, and a second on UDP when QUIC is configured. 3 //! on the unix socket, and a second on UDP when QUIC is configured.
4 //! Up to max_clients attached interactive clients plus a few one-shot 4 //! Up to max_clients attached interactive clients plus a few one-shot
@@ -152,7 +152,7 @@ fn onShutdownSignal(_: c_int) callconv(.c) void {
152 shutdown_flag.store(true, .release); 152 shutdown_flag.store(true, .release);
153 } 153 }
154 154
155 /// Install SIGINT/SIGTERM handlers so a foreground `mux d run` shuts down 155 /// Install SIGINT/SIGTERM handlers so a foreground `mux d start` shuts down
156 /// cleanly (socket file removed, shell reaped). Called by main; one test 156 /// cleanly (socket file removed, shell reaped). Called by main; one test
157 /// borrows it for the SIGPIPE ignore. 157 /// borrows it for the SIGPIPE ignore.
158 pub fn installSignalHandlers() void { 158 pub fn installSignalHandlers() void {
@@ -575,7 +575,7 @@ pub const Server = struct {
575 /// default: the shim is not free — under zsh it costs the user their 575 /// default: the shim is not free — under zsh it costs the user their
576 /// `~/.zshenv`, under bash it displaces their DEBUG trap — and what 576 /// `~/.zshenv`, under bash it displaces their DEBUG trap — and what
577 /// it buys, a knowable exit code, is read only by `mux a`. A caller 577 /// it buys, a knowable exit code, is read only by `mux a`. A caller
578 /// that wants marks says so; `mux d run` says so for 578 /// that wants marks says so; `mux d start` says so for
579 /// `MUX_SHELL_INTEGRATION=1`. A shell shellint has no scripts for is 579 /// `MUX_SHELL_INTEGRATION=1`. A shell shellint has no scripts for is
580 /// unaffected either way. 580 /// unaffected either way.
581 shell_integration: bool = false, 581 shell_integration: bool = false,
@@ -949,7 +949,7 @@ pub const Server = struct {
949 // `term_grace_ms`, not one each — a supervisor's stop timeout does 949 // `term_grace_ms`, not one each — a supervisor's stop timeout does
950 // not grow with `max_sessions`, and a daemon SIGKILLed halfway 950 // not grow with `max_sessions`, and a daemon SIGKILLed halfway
951 // through leaves its socket and every per-session agent socket for 951 // through leaves its socket and every per-session agent socket for
952 // the next `mux d run` to reason about. 952 // the next `mux d start` to reason about.
953 for (&self.sessions.table) |*slot| { 953 for (&self.sessions.table) |*slot| {
954 if (slot.* != null) slot.*.?.pty.requestExit(); 954 if (slot.* != null) slot.*.?.pty.requestExit();
955 } 955 }
@@ -1164,7 +1164,7 @@ pub const Server = struct {
1164 } 1164 }
1165 1165
1166 /// Pumps until something asks the daemon to stop. The only nonzero exit 1166 /// Pumps until something asks the daemon to stop. The only nonzero exit
1167 /// `mux d run` has is a boot failure in main; getting here means the 1167 /// `mux d start` has is a boot failure in main; getting here means the
1168 /// daemon served, so the answer is 0 whether the ask was `mux d stop` or 1168 /// daemon served, so the answer is 0 whether the ask was `mux d stop` or
1169 /// a SIGTERM from a supervisor — which reads a nonzero exit on a clean 1169 /// a SIGTERM from a supervisor — which reads a nonzero exit on a clean
1170 /// shutdown as a crash. 1170 /// shutdown as a crash.
@@ -3346,7 +3346,7 @@ pub const Server = struct {
3346 return a.dupe(u8, "check: oom") catch null; 3346 return a.dupe(u8, "check: oom") catch null;
3347 const result = std.process.Child.run(.{ 3347 const result = std.process.Child.run(.{
3348 .allocator = a, 3348 .allocator = a,
3349 .argv = &.{ path, "d", "run", "--resume-fd", fd_str, "--check" }, 3349 .argv = &.{ path, "d", "start", "--resume-fd", fd_str, "--check" },
3350 .max_output_bytes = 4096, 3350 .max_output_bytes = 4096,
3351 }) catch return a.dupe(u8, "check: cannot spawn candidate") catch null; 3351 }) catch return a.dupe(u8, "check: cannot spawn candidate") catch null;
3352 defer a.free(result.stdout); 3352 defer a.free(result.stdout);
@@ -3470,7 +3470,7 @@ pub const Server = struct {
3470 } 3470 }
3471 @memset(&self.observers, null); 3471 @memset(&self.observers, null);
3472 3472
3473 // Build argv: {"mux", "d", "run", "--resume-fd", "<n>"}. The 3473 // Build argv: {"mux", "d", "start", "--resume-fd", "<n>"}. The
3474 // candidate is by definition NEWER than this binary, so it reads the 3474 // candidate is by definition NEWER than this binary, so it reads the
3475 // mode word. 3475 // mode word.
3476 var fd_buf: [12]u8 = undefined; 3476 var fd_buf: [12]u8 = undefined;
@@ -3478,7 +3478,7 @@ pub const Server = struct {
3478 const argv = [_:null]?[*:0]const u8{ 3478 const argv = [_:null]?[*:0]const u8{
3479 "mux", 3479 "mux",
3480 "d", 3480 "d",
3481 "run", 3481 "start",
3482 "--resume-fd", 3482 "--resume-fd",
3483 fd_str.ptr, 3483 fd_str.ptr,
3484 }; 3484 };
src/server/server_test_session.zig
Old New
@@ -851,7 +851,7 @@ test "Server: an attach past max_sessions is refused with exit_status, sessions
851 // session cannot tell "one grace for the table" from "one grace each". A 851 // session cannot tell "one grace for the table" from "one grace each". A
852 // supervisor with a stop timeout kills a daemon that spends the second shape, 852 // supervisor with a stop timeout kills a daemon that spends the second shape,
853 // and a killed daemon leaves its socket and its agent sockets unlinked for 853 // and a killed daemon leaves its socket and its agent sockets unlinked for
854 // the next `mux d run` to reason about. 854 // the next `mux d start` to reason about.
855 test "Server: a table of TERM-ignoring shells costs one grace, not one each" { 855 test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
856 const alloc = std.testing.allocator; 856 const alloc = std.testing.allocator;
857 857
src/sockpath.zig
Old New
@@ -112,7 +112,7 @@ pub fn claim(path: []const u8) !void {
112 // Nobody is listening — but this is NOT yet proof of a stale 112 // Nobody is listening — but this is NOT yet proof of a stale
113 // socket: Linux answers ECONNREFUSED for a regular file at the 113 // socket: Linux answers ECONNREFUSED for a regular file at the
114 // path exactly as it does for a dead socket, so connect alone 114 // path exactly as it does for a dead socket, so connect alone
115 // cannot tell a dead daemon from `mux d run --sock notes.txt`. 115 // cannot tell a dead daemon from `mux d start --sock notes.txt`.
116 // The stat below is what separates them. 116 // The stat below is what separates them.
117 error.ConnectionRefused => {}, 117 error.ConnectionRefused => {},
118 else => |e| return e, 118 else => |e| return e,
test/agent.sh
Old New
@@ -106,7 +106,7 @@ cleanup() {
106 "$RELAY_PID" "$SINK_PID" "$CLI_PID"; do 106 "$RELAY_PID" "$SINK_PID" "$CLI_PID"; do
107 [ -n "$p" ] && kill "$p" 2>/dev/null 107 [ -n "$p" ] && kill "$p" 2>/dev/null
108 done 108 done
109 # ...and by socket, for the window between `mux d start`'s fork and the 109 # ...and by socket, for the window between `mux d start -d`'s fork and the
110 # up-line this script reads its pid from. `mux d stop` on a path nobody 110 # up-line this script reads its pid from. `mux d stop` on a path nobody
111 # serves is a no-op. These must precede the rm -rf: unlinking the sockets 111 # serves is a no-op. These must precede the rm -rf: unlinking the sockets
112 # first would leave a live daemon nothing could reach by path. 112 # first would leave a live daemon nothing could reach by path.
@@ -280,7 +280,7 @@ leakcheck() {
280 # unreadable verdict AND the `mux d stop` that failed to kill anything. 280 # unreadable verdict AND the `mux d stop` that failed to kill anything.
281 kill -0 "$1" 2>/dev/null && { why "daemon still alive ${_i}x50ms after stop — leak verdict unreadable"; return 1; } 281 kill -0 "$1" 2>/dev/null && { why "daemon still alive ${_i}x50ms after stop — leak verdict unreadable"; return 1; }
282 _dl=$(daemon_log "$2") 282 _dl=$(daemon_log "$2")
283 # A missing log is a broken convention, not a clean daemon: `mux d start` 283 # A missing log is a broken convention, not a clean daemon: `mux d start -d`
284 # creates this file before it forks, so every daemon that ever existed has 284 # creates this file before it forks, so every daemon that ever existed has
285 # one. Passing on its absence is how this gate would go quietly vacuous. 285 # one. Passing on its absence is how this gate would go quietly vacuous.
286 [ -f "$_dl" ] || { why "no daemon log at $_dl — the leak verdict was never captured"; return 1; } 286 [ -f "$_dl" ] || { why "no daemon log at $_dl — the leak verdict was never captured"; return 1; }
@@ -292,10 +292,10 @@ leakcheck() {
292 # script guessed from a process name: the suite kills what it started, and a 292 # script guessed from a process name: the suite kills what it started, and a
293 # name match can only ever name a bystander. 293 # name match can only ever name a bystander.
294 # 294 #
295 # $_log catches what `mux d start` ITSELF prints (the up-line, or the refusal). 295 # $_log catches what `mux d start -d` ITSELF prints (the up-line, or the refusal).
296 # The daemon's own output goes somewhere else entirely: start detaches the 296 # The daemon's own output goes somewhere else entirely: start detaches the
297 # child onto $XDG_STATE_HOME/mux/muxd.log — a path computed from the 297 # child onto $XDG_STATE_HOME/mux/muxd.log — a path computed from the
298 # environment `mux d start` is run with — and TRUNCATES it at every spawn. One 298 # environment `mux d start -d` is run with — and TRUNCATES it at every spawn. One
299 # state home for the whole suite would therefore leave exactly one daemon's 299 # state home for the whole suite would therefore leave exactly one daemon's
300 # allocator verdict readable, the last one's, with every earlier verdict 300 # allocator verdict readable, the last one's, with every earlier verdict
301 # overwritten by the next scenario's spawn. So each daemon gets a state home 301 # overwritten by the next scenario's spawn. So each daemon gets a state home
@@ -306,7 +306,7 @@ start_daemon() {
306 shift 306 shift
307 _tag=$(basename "$_log" .log) 307 _tag=$(basename "$_log" .log)
308 mkdir -p "$TMP/state/$_tag/mux" || return 1 308 mkdir -p "$TMP/state/$_tag/mux" || return 1
309 XDG_STATE_HOME="$TMP/state/$_tag" "$MUX" d start "$@" >"$_log" 2>&1 309 XDG_STATE_HOME="$TMP/state/$_tag" "$MUX" d start -d "$@" >"$_log" 2>&1
310 sed -n 's/^up .*pid=\([0-9]*\).*/\1/p' "$_log" | head -1 310 sed -n 's/^up .*pid=\([0-9]*\).*/\1/p' "$_log" | head -1
311 } 311 }
312 312
@@ -358,7 +358,7 @@ start_ready() {
358 # moment ago, and between that proof and this bind a concurrent suite or an 358 # moment ago, and between that proof and this bind a concurrent suite or an
359 # outgoing connection can take it — the daemon holds no SO_REUSEADDR and refuses to 359 # outgoing connection can take it — the daemon holds no SO_REUSEADDR and refuses to
360 # share a port rather than split its datagrams, so it prints `a daemon is 360 # share a port rather than split its datagrams, so it prints `a daemon is
361 # already listening on udp ...` into its own log and exits, and `mux d start` 361 # already listening on udp ...` into its own log and exits, and `mux d start -d`
362 # reports no up-line. That refusal is the ONLY failure another port can fix, 362 # reports no up-line. That refusal is the ONLY failure another port can fix,
363 # and it is told apart from the rest by the daemon's own words: anything else 363 # and it is told apart from the rest by the daemon's own words: anything else
364 # stops here and is reported, because a daemon that cannot start is a defect and 364 # stops here and is reported, because a daemon that cannot start is a defect and
@@ -450,7 +450,7 @@ run_scenario "marks: exit codes and output come back from a bash session" scen_m
450 450
451 # --- The ephemeral TUI, which two scenarios share -------------------------- 451 # --- The ephemeral TUI, which two scenarios share --------------------------
452 # The spec's field specimen: a daemon whose session is not a shell at all but a 452 # The spec's field specimen: a daemon whose session is not a shell at all but a
453 # throwaway full-screen program. `mux d start` has no `--` argv, and --shell 453 # throwaway full-screen program. `mux d start -d` has no `--` argv, and --shell
454 # execs whatever path it is given, so a one-line wrapper carries the argument. 454 # execs whatever path it is given, so a one-line wrapper carries the argument.
455 # HOME points into the tmpdir for e2e.sh's $SHELL reason: ~/.lesskey and 455 # HOME points into the tmpdir for e2e.sh's $SHELL reason: ~/.lesskey and
456 # ~/.vimrc are arbitrary code on the session under test. 456 # ~/.vimrc are arbitrary code on the session under test.
test/bench.sh
Old New
@@ -10,7 +10,7 @@ XDG_STATE_HOME="${TMPDIR:-/tmp}/muxd-bench-$$-state"; export XDG_STATE_HOME
10 cleanup() { kill "$DPID" 2>/dev/null || true; rm -rf "$SOCK" "$XDG_STATE_HOME"; } 10 cleanup() { kill "$DPID" 2>/dev/null || true; rm -rf "$SOCK" "$XDG_STATE_HOME"; }
11 trap cleanup EXIT INT TERM 11 trap cleanup EXIT INT TERM
12 12
13 "$MUX" d run --sock "$SOCK" --shell /bin/sh & 13 "$MUX" d start --sock "$SOCK" --shell /bin/sh &
14 DPID=$! 14 DPID=$!
15 i=0 15 i=0
16 while [ ! -S "$SOCK" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done 16 while [ ! -S "$SOCK" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
test/coverage.sh
Old New
@@ -22,13 +22,13 @@
22 # the one signal a wrapper cannot forward, so a leg that kill -9s a 22 # the one signal a wrapper cannot forward, so a leg that kill -9s a
23 # daemon would leave the real one alive holding its socket. e2e.sh's 23 # daemon would leave the real one alive holding its socket. e2e.sh's
24 # hardkill answers that; see the comment there. 24 # hardkill answers that; see the comment there.
25 # - kcov waits for the LAST traced descendant, and `mux d start` daemonises: 25 # - kcov waits for the LAST traced descendant, and `mux d start -d` daemonises:
26 # its launcher forks, the parent exits, and the daemon reparents to init. 26 # its launcher forks, the parent exits, and the daemon reparents to init.
27 # Traced, that daemon outlives the scenario, so kcov never exits and the 27 # Traced, that daemon outlives the scenario, so kcov never exits and the
28 # suite's `wait` on it never returns — a hang at the auto-start leg that 28 # suite's `wait` on it never returns — a hang at the auto-start leg that
29 # reads as a slow run, and one that ignores SIGTERM, so `timeout` will not 29 # reads as a slow run, and one that ignores SIGTERM, so `timeout` will not
30 # free it either. The shims therefore run `start` BARE, exporting 30 # free it either. The shims therefore run `start` BARE, exporting
31 # MUX_KCOV_ACTIVE so the `mux d run` it re-execs stays bare too. The cost 31 # MUX_KCOV_ACTIVE so the `mux d start -d` it re-execs stays bare too. The cost
32 # is the auto-started daemons' lines; the alternative was a suite that 32 # is the auto-started daemons' lines; the alternative was a suite that
33 # could not finish. `endpoint` is the same shape one hop out: it spawns 33 # could not finish. `endpoint` is the same shape one hop out: it spawns
34 # the daemon it announces, and the ssh handoff client then kill()s its 34 # the daemon it announces, and the ssh handoff client then kill()s its
test/e2e.sh
Old New
@@ -23,8 +23,8 @@ WSCLIENT="$6"
23 # The ssh recipes a handoff leg drives name `mux` and let the remote's PATH 23 # The ssh recipes a handoff leg drives name `mux` and let the remote's PATH
24 # find it, so the build's own bin directory goes in front of the shims that 24 # find it, so the build's own bin directory goes in front of the shims that
25 # stand in for a remote. The client's LOCAL auto-start no longer reads PATH 25 # stand in for a remote. The client's LOCAL auto-start no longer reads PATH
26 # at all — it execs this same binary with argv `mux d run …` (spawn.zig) — 26 # at all — it execs this same binary with argv `mux d start …` (spawn.zig) —
27 # so a leg that forgets its `mux d run &` can no longer silently grade an 27 # so a leg that forgets its `mux d start &` can no longer silently grade an
28 # installed release, which it did against a v0.0.1-10 with no agent code. 28 # installed release, which it did against a v0.0.1-10 with no agent code.
29 PATH="$(cd "$(dirname "$MUX")" && pwd):$PATH" 29 PATH="$(cd "$(dirname "$MUX")" && pwd):$PATH"
30 export PATH 30 export PATH
test/e2e_01_boot.sh
Old New
@@ -27,7 +27,7 @@ SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock"
27 defer_sock "$SOCK10" 27 defer_sock "$SOCK10"
28 QPORT2=$(( 26000 + ($$ % 4000) )) 28 QPORT2=$(( 26000 + ($$ % 4000) ))
29 QPORT3=$(( 31000 + ($$ % 4000) )) 29 QPORT3=$(( 31000 + ($$ % 4000) ))
30 # M10 `mux d start`. These daemons are spawned DETACHED, so the suite never 30 # M10 `mux d start -d`. These daemons are spawned DETACHED, so the suite never
31 # holds their pids as shell jobs — it reads them off the up-line and kills 31 # holds their pids as shell jobs — it reads them off the up-line and kills
32 # by that tracked pid, never by name. 32 # by that tracked pid, never by name.
33 SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock" 33 SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock"
@@ -406,15 +406,15 @@ refuse() {
406 # Timed out rather than trusted to exit: every case here is a refusal, so 406 # Timed out rather than trusted to exit: every case here is a refusal, so
407 # a regression that ACCEPTS one would otherwise run a daemon forever and 407 # a regression that ACCEPTS one would otherwise run a daemon forever and
408 # hang the suite instead of failing it. 124 is a distinguishable answer. 408 # hang the suite instead of failing it. 124 is a distinguishable answer.
409 env XDG_CONFIG_HOME="$_cfg" timeout 10 "$MUX" d run --sock "$SOCK4" \ 409 env XDG_CONFIG_HOME="$_cfg" timeout 10 "$MUX" d start --sock "$SOCK4" \
410 --shell /bin/sh "$@" > "$OUT.q" 2>&1 410 --shell /bin/sh "$@" > "$OUT.q" 2>&1
411 _rc=$? 411 _rc=$?
412 set -e 412 set -e
413 [ "$_rc" -eq "$_want" ] || { 413 [ "$_rc" -eq "$_want" ] || {
414 echo "e2e FAIL: mux d run $* exited $_rc (want $_want)"; cat "$OUT.q"; exit 1; 414 echo "e2e FAIL: mux d start $* exited $_rc (want $_want)"; cat "$OUT.q"; exit 1;
415 } 415 }
416 [ ! -e "$SOCK4" ] || { 416 [ ! -e "$SOCK4" ] || {
417 echo "e2e FAIL: mux d run $* was refused but left $SOCK4 behind"; exit 1; 417 echo "e2e FAIL: mux d start $* was refused but left $SOCK4 behind"; exit 1;
418 } 418 }
419 # One line of complaint plus the usage block, and not a stack trace: a 419 # One line of complaint plus the usage block, and not a stack trace: a
420 # Zig panic runs to dozens of lines and names a source file, which is 420 # Zig panic runs to dozens of lines and names a source file, which is
@@ -429,7 +429,7 @@ refuse() {
429 _usage_lines=$("$MUX" d 2>&1 | wc -l) 429 _usage_lines=$("$MUX" d 2>&1 | wc -l)
430 _lines=$(wc -l < "$OUT.q") 430 _lines=$(wc -l < "$OUT.q")
431 { [ "$_lines" -le $((_usage_lines + 2)) ] && [ "$_lines" -le 15 ]; } || { 431 { [ "$_lines" -le $((_usage_lines + 2)) ] && [ "$_lines" -le 15 ]; } || {
432 echo "e2e FAIL: mux d run $* answered with more than a message:"; cat "$OUT.q"; exit 1; 432 echo "e2e FAIL: mux d start $* answered with more than a message:"; cat "$OUT.q"; exit 1;
433 } 433 }
434 } 434 }
435 435
@@ -454,7 +454,7 @@ refuse 1 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY.bad"
454 refuse 1 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY.missing" 454 refuse 1 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY.missing"
455 455
456 # The key-missing message must name the way out, not just the absence. 456 # The key-missing message must name the way out, not just the absence.
457 env XDG_CONFIG_HOME="$NOKEY_CFG" "$MUX" d run --sock "$SOCK4.nokey" \ 457 env XDG_CONFIG_HOME="$NOKEY_CFG" "$MUX" d start --sock "$SOCK4.nokey" \
458 --quic "127.0.0.1:$QPORT" 2> "$OUT.nokey" || true 458 --quic "127.0.0.1:$QPORT" 2> "$OUT.nokey" || true
459 grep -q "mux d keygen" "$OUT.nokey" || { 459 grep -q "mux d keygen" "$OUT.nokey" || {
460 echo "e2e FAIL: key-missing message does not name keygen"; cat "$OUT.nokey"; exit 1; } 460 echo "e2e FAIL: key-missing message does not name keygen"; cat "$OUT.nokey"; exit 1; }
@@ -484,7 +484,7 @@ grep -qi " $QHEX " /proc/net/udp || {
484 set +e 484 set +e
485 # Same reasoning as refuse(): if the second daemon ever succeeds it runs 485 # Same reasoning as refuse(): if the second daemon ever succeeds it runs
486 # until killed, so the failure has to be a timeout rather than a hang. 486 # until killed, so the failure has to be a timeout rather than a hang.
487 timeout 10 "$MUX" d run --sock "$SOCK4.second" --shell /bin/sh \ 487 timeout 10 "$MUX" d start --sock "$SOCK4.second" --shell /bin/sh \
488 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000 > "$OUT.q" 2>&1 488 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000 > "$OUT.q" 2>&1
489 RC=$? 489 RC=$?
490 set -e 490 set -e
@@ -764,7 +764,7 @@ D4PID=""
764 # 1. MUX_KEY_FILE with no --key anywhere. The daemon's env support had no 764 # 1. MUX_KEY_FILE with no --key anywhere. The daemon's env support had no
765 # automated coverage at all: envKey() reads the real environment, so 765 # automated coverage at all: envKey() reads the real environment, so
766 # only a daemon started with it set can prove the path works. 766 # only a daemon started with it set can prove the path works.
767 env MUX_KEY_FILE="$QKEY" "$MUX" d run --sock "$SOCK9" --shell /bin/sh \ 767 env MUX_KEY_FILE="$QKEY" "$MUX" d start --sock "$SOCK9" --shell /bin/sh \
768 --quic "127.0.0.1:$QPORT2" --quic-idle-ms 3000 > "$OUT.d9.d" 2>&1 & 768 --quic "127.0.0.1:$QPORT2" --quic-idle-ms 3000 > "$OUT.d9.d" 2>&1 &
769 D9PID=$! 769 D9PID=$!
770 defer_kill "$D9PID" 770 defer_kill "$D9PID"
@@ -786,7 +786,7 @@ D9PID=""
786 # This is the shape that catches the two being swapped at the call site 786 # This is the shape that catches the two being swapped at the call site
787 # in run() — pickKey's unit test pins the ordering inside the function 787 # in run() — pickKey's unit test pins the ordering inside the function
788 # and is blind to the order they are handed to it. 788 # and is blind to the order they are handed to it.
789 env MUX_KEY_FILE="$QKEY.wrong" "$MUX" d run --sock "$SOCK10" --shell /bin/sh \ 789 env MUX_KEY_FILE="$QKEY.wrong" "$MUX" d start --sock "$SOCK10" --shell /bin/sh \
790 --quic "127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 3000 > "$OUT.d10.d" 2>&1 & 790 --quic "127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 3000 > "$OUT.d10.d" 2>&1 &
791 D10PID=$! 791 D10PID=$!
792 defer_kill "$D10PID" 792 defer_kill "$D10PID"
@@ -807,13 +807,13 @@ ok "daemon honours MUX_KEY_FILE, and --key beats it"
807 rm_swept "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" \ 807 rm_swept "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" \
808 "$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err" 808 "$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err"
809 809
810 # --- M10: mux d start — detached spawn, no-op rerun, race, pinned lines. 810 # --- M10: mux d start -d — detached spawn, no-op rerun, race, pinned lines.
811 # 811 #
812 # Every scenario below asserts a MARKER through the session, never just $?. 812 # Every scenario below asserts a MARKER through the session, never just $?.
813 # A client that fails to authenticate exits 0 by way of "aborted before 813 # A client that fails to authenticate exits 0 by way of "aborted before
814 # attaching", so an exit code cannot distinguish a working session from a 814 # attaching", so an exit code cannot distinguish a working session from a
815 # key or transport regression — only bytes coming back out of the shell can. 815 # key or transport regression — only bytes coming back out of the shell can.
816 "$MUX" d start --sock "$SOCK8" 2> "$OUT.start" 816 "$MUX" d start -d --sock "$SOCK8" 2> "$OUT.start"
817 grep -q '^mux d: starting' "$OUT.start" || { 817 grep -q '^mux d: starting' "$OUT.start" || {
818 echo "e2e FAIL: start printed no starting line"; cat "$OUT.start"; exit 1; } 818 echo "e2e FAIL: start printed no starting line"; cat "$OUT.start"; exit 1; }
819 grep -q '^up (' "$OUT.start" || { 819 grep -q '^up (' "$OUT.start" || {
@@ -827,14 +827,14 @@ SPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start")
827 defer_kill "$SPID" 827 defer_kill "$SPID"
828 [ -n "$SPID" ] || { echo "e2e FAIL: up line carries no pid"; exit 1; } 828 [ -n "$SPID" ] || { echo "e2e FAIL: up line carries no pid"; exit 1; }
829 kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; } 829 kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; }
830 # `mux d start` is the OTHER production spawn, and it wears the name too. 830 # `mux d start -d` is the OTHER production spawn, and it wears the name too.
831 # It shares `spawn.selfExe` with the client's auto-start (pinned the same 831 # It shares `spawn.selfExe` with the client's auto-start (pinned the same
832 # way in 03_side), so this holds today by construction — which is exactly 832 # way in 03_side), so this holds today by construction — which is exactly
833 # why it is asserted here: a later split of the two callers would leave 833 # why it is asserted here: a later split of the two callers would leave
834 # `d start` free to hand execve the /proc link and name its daemon `exe`. 834 # `d start` free to hand execve the /proc link and name its daemon `exe`.
835 SCOMM=$(cat "/proc/$SPID/comm") 835 SCOMM=$(cat "/proc/$SPID/comm")
836 [ "$SCOMM" = "mux" ] || { 836 [ "$SCOMM" = "mux" ] || {
837 echo "e2e FAIL: the daemon mux d start brought up has comm '$SCOMM', want 'mux'" 837 echo "e2e FAIL: the daemon mux d start -d brought up has comm '$SCOMM', want 'mux'"
838 exit 1; } 838 exit 1; }
839 # Non-tty stderr: exactly two lines, no dots. 839 # Non-tty stderr: exactly two lines, no dots.
840 [ "$(wc -l < "$OUT.start")" = "2" ] || { 840 [ "$(wc -l < "$OUT.start")" = "2" ] || {
@@ -858,7 +858,7 @@ LPERMS=$(stat -c %a "$MUXLOG")
858 [ "$LPERMS" = "600" ] || { echo "e2e FAIL: daemon log perms $LPERMS, want 600"; exit 1; } 858 [ "$LPERMS" = "600" ] || { echo "e2e FAIL: daemon log perms $LPERMS, want 600"; exit 1; }
859 859
860 # Rerun: silent no-op beyond the already-running line, exit 0, same daemon. 860 # Rerun: silent no-op beyond the already-running line, exit 0, same daemon.
861 "$MUX" d start --sock "$SOCK8" 2> "$OUT.start2" 861 "$MUX" d start -d --sock "$SOCK8" 2> "$OUT.start2"
862 grep -q "already running on $SOCK8 (stop it first with \`mux d stop --sock $SOCK8\`" "$OUT.start2" || { 862 grep -q "already running on $SOCK8 (stop it first with \`mux d stop --sock $SOCK8\`" "$OUT.start2" || {
863 echo "e2e FAIL: rerun did not say already running"; cat "$OUT.start2"; exit 1; } 863 echo "e2e FAIL: rerun did not say already running"; cat "$OUT.start2"; exit 1; }
864 "$MUX" d dump --sock "$SOCK8" | grep -q "start-works" || { 864 "$MUX" d dump --sock "$SOCK8" | grep -q "start-works" || {
@@ -869,7 +869,7 @@ grep -q "already running on $SOCK8 (stop it first with \`mux d stop --sock $SOCK
869 # restart. Padded, then a real spawn on a fresh path must shrink it. 869 # restart. Padded, then a real spawn on a fresh path must shrink it.
870 dd if=/dev/zero bs=1024 count=8 2>/dev/null >> "$MUXLOG" 870 dd if=/dev/zero bs=1024 count=8 2>/dev/null >> "$MUXLOG"
871 PADDED=$(stat -c %s "$MUXLOG") 871 PADDED=$(stat -c %s "$MUXLOG")
872 "$MUX" d start --sock "$SOCK8T" 2> "$OUT.start" 872 "$MUX" d start -d --sock "$SOCK8T" 2> "$OUT.start"
873 TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start") 873 TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start")
874 defer_kill "$TPID" 874 defer_kill "$TPID"
875 [ -n "$TPID" ] || { echo "e2e FAIL: truncation spawn reported no pid"; exit 1; } 875 [ -n "$TPID" ] || { echo "e2e FAIL: truncation spawn reported no pid"; exit 1; }
@@ -883,8 +883,8 @@ TPID=""
883 # survives — a second daemon on the path would have started a fresh shell). 883 # survives — a second daemon on the path would have started a fresh shell).
884 softkill "$SPID" && wait_gone "$SOCK8" 884 softkill "$SPID" && wait_gone "$SOCK8"
885 SPID="" 885 SPID=""
886 "$MUX" d start --sock "$SOCK8" 2> "$OUT.ra" & RA=$!; defer_kill "$RA" 886 "$MUX" d start -d --sock "$SOCK8" 2> "$OUT.ra" & RA=$!; defer_kill "$RA"
887 "$MUX" d start --sock "$SOCK8" 2> "$OUT.rb" & RB=$!; defer_kill "$RB" 887 "$MUX" d start -d --sock "$SOCK8" 2> "$OUT.rb" & RB=$!; defer_kill "$RB"
888 set +e 888 set +e
889 wait "$RA"; RCA=$? 889 wait "$RA"; RCA=$?
890 wait "$RB"; RCB=$? 890 wait "$RB"; RCB=$?
@@ -905,26 +905,26 @@ defer_kill "$SPID"
905 # instead of unlinking the winner's socket and binding over it. The race 905 # instead of unlinking the winner's socket and binding over it. The race
906 # above cannot see that — both starts report success either way, and the 906 # above cannot see that — both starts report success either way, and the
907 # marker comes back from whichever daemon owns the path — so it is pinned 907 # marker comes back from whichever daemon owns the path — so it is pinned
908 # head-on: a second `mux d run` against a live socket must refuse. 908 # head-on: a second `mux d start` against a live socket must refuse.
909 set +e 909 set +e
910 timeout 10 "$MUX" d run --sock "$SOCK8" --shell /bin/sh > "$OUT.rb" 2>&1 910 timeout 10 "$MUX" d start --sock "$SOCK8" --shell /bin/sh > "$OUT.rb" 2>&1
911 RCS=$? 911 RCS=$?
912 set -e 912 set -e
913 [ "$RCS" -eq 1 ] || { 913 [ "$RCS" -eq 1 ] || {
914 echo "e2e FAIL: second mux d run on a live socket exited $RCS (want 1; 124 means it took the path)" 914 echo "e2e FAIL: second mux d start on a live socket exited $RCS (want 1; 124 means it took the path)"
915 cat "$OUT.rb"; exit 1; } 915 cat "$OUT.rb"; exit 1; }
916 grep -q "already running" "$OUT.rb" || { 916 grep -q "already running" "$OUT.rb" || {
917 echo "e2e FAIL: second mux d run refused, but not as already-running"; cat "$OUT.rb"; exit 1; } 917 echo "e2e FAIL: second mux d start refused, but not as already-running"; cat "$OUT.rb"; exit 1; }
918 # ...and the session the winner was serving is undisturbed. 918 # ...and the session the winner was serving is undisturbed.
919 "$MUX" d dump --sock "$SOCK8" | grep -q "race-one" || { 919 "$MUX" d dump --sock "$SOCK8" | grep -q "race-one" || {
920 echo "e2e FAIL: the refused daemon disturbed the live session"; exit 1; } 920 echo "e2e FAIL: the refused daemon disturbed the live session"; exit 1; }
921 # The race client's own capture, checked here rather than above: the refused 921 # The race client's own capture, checked here rather than above: the refused
922 # `mux d run` between the two touches no session, so the grid has not moved. 922 # `mux d start` between the two touches no session, so the grid has not moved.
923 assert_converged "$OUT.race" "$SOCK8" "start race" 923 assert_converged "$OUT.race" "$SOCK8" "start race"
924 924
925 softkill "$SPID" || true 925 softkill "$SPID" || true
926 SPID="" 926 SPID=""
927 ok "mux d start — spawn, no-op rerun, log truncation, race" 927 ok "mux d start -d — spawn, no-op rerun, log truncation, race"
928 928
929 # --- `mux d endpoint --start` ensures the daemon ON THE SOCKET IT PROBES. 929 # --- `mux d endpoint --start` ensures the daemon ON THE SOCKET IT PROBES.
930 # 930 #
@@ -1004,7 +1004,7 @@ ESPID=""
1004 ok "mux d endpoint --start ensures a daemon on the socket it announces, and on no other" 1004 ok "mux d endpoint --start ensures a daemon on the socket it announces, and on no other"
1005 1005
1006 # --- M10: a start whose daemon dies young REPORTS it. This is the first-run 1006 # --- M10: a start whose daemon dies young REPORTS it. This is the first-run
1007 # mistake the failure line exists for — `ssh HOST 'mux d start --quic 0.0.0.0'` 1007 # mistake the failure line exists for — `ssh HOST 'mux d start -d --quic 0.0.0.0'`
1008 # before the key was ever scp'd — so it must be a message, not a panic. The 1008 # before the key was ever scp'd — so it must be a message, not a panic. The
1009 # child exits on the missing key, the poll loop reaps it, and polling 1009 # child exits on the missing key, the poll loop reaps it, and polling
1010 # continues to the deadline; a second waitpid there gets ECHILD, which the 1010 # continues to the deadline; a second waitpid there gets ECHILD, which the
@@ -1013,7 +1013,7 @@ ok "mux d endpoint --start ensures a daemon on the socket it announces, and on n
1013 DEADCFG="${TMPDIR:-/tmp}/mux-e2e-deadchild-$$" 1013 DEADCFG="${TMPDIR:-/tmp}/mux-e2e-deadchild-$$"
1014 defer_rm "$DEADCFG" 1014 defer_rm "$DEADCFG"
1015 set +e 1015 set +e
1016 env XDG_CONFIG_HOME="$DEADCFG" timeout 30 "$MUX" d start --sock "$SOCK8T" \ 1016 env XDG_CONFIG_HOME="$DEADCFG" timeout 30 "$MUX" d start -d --sock "$SOCK8T" \
1017 --quic "127.0.0.1:1" --key /nonexistent > "$OUT.dead" 2>&1 1017 --quic "127.0.0.1:1" --key /nonexistent > "$OUT.dead" 2>&1
1018 DRC=$? 1018 DRC=$?
1019 set -e 1019 set -e
@@ -1041,7 +1041,7 @@ ok "a start whose daemon dies young says so, with the log path"
1041 # explicit loopback port (4433 on the suite machine is somebody's daemon). 1041 # explicit loopback port (4433 on the suite machine is somebody's daemon).
1042 # No --key on either side — the key is the one `mux d keygen` wrote at the 1042 # No --key on either side — the key is the one `mux d keygen` wrote at the
1043 # hermetic default path near the top of this suite. 1043 # hermetic default path near the top of this suite.
1044 "$MUX" d start --sock "$SOCK11" --quic "127.0.0.1:$QPORT4" 2> "$OUT.goal" 1044 "$MUX" d start -d --sock "$SOCK11" --quic "127.0.0.1:$QPORT4" 2> "$OUT.goal"
1045 GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal") 1045 GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal")
1046 defer_kill "$GPID" 1046 defer_kill "$GPID"
1047 [ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; } 1047 [ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; }
test/e2e_03_side.sh
Old New
@@ -25,7 +25,7 @@ defer_sock "$SOCK15"
25 SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock" 25 SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock"
26 defer_sock "$SOCK24" 26 defer_sock "$SOCK24"
27 27
28 # --- M13: proxy reaches a daemon, `mux d start` makes one, mux d stop ---- 28 # --- M13: proxy reaches a daemon, `mux d start -d` makes one, mux d stop ----
29 # 29 #
30 # Nothing is serving SOCK14, and `mux d proxy` no longer answers that by 30 # Nothing is serving SOCK14, and `mux d proxy` no longer answers that by
31 # starting a daemon: it is the verb a client reaches over ssh, and a wall 31 # starting a daemon: it is the verb a client reaches over ssh, and a wall
@@ -61,12 +61,12 @@ grep -q "mux d proxy: cannot connect to $SOCK14" "$OUT.ascold" || {
61 # zero. Unlike the M10 start block's version of this gap (:1150) it is 61 # zero. Unlike the M10 start block's version of this gap (:1150) it is
62 # covered anyway: the trap also stops SOCK14/SOCK15 by PATH, which needs 62 # covered anyway: the trap also stops SOCK14/SOCK15 by PATH, which needs
63 # no pid. 63 # no pid.
64 "$MUX" d start --sock "$SOCK14" --shell /bin/sh 2> "$OUT.as.start" || { 64 "$MUX" d start -d --sock "$SOCK14" --shell /bin/sh 2> "$OUT.as.start" || {
65 echo "e2e FAIL: mux d start refused $SOCK14:"; cat "$OUT.as.start"; exit 1; } 65 echo "e2e FAIL: mux d start -d refused $SOCK14:"; cat "$OUT.as.start"; exit 1; }
66 APID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.as.start" | head -1) 66 APID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.as.start" | head -1)
67 defer_kill "$APID" 67 defer_kill "$APID"
68 [ -n "$APID" ] || { 68 [ -n "$APID" ] || {
69 echo "e2e FAIL: mux d start's up-line carries no pid"; cat "$OUT.as.start"; exit 1; } 69 echo "e2e FAIL: mux d start -d's up-line carries no pid"; cat "$OUT.as.start"; exit 1; }
70 kill -0 "$APID" || { echo "e2e FAIL: the started daemon is not alive"; exit 1; } 70 kill -0 "$APID" || { echo "e2e FAIL: the started daemon is not alive"; exit 1; }
71 71
72 pipe_mux "$OUT.as" "$OUT.as.err" env SHELL=/bin/sh timeout 30 "$MUX" --via "$MUX d proxy --sock $SOCK14" 72 pipe_mux "$OUT.as" "$OUT.as.err" env SHELL=/bin/sh timeout 30 "$MUX" --via "$MUX d proxy --sock $SOCK14"
@@ -134,7 +134,7 @@ grep -q "nothing listening on $SOCK14" "$OUT.stop2" || {
134 echo "e2e FAIL: stop-when-nothing said the wrong thing"; cat "$OUT.stop2"; exit 1; } 134 echo "e2e FAIL: stop-when-nothing said the wrong thing"; cat "$OUT.stop2"; exit 1; }
135 rm_swept "$OUT.ascold" "$OUT.as.start" "$OUT.as" "$OUT.as.err" "$OUT.as2" \ 135 rm_swept "$OUT.ascold" "$OUT.as.start" "$OUT.as" "$OUT.as.err" "$OUT.as2" \
136 "$OUT.as2.err" "$OUT.stop" "$OUT.stop2" 136 "$OUT.as2.err" "$OUT.stop" "$OUT.stop2"
137 ok "a cold --via proxy refuses and starts nothing; mux d start makes the daemon and mux d stop tears it down" 137 ok "a cold --via proxy refuses and starts nothing; mux d start -d makes the daemon and mux d stop tears it down"
138 138
139 # --- M13: local mux auto-start, under the pty fixture ------------------ 139 # --- M13: local mux auto-start, under the pty fixture ------------------
140 # 140 #
@@ -201,7 +201,7 @@ PAWANT=$(readlink -f "$MUX_ELF")
201 # from the basename of the FILENAME handed to execve, so exec'ing the 201 # from the basename of the FILENAME handed to execve, so exec'ing the
202 # /proc/self/exe link itself leaves every daemon on the box called `exe` — 202 # /proc/self/exe link itself leaves every daemon on the box called `exe` —
203 # invisible to `pgrep mux`, `killall mux`, `ps -o comm` and systemd's 203 # invisible to `pgrep mux`, `killall mux`, `ps -o comm` and systemd's
204 # MainPID name, with only the args still saying `mux d run`. 204 # MainPID name, with only the args still saying `mux d start`.
205 PACOMM=$(cat "/proc/$PAPID/comm") 205 PACOMM=$(cat "/proc/$PAPID/comm")
206 [ "$PACOMM" = "mux" ] || { 206 [ "$PACOMM" = "mux" ] || {
207 echo "e2e FAIL: the auto-started daemon's comm is '$PACOMM', want 'mux'" 207 echo "e2e FAIL: the auto-started daemon's comm is '$PACOMM', want 'mux'"
test/e2e_04_handoff.sh
Old New
@@ -133,7 +133,7 @@ wait_for "$OUT.h1" "cold-one" 25 || {
133 # Exactly ONE ssh run, from the log rather than from ps. Three was the cold 133 # Exactly ONE ssh run, from the log rather than from ps. Three was the cold
134 # shape while the CLIENT decided to start: `mux d endpoint` refused an empty 134 # shape while the CLIENT decided to start: `mux d endpoint` refused an empty
135 # box, the client reaped the exit code to tell that refusal from ssh's own 135 # box, the client reaped the exit code to tell that refusal from ssh's own
136 # 255, ran `mux d start`, and asked again. The remote decides now, so the 136 # 255, ran `mux d start -d`, and asked again. The remote decides now, so the
137 # run that starts the daemon is the run that announces it. 137 # run that starts the daemon is the run that announces it.
138 HSHIMS=$(wc -l < "$SSHIM_PIDLOG") 138 HSHIMS=$(wc -l < "$SSHIM_PIDLOG")
139 [ "$HSHIMS" -eq 1 ] || { 139 [ "$HSHIMS" -eq 1 ] || {
test/e2e_05_session.sh
Old New
@@ -328,7 +328,7 @@ chmod 600 "$M18KEY"
328 # is reported with the daemon's own words; only the taken-port case walks. 328 # is reported with the daemon's own words; only the taken-port case walks.
329 M18_TRIES=0 329 M18_TRIES=0
330 while : ; do 330 while : ; do
331 "$MUX" d run --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \ 331 "$MUX" d start --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \
332 --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 & 332 --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 &
333 D20PID=$! 333 D20PID=$!
334 defer_kill "$D20PID" 334 defer_kill "$D20PID"
test/e2e_09_hosts.sh
Old New
@@ -918,7 +918,7 @@ ok "the picker births on the host a digit names, forgets a host without ending i
918 # the client does with the answer. 918 # the client does with the answer.
919 # 919 #
920 # The oracle is the OS, never the daemon: the socket INODE on the far side, 920 # The oracle is the OS, never the daemon: the socket INODE on the far side,
921 # and a walk of /proc for a `mux d run` born under that box's runtime dir. A 921 # and a walk of /proc for a `mux d start` born under that box's runtime dir. A
922 # daemon asked whether it exists cannot answer no. The shim log is the 922 # daemon asked whether it exists cannot answer no. The shim log is the
923 # other half — it records the remote command, so "a read started nothing" 923 # other half — it records the remote command, so "a read started nothing"
924 # is checked against the word that ran and not only against the outcome. 924 # is checked against the word that ran and not only against the outcome.
@@ -962,13 +962,13 @@ chmod +x "$NSHIM/ssh"
962 NPATH="$NSHIM:$(cd "$(dirname "$MUX")" && pwd):$PATH" 962 NPATH="$NSHIM:$(cd "$(dirname "$MUX")" && pwd):$PATH"
963 printf 'nostart-a@127.0.0.1\nnostart-b@127.0.0.1\n' > "$NSTATE/mux/hosts" 963 printf 'nostart-a@127.0.0.1\nnostart-b@127.0.0.1\n' > "$NSTATE/mux/hosts"
964 964
965 # The /proc walk. `mux d run` and not any `mux` — the transient `mux d 965 # The /proc walk. `mux d start` and not any `mux` — the transient `mux d
966 # endpoint` processes share the runtime dir and are not daemons — and 966 # endpoint` processes share the runtime dir and are not daemons — and
967 # `environ` rather than the argv's `--sock`: the path the ensure forwards is 967 # `environ` rather than the argv's `--sock`: the path the ensure forwards is
968 # whatever this variable resolved to, so the variable is the box's identity 968 # whatever this variable resolved to, so the variable is the box's identity
969 # and the argv is derived from it. The shim gives each fake host a runtime 969 # and the argv is derived from it. The shim gives each fake host a runtime
970 # dir of its own, which is the whole of what makes them two machines. The 970 # dir of its own, which is the whole of what makes them two machines. The
971 # daemon spells `d run` because that is the argv spawn.zig 971 # daemon spells `d start` because that is the argv spawn.zig
972 # execs it with, which is also what makes `ps` legible. 972 # execs it with, which is also what makes `ps` legible.
973 nostart_daemons() { 973 nostart_daemons() {
974 _nd=0 974 _nd=0
@@ -979,7 +979,7 @@ nostart_daemons() {
979 # on one that exits mid-walk), so the silencing has to wrap the 979 # on one that exits mid-walk), so the silencing has to wrap the
980 # whole command and not just `tr`. 980 # whole command and not just `tr`.
981 { tr '\0' '\n' < "$_ne"; } 2>/dev/null | grep -qxF "XDG_RUNTIME_DIR=$1" || continue 981 { tr '\0' '\n' < "$_ne"; } 2>/dev/null | grep -qxF "XDG_RUNTIME_DIR=$1" || continue
982 { tr '\0' ' ' < "/proc/$_np/cmdline"; } 2>/dev/null | grep -q 'mux d run' || continue 982 { tr '\0' ' ' < "/proc/$_np/cmdline"; } 2>/dev/null | grep -q 'mux d start' || continue
983 _nd=$((_nd + 1)) 983 _nd=$((_nd + 1))
984 done 984 done
985 echo "$_nd" 985 echo "$_nd"
@@ -988,7 +988,7 @@ nostart_daemons() {
988 nostart_empty() { # $1 = runtime dir, $2 = socket, $3 = what was just done 988 nostart_empty() { # $1 = runtime dir, $2 = socket, $3 = what was just done
989 _nc=$(nostart_daemons "$1") 989 _nc=$(nostart_daemons "$1")
990 [ "$_nc" -eq 0 ] || { 990 [ "$_nc" -eq 0 ] || {
991 echo "e2e FAIL: no-start: $3 left $_nc mux d run under $1, want 0"; exit 1; } 991 echo "e2e FAIL: no-start: $3 left $_nc mux d start under $1, want 0"; exit 1; }
992 [ ! -e "$2" ] || { 992 [ ! -e "$2" ] || {
993 echo "e2e FAIL: no-start: $3 bound $2 — a read started a daemon"; exit 1; } 993 echo "e2e FAIL: no-start: $3 bound $2 — a read started a daemon"; exit 1; }
994 } 994 }
@@ -1046,7 +1046,7 @@ nostart_empty "$NRUNA" "$NSOCKA" "a wall polling it"
1046 # every poll cycle, would show as several. 1046 # every poll cycle, would show as several.
1047 NCB=$(nostart_daemons "$NRUNB") 1047 NCB=$(nostart_daemons "$NRUNB")
1048 [ "$NCB" -eq 1 ] || { 1048 [ "$NCB" -eq 1 ] || {
1049 echo "e2e FAIL: no-start: picker Enter left $NCB mux d run under $NRUNB, want 1"; exit 1; } 1049 echo "e2e FAIL: no-start: picker Enter left $NCB mux d start under $NRUNB, want 1"; exit 1; }
1050 [ -S "$NSOCKB" ] || { 1050 [ -S "$NSOCKB" ] || {
1051 echo "e2e FAIL: no-start: picker Enter started nothing on box B ($NSOCKB)"; exit 1; } 1051 echo "e2e FAIL: no-start: picker Enter started nothing on box B ($NSOCKB)"; exit 1; }
1052 # ONE ask in the whole run, and it is box B's. Three seconds of polling 1052 # ONE ask in the whole run, and it is box B's. Three seconds of polling
@@ -1123,7 +1123,7 @@ await_out "$OUT.nsa" "ns-cold-ok" "no-start: the cold entry never served a sessi
1123 pipe_detach "no-start cold entry client" 1123 pipe_detach "no-start cold entry client"
1124 NCA=$(nostart_daemons "$NRUNA") 1124 NCA=$(nostart_daemons "$NRUNA")
1125 [ "$NCA" -eq 1 ] || { 1125 [ "$NCA" -eq 1 ] || {
1126 echo "e2e FAIL: no-start: a cold mux HOST left $NCA mux d run under $NRUNA, want 1" 1126 echo "e2e FAIL: no-start: a cold mux HOST left $NCA mux d start under $NRUNA, want 1"
1127 cat "$OUT.nsa.err"; exit 1; } 1127 cat "$OUT.nsa.err"; exit 1; }
1128 [ -S "$NSOCKA" ] || { 1128 [ -S "$NSOCKA" ] || {
1129 echo "e2e FAIL: no-start: the cold entry started nothing on box A"; exit 1; } 1129 echo "e2e FAIL: no-start: the cold entry started nothing on box A"; exit 1; }
@@ -1149,7 +1149,7 @@ NASKS_ALL=$(grep -c -- '--start' "$NSHIMLOG" || true)
1149 # ...and box A goes down by the daemon's own verb, which makes the teardown 1149 # ...and box A goes down by the daemon's own verb, which makes the teardown
1150 # one more assertion rather than a daemon handed to the trap: what a `mux d 1150 # one more assertion rather than a daemon handed to the trap: what a `mux d
1151 # stop` owes on a box somebody started is exactly what an untouched box 1151 # stop` owes on a box somebody started is exactly what an untouched box
1152 # already shows — no `mux d run` under that runtime dir, and no socket. 1152 # already shows — no `mux d start` under that runtime dir, and no socket.
1153 # `assert_stopped` is not usable here: it wants a pid, and this daemon was 1153 # `assert_stopped` is not usable here: it wants a pid, and this daemon was
1154 # forked by an ssh shim on the far side of a client. 1154 # forked by an ssh shim on the far side of a client.
1155 "$MUX" d stop --sock "$NSOCKA" > "$OUT.nsastop" 2>&1 || { 1155 "$MUX" d stop --sock "$NSOCKA" > "$OUT.nsastop" 2>&1 || {
test/e2e_13_birth.sh
Old New
@@ -191,7 +191,7 @@ ok "the picker's a adds a host by spelling: its sessions become tiles, recorded,
191 # The snapshot tells the two apart — a served attach always sends one first 191 # The snapshot tells the two apart — a served attach always sends one first
192 # — and what this leg pins is the words an agent actually parses. 192 # — and what this leg pins is the words an agent actually parses.
193 mkdir -p "$REFSTATE" 193 mkdir -p "$REFSTATE"
194 XDG_STATE_HOME="$REFSTATE" "$MUX" d run --sock "$SOCK66" --shell /bin/sh > "$OUT.ref.d" 2>&1 & 194 XDG_STATE_HOME="$REFSTATE" "$MUX" d start --sock "$SOCK66" --shell /bin/sh > "$OUT.ref.d" 2>&1 &
195 D66PID=$! 195 D66PID=$!
196 defer_kill "$D66PID" 196 defer_kill "$D66PID"
197 wait_sock "$SOCK66" "$OUT.ref.d" "refused-attach daemon never bound" 197 wait_sock "$SOCK66" "$OUT.ref.d" "refused-attach daemon never bound"
test/e2e_14_upgrade.sh
Old New
@@ -68,7 +68,7 @@ case "$UPVER" in
68 exit 1;; 68 exit 1;;
69 esac 69 esac
70 mkdir -p "$UPHOME" 70 mkdir -p "$UPHOME"
71 MUX_SHELL_INTEGRATION=1 HOME="$UPHOME" "$MUX" d run --sock "$SOCK69" \ 71 MUX_SHELL_INTEGRATION=1 HOME="$UPHOME" "$MUX" d start --sock "$SOCK69" \
72 --shell /bin/bash > "$OUT.up.d" 2>&1 & 72 --shell /bin/bash > "$OUT.up.d" 2>&1 &
73 D69PID=$! 73 D69PID=$!
74 defer_kill "$D69PID" 74 defer_kill "$D69PID"
@@ -578,7 +578,7 @@ ok "a QUIC client is served again within a breath of the exec, not after the idl
578 # 578 #
579 # The unlink is the first thing a stopping daemon does; reaping its shells 579 # The unlink is the first thing a stopping daemon does; reaping its shells
580 # and deleting its dirs come after. A stop that said "stopped" at the 580 # and deleting its dirs come after. A stop that said "stopped" at the
581 # unlink handed a scripted `mux d start`, or a supervisor's "is it down", 581 # unlink handed a scripted `mux d start -d`, or a supervisor's "is it down",
582 # a daemon still running — `wait_pid_gone` after every stop in this suite 582 # a daemon still running — `wait_pid_gone` after every stop in this suite
583 # was that gap, papered. Shells that ignore TERM make the window a real 583 # was that gap, papered. Shells that ignore TERM make the window a real
584 # grace rather than a race the assertion could win by luck, and two 584 # grace rather than a race the assertion could win by luck, and two
test/e2e_lib.sh
Old New
@@ -67,7 +67,7 @@ $E2E_KILL"
67 67
68 # defer_sock PATH... — a path a daemon may be listening on. The trap asks 68 # defer_sock PATH... — a path a daemon may be listening on. The trap asks
69 # `mux d stop` there before it kills anything and long before it unlinks the 69 # `mux d stop` there before it kills anything and long before it unlinks the
70 # path: a daemon this shell never forked — one a proxy, `mux d start` or the 70 # path: a daemon this shell never forked — one a proxy, `mux d start -d` or the
71 # handoff spawned — has no pid here, and the socket is the only handle 71 # handoff spawned — has no pid here, and the socket is the only handle
72 # there is. 72 # there is.
73 defer_sock() { 73 defer_sock() {
@@ -152,7 +152,7 @@ export XDG_CONFIG_HOME XDG_STATE_HOME XDG_CACHE_HOME
152 # resolves $SHELL, so without this the suite runs the developer's login 152 # resolves $SHELL, so without this the suite runs the developer's login
153 # shell and its whole rc — arbitrary code, on the session under test. 153 # shell and its whole rc — arbitrary code, on the session under test.
154 # 154 #
155 # Found by soak, not by reasoning. The M10 `mux d start` block inherited zsh, 155 # Found by soak, not by reasoning. The M10 `mux d start -d` block inherited zsh,
156 # whose plugin manager roots itself at $XDG_CACHE_HOME; pointing that at a 156 # whose plugin manager roots itself at $XDG_CACHE_HOME; pointing that at a
157 # fresh directory (the line above) made every session re-clone its plugins 157 # fresh directory (the line above) made every session re-clone its plugins
158 # from the network before the shell would answer, and the scenario's marker 158 # from the network before the shell would answer, and the scenario's marker
@@ -325,7 +325,7 @@ wait_gone() {
325 } 325 }
326 326
327 # wait_sock PATH LOG LABEL — poll until a daemon has bound PATH (5s), then 327 # wait_sock PATH LOG LABEL — poll until a daemon has bound PATH (5s), then
328 # ASSERT it. Every `mux d run &` in this file needs this: the bind happens 328 # ASSERT it. Every `mux d start &` in this file needs this: the bind happens
329 # after the fork, so the very next command would otherwise race it. LOG is 329 # after the fork, so the very next command would otherwise race it. LOG is
330 # the spawn's own capture, printed on failure because a daemon that failed 330 # the spawn's own capture, printed on failure because a daemon that failed
331 # to bind almost always said why; pass "" for the spawns that have none. 331 # to bind almost always said why; pass "" for the spawns that have none.
@@ -348,7 +348,7 @@ wait_sock() {
348 # own. The long-lived daemon is $D1PID for exactly that reason — it is 348 # own. The long-lived daemon is $D1PID for exactly that reason — it is
349 # read some 5,000 lines after it is started. 349 # read some 5,000 lines after it is started.
350 # 350 #
351 # The registration is the whole reason this exists. A `mux d run &` written 351 # The registration is the whole reason this exists. A `mux d start &` written
352 # out by hand is three statements — the spawn, the pid, the wait — plus a 352 # out by hand is three statements — the spawn, the pid, the wait — plus a
353 # fourth in cleanup() saying how to end it, and the fourth is the one that 353 # fourth in cleanup() saying how to end it, and the fourth is the one that
354 # got forgotten. Here the spawn IS the registration and there is no fourth 354 # got forgotten. Here the spawn IS the registration and there is no fourth
@@ -361,7 +361,7 @@ wait_sock() {
361 # scenario after it. 361 # scenario after it.
362 start_daemon() { 362 start_daemon() {
363 _sds="$1"; _sdl="$2"; _sdlab="$3"; shift 3 363 _sds="$1"; _sdl="$2"; _sdlab="$3"; shift 3
364 "$MUX" d run --sock "$_sds" "$@" > "$_sdl" 2>&1 & 364 "$MUX" d start --sock "$_sds" "$@" > "$_sdl" 2>&1 &
365 DPID=$! 365 DPID=$!
366 defer_kill "$DPID" 366 defer_kill "$DPID"
367 defer_sock "$_sds" 367 defer_sock "$_sds"
@@ -1022,7 +1022,7 @@ leak_sweep() {
1022 cat "$LEAKBANK" 1022 cat "$LEAKBANK"
1023 _lsbad=1 1023 _lsbad=1
1024 fi 1024 fi
1025 # The detached (`mux d start`) daemons log via XDG_STATE_HOME — and every 1025 # The detached (`mux d start -d`) daemons log via XDG_STATE_HOME — and every
1026 # wall on a terminal now runs under a state home of its OWN (hostroom, 1026 # wall on a terminal now runs under a state home of its OWN (hostroom,
1027 # and the per-leg $*STATE dirs), so reading the suite's shared home alone 1027 # and the per-leg $*STATE dirs), so reading the suite's shared home alone
1028 # swept none of the daemons those legs start: the local auto-start, the 1028 # swept none of the daemons those legs start: the local auto-start, the
@@ -1095,7 +1095,7 @@ cleanup() {
1095 ' 1095 '
1096 # Sockets first, and by the daemon's own verb. `mux d stop` on a path 1096 # Sockets first, and by the daemon's own verb. `mux d stop` on a path
1097 # nobody serves is a no-op that exits 0, and on a daemon this shell never 1097 # nobody serves is a no-op that exits 0, and on a daemon this shell never
1098 # forked — one a proxy, `mux d start` or the handoff spawned — it is the 1098 # forked — one a proxy, `mux d start -d` or the handoff spawned — it is the
1099 # only handle there is. Before the kills, and long before the unlink 1099 # only handle there is. Before the kills, and long before the unlink
1100 # below: unlinking first would leave a live daemon nothing could reach 1100 # below: unlinking first would leave a live daemon nothing could reach
1101 # by path. 1101 # by path.
test/soak.sh
Old New
@@ -75,7 +75,7 @@ CYCLES="${SOAK_CYCLES:-20}"
75 WARMUP=3 75 WARMUP=3
76 RSS_BOUND_KB=4096 76 RSS_BOUND_KB=4096
77 PERSIST_FAILED_BEFORE=$FAILED 77 PERSIST_FAILED_BEFORE=$FAILED
78 "$MUX" d run --sock "$PSOCK" --shell /bin/sh > "$PLOG" 2>&1 & 78 "$MUX" d start --sock "$PSOCK" --shell /bin/sh > "$PLOG" 2>&1 &
79 PDPID=$! 79 PDPID=$!
80 _i=0 80 _i=0
81 while [ ! -S "$PSOCK" ] && [ "$_i" -lt 100 ]; do sleep 0.05; _i=$((_i + 1)); done 81 while [ ! -S "$PSOCK" ] && [ "$_i" -lt 100 ]; do sleep 0.05; _i=$((_i + 1)); done
test/throughput.sh
Old New
@@ -88,7 +88,7 @@ cleanup() {
88 } 88 }
89 trap cleanup EXIT INT TERM 89 trap cleanup EXIT INT TERM
90 90
91 "$MUX" d run --sock "$SOCK" --shell /bin/sh --cols 80 --rows 24 & 91 "$MUX" d start --sock "$SOCK" --shell /bin/sh --cols 80 --rows 24 &
92 DPID=$! 92 DPID=$!
93 i=0 93 i=0
94 while [ ! -S "$SOCK" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done 94 while [ ! -S "$SOCK" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
test/wan.sh
Old New
@@ -1133,7 +1133,7 @@ if [ "${MUX_WAN_QUIC:-}" = "1" ]; then
1133 fi 1133 fi
1134 1134
1135 say "starting the remote daemon" 1135 say "starting the remote daemon"
1136 $MUX_WAN_SSH "rm -f $RSOCK; nohup $RBIN d run --sock $RSOCK --shell /bin/bash \ 1136 $MUX_WAN_SSH "rm -f $RSOCK; nohup $RBIN d start --sock $RSOCK --shell /bin/bash \
1137 $QUIC_ARGS >$RLOG 2>&1 </dev/null & sleep 0.2" 1137 $QUIC_ARGS >$RLOG 2>&1 </dev/null & sleep 0.2"
1138 i=0 1138 i=0
1139 until $MUX_WAN_SSH "test -S $RSOCK" 2>/dev/null; do 1139 until $MUX_WAN_SSH "test -S $RSOCK" 2>/dev/null; do
test/xversion.sh
Old New
@@ -129,7 +129,7 @@ trap cleanup EXIT INT TERM
129 # own filesystem and process table, which is what this gate is actually 129 # own filesystem and process table, which is what this gate is actually
130 # asking about. 130 # asking about.
131 # $5 is how THAT side spells its daemon — `muxd run` on the old prefix, 131 # $5 is how THAT side spells its daemon — `muxd run` on the old prefix,
132 # `mux d run` on the new one. The old binary cannot answer `mux d`, and 132 # `mux d start` on the new one. The old binary cannot answer `mux d`, and
133 # asking it to would test this gate's own wording instead of the wire. 133 # asking it to would test this gate's own wording instead of the wire.
134 start_daemon() { 134 start_daemon() {
135 # shellcheck disable=SC2086 # $5 is a command spelling of one or two words 135 # shellcheck disable=SC2086 # $5 is a command spelling of one or two words
@@ -163,7 +163,7 @@ echo "xver: image=$IMAGE engine=$ENGINE ports=$PORT_OLD/$PORT_NEW"
163 163
164 start_daemon "$CN_OLD" "$OLD_BIN" "$RUN_OLD" "$PORT_OLD" "/mux/muxd run" || 164 start_daemon "$CN_OLD" "$OLD_BIN" "$RUN_OLD" "$PORT_OLD" "/mux/muxd run" ||
165 { echo "xver FAIL: could not start the old daemon's container"; exit 1; } 165 { echo "xver FAIL: could not start the old daemon's container"; exit 1; }
166 start_daemon "$CN_NEW" "$NEW_BIN" "$RUN_NEW" "$PORT_NEW" "/mux/mux d run" || 166 start_daemon "$CN_NEW" "$NEW_BIN" "$RUN_NEW" "$PORT_NEW" "/mux/mux d start" ||
167 { echo "xver FAIL: could not start the new daemon's container"; exit 1; } 167 { echo "xver FAIL: could not start the new daemon's container"; exit 1; }
168 168
169 SOCK_OLD="$RUN_OLD/muxd.sock" 169 SOCK_OLD="$RUN_OLD/muxd.sock"