a73x

d587aa2c

chore: delete socket activation and contrib units — muxd start supersedes them

a73x   2026-08-09 15:20

Commit message
chore: delete socket activation and contrib units — muxd start supersedes them

Socket activation was verified working 2026-08-09 before removal; this
commit is the known-good pattern to resurrect if a KillUserProcesses=yes
box ever earns it back.

README.md
Old New
@@ -43,15 +43,16 @@ Inside a session:
43 43
44 ## Quick start, remote over ssh 44 ## Quick start, remote over ssh
45 45
46 Works anywhere ssh works. On the remote host: put `muxd` on PATH and start 46 Works anywhere ssh works. On the remote host: put `muxd` on PATH, then:
47 the daemon once (`loginctl enable-linger $USER` keeps it alive after you
48 log out; systemd user units are in `contrib/`).
49 47
50 ```sh 48 ```sh
51 ssh HOST 'setsid nohup muxd run >/dev/null 2>&1 &' 49 ssh HOST 'muxd start'
52 mux HOST # attach; Ctrl-\ detaches, running it again reattaches 50 mux HOST # attach; Ctrl-\ detaches, running it again reattaches
53 ``` 51 ```
54 52
53 `muxd start` spawns the daemon detached and waits until it answers, so it
54 is safe to run every time — if one is already up it says so and exits 0.
55
55 `mux HOST` is sugar for `mux --via "ssh HOST muxd proxy"` — ssh config 56 `mux HOST` is sugar for `mux --via "ssh HOST muxd proxy"` — ssh config
56 (aliases, ports, ProxyJump) all keeps working, since `mux` never parses 57 (aliases, ports, ProxyJump) all keeps working, since `mux` never parses
57 HOST. It **is** interpolated into a shell command, so HOST must be trusted: 58 HOST. It **is** interpolated into a shell command, so HOST must be trusted:
@@ -68,13 +69,20 @@ No ssh in the path; needs inbound UDP to the host. Authentication is a
68 shared 32-byte key (TLS 1.3 external PSK — no certificates), so the 69 shared 32-byte key (TLS 1.3 external PSK — no certificates), so the
69 handshake is one round trip. 70 handshake is one round trip.
70 71
72 Put `muxd` on the host (`scp` a static binary), then:
73
71 ```sh 74 ```sh
72 head -c 32 /dev/urandom > ~/.mux.key && chmod 600 ~/.mux.key # 32 raw bytes or 64 hex chars 75 muxd keygen # once
73 scp ~/.mux.key muxd HOST: # the key and a static binary 76 ssh HOST 'mkdir -p -m 700 ~/.config/mux && cat > ~/.config/mux/key \
74 ssh HOST 'chmod 600 ~/.mux.key; setsid nohup muxd run --quic 0.0.0.0:4433 --key ~/.mux.key >/dev/null 2>&1 &' 77 && chmod 600 ~/.config/mux/key' < ~/.config/mux/key # once per host
75 mux quic://HOST:4433 --key ~/.mux.key # or set MUX_KEY_FILE 78 ssh HOST 'muxd start --quic 0.0.0.0' # once per host boot
79 mux quic://HOST # every attach
76 ``` 80 ```
77 81
82 The port defaults to 4433 on both ends, and both binaries look for the key
83 at `~/.config/mux/key` — `--key FILE` or `MUX_KEY_FILE` override it, in
84 that order.
85
78 **Trust model, in one sentence:** anyone holding that key file can attach 86 **Trust model, in one sentence:** anyone holding that key file can attach
79 to that session, so it is exactly as sensitive as an ssh private key — 87 to that session, so it is exactly as sensitive as an ssh private key —
80 `muxd` and `mux` both refuse one that is readable by group or other, and 88 `muxd` and `mux` both refuse one that is readable by group or other, and
@@ -90,7 +98,9 @@ make bench # typing-workload byte-ratio measurement
90 98
91 Multiple clients may attach to one session; the grid follows the most 99 Multiple clients may attach to one session; the grid follows the most
92 recently active client — typing, attaching, or resizing claims it (latest 100 recently active client — typing, attaching, or resizing claims it (latest
93 wins). With linger enabled a session survives logout, though not a reboot. 101 wins). A session survives logout (this assumes systemd-logind's default
102 `KillUserProcesses=no`; a box configured to kill user processes at logout
103 kills the daemon with them), though not a reboot.
94 `muxd run` refuses a socket another daemon already owns; there is no 104 `muxd run` refuses a socket another daemon already owns; there is no
95 socket-stealing. 105 socket-stealing.
96 106
contrib/muxd.service
Old New
@@ -1,8 +0,0 @@
1 [Unit]
2 Description=mux daemon (prototype)
3 Requires=muxd.socket
4
5 [Service]
6 # Adjust the path to your checkout; the prototype is not installed system-wide.
7 ExecStart=%h/code/rad/mux/zig-out/bin/muxd run
8 Restart=no
contrib/muxd.socket
Old New
@@ -1,8 +0,0 @@
1 [Unit]
2 Description=mux daemon socket
3
4 [Socket]
5 ListenStream=%t/muxd.sock
6
7 [Install]
8 WantedBy=sockets.target
docs/superpowers/plans/2026-08-09-m10-quic-ergonomics.md
Old New
@@ -1401,7 +1401,7 @@ git commit -m "fix: a --via command that died before connecting no longer report
1401 - Modify: `src/server.zig` (~350, ~405–460, ~507–514, ~532) 1401 - Modify: `src/server.zig` (~350, ~405–460, ~507–514, ~532)
1402 - Modify: `README.md` (ssh quick start ~44–63, QUIC quick start ~65–81, multi-client paragraph ~90–95) 1402 - Modify: `README.md` (ssh quick start ~44–63, QUIC quick start ~65–81, multi-client paragraph ~90–95)
1403 1403
1404 - [ ] **Step 1: server.zig — remove the activation path** 1404 - [x] **Step 1: server.zig — remove the activation path**
1405 1405
1406 - Delete `listenFdFromSystemd` (lines ~507–514). 1406 - Delete `listenFdFromSystemd` (lines ~507–514).
1407 - In `Server.init` (~405): delete the `systemd_fd` read and its comment, delete the whole `if (systemd_fd) |fd| { ... }` return block (~428–441), and change the claim line to unconditional: 1407 - In `Server.init` (~405): delete the `systemd_fd` read and its comment, delete the whole `if (systemd_fd) |fd| { ... }` return block (~428–441), and change the claim line to unconditional:
@@ -1415,13 +1415,13 @@ git commit -m "fix: a --via command that died before connecting no longer report
1415 - Delete the `owns_sock_file` field (~350) and its `.owns_sock_file = true` initializer (~455). 1415 - Delete the `owns_sock_file` field (~350) and its `.owns_sock_file = true` initializer (~455).
1416 - In teardown (~532): delete the line `if (!self.owns_sock_file) break :ours false;`. The dev/ino identity check STAYS — "only unlink the socket you created" guards against a different hazard (another daemon's socket at the same path) than activation was. 1416 - In teardown (~532): delete the line `if (!self.owns_sock_file) break :ours false;`. The dev/ino identity check STAYS — "only unlink the socket you created" guards against a different hazard (another daemon's socket at the same path) than activation was.
1417 1417
1418 - [ ] **Step 2: Delete contrib** 1418 - [x] **Step 2: Delete contrib**
1419 1419
1420 ```bash 1420 ```bash
1421 git rm contrib/muxd.service contrib/muxd.socket 1421 git rm contrib/muxd.service contrib/muxd.socket
1422 ``` 1422 ```
1423 1423
1424 - [ ] **Step 3: README** 1424 - [x] **Step 3: README**
1425 1425
1426 Replace the ssh quick-start's daemon line (~50–53): 1426 Replace the ssh quick-start's daemon line (~50–53):
1427 1427
@@ -1454,12 +1454,12 @@ A session survives logout (this assumes systemd-logind's default
1454 kills the daemon with them), though not a reboot. 1454 kills the daemon with them), though not a reboot.
1455 ``` 1455 ```
1456 1456
1457 - [ ] **Step 4: Verify nothing referenced the deleted code** 1457 - [x] **Step 4: Verify nothing referenced the deleted code**
1458 1458
1459 Run: `grep -rn "LISTEN_FDS\|listenFdFromSystemd\|owns_sock_file\|contrib/" src/ test/ README.md Makefile build.zig` 1459 Run: `grep -rn "LISTEN_FDS\|listenFdFromSystemd\|owns_sock_file\|contrib/" src/ test/ README.md Makefile build.zig`
1460 Expected: no matches. Then `make test && make build && make e2e` — all pass (the server module's existing socket tests cover the always-claim path). 1460 Expected: no matches. Then `make test && make build && make e2e` — all pass (the server module's existing socket tests cover the always-claim path).
1461 1461
1462 - [ ] **Step 5: Commit** 1462 - [x] **Step 5: Commit**
1463 1463
1464 ```bash 1464 ```bash
1465 git add -A 1465 git add -A
src/server.zig
Old New
@@ -347,7 +347,6 @@ pub const Server = struct {
347 pty: Pty, 347 pty: Pty,
348 listener: std.net.Server, 348 listener: std.net.Server,
349 sock_path: []const u8, 349 sock_path: []const u8,
350 owns_sock_file: bool,
351 /// The socket file's identity at the moment it was bound, so teardown 350 /// The socket file's identity at the moment it was bound, so teardown
352 /// can tell our socket from one that replaced it. 351 /// can tell our socket from one that replaced it.
353 /// 352 ///
@@ -358,8 +357,8 @@ pub const Server = struct {
358 /// and was unconditionally false, which meant the daemon never unlinked 357 /// and was unconditionally false, which meant the daemon never unlinked
359 /// its socket on a clean exit at all — masked ever since by the stale 358 /// its socket on a clean exit at all — masked ever since by the stale
360 /// socket recovery in `claimSockPath` cleaning up on the next start. 359 /// socket recovery in `claimSockPath` cleaning up on the next start.
361 sock_dev: u64 = 0, 360 sock_dev: u64,
362 sock_ino: u64 = 0, 361 sock_ino: u64,
363 /// The attached interactive clients. All of them see every update. 362 /// The attached interactive clients. All of them see every update.
364 clients: [max_clients]?ClientSlot = @splat(null), 363 clients: [max_clients]?ClientSlot = @splat(null),
365 /// How much unsent output one client may accumulate before the daemon 364 /// How much unsent output one client may accumulate before the daemon
@@ -403,15 +402,9 @@ pub const Server = struct {
403 }; 402 };
404 403
405 pub fn init(alloc: std.mem.Allocator, opts: Options) !Server { 404 pub fn init(alloc: std.mem.Allocator, opts: Options) !Server {
406 // systemd socket activation: LISTEN_FDS=1 hands us the listener as
407 // fd 3, and the path is systemd's to manage — we neither probe nor
408 // unlink it. Read once, up front, so the claim below can be the
409 // first thing that happens on the self-bind path.
410 const systemd_fd = listenFdFromSystemd();
411
412 // Before the shell is spawned, so refusing costs nobody a fork and 405 // Before the shell is spawned, so refusing costs nobody a fork and
413 // leaves no process to reap. 406 // leaves no process to reap.
414 if (systemd_fd == null) try claimSockPath(opts.sock_path); 407 try claimSockPath(opts.sock_path);
415 408
416 const eng = try Engine.init(alloc, .{ .cols = opts.cols, .rows = opts.rows }); 409 const eng = try Engine.init(alloc, .{ .cols = opts.cols, .rows = opts.rows });
417 errdefer eng.deinit(); 410 errdefer eng.deinit();
@@ -425,21 +418,6 @@ pub const Server = struct {
425 var epoch: u64 = 0; 418 var epoch: u64 = 0;
426 while (epoch == 0) epoch = std.crypto.random.int(u64); 419 while (epoch == 0) epoch = std.crypto.random.int(u64);
427 420
428 if (systemd_fd) |fd| {
429 return .{
430 .alloc = alloc,
431 .eng = eng,
432 .pty = pty,
433 .listener = .{
434 .listen_address = undefined,
435 .stream = .{ .handle = fd },
436 },
437 .sock_path = opts.sock_path,
438 .owns_sock_file = false,
439 .epoch = epoch,
440 };
441 }
442
443 const addr = try std.net.Address.initUnix(opts.sock_path); 421 const addr = try std.net.Address.initUnix(opts.sock_path);
444 const listener = try addr.listen(.{}); 422 const listener = try addr.listen(.{});
445 // Stat the path we just created, not the descriptor: this is the 423 // Stat the path we just created, not the descriptor: this is the
@@ -452,7 +430,6 @@ pub const Server = struct {
452 .pty = pty, 430 .pty = pty,
453 .listener = listener, 431 .listener = listener,
454 .sock_path = opts.sock_path, 432 .sock_path = opts.sock_path,
455 .owns_sock_file = true,
456 .sock_dev = @intCast(st.dev), 433 .sock_dev = @intCast(st.dev),
457 .sock_ino = @intCast(st.ino), 434 .sock_ino = @intCast(st.ino),
458 .epoch = epoch, 435 .epoch = epoch,
@@ -504,15 +481,6 @@ pub const Server = struct {
504 }; 481 };
505 } 482 }
506 483
507 fn listenFdFromSystemd() ?std.posix.fd_t {
508 const pid_s = std.posix.getenv("LISTEN_PID") orelse return null;
509 const nfds_s = std.posix.getenv("LISTEN_FDS") orelse return null;
510 const pid = std.fmt.parseInt(std.posix.pid_t, pid_s, 10) catch return null;
511 const nfds = std.fmt.parseInt(u32, nfds_s, 10) catch return null;
512 if (pid != std.os.linux.getpid() or nfds < 1) return null;
513 return 3; // SD_LISTEN_FDS_START
514 }
515
516 pub fn deinit(self: *Server) void { 484 pub fn deinit(self: *Server) void {
517 for (&self.clients) |*slot| { 485 for (&self.clients) |*slot| {
518 if (slot.*) |*c| { 486 if (slot.*) |*c| {
@@ -529,7 +497,6 @@ pub const Server = struct {
529 // deleting that one would hand its clients the same field incident 497 // deleting that one would hand its clients the same field incident
530 // the socket-steal fix exists to prevent. 498 // the socket-steal fix exists to prevent.
531 const ours: bool = ours: { 499 const ours: bool = ours: {
532 if (!self.owns_sock_file) break :ours false;
533 const pst = std.posix.fstatat(std.posix.AT.FDCWD, self.sock_path, 0) catch break :ours false; 500 const pst = std.posix.fstatat(std.posix.AT.FDCWD, self.sock_path, 0) catch break :ours false;
534 break :ours pst.dev == self.sock_dev and pst.ino == self.sock_ino; 501 break :ours pst.dev == self.sock_dev and pst.ino == self.sock_ino;
535 }; 502 };