0.10.0 follow-up 2: no orphaned inhibitor command on Linux, bounded terminal-route blocking tasks - #6372
Conversation
… a signal `systemd-inhibit` holds the idle lock around a child of its own, and the guard released it with SIGKILL — which is never forwarded — so every interactive turn on Linux left a `sleep infinity` orphan behind once the inhibitor died (the lock itself was released; the process was not). The command is now `cat` reading a pipe the guard holds: dropping the guard closes the pipe, `cat` exits on EOF, and `systemd-inhibit` follows, with `kill_on_drop` still sending the release signal immediately. A Linux test lists the inhibitor's children before the drop and asserts none outlives it; without logind the inhibitor exits at once and the list is empty, so the test bites where an inhibitor actually runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
… threads `with_session` moved every terminal route onto the blocking pool, but an unbounded number of them: one write to a child that stopped reading holds the session lock indefinitely, and each further call — including ones whose client has since disconnected, since a started blocking task cannot be cancelled — would occupy another pool thread waiting on that lock, until the runtime's unrelated blocking work stalled too. A static semaphore now admits eight session touches at a time; the rest wait asynchronously in the handler, where a disconnect simply drops them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
|
Claude finished @Hmbown's task in 1m 50s —— View job Review: PR #6372 (already merged into
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Docs only: the environment guide names `cat` on the guard's pipe as the inhibitor's command and the pipe closing as the release, matching #6372. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
Hmbown#6372 replaced the inhibitor's `sleep infinity` with `cat` on a pipe the guard holds, so the release is the pipe closing rather than a signal; the environment guide still showed the old command. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
Summary
Two findings from the post-merge reviews of #6371, both verified against main
b233045.sleep infinitybehind per interactive turn (Claude Code Review's pre-existing note, confirmed).systemd-inhibitholds the idle lock around a child of its own, and the guard released it with SIGKILL, which is never forwarded to that grandchild: the lock was released, the process was not, and a long session accumulated orphans. The inhibitor's command is nowcatreading a pipe the guard holds; dropping the guard closes the pipe,catexits on EOF andsystemd-inhibitfollows, withkill_on_dropstill sending the release signal at once. macOScaffeinatehas no grandchild and is unaffected (it just gains an unused pipe). New Linux testa_released_guard_leaves_no_grandchild_behindlists the inhibitor's children before the drop and asserts none outlives it; where no logind exists the inhibitor exits at once and the list is empty, so the test bites only where an inhibitor really runs.with_sessionmoved every route onto the blocking pool, but an unbounded number of them: oneinputwrite to a child that stopped reading holds the session lock indefinitely, and each further call, including ones whose client has disconnected (a started blocking task cannot be cancelled), would pin another pool thread waiting on that lock until unrelated blocking work stalled. A static 8-permit semaphore now gates the hop; the rest wait asynchronously in the handler, where a disconnect simply drops them. A stuck session can stall terminal routes, never the rest of the runtime.No-Issue: post-merge review findings on #6371; no tracker issue exists for them.
Testing
CI on
dfa9891, all green: Lint ✓, Test (ubuntu-latest) ✓, Test (macos-latest) ✓, Test (windows-latest) ✓, cargo check (aarch64-unknown-linux-ohos) ✓, Mobile runtime smoke ✓, Safety gate ✓, npm wrapper smoke ✓, Version drift ✓, Integrations ✓, VS Code ✓, link ✓, CodeQL (rust/python/js/actions) ✓, GitGuardian ✓.Local, on this tree (
dfa9891), tui lib test binary fromcargo rustc -p codewhale-tui --lib --profile test --all-features --locked, sealed HOME, proxy unset:sleep_guard::—test result: ok. 3 passed; 0 failed(the_inhibitor_lives_exactly_as_long_as_the_guard,holding_twice_holds_two_independent_inhibitors,a_released_guard_leaves_no_grandchild_behind). This container hassystemd-inhibitbut no logind, so the inhibitor exits at once and the grandchild list is empty: the new test passes vacuously here and is meaningful only on a host where the inhibitor really runs.runtime_api::tests::terminal_*— 2 passed (the live-session route test drives output/input/resize/kill through the gated helper against a real PTY; unknown session 404).runtime_api::tests::runtime_info_advertises_terminal_capabilities— 1 passed.Non-test
cargo check -p codewhale-tui --lib --locked— exit 0, zero warnings (the previous follow-up's-D warningsdead-code trap does not recur).scripts/check-blocking-calls-budget.py— 603 sites across 178 files, within budget;scripts/check-dead-code-budget.py— PASS, 174 attributes, exactly at budget.cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features --locked(warning-free under the CI allow list) — CI Lint ✓ ondfa9891cargo test --workspace --all-features --locked— CI Test ubuntu ✓ / macos ✓ / windows ✓ ondfa9891Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv