44755603
feat: `mux d endpoint --start` ensures a daemon before it announces
a73x 2026-08-29 14:38
Commit message
src/cli/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -26,7 +26,7 @@ const usage = | |||
| 26 | \\ mux d stats [--sock PATH] | 26 | \\ mux d stats [--sock PATH] |
| 27 | \\ mux d stop [--sock PATH] (ask the daemon on PATH to exit) | 27 | \\ mux d stop [--sock PATH] (ask the daemon on PATH to exit) |
| 28 | \\ mux d proxy [--sock PATH] (byte pump: stdio <-> session socket) | 28 | \\ mux d proxy [--sock PATH] (byte pump: stdio <-> session socket) |
| 29 | \\ mux d endpoint [--sock PATH] (proxy that first announces QUIC port+key) | 29 | \\ mux d endpoint [--sock PATH] [--start] (announce QUIC port+key, then proxy) |
| 30 | \\ mux d keygen (write a fresh key to ~/.config/mux/key) | 30 | \\ 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) | 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) |
| @@ -161,6 +161,14 @@ const Opts = struct { | |||
| 161 | /// `upgrade`'s one exception to the strictly-newer rule. It exists for | 161 | /// `upgrade`'s one exception to the strictly-newer rule. It exists for |
| 162 | /// the e2e leg, which has only one binary to upgrade with. | 162 | /// the e2e leg, which has only one binary to upgrade with. |
| 163 | allow_same_version: bool = false, | 163 | allow_same_version: bool = false, |
| 164 | /// `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 | ||
| 166 | /// 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 | ||
| 168 | /// 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 | ||
| 170 | /// nothing, an ask spells this. | ||
| 171 | start: bool = false, | ||
| 164 | }; | 172 | }; |
| 165 | 173 | ||
| 166 | // The three flags left out are written into argv by the OLD daemon on an | 174 | // The three flags left out are written into argv by the OLD daemon on an |
| @@ -212,6 +220,15 @@ fn parseArgs(args: []const [:0]const u8) ParseResult { | |||
| 212 | .bad_value => |f| return .{ .err = .{ .bad_value = f } }, | 220 | .bad_value => |f| return .{ .err = .{ .bad_value = f } }, |
| 213 | } | 221 | } |
| 214 | 222 | ||
| 223 | // `--start` names the one verb it configures, so every other verb has | ||
| 224 | // to refuse it rather than ignore it: `mux d stats --start` and `mux d | ||
| 225 | // start --start` are the reachable typos, and a flag that vanishes | ||
| 226 | // there tells a user they asked for something when they asked for | ||
| 227 | // nothing. Reported as the unknown argument it is on that verb — the | ||
| 228 | // shared refusal, so the exit code and the page are the ones every | ||
| 229 | // other mistyped flag gets. | ||
| 230 | if (o.start and spec.cmd != .endpoint) return .{ .err = .{ .unknown_arg = "--start" } }; | ||
| 231 | |||
| 215 | // A key with nowhere to listen is a mistake parse can see the whole of. | 232 | // A key with nowhere to listen is a mistake parse can see the whole of. |
| 216 | // The mirror case is NOT one: `--quic` with no `--key` may still be | 233 | // The mirror case is NOT one: `--quic` with no `--key` may still be |
| 217 | // answered by MUX_KEY_FILE or the default key path, neither of which | 234 | // answered by MUX_KEY_FILE or the default key path, neither of which |
| @@ -310,7 +327,7 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 310 | // what `mux --via 'ssh HOST mux d proxy'` shows a user whose remote | 327 | // what `mux --via 'ssh HOST mux d proxy'` shows a user whose remote |
| 311 | // has none — README's `ssh HOST 'mux d start'` is the answer. | 328 | // has none — README's `ssh HOST 'mux d start'` is the answer. |
| 312 | .proxy => return proxy.run(sock_path), | 329 | .proxy => return proxy.run(sock_path), |
| 313 | .endpoint => return endpointCmd(alloc, sock_path, std.posix.STDOUT_FILENO), | 330 | .endpoint => return endpointCmd(alloc, sock_path, std.posix.STDOUT_FILENO, o.start), |
| 314 | } | 331 | } |
| 315 | } | 332 | } |
| 316 | 333 | ||
| @@ -964,8 +981,10 @@ fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 { | |||
| 964 | /// STDOUT_FILENO — which is what makes "the announce, then the frames, on | 981 | /// STDOUT_FILENO — which is what makes "the announce, then the frames, on |
| 965 | /// one stream" true, since `proxy.run` below writes the pump to the real | 982 | /// one stream" true, since `proxy.run` below writes the pump to the real |
| 966 | /// stdout and knows nothing of this argument. | 983 | /// stdout and knows nothing of this argument. |
| 967 | /// This is what `mux HOST` runs over ssh — and what its wall polls, which | 984 | /// This is what `mux HOST` runs over ssh — and what its wall polls. The two |
| 968 | /// is why no daemon means exit 1 and not a daemon. | 985 | /// are told apart by `start`, not by anything the client does with the |
| 986 | /// answer: an ASKED dial spells `--start` and gets a daemon ensured, a poll | ||
| 987 | /// spells the bare verb and an empty box stays empty at exit 1. | ||
| 969 | /// | 988 | /// |
| 970 | /// The announce is mandatory in both directions. The client blocks on one | 989 | /// The announce is mandatory in both directions. The client blocks on one |
| 971 | /// newline-terminated line, and the daemon side of the frame protocol | 990 | /// newline-terminated line, and the daemon side of the frame protocol |
| @@ -991,11 +1010,40 @@ fn logHint(alloc: std.mem.Allocator, buf: []u8) []const u8 { | |||
| 991 | /// `mux d proxy: cannot connect to …`. That is deliberate — reusing | 1010 | /// `mux d proxy: cannot connect to …`. That is deliberate — reusing |
| 992 | /// `proxy.run` is the whole design, and the line names the code that | 1011 | /// `proxy.run` is the whole design, and the line names the code that |
| 993 | /// failed rather than the verb that was typed. | 1012 | /// failed rather than the verb that was typed. |
| 994 | fn endpointCmd(alloc: std.mem.Allocator, sock_path: []const u8, out_fd: std.posix.fd_t) !u8 { | 1013 | fn endpointCmd(alloc: std.mem.Allocator, sock_path: []const u8, out_fd: std.posix.fd_t, start: bool) !u8 { |
| 995 | // Silent, and that is the whole of the verb's new manners: the wall | 1014 | // The ask, and the whole of what `--start` means: ensure a daemon, then |
| 996 | // runs this over ssh once a second per listed host, and a word on | 1015 | // answer as always. |
| 997 | // stderr here is a word on the wall's alternate screen. The client | 1016 | // |
| 998 | // that ASKED for the session narrates instead, and runs `mux d start`. | 1017 | // `--sock` is the ONE run flag forwarded, and it is not optional: the |
| 1018 | // path this verb is about to probe has to be the path the daemon binds. | ||
| 1019 | // Without it (`&.{}`) a `mux d endpoint --sock PATH --start` starts a | ||
| 1020 | // daemon on the DEFAULT socket, probes PATH, finds nothing and exits 1 | ||
| 1021 | // — a stray daemon nothing reports. `spawn.ensureForAttach` forwards it | ||
| 1022 | // 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 | ||
| 1024 | // `mux d start` already produces a port. | ||
| 1025 | // | ||
| 1026 | // Truncating, like `mux d start`: the user asked for a daemon, so the | ||
| 1027 | // log they go on to read must be about the one they just got. | ||
| 1028 | // | ||
| 1029 | // A spawn that failed has already said so on stderr, which ssh carries | ||
| 1030 | // to the asker; returning here leaves the announce UNWRITTEN, and an | ||
| 1031 | // absent announce is the "no daemon" shape the client already reads as | ||
| 1032 | // a failed handoff. Announcing first and dying after is the one | ||
| 1033 | // dishonest option, for the reason the doc comment above gives. | ||
| 1034 | if (start) { | ||
| 1035 | const sock_z = try alloc.dupeZ(u8, sock_path); | ||
| 1036 | defer alloc.free(sock_z); | ||
| 1037 | const run_args = [_][:0]const u8{ "--sock", sock_z }; | ||
| 1038 | if (spawn.ensure(alloc, &run_args, sock_path, "mux d endpoint", .{ .truncate = true }) == null) | ||
| 1039 | return 1; | ||
| 1040 | } | ||
| 1041 | |||
| 1042 | // Silent otherwise, and that is the whole of the verb's manners: the | ||
| 1043 | // wall runs this over ssh once a second per listed host, and a word on | ||
| 1044 | // stderr here is a word on the wall's alternate screen. The poll spells | ||
| 1045 | // the verb WITHOUT `--start`, which is what keeps a read from starting | ||
| 1046 | // anything — the rule is argv's now, not a branch in the client. | ||
| 999 | if (!spawn.probe(sock_path)) return 1; | 1047 | if (!spawn.probe(sock_path)) return 1; |
| 1000 | 1048 | ||
| 1001 | // The announce goes out on the same stdout the pump is about to use, | 1049 | // The announce goes out on the same stdout the pump is about to use, |
| @@ -1228,6 +1276,12 @@ fn parse(comptime argv: []const [:0]const u8) ParseResult { | |||
| 1228 | return parseArgs(argv); | 1276 | return parseArgs(argv); |
| 1229 | } | 1277 | } |
| 1230 | 1278 | ||
| 1279 | /// `specs` holds plain slices; a table-driven test needs argsAlloc's type. | ||
| 1280 | fn nameZ(comptime name: []const u8) [:0]const u8 { | ||
| 1281 | const buf = name ++ "\x00"; | ||
| 1282 | return buf[0..name.len :0]; | ||
| 1283 | } | ||
| 1284 | |||
| 1231 | test "parseArgs: subcommands and their existing flags" { | 1285 | test "parseArgs: subcommands and their existing flags" { |
| 1232 | const r = parse(&.{ "d", "run" }); | 1286 | const r = parse(&.{ "d", "run" }); |
| 1233 | try std.testing.expect(r == .ok); | 1287 | try std.testing.expect(r == .ok); |
| @@ -1460,6 +1514,43 @@ test "parseArgs: endpoint is a command and takes --sock" { | |||
| 1460 | try std.testing.expectEqualStrings("--sock", missing.err.missing_value); | 1514 | try std.testing.expectEqualStrings("--sock", missing.err.missing_value); |
| 1461 | } | 1515 | } |
| 1462 | 1516 | ||
| 1517 | test "parseArgs: --start belongs to endpoint alone, and every other verb refuses it" { | ||
| 1518 | // The flag that makes a cold `mux HOST` one ssh run: the client no | ||
| 1519 | // longer decides to start, so the word has to reach this side. | ||
| 1520 | const on = parse(&.{ "d", "endpoint", "--start" }); | ||
| 1521 | try std.testing.expect(on == .ok); | ||
| 1522 | try std.testing.expect(on.ok._cmd == .endpoint); | ||
| 1523 | try std.testing.expect(on.ok.start); | ||
| 1524 | // Off unless typed. A reading verb that inherited a true here would | ||
| 1525 | // start a daemon on every wall poll — the bug this whole rule exists | ||
| 1526 | // to keep dead. | ||
| 1527 | try std.testing.expect(!parse(&.{ "d", "endpoint" }).ok.start); | ||
| 1528 | |||
| 1529 | // Every OTHER verb, off the table, not a chosen one: a flag scoped by | ||
| 1530 | // a hand-written `!= .endpoint` is scoped correctly for whichever verb | ||
| 1531 | // the author happened to think of, and a verb added later inherits | ||
| 1532 | // nothing. `mux d start --start` is the reading that matters — it is | ||
| 1533 | // the plausible typo, and accepting it silently would tell a user they | ||
| 1534 | // configured something when they configured nothing. | ||
| 1535 | // | ||
| 1536 | // `.ignored` rows are excluded because their row says so: trailing | ||
| 1537 | // words after `--version`/`--help` are ACCEPTED and vanish (see Spec), | ||
| 1538 | // and e2e pins that for an over-long `--sock`. Narrowing them here | ||
| 1539 | // would be that contract's change, not this flag's. | ||
| 1540 | inline for (specs) |s| { | ||
| 1541 | if (s.cmd == .endpoint or s.flags == .ignored) continue; | ||
| 1542 | const r = parseArgs(&.{ "d", nameZ(s.name), "--start" }); | ||
| 1543 | // expect() alone prints "expected true", which does not say which | ||
| 1544 | // verb let the flag through. | ||
| 1545 | if (r != .err) std.debug.print( | ||
| 1546 | "`mux d {s} --start` was accepted; --start is endpoint's alone\n", | ||
| 1547 | .{s.name}, | ||
| 1548 | ); | ||
| 1549 | try std.testing.expect(r == .err); | ||
| 1550 | try std.testing.expectEqual(@as(u8, 2), usageCode(r.err)); | ||
| 1551 | } | ||
| 1552 | } | ||
| 1553 | |||
| 1463 | test "parseArgs: run --resume-fd N --check is the old daemon's dry run" { | 1554 | test "parseArgs: run --resume-fd N --check is the old daemon's dry run" { |
| 1464 | const r = parse(&.{ "d", "run", "--resume-fd", "7", "--check" }); | 1555 | const r = parse(&.{ "d", "run", "--resume-fd", "7", "--check" }); |
| 1465 | try std.testing.expect(r == .ok); | 1556 | try std.testing.expect(r == .ok); |
| @@ -1666,11 +1757,16 @@ test "endpointCmd: a box with no daemon is refused, never started — the wall p | |||
| 1666 | 1757 | ||
| 1667 | try std.testing.expectEqual( | 1758 | try std.testing.expectEqual( |
| 1668 | @as(u8, 1), | 1759 | @as(u8, 1), |
| 1669 | try endpointCmd(std.testing.allocator, sock, out.handle), | 1760 | // `false` is the poll's spelling, and it is the one under test: the |
| 1761 | // asking spelling forks a daemon, which no unit test may do here — | ||
| 1762 | // `spawn.ensure` takes the XDG log path with no override, so a | ||
| 1763 | // `true` would truncate the operator's live `muxd.log`. That half | ||
| 1764 | // is e2e's (`e2e_04_handoff.sh` cold leg, `e2e_09_hosts.sh`). | ||
| 1765 | try endpointCmd(std.testing.allocator, sock, out.handle, false), | ||
| 1670 | ); | 1766 | ); |
| 1671 | // The verb READS a box. Starting a daemon here gave a listed machine | 1767 | // The verb READS a box without `--start`. Starting a daemon here gave a |
| 1672 | // one (and a shell in session 0) from a poll, and undid a `mux d stop` | 1768 | // listed machine one (and a shell in session 0) from a poll, and undid a |
| 1673 | // on the next cycle a second later. | 1769 | // `mux d stop` on the next cycle a second later. |
| 1674 | try std.testing.expect(!spawn.probe(sock)); | 1770 | try std.testing.expect(!spawn.probe(sock)); |
| 1675 | try std.testing.expectEqual(@as(u64, 0), (try out.stat()).size); | 1771 | try std.testing.expectEqual(@as(u64, 0), (try out.stat()).size); |
| 1676 | } | 1772 | } |
src/cli/spawn.zig
| Old | New | ||
|---|---|---|---|
| @@ -63,11 +63,10 @@ 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 only for `muxd start`, the one caller whose user asked for a | 66 | /// True for the two verbs whose user asked for a daemon — `mux d start` |
| 67 | /// (re)start and is owed a log about the daemon they just started | 67 | /// and `mux d endpoint --start`; both probe first, so a daemon already |
| 68 | /// rather than a stale one. Auto-start appends instead: an attach is | 68 | /// writing its log is never truncated under it. Auto-start appends |
| 69 | /// not a restart, and one `mux` over ssh must not zero the log of an | 69 | /// instead: an attach is not a restart. |
| 70 | /// interactive daemon that is still writing into it. | ||
| 71 | truncate: bool, | 70 | truncate: bool, |
| 72 | }; | 71 | }; |
| 73 | 72 | ||
src/xdg.zig
| Old | New | ||
|---|---|---|---|
| @@ -76,8 +76,7 @@ pub fn keyPathFrom(alloc: std.mem.Allocator, xdg_config_home: ?[]const u8, home: | |||
| 76 | return pathFrom(alloc, xdg_config_home, home, ".config", "key"); | 76 | return pathFrom(alloc, xdg_config_home, home, ".config", "key"); |
| 77 | } | 77 | } |
| 78 | 78 | ||
| 79 | /// Truncated by `mux d start`, appended by auto-starts | 79 | /// Truncated by a verb that asks for a daemon; spawn.zig owns which and why. |
| 80 | /// (spawn.zig owns why). | ||
| 81 | pub fn logPath(alloc: std.mem.Allocator) ![]const u8 { | 80 | pub fn logPath(alloc: std.mem.Allocator) ![]const u8 { |
| 82 | return statePath(alloc, "muxd.log"); | 81 | return statePath(alloc, "muxd.log"); |
| 83 | } | 82 | } |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -170,8 +170,8 @@ done | |||
| 170 | # one of those and adds a convergence point would be pinning a fact every | 170 | # one of those and adds a convergence point would be pinning a fact every |
| 171 | # leg above already establishes. | 171 | # leg above already establishes. |
| 172 | 172 | ||
| 173 | [ "$OK_COUNT" = "89" ] || { | 173 | [ "$OK_COUNT" = "90" ] || { |
| 174 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 89 —" | 174 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 90 —" |
| 175 | echo " a scenario was added (update the pin) or silently lost" | 175 | echo " a scenario was added (update the pin) or silently lost" |
| 176 | exit 1 | 176 | exit 1 |
| 177 | } | 177 | } |
test/e2e_01_boot.sh
| Old | New | ||
|---|---|---|---|
| @@ -926,6 +926,83 @@ softkill "$SPID" || true | |||
| 926 | SPID="" | 926 | SPID="" |
| 927 | ok "mux d start — spawn, no-op rerun, log truncation, race" | 927 | ok "mux d start — spawn, no-op rerun, log truncation, race" |
| 928 | 928 | ||
| 929 | # --- `mux d endpoint --start` ensures the daemon ON THE SOCKET IT PROBES. | ||
| 930 | # | ||
| 931 | # The verb a cold `mux HOST` runs over ssh: it ensures a daemon and then | ||
| 932 | # announces that daemon's QUIC coordinates on the same stdout, which is what | ||
| 933 | # makes the attach one ssh login instead of three. What it forwards to the | ||
| 934 | # ensure is the whole of this scenario, and `--sock` is the only flag that | ||
| 935 | # may be: forwarding nothing starts the daemon on the runtime dir's DEFAULT | ||
| 936 | # socket, probes the named one, finds nothing and exits 1 — a daemon on a | ||
| 937 | # path nobody asked for, and no announce to say it happened. | ||
| 938 | # | ||
| 939 | # A runtime dir of this leg's own is what makes that default socket | ||
| 940 | # observable at all: on the suite's shared one it is indistinguishable from | ||
| 941 | # some other leg's daemon. Short, because a unix path is capped at 107 | ||
| 942 | # bytes and this one carries a socket name. | ||
| 943 | ESRUN="${TMPDIR:-/tmp}/mux-e2e-esr-$$" | ||
| 944 | defer_rm "$ESRUN" | ||
| 945 | ESSTATE="${TMPDIR:-/tmp}/mux-e2e-ess-$$" | ||
| 946 | defer_rm "$ESSTATE" | ||
| 947 | mkdir -p "$ESRUN" "$ESSTATE" | ||
| 948 | ESOWN="$ESRUN/own.sock" | ||
| 949 | ESDEF="$ESRUN/muxd.sock" | ||
| 950 | defer_sock "$ESOWN" "$ESDEF" | ||
| 951 | # stdin from /dev/null: this verb becomes `mux d proxy` once it has | ||
| 952 | # announced, and a pump with nothing left to read ends by itself. The | ||
| 953 | # announce is the first bytes of that stdout either way, which is the | ||
| 954 | # contract the client depends on. | ||
| 955 | set +e | ||
| 956 | env XDG_RUNTIME_DIR="$ESRUN" XDG_STATE_HOME="$ESSTATE" SHELL=/bin/sh timeout 30 \ | ||
| 957 | "$MUX" d endpoint --sock "$ESOWN" --start < /dev/null > "$OUT.esa" 2> "$OUT.esa.err" | ||
| 958 | ESRC=$? | ||
| 959 | set -e | ||
| 960 | [ "$ESRC" -eq 0 ] || { | ||
| 961 | echo "e2e FAIL: mux d endpoint --sock ... --start exited $ESRC (want 0)" | ||
| 962 | cat "$OUT.esa.err"; exit 1; } | ||
| 963 | # (a) The named socket is bound, and bound by a `mux` — asked of the OS by | ||
| 964 | # INODE, not by a name. A `mux` running anywhere would satisfy a name check | ||
| 965 | # while binding something else entirely; the listening inode for this path | ||
| 966 | # out of /proc/net/unix, found among that pid's open fds, cannot. | ||
| 967 | ESPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.esa.err" | head -1) | ||
| 968 | defer_kill "$ESPID" | ||
| 969 | [ -n "$ESPID" ] || { | ||
| 970 | echo "e2e FAIL: endpoint --start printed no up-line pid"; cat "$OUT.esa.err"; exit 1; } | ||
| 971 | [ -S "$ESOWN" ] || { | ||
| 972 | echo "e2e FAIL: endpoint --start bound nothing at the socket it was given" | ||
| 973 | ls -la "$ESRUN"; exit 1; } | ||
| 974 | ESINO=$(awk -v p="$ESOWN" '$NF == p {print $7}' /proc/net/unix | head -1) | ||
| 975 | [ -n "$ESINO" ] || { | ||
| 976 | echo "e2e FAIL: the kernel lists no listening socket at $ESOWN"; exit 1; } | ||
| 977 | readlink /proc/"$ESPID"/fd/* 2>/dev/null | grep -qx "socket:\[$ESINO\]" || { | ||
| 978 | echo "e2e FAIL: pid $ESPID does not hold the socket at $ESOWN — the daemon" | ||
| 979 | echo " that answered is not the daemon --start reported starting" | ||
| 980 | exit 1; } | ||
| 981 | ESCOMM=$(cat "/proc/$ESPID/comm") | ||
| 982 | [ "$ESCOMM" = "mux" ] || { | ||
| 983 | echo "e2e FAIL: the socket at $ESOWN is served by comm '$ESCOMM', want 'mux'"; exit 1; } | ||
| 984 | # (b) ...and the announce came back on stdout, first line, in the grammar | ||
| 985 | # `handoff.parseAnnounce` reads. Either arm is a pass: this box may or may | ||
| 986 | # not have a key to announce, and "which" is another scenario's business. | ||
| 987 | ESLINE=$(head -1 "$OUT.esa") | ||
| 988 | printf '%s\n' "$ESLINE" | grep -qE '^endpoint (none|[1-9][0-9]* [0-9a-f]{64})$' || { | ||
| 989 | echo "e2e FAIL: endpoint --start announced '$ESLINE', which is not an announce" | ||
| 990 | cat -v "$OUT.esa" | head -3; exit 1; } | ||
| 991 | # (c) ...and NOTHING appeared on this runtime dir's default socket. This is | ||
| 992 | # the assertion the forwarded `--sock` exists for: without it the daemon | ||
| 993 | # lands here instead, and every check above fails with it. | ||
| 994 | [ ! -e "$ESDEF" ] || { | ||
| 995 | echo "e2e FAIL: endpoint --start bound the DEFAULT socket $ESDEF as well as" | ||
| 996 | echo " the one it was given — the ensure did not forward --sock" | ||
| 997 | ls -la "$ESRUN"; exit 1; } | ||
| 998 | ESSOCKS=$(ls -1 "$ESRUN"/*.sock 2>/dev/null | wc -l) | ||
| 999 | [ "$ESSOCKS" -eq 1 ] || { | ||
| 1000 | echo "e2e FAIL: endpoint --start left $ESSOCKS sockets under $ESRUN, want 1" | ||
| 1001 | ls -la "$ESRUN"; exit 1; } | ||
| 1002 | assert_stopped "$ESOWN" "$ESPID" "endpoint --start daemon" "$OUT.esstop" | ||
| 1003 | ESPID="" | ||
| 1004 | ok "mux d endpoint --start ensures a daemon on the socket it announces, and on no other" | ||
| 1005 | |||
| 929 | # --- 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 |
| 930 | # 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 --quic 0.0.0.0'` |
| 931 | # 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 |