43533342
fix: a resolved path can name nothing — fall back to the link, pin the other spawn
a73x 2026-08-29 02:51
Commit message
src/cli/spawn.zig
| Old | New | ||
|---|---|---|---|
| @@ -28,7 +28,21 @@ pub fn selfExe(buf: *[std.fs.max_path_bytes]u8) []const u8 { | |||
| 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 run`. 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 std.fs.selfExePath(buf) catch self_exe; | 31 | return execOrLink(std.fs.selfExePath(buf) catch return self_exe); |
| 32 | } | ||
| 33 | |||
| 34 | /// The resolved path if it can still be exec'd, the /proc link if it cannot. | ||
| 35 | /// Split from `selfExe` so the fallback is assertable without deleting a | ||
| 36 | /// binary out from under a running process. | ||
| 37 | fn execOrLink(resolved: []const u8) []const u8 { | ||
| 38 | // A readlink that SUCCEEDS can still name nothing: `make install` | ||
| 39 | // replaces the file under a running wall and the kernel then answers | ||
| 40 | // `/…/mux (deleted)`, which is a description and not a path. The link | ||
| 41 | // still opens the old inode, so the daemon starts wearing `exe` — the | ||
| 42 | // name is worth less than the start, and a refusal here would be an | ||
| 43 | // auto-start that fails for the length of one install. | ||
| 44 | std.posix.access(resolved, std.posix.X_OK) catch return self_exe; | ||
| 45 | return resolved; | ||
| 32 | } | 46 | } |
| 33 | pub const Ensured = enum { already_running, started }; | 47 | pub const Ensured = enum { already_running, started }; |
| 34 | 48 | ||
| @@ -359,6 +373,20 @@ test "selfExe: the exec'd name is a real file, not the /proc link" { | |||
| 359 | try std.posix.access(exe, std.posix.X_OK); | 373 | try std.posix.access(exe, std.posix.X_OK); |
| 360 | } | 374 | } |
| 361 | 375 | ||
| 376 | test "selfExe: a resolved path that is no longer a file falls back to the link" { | ||
| 377 | // What `make install` does to a running wall: the readlink SUCCEEDS and | ||
| 378 | // answers `/…/mux (deleted)`, so the only thing that can tell is asking | ||
| 379 | // whether the answer is still executable. Spelled as the suffix the | ||
| 380 | // kernel actually appends, because that is the string this must survive. | ||
| 381 | var buf: [std.fs.max_path_bytes]u8 = undefined; | ||
| 382 | const live = try std.fs.selfExePath(&buf); | ||
| 383 | try std.testing.expectEqualStrings(live, execOrLink(live)); | ||
| 384 | |||
| 385 | var gone: [std.fs.max_path_bytes]u8 = undefined; | ||
| 386 | const deleted = try std.fmt.bufPrint(&gone, "{s} (deleted)", .{live}); | ||
| 387 | try std.testing.expectEqualStrings(self_exe, execOrLink(deleted)); | ||
| 388 | } | ||
| 389 | |||
| 362 | test "ensureDaemon: a child that dies young is reported, not panicked on" { | 390 | test "ensureDaemon: a child that dies young is reported, not panicked on" { |
| 363 | var tmp = try testtmp.TmpDir.make(); | 391 | var tmp = try testtmp.TmpDir.make(); |
| 364 | defer tmp.cleanup(); | 392 | defer tmp.cleanup(); |
test/e2e_01_boot.sh
| Old | New | ||
|---|---|---|---|
| @@ -827,6 +827,15 @@ 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. | ||
| 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 | ||
| 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`. | ||
| 835 | SCOMM=$(cat "/proc/$SPID/comm") | ||
| 836 | [ "$SCOMM" = "mux" ] || { | ||
| 837 | echo "e2e FAIL: the daemon mux d start brought up has comm '$SCOMM', want 'mux'" | ||
| 838 | exit 1; } | ||
| 830 | # Non-tty stderr: exactly two lines, no dots. | 839 | # Non-tty stderr: exactly two lines, no dots. |
| 831 | [ "$(wc -l < "$OUT.start")" = "2" ] || { | 840 | [ "$(wc -l < "$OUT.start")" = "2" ] || { |
| 832 | echo "e2e FAIL: non-tty start not exactly two lines:"; cat "$OUT.start"; exit 1; } | 841 | echo "e2e FAIL: non-tty start not exactly two lines:"; cat "$OUT.start"; exit 1; } |
tools/collect.sh
| Old | New | ||
|---|---|---|---|
| @@ -85,6 +85,13 @@ for h in $HOSTS; do | |||
| 85 | section "box: $h" | 85 | section "box: $h" |
| 86 | # One ssh round trip per box, everything in one remote shell. BatchMode | 86 | # One ssh round trip per box, everything in one remote shell. BatchMode |
| 87 | # so an interactive prompt fails fast instead of hanging the bundle. | 87 | # so an interactive prompt fails fast instead of hanging the bundle. |
| 88 | # | ||
| 89 | # The pattern is BRACKETED and double-quoted, both for the same reason: | ||
| 90 | # this whole script arrives on the remote as one `sh -c` argument, so the | ||
| 91 | # remote shell's own cmdline contains the pattern and an unbracketed | ||
| 92 | # `mux d run` lists that shell as a daemon. Single quotes would be worse | ||
| 93 | # than wrong — they close this argument's own quoting, and the remote | ||
| 94 | # would run `pgrep -af mux d run`, three operands pgrep refuses. | ||
| 88 | ssh -o BatchMode=yes -o ConnectTimeout=10 "$h" ' | 95 | ssh -o BatchMode=yes -o ConnectTimeout=10 "$h" ' |
| 89 | printf "version: %s\n" "$(mux --version 2>&1)" | 96 | printf "version: %s\n" "$(mux --version 2>&1)" |
| 90 | k=~/.config/mux/key | 97 | k=~/.config/mux/key |
| @@ -93,7 +100,7 @@ for h in $HOSTS; do | |||
| 93 | else | 100 | else |
| 94 | echo "key: none" | 101 | echo "key: none" |
| 95 | fi | 102 | fi |
| 96 | pgrep -af 'mux d run' 2>/dev/null || echo "no mux daemons" | 103 | pgrep -af "[m]ux d run" 2>/dev/null || echo "no mux daemons" |
| 97 | echo "stats: $(mux d stats 2>&1)" | 104 | echo "stats: $(mux d stats 2>&1)" |
| 98 | l=${XDG_STATE_HOME:-$HOME/.local/state}/mux/muxd.log | 105 | l=${XDG_STATE_HOME:-$HOME/.local/state}/mux/muxd.log |
| 99 | if [ -f "$l" ]; then | 106 | if [ -f "$l" ]; then |