fix(cmux-config): macOS file watch uses kqueue, so the daemon sees acpmux bind its socket
Summary
On macOS an idle cmux-tui daemon never connected to acpmux when acpmux started after the daemon, as long as acpmux's folder already existed. cmux-tui::idle_wakeups cmux_next_daemon_idle_has_no_periodic_wakeups is red on macOS for this reason ("the daemon did not connect to acpmux within 3 s of its socket appearing", also when run alone, job 509e39756a834d8f1fc1b527).
The agent chat notices thread (agent_chat_notices.rs watch()) waits on cmux_config::watch for acpmux's socket. On macOS that watcher used FSEvents for every folder that holds a watched name. FSEvents does not report a Unix socket being bound at all. Measured with a raw notify FSEvents watch of the folder (scratch diag job 313c5cbdc973cc41db671b58, aws-m4pro-6, two tries): a socket bind gave no event within 8 s, both times; in the same folder a file create came 6 ms after the write and the socket's removal 1.4 ms after the unlink. The paths were already canonical (/private/tmp/...), so it is not a /var vs /private/var mismatch, and not lateness. The focused test below fails the same way through cmux_config::watch. FSEvents teardown is also slow on a loaded Mac: two existing tests, agent_chat_notices "shutdown/drop ends a watch that waits for acpmux", failed on the first try in 2 of 5 base jobs on macOS ("the watch outlived ...": the watch thread, which starts and stops an FSEvents stream, took over 5 s to end) and passed on the gate rerun.
macOS now has its own backend, cmux-config/src/watch/kqueue.rs, built from the kqueue code that already watched a missing folder's ancestors (ancestors.rs, removed):
- every planned folder gets a vnode watch of its entries (
NOTE_WRITE, plusNOTE_DELETE/RENAME/REVOKEfor the folder itself); - each watched name that is a regular file gets a content and metadata watch (a write in place changes no entry);
- a folder event names no entry, so each folder keeps the identity (dev, inode, mode, ctime) of its watched names and hints only when one changed; other entries of the folder never hint;
- one thread blocks in
keventwith no timeout outside a burst, so an idle daemon does not wake. Dropping the watcher triggers a user event; no fseventsd call on teardown.
The folders watched are the same as before (the settings file's folder, the nearest existing ancestor of a missing one, and acpmux's own socket folder); no new folder is watched. Linux (inotify.rs) is unchanged; portable.rs now serves only platforms other than Linux and macOS.
Testing
Red first: fefd9a968bf adds cmux-tui/crates/cmux-config/tests/watch.rs alone.
| what | ref | job | result |
|---|---|---|---|
test watch_ (macOS, aws-m4pro-2) | base + test fefd9a968bf | 6a56674b79d3d0183e77ce84 | watch_hints_when_a_socket_is_bound_in_a_watched_folder FAIL on the first try and on the gate rerun ("no hint within 2s of a socket bound at a watched name"); the file and other-entry tests PASS |
notices shutdown/drop, test ends_a_watch_that_waits_for_acpmux, 5 jobs (macOS) | base e820cedb59f | 96c47517, df5a6aab, d24dd90c, 6d014553, f0872aaa | failed on the first try in 2 of 5 base jobs (96c47517, df5a6aab: "the watch outlived ..."), passed on the gate rerun (gate-notice: FLAKY); the other 3 passed (0.5-5.9 s) |
| idle test, 5 jobs (macOS) | head e22fa881f4e | 1e8d3215, 4f1b24d2, 96510a02, b771dabb, 118fa038 | 5 of 5 PASS on the first try, no FLAKY (118fa038 also ran ..._ignores_writes_below_a_missing_settings_folder: PASS) |
| notices shutdown/drop, 4 jobs (macOS) | head | f4866419, 49c68bfc, 773bcf20, 2ff97975 | 4 of 4 PASS on the first try (0.15-4.3 s) |
all watch (Linux: fmt, clippy -D warnings, tests) | head | 8af7def1c888a8747fdc0a82 | pass; 108 tests passed, including the 3 new watch tests |
all watch (macOS: fmt, clippy -D warnings, tests) | head | 21155f8bc4aacd339b653cd5 | pass; 106 tests passed, including the 3 new watch tests and the 4 notices shutdown/drop tests, no FLAKY |
all cmux_next_daemon_idle (macOS: fmt, clippy, tests) | head | 1b35d91ba116484703fbf888 | pass; 2 of 2 idle tests passed |
test cmux_next_daemon_idle (Linux) | head | 2a18462dda8e919c20df96fc | 2 of 2 idle tests passed |
The scratch diag ran on branch scratch/acpmux-hint-diag (never merged).
Observed / Not run
Observed: the jobs above, read from their logs (each attempt counted, gate-notice: FLAKY searched).
Not run:
- b28f08d6a57ceaa0bde5dc08 (Linux) and d67189fe2e4f7f3e92e1b7f0 (macOS): the filter
idle_wakeupsis the test binary's name, and nextest filters test names, so 0 tests ran. They count as not run (d67189fe's fmt and clippy did pass on macOS). - Windows:
portable.rsis now the backend only for platforms other than Linux and macOS, so Windows still uses notify'sRecommendedWatcher(ReadDirectoryChangesW) with the same code as before; this PR only removed its macOS branches.#[cfg(target_os = "macos")]keepskqueue.rsout of every other build. The PR'sbuild-windows / build x86_64-pc-windows-gnucheck compiles it (pass on this head); no Windows test of the watcher ran. - No tagged app build: the change is in the daemon's file watcher. The settings watcher's file behavior is covered by
watch_hints_when_a_watched_file_is_created_written_and_removedand the idle tests above.
Performance
No perf numbers: this changes which kernel API wakes the watch thread, not a measured speed path. The idle wakeup check is cmux_next_daemon_idle_has_no_periodic_wakeups (and ..._ignores_writes_below_a_missing_settings_folder), which pass at the head on macOS.
Changelog
Fixed: On macOS, agent chat notifications start as soon as acpmux starts, instead of never when acpmux's folder already existed.