One daemon owns the hotkey - #12
Merged
Merged
Conversation
Two yap daemons could run at once, and both would tap the same key: every press captured twice and the transcript typed twice. Nothing prevented it, because the two ways the daemon starts cannot see each other. launchd execs the binary directly for the login item, LaunchServices launches the .app when you double-click it, and only the second registers as an application — so macOS's own "already running" handling never sees the pair. The cask makes it routine rather than exotic. Its postflight runs `launchctl kickstart -k`, which starts the login job whether or not it was running, so anyone whose daemon came up by double-click gets a second one on their next `brew upgrade` — the old image and the new one listening together. `yap run` now claims a POSIX write lock on ~/Library/Application Support/yap/daemon.lock before it loads the model. The kernel holds it, releases it however the holder dies, and F_GETLK names the holder, so there is no stale pid file to sweep up and no recorded pid that may since have been recycled onto something unrelated. Last launch wins: the incumbent gets SIGTERM, which its own handler routes through applicationWillTerminate, so a meeting recording in flight is finalized rather than lost. Refusing to start instead would leave an upgrade running the code it just replaced while `yap --version` reads the new binary and agrees with what you installed — the worst shape for a bug. An incumbent that will not yield inside five seconds keeps the hotkey, and the newcomer exits 0 so KeepAlive does not spin it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two yap daemons could run at once, and both tap the same key — every press captured twice, every transcript typed twice.
Nothing prevented it, and it is not really about shipping a CLI inside a
.app: there is one binary, and the bundle is only there so the build can be notarized and TCC has a stable identity to hang grants on. The problem is that the daemon has two entry routes that cannot see each other. launchdexecs the binary directly for the login item; LaunchServices launches the app when you double-click it in Applications. Only the second registers as an application —launchctl listshows a double-clicked yap asapplication.com.terrifiedbug.yap.…and the agent merely as its label — so macOS's own "this app is already running" handling never sees the pair.The cask makes it routine rather than exotic. Its postflight runs
launchctl kickstart -k, which starts the login job whether or not it was running, so anyone whose daemon came up by double-click (or who used "Quit yap" and reopened it from Applications) gets a second daemon on their nextbrew upgrade: the old mapped image and the new one, listening together.Fix
yap runclaims a POSIX write lock on~/Library/Application Support/yap/daemon.lockbefore it loads the model — after flag/config validation and the doctor report, so a typo or a missing grant never costs someone the daemon they already had, and before the model load, so a takeover never holds two copies of 220 MB.A record lock rather than a pid file: the kernel holds it, it is released however the holder dies including a crash, and
F_GETLKnames the holder — no stale file to sweep up, and no recorded pid that may since have been recycled onto an unrelated process.Last launch wins. The incumbent gets SIGTERM, which its own handler already routes through
applicationWillTerminate, so a meeting recording in flight is finalized and transcribes on the next start instead of losing itsmeta.json, and the mic gain lease goes back. It exits 0, soKeepAlive SuccessfulExit:falseleaves the job down rather than racing the newcomer. The alternative — newcomer refuses to start — would leave an upgrade running the code it just replaced whileyap --versionreads the new binary and agrees with the version you installed, which is the worst shape for a bug.An incumbent that will not yield inside five seconds keeps the hotkey; the newcomer says so and exits 0, so launchd does not spin it.
Verification
Two real daemons, launched the way a user would, dev build:
listening on fn hold …another yap daemon has the hotkey (pid 49933) — replacing it→ loads →listening; A:shutting down; exactly one process leftkill -STOP), daemon C startedpid 50082 did not stop — leaving it the hotkey, exit code 0 after the 5 s wait; incumbent untouchedNo change to any measured path: the claim is one
openplus onefcntlat daemon startup, not per press, so the bench table and the key-down budget are untouched by construction.Note on the first upgrade
The guard only binds processes that have it. Upgrading to the release carrying this can still leave a pre-0.1.5 incumbent alongside the new daemon for that one upgrade — the postflight's
kickstart -kcovers the launchd case, and after that both sides hold the lock.