Make the plugin watcher die with the shell - #10177
Open
olafura wants to merge 2 commits into
Open
Conversation
The plugin watcher is an inotifywait the registry restarts whenever it exits, and nothing ends it when the shell itself goes. Quickshell only reaps a Process on an orderly QML teardown, and the shell often does not leave that way: Qt calls _exit() when the Wayland connection drops, and SIGTERM or SIGKILL are just as abrupt. Every such exit left one immortal inotifywait under systemd --user, each holding an inotify instance, until the user's max_user_instances quota ran out and nothing on the desktop could watch a file any more. Spawn it under setpriv --pdeathsig TERM, the kernel-level lifetime binding the clipboard watchers already use, so it dies with the shell however the shell exits. The new test SIGKILLs a shell that runs only the registry and checks that its watcher is gone. Fixes omacom#7150 Fixes omacom#7124 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GMSgUsKyhyZMRSSaXkm3F
The pdeathsig only helps shells started after it landed. An install that has been crashing for weeks still carries every watcher those crashes orphaned, each holding one of the user's 1024 inotify instances, and nothing collects them: the update that brings the fix restarts the old shell, which orphans one more on its way out. So before the registry starts its own watcher it kills any leftover copy of it. The reap runs from the init step that creates the plugins dir, whether or not mkdir succeeded, and the shell's own watcher only starts once that step has exited, so it is never the one being reaped. This is what the clipboard plugin already does for its wl-paste watchers. The watcher argv lives in one property that both the process and the reap pattern are built from, so the two cannot drift apart. pkill -f matches an extended regex against each process's argv, so every argument is escaped and the pattern is anchored at the end: it reaps exactly this command, with or without the setpriv prefix, and leaves any other inotifywait alone, whether it watches another user's dir, a test fixture's, or was started by hand. A home path holding a regex metacharacter cannot silently turn the match off either. It also closes the one path the pdeathsig leaves half open: when the crash handler execs a fresh shell in place, the PID survives, so the parent never dies and the old image's watcher keeps running beside the new one. The new image now reaps it at start. The test plants a watcher with the shell's exact argv before starting the shell and checks that it died from the shell's SIGTERM by the time the shell's own watcher is up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GMSgUsKyhyZMRSSaXkm3F
olafura
force-pushed
the
fix/plugin-watcher-dies-with-shell
branch
from
September 4, 2026 15:36
97d5ced to
2931e83
Compare
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.
Problem
PluginRegistryrunsinotifywait -m -r … ~/.config/omarchy/pluginsfor the life of the shell and restarts it whenever it exits, but nothing ends it when the shell itself goes. Quickshell'sProcessonly reaps its child on an orderly QML teardown, and the shell often does not leave that way: when the Wayland connection drops Qt calls_exit(), and SIGTERM or SIGKILL are just as abrupt. Each such exit leaves oneinotifywaitreparented tosystemd --user, immortal because the plugins dir is quiet. Every one holds an inotify instance, so after enough compositor restarts the user hitsfs.inotify.max_user_instances(1024) and nothing on the desktop can watch a file any more (#7150). The machine this was written on had 978 of them before they were cleared by hand.#7230 attaches the cleanup to
Component.onDestruction, but that hook never runs on the exits that cause the leak, which the live measurement in its review bears out. That is why this takes a different route.Fix
Spawn the watcher under
setpriv --pdeathsig TERM, the same kernel-level lifetime binding the clipboard watchers (Clipboard.qml, 60e0a2a) andomarchy-voxtype-statusalready rely on.PR_SET_PDEATHSIGfires when the thread that forked the child exits, and Quickshell startsProcesscommands synchronously on the QML thread, so it covers_exit(), SIGTERM and SIGKILL alike.setprivis util-linux, so it is on every Omarchy install already.This also closes #7124: the contract and screenshot tests end their shell with SIGTERM, which now takes the watcher with it.
The second commit deals with what is already there. The pdeathsig only helps shells started after it lands: an install that has been crashing for weeks keeps every watcher those crashes orphaned, and the update that brings the fix restarts the old shell, which orphans one more on its way out. So before the registry starts its own watcher it now runs
pkill -fagainst anyinotifywaitstill watching its plugins dir, from the same init step that creates the dir, the wayClipboard.qmlreaps stalewl-pastewatchers. The match is anchored on the exact plugins dir, so watchers of other dirs (another user's, a test fixture's) are left alone, and the shell's own watcher only starts once the reaper has exited, so it is never the one being reaped. This also covers the one path the pdeathsig leaves half open: Quickshell's crash handler re-execves the same PID in place, the forking thread does not exit there, and the old image's watcher would otherwise keep running beside the new one until the next real exit. The new image reaps it at start.Test
test/shell.d/plugin-watcher-test.shplants a watcher of the fixture's plugins dir, then launches a shell that runs only the registry (fixtures/plugin-watcher/shell.qml, no windows), waits for the shell's own watcher on that dir, checks the planted one is gone, SIGKILLs the shell and checks that the shell's watcher is gone too. It skips without a compositor, like the other Quickshell tests, and it kills both watchers itself on the way out so a failing run never leaks one.Verification
inotifywaitunder Quickshell 0.3.1. It fails on unfixedquattrowithplugin watcher outlived the shell, and with the pdeathsig but without the reaper withstale plugin watcher survived shell start.inotifywaitand/usr/bin/inotifywait, the form older shells leave behind) and leaves the ones on a sibling dir and a subdirectory alone. The init step exits 0 when it reaped something and 1 when there was nothing, and the shell starts its watcher either way.quattroand dies with this change. The contract test now leaves theinotifywaitcount unchanged._exit(), SIGKILL and SIGTERM and lands undersystemd --user; the same child undersetpriv --pdeathsig TERMdies in all three cases, and also dies after the parent re-execves itself in place and then exits.setpriv --pdeathsig TERMexecs the target in place: same PID, stdout, environment, uid and capabilities as without it.inotifywaitneither ignores nor catches SIGTERM.plugin-registry-contract-test.shandqml-text-format-test.shstill pass.Validated on a live shell
On this machine the real Omarchy shell has been exiting through the Wayland
_exit()path about 350 times a day (an HDMI hotplug storm; 20,000 connector disconnect events since boot), which is how 978 watchers piled up. The fixed shell was run as the real session shell (launched throughomarchy-launch-shellwithOMARCHY_PATHpointing at this branch) and killed with SIGKILL twice so the launcher relaunched it:inotifywait, owned by the live shell: before, after the clean stop, with the fixed shell up, after each relaunch, and after switching back to the packaged shell.inotifywaiton another dir alone.Fixes #7150
Fixes #7124
🤖 Generated with Claude Code
https://claude.ai/code/session_012GMSgUsKyhyZMRSSaXkm3F