Fix input thread busy-looping at 100% CPU when stdin reaches EOF - #6690
Open
m-k-l-s wants to merge 3 commits into
Open
Fix input thread busy-looping at 100% CPU when stdin reaches EOF#6690m-k-l-s wants to merge 3 commits into
m-k-l-s wants to merge 3 commits into
Conversation
A selector reports a file descriptor at EOF as readable, so select() returns immediately, forever. The input thread never notices, so it busy-loops and pegs a CPU core for the life of the app. Covers LinuxDriver and LinuxInlineDriver, which carry independent copies of the same loop. Expected to fail at this commit; the fix follows.
os.read() returning b"" means stdin is at EOF, but it was conflated with a decode that yields "" for an incomplete UTF-8 sequence, and the resulting break only left the inner loop. The outer loop kept calling select() on a descriptor that EOF reports as permanently readable. Detect EOF on the raw read and stop selecting. The app is left running, as it is on the other drivers when their input ends. Applies to both LinuxDriver and LinuxInlineDriver.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a runaway CPU usage issue in the POSIX (Linux/macOS) input thread when stdin reaches EOF (e.g., stdin redirected from /dev/null or an SSH session drops and stdin closes). The fix ensures the selector loop terminates instead of repeatedly treating EOF as a permanently-readable file descriptor.
Changes:
- Detect
os.read(...) == b""(EOF) inLinuxDriverandLinuxInlineDriverinput loops and stop selecting to prevent busy-looping. - Add a regression test that asserts the input thread terminates promptly when its input FD is permanently at EOF.
- Document the fix in the changelog under “Unreleased”.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/textual/drivers/linux_driver.py |
Stops the POSIX input thread when stdin hits EOF to prevent a 100% CPU busy loop. |
src/textual/drivers/linux_inline_driver.py |
Applies the same EOF handling to the inline POSIX driver input thread. |
tests/test_driver_input_eof.py |
Adds a regression test ensuring the input thread exits when reading from an EOF FD. |
CHANGELOG.md |
Notes the bugfix under “Unreleased”. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Motivation
We are using Textual for an in-house TUI for monitoring long-running jobs (usually from a remote machine via SSH). We've noticed weird hanging TUI processes on the remote machine when the SSH connections dropped/died without receiving SIGHUP.
The TUI process (that started and then monitored a job) would then live on with a dead stdin and it would use 100% CPU indefinitely.
Cause
Turns out that if a textual process's stdin reaches EOF (can also be easily reproduced locally, see below), the input thread loops forever and cannot be cleanly exited. Only tested on macOS and Linux (i.e., using
LinuxDrivers).Fix
Check for EOF before decoding, and stop reading instead of looping forever.
Changes
After the fix, a textual app whose stdin is at EOF no longer uses 100% of a core: the input thread stops and the app keeps running, as it does on the other drivers when their input ends.
MRE / test / CI
(Links are from my own fork for now, cause CI doesn't run here without manual approval.)
Exiting on EOF instead?
In our case, we would actually prefer to cleanly exit on stdin EOF, but I suppose that is not what the default behaviour should be. Either way, it would look like this:
And similarly
LinuxInlineDriver.send_messageis thread-safe, so the app then shuts down through its normal path, which also restores the terminal.Reproducing manually
To test (tested with
textual==8.2.8):python -m textual < /dev/null, then watch that process intoppython -m textual— an ordinary run, as a controlpython -m textualover SSH or in a VS Code terminal, then close the terminal so the connection drops without SIGHUPpytest tests/test_driver_input_eof.pyAI policy
Discovering the root cause was a lot of trial and error, both human and LLMs. Fix itself is implemented via Claude Code, Opus 5. PR description (except for the table above^) is written by hand.