1a22f560
refactor: one owner for the default-socket sentence and the sun_path guard
a73x 2026-08-29 10:01
Commit message
src/cli/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -305,21 +305,10 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 305 | else if (!uses_socket) | 305 | else if (!uses_socket) |
| 306 | try alloc.dupe(u8, "") | 306 | try alloc.dupe(u8, "") |
| 307 | else | 307 | else |
| 308 | sockpath.defaultSockPath(alloc) catch |err| switch (err) { | 308 | try sockpath.defaultOrExplain(alloc, "mux d") orelse return 1; |
| 309 | // Named in this binary's own words, like the sun_path bound | ||
| 310 | // below: sockpath owns the rule, each binary owns the wording. | ||
| 311 | error.NoRuntimeDir => { | ||
| 312 | std.debug.print( | ||
| 313 | "mux d: XDG_RUNTIME_DIR is unset, so there is no default socket path (name one with --sock)\n", | ||
| 314 | .{}, | ||
| 315 | ); | ||
| 316 | return 1; | ||
| 317 | }, | ||
| 318 | else => |e| return e, | ||
| 319 | }; | ||
| 320 | defer alloc.free(sock_path); | 309 | defer alloc.free(sock_path); |
| 321 | 310 | ||
| 322 | // The sun_path bound (sockpath.max_sun_path). Checked here, once, | 311 | // The sun_path bound (sockpath.tooLong). Checked here, once, |
| 323 | // before any command acts: the alternative is a spawned daemon that can | 312 | // before any command acts: the alternative is a spawned daemon that can |
| 324 | // never answer and a 2s timeout story about a path doomed at parse. | 313 | // never answer and a 2s timeout story about a path doomed at parse. |
| 325 | // `--version` and `keygen` are dispatched from the switch below, i.e. | 314 | // `--version` and `keygen` are dispatched from the switch below, i.e. |
| @@ -327,13 +316,7 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 327 | // order — neither touches the socket, and neither should be refused | 316 | // order — neither touches the socket, and neither should be refused |
| 328 | // over it. Which verbs those are is their rows' business, not this | 317 | // over it. Which verbs those are is their rows' business, not this |
| 329 | // line's. | 318 | // line's. |
| 330 | if (uses_socket and sock_path.len > sockpath.max_sun_path) { | 319 | if (uses_socket and sockpath.tooLong("mux d", sock_path)) return 1; |
| 331 | std.debug.print( | ||
| 332 | "mux d: socket path too long ({d} bytes, max {d}): {s}\n", | ||
| 333 | .{ sock_path.len, sockpath.max_sun_path, sock_path }, | ||
| 334 | ); | ||
| 335 | return 1; | ||
| 336 | } | ||
| 337 | 320 | ||
| 338 | switch (o._cmd) { | 321 | switch (o._cmd) { |
| 339 | // The socket path resolved above is unused here and unchecked (see | 322 | // The socket path resolved above is unused here and unchecked (see |
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -369,28 +369,13 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 369 | const sock_path = if (t.sock) |s| | 369 | const sock_path = if (t.sock) |s| |
| 370 | try alloc.dupe(u8, s) | 370 | try alloc.dupe(u8, s) |
| 371 | else | 371 | else |
| 372 | try defaultSock(alloc) orelse return 1; | 372 | try sockpath.defaultOrExplain(alloc, "mux") orelse return 1; |
| 373 | defer alloc.free(sock_path); | 373 | defer alloc.free(sock_path); |
| 374 | return attachLocal(alloc, sock_path, t.session, t.agent); | 374 | return attachLocal(alloc, sock_path, t.session, t.agent); |
| 375 | }, | 375 | }, |
| 376 | } | 376 | } |
| 377 | } | 377 | } |
| 378 | 378 | ||
| 379 | /// The default socket path, or the one refusal that names `--sock` as the | ||
| 380 | /// way out. | ||
| 381 | fn defaultSock(alloc: std.mem.Allocator) !?[]const u8 { | ||
| 382 | return sockpath.defaultSockPath(alloc) catch |err| switch (err) { | ||
| 383 | error.NoRuntimeDir => { | ||
| 384 | std.debug.print( | ||
| 385 | "mux: XDG_RUNTIME_DIR is unset, so there is no default socket path (name one with --sock)\n", | ||
| 386 | .{}, | ||
| 387 | ); | ||
| 388 | return null; | ||
| 389 | }, | ||
| 390 | else => |e| return e, | ||
| 391 | }; | ||
| 392 | } | ||
| 393 | |||
| 394 | /// The ONLY auto-start left, and it starts this image by its own /proc | 379 | /// The ONLY auto-start left, and it starts this image by its own /proc |
| 395 | /// link — no name to look up, so none to get wrong. | 380 | /// link — no name to look up, so none to get wrong. |
| 396 | fn ensureLocalDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !bool { | 381 | fn ensureLocalDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !bool { |
| @@ -431,20 +416,12 @@ fn attachLocal( | |||
| 431 | return 2; | 416 | return 2; |
| 432 | } | 417 | } |
| 433 | 418 | ||
| 434 | // The same `sun_path` guard the daemon applies (main.zig), for the same | 419 | // WHERE the shared guard (sockpath.tooLong) sits is this binary's |
| 435 | // reason and off the same constant. It sits before the PATH search | 420 | // decision: before the auto-start, not at the connect. Otherwise mux |
| 436 | // rather than at the connect because auto-start would otherwise reach | 421 | // forks a daemon that refuses the path instantly, then polls the full |
| 437 | // it first: mux forks a daemon that refuses the path | 422 | // 2s into "daemon did not answer" — a timeout story about a path that |
| 438 | // instantly, and polls the full 2s into "daemon did not answer" — a | 423 | // was doomed at parse. |
| 439 | // timeout story about a path that was doomed at parse. Refusing here | 424 | if (sockpath.tooLong("mux", sock_path)) return 1; |
| 440 | // costs nothing and says the real thing. | ||
| 441 | if (sock_path.len > sockpath.max_sun_path) { | ||
| 442 | std.debug.print( | ||
| 443 | "mux: socket path too long ({d} bytes, max {d}): {s}\n", | ||
| 444 | .{ sock_path.len, sockpath.max_sun_path, sock_path }, | ||
| 445 | ); | ||
| 446 | return 1; | ||
| 447 | } | ||
| 448 | 425 | ||
| 449 | if (!try ensureLocalDaemon(alloc, sock_path)) return 1; | 426 | if (!try ensureLocalDaemon(alloc, sock_path)) return 1; |
| 450 | return wallview.runAttach( | 427 | return wallview.runAttach( |
| @@ -493,7 +470,7 @@ fn wallOfHosts(alloc: std.mem.Allocator) !u8 { | |||
| 493 | // it is only that `mux` reaches for it when nothing is listed. The | 470 | // it is only that `mux` reaches for it when nothing is listed. The |
| 494 | // line itself is written once the attach answers, by | 471 | // line itself is written once the attach answers, by |
| 495 | // `wallview.runAttach`. | 472 | // `wallview.runAttach`. |
| 496 | const sock_path = try defaultSock(arena) orelse return 1; | 473 | const sock_path = try sockpath.defaultOrExplain(arena, "mux") orelse return 1; |
| 497 | return attachLocal(alloc, sock_path, "", false); | 474 | return attachLocal(alloc, sock_path, "", false); |
| 498 | } | 475 | } |
| 499 | 476 | ||
src/sockpath.zig
| Old | New | ||
|---|---|---|---|
| @@ -11,10 +11,39 @@ const std = @import("std"); | |||
| 11 | 11 | ||
| 12 | /// The usable bytes of `sockaddr_un.sun_path`: the field is 108 and the | 12 | /// The usable bytes of `sockaddr_un.sun_path`: the field is 108 and the |
| 13 | /// last one belongs to the NUL. Every binary refuses a longer path by | 13 | /// last one belongs to the NUL. Every binary refuses a longer path by |
| 14 | /// name before acting on it, each in its own words — the number is the | 14 | /// name before acting on it — the number is the kernel's and belongs in |
| 15 | /// kernel's and belongs in one place, the wording is theirs. | 15 | /// one place. |
| 16 | pub const max_sun_path = 107; | 16 | pub const max_sun_path = 107; |
| 17 | 17 | ||
| 18 | /// Checked before anything acts on the path: connect would bounce a | ||
| 19 | /// too-long one off the kernel with a generic error, and the path is the | ||
| 20 | /// whole story. `mux a` answers in JSON and keeps its own wording. | ||
| 21 | pub fn tooLong(prefix: []const u8, path: []const u8) bool { | ||
| 22 | if (path.len <= max_sun_path) return false; | ||
| 23 | std.debug.print( | ||
| 24 | "{s}: socket path too long ({d} bytes, max {d}): {s}\n", | ||
| 25 | .{ prefix, path.len, max_sun_path, path }, | ||
| 26 | ); | ||
| 27 | return true; | ||
| 28 | } | ||
| 29 | |||
| 30 | /// `defaultSockPath`, or null having printed the ONE sentence that names | ||
| 31 | /// `--sock` as the way out. The daemon and the client printed it byte for | ||
| 32 | /// byte in two places; only the program word differs, so only that is a | ||
| 33 | /// parameter. | ||
| 34 | pub fn defaultOrExplain(alloc: std.mem.Allocator, prefix: []const u8) !?[]const u8 { | ||
| 35 | return defaultSockPath(alloc) catch |err| switch (err) { | ||
| 36 | error.NoRuntimeDir => { | ||
| 37 | std.debug.print( | ||
| 38 | "{s}: XDG_RUNTIME_DIR is unset, so there is no default socket path (name one with --sock)\n", | ||
| 39 | .{prefix}, | ||
| 40 | ); | ||
| 41 | return null; | ||
| 42 | }, | ||
| 43 | else => |e| return e, | ||
| 44 | }; | ||
| 45 | } | ||
| 46 | |||
| 18 | /// No fallback when `$XDG_RUNTIME_DIR` is unset: a guess cannot make two | 47 | /// No fallback when `$XDG_RUNTIME_DIR` is unset: a guess cannot make two |
| 19 | /// binaries agree on one daemon, so the caller names it. | 48 | /// binaries agree on one daemon, so the caller names it. |
| 20 | pub fn defaultSockPath(alloc: std.mem.Allocator) ![]const u8 { | 49 | pub fn defaultSockPath(alloc: std.mem.Allocator) ![]const u8 { |
test/e2e_11_select.sh
| Old | New | ||
|---|---|---|---|
| @@ -318,16 +318,21 @@ ok "a resize is reported in-band to an app that turned on mode 2048" | |||
| 318 | # An EMPTY hosts file, because that is the case that needs the default | 318 | # An EMPTY hosts file, because that is the case that needs the default |
| 319 | # path: bare `mux` reaches for the local daemon only when nothing is | 319 | # path: bare `mux` reaches for the local daemon only when nothing is |
| 320 | # listed, and the suite's shared file is every daemon it has attached to. | 320 | # listed, and the suite's shared file is every daemon it has attached to. |
| 321 | # WORD:ARGS — the refusal names the program that refused, so a prefix | ||
| 322 | # crossed between the two binaries (or dropped to "") is caught here rather | ||
| 323 | # than sending an operator to the wrong command. | ||
| 321 | hostroom nort | 324 | hostroom nort |
| 322 | for BIN in "$MUX d stats" "$MUX"; do | 325 | for SPEC in "mux d:d stats" "mux:"; do |
| 326 | WORD=${SPEC%%:*}; ARGS=${SPEC#*:} | ||
| 323 | # shellcheck disable=SC2086 # the two-word `mux d` form is deliberate | 327 | # shellcheck disable=SC2086 # the two-word `mux d` form is deliberate |
| 324 | if env -u XDG_RUNTIME_DIR XDG_STATE_HOME="$HOSTROOM" $BIN > "$OUT.nort" 2>&1; then | 328 | if env -u XDG_RUNTIME_DIR XDG_STATE_HOME="$HOSTROOM" "$MUX" $ARGS > "$OUT.nort" 2>&1; then |
| 325 | echo "e2e FAIL: '$BIN' with no XDG_RUNTIME_DIR did not refuse"; exit 1 | 329 | echo "e2e FAIL: 'mux $ARGS' with no XDG_RUNTIME_DIR did not refuse"; exit 1 |
| 326 | fi | 330 | fi |
| 327 | grep -q 'XDG_RUNTIME_DIR is unset' "$OUT.nort" || { | 331 | grep -q "^$WORD: XDG_RUNTIME_DIR is unset" "$OUT.nort" || { |
| 328 | echo "e2e FAIL: '$BIN' refusal does not name the variable:"; cat "$OUT.nort"; exit 1; } | 332 | echo "e2e FAIL: 'mux $ARGS' refusal is not '$WORD:' naming the variable:" |
| 333 | cat "$OUT.nort"; exit 1; } | ||
| 329 | grep -q -- '--sock' "$OUT.nort" || { | 334 | grep -q -- '--sock' "$OUT.nort" || { |
| 330 | echo "e2e FAIL: '$BIN' refusal does not say how to fix it:"; cat "$OUT.nort"; exit 1; } | 335 | echo "e2e FAIL: 'mux $ARGS' refusal does not say how to fix it:"; cat "$OUT.nort"; exit 1; } |
| 331 | done | 336 | done |
| 332 | # mux a answers agents in JSON, so its refusal has to arrive as a reply. | 337 | # mux a answers agents in JSON, so its refusal has to arrive as a reply. |
| 333 | env -u XDG_RUNTIME_DIR "$MUX" a status > "$OUT.nort.a" 2>&1 && { | 338 | env -u XDG_RUNTIME_DIR "$MUX" a status > "$OUT.nort.a" 2>&1 && { |