997290ad
docs: one start verb, one fork, one owner of the sun_path bound
a73x 2026-08-30 08:13
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -182,12 +182,12 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 182 | session, never a box: `reap` and `pumpOnce` answer nothing, a shell's code | 182 | session, never a box: `reap` and `pumpOnce` answer nothing, a shell's code |
| 183 | reaches that shell's own clients as `exit_status`, and an emptied daemon | 183 | reaches that shell's own clients as `exit_status`, and an emptied daemon |
| 184 | keeps serving so a birth can take the default name back. One idle process | 184 | keeps serving so a birth can take the default name back. One idle process |
| 185 | per machine is the accepted cost. `mux d run` exits 0 on every shutdown — | 185 | per machine is the accepted cost. `mux d start` exits 0 on every shutdown — |
| 186 | a supervisor reads nonzero on a clean stop as a crash — and nonzero only | 186 | a supervisor reads nonzero on a clean stop as a crash — and nonzero only |
| 187 | for the boot failures `main.run` picks before anything binds. | 187 | for the boot failures `main.run` picks before anything binds. |
| 188 | - **A daemon starts only when someone asks; a read never starts one, and | 188 | - **A daemon starts only when someone asks; a read never starts one, and |
| 189 | the ask is a WORD.** `mux d start`, `mux d endpoint --start`, or the local | 189 | the ask is a WORD.** `mux d start -d`, `mux d endpoint --start`, or the |
| 190 | client's own entry spawn are the only starters. Bare `mux d endpoint` and | 190 | local client's own entry ask are the only starters. Bare `mux d endpoint` and |
| 191 | `mux d proxy` answer what is on the box and exit 1 when nothing is — the | 191 | `mux d proxy` answer what is on the box and exit 1 when nothing is — the |
| 192 | wall polls `endpoint` over ssh once a second per host, and a poll that | 192 | wall polls `endpoint` over ssh once a second per host, and a poll that |
| 193 | started daemons undid every remote `mux d stop` within a second (found on | 193 | started daemons undid every remote `mux d stop` within a second (found on |
| @@ -201,16 +201,25 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 201 | `asked` DEFAULTS to false: a new dial path that forgets the field starts | 201 | `asked` DEFAULTS to false: a new dial path that forgets the field starts |
| 202 | nothing, because a missing start says so on screen while a spurious one is | 202 | nothing, because a missing start says so on screen while a spurious one is |
| 203 | a daemon on someone else's box that nothing reports. | 203 | a daemon on someone else's box that nothing reports. |
| 204 | - **A local start execs THIS image; nothing looks a daemon up by name.** | 204 | - **The daemon starts itself, and it execs THIS image.** `mux d start` IS |
| 205 | `spawn.ensureDaemon` execs `spawn.selfExe` — the running image, `/proc/self/exe` | 205 | the daemon in the foreground; `-d` is the flag that forks one, and |
| 206 | read THROUGH to the file it names — with argv `mux d run …`. No `execvp`, no | 206 | `main.forkDaemon` is the only `posix.fork` under `src/` — build.zig's |
| 207 | PATH walk, so an auto-start can only run the binary that is already | 207 | folder rule 6, whose `except` names the one file. Every other starter |
| 208 | running. (It used to exec a `muxd` off PATH, and an e2e leg whose daemon | 208 | spells the argv and execs: `endpoint --start` calls in-process, the |
| 209 | local client's entry runs `mux d start -d --sock PATH` with its own fd 2 | ||
| 210 | lent to it, so the daemon's refusals reach the user in the daemon's own | ||
| 211 | words. The fork execs `spawn.selfExe` — the running image, | ||
| 212 | `/proc/self/exe` read THROUGH to the file it names — with argv | ||
| 213 | `mux d start …`. No `execvp`, no PATH walk, so an auto-start can only run | ||
| 214 | the binary that is already running. (It used to exec a `muxd` off PATH, and an e2e leg whose daemon | ||
| 209 | had died graded an installed v0.0.1-10 with no agent code in it.) The | 215 | had died graded an installed v0.0.1-10 with no agent code in it.) The |
| 210 | resolution is not cosmetic: `comm` is the basename of the filename handed | 216 | resolution is not cosmetic: `comm` is the basename of the filename handed |
| 211 | to execve, so exec'ing the link itself names every daemon `exe` and hides | 217 | to execve, so exec'ing the link itself names every daemon `exe` and hides |
| 212 | it from `pgrep mux` and `killall mux`. Both pins are in `e2e_03_side` — | 218 | it from `pgrep mux` and `killall mux`. Both pins are in `e2e_03_side` — |
| 213 | `readlink /proc/PID/exe` for the image, `/proc/PID/comm` for the name. | 219 | `readlink /proc/PID/exe` for the image, `/proc/PID/comm` for the name, |
| 220 | and `/proc/PID/cmdline` for the argv the attach may say and no more. | ||
| 221 | The detached child's log is APPENDED: one xdg log serves every socket on | ||
| 222 | the box, so truncating would zero a daemon still writing to it. | ||
| 214 | Running the daemon IN the fork instead was tried and crashes every Debug | 223 | Running the daemon IN the fork instead was tried and crashes every Debug |
| 215 | build: `std.debug.MemoryAccessor` caches the pid it reads memory through, | 224 | build: `std.debug.MemoryAccessor` caches the pid it reads memory through, |
| 216 | so the child's first DebugAllocator stack trace calls `process_vm_readv` | 225 | so the child's first DebugAllocator stack trace calls `process_vm_readv` |
| @@ -224,7 +233,8 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 224 | released v0.0.1-15 tarball). There is no in-place upgrade across the | 233 | released v0.0.1-15 tarball). There is no in-place upgrade across the |
| 225 | rename either: a ≤15 daemon probes the candidate for `muxd <version>` and | 234 | rename either: a ≤15 daemon probes the candidate for `muxd <version>` and |
| 226 | this binary answers `mux <version>`, so that one migration is | 235 | this binary answers `mux <version>`, so that one migration is |
| 227 | `mux d stop` then `mux d start`. | 236 | `mux d stop` then `mux d start -d` — as is 16 to the next release, whose |
| 237 | daemon execs its candidate as `d run` and finds no such verb. | ||
| 228 | - **The layout sidecar is derived convenience, not authored intent.** | 238 | - **The layout sidecar is derived convenience, not authored intent.** |
| 229 | `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from | 239 | `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from |
| 230 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall | 240 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall |
| @@ -247,7 +257,11 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 247 | dropped on purpose. A return watermark is RE-STAMPED, never carried — it | 257 | dropped on purpose. A return watermark is RE-STAMPED, never carried — it |
| 248 | belongs to the seq space that minted it. Rollback is another exec, of the | 258 | belongs to the seq space that minted it. Rollback is another exec, of the |
| 249 | binary the manifest names, with `MUX_UPGRADE_ROLLBACK` as the loop guard. | 259 | binary the manifest names, with `MUX_UPGRADE_ROLLBACK` as the loop guard. |
| 250 | - No socket stealing: `mux d run` refuses a path another daemon owns. | 260 | - **The sun_path bound belongs to the one binder.** `sockpath.max_sun_path` |
| 261 | is private and `mux d` is `tooLong`'s only caller: it refuses at parse, | ||
| 262 | before any fork, on the stderr the asking client lent it. Everyone else | ||
| 263 | dials and reads the kernel's `NameTooLong`, which truncates nothing. | ||
| 264 | - No socket stealing: `mux d start` refuses a path another daemon owns. | ||
| 251 | 265 | ||
| 252 | ## Working rules | 266 | ## Working rules |
| 253 | 267 | ||
README.md
| Old | New | ||
|---|---|---|---|
| @@ -500,6 +500,7 @@ has none. | |||
| 500 | ## Everything else | 500 | ## Everything else |
| 501 | 501 | ||
| 502 | ```sh | 502 | ```sh |
| 503 | mux d start [-d] # the daemon: in this shell, or -d to fork one and wait for it | ||
| 503 | mux d dump [--vt] # debug: print the authoritative grid (what the screen *should* be) | 504 | mux d dump [--vt] # debug: print the authoritative grid (what the screen *should* be) |
| 504 | mux d stats # live sessions by name + clients each; wire stats (deltas vs snapshot bytes) | 505 | mux d stats # live sessions by name + clients each; wire stats (deltas vs snapshot bytes) |
| 505 | mux d upgrade # exec a newly installed binary in place; every session keeps running | 506 | mux d upgrade # exec a newly installed binary in place; every session keeps running |
| @@ -557,7 +558,8 @@ ending, retry` rather than carrying a shell with no master across the exec. | |||
| 557 | Coming from v0.0.1-15 or older is the one upgrade `mux d upgrade` cannot | 558 | 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 | 559 | 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 -d`, | 560 | 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 | 561 | once per box. A v0.0.1-16 daemon is the same story for the same reason: it |
| 562 | execs its candidate as `mux d run`, and the verb is `start` now. `make install` deletes `muxd`, `muxa` and `muxweb` from the | ||
| 561 | install directory on its way in — they are modes of `mux` now, and a | 563 | 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. | 564 | `muxd` left on PATH is a second daemon waiting for someone to start it. |
| 563 | 565 | ||