Fix app looping infinitely at 100% CPU when stdin reaches EOF, exit instead - #1
Closed
m-k-l-s wants to merge 3 commits into
Closed
Fix app looping infinitely at 100% CPU when stdin reaches EOF, exit instead#1m-k-l-s wants to merge 3 commits into
m-k-l-s wants to merge 3 commits into
Conversation
… EOF A POSIX selector reports a file descriptor at EOF as being readable, so once stdin hits permanent EOF the input thread's select() returns immediately forever: the app pegs a CPU core and, having no input left, can never be quit. This happens whenever stdin is at EOF while the app keeps running -- a redirect from /dev/null or an exhausted file, or a terminal that goes away without delivering SIGHUP (a closed VS Code remote terminal, a dropped SSH session). The test covers both LinuxDriver and LinuxInlineDriver, which carry independent copies of the same loop. It is expected to FAIL at this commit; the fix follows.
os.read() returning b"" means stdin is at permanent EOF, but the read 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 forever readable, so the input thread spun at 100% CPU for the life of the app. Detect the EOF on the raw read instead, leave the outer loop, and ask the app to exit: with no input left the user has no way of quitting it, and going through the normal shutdown path also restores the terminal (without this the app holds the terminal in application mode -- alt screen on, cursor hidden -- until it is killed). The exit is requested via Driver.send_message, which posts through the event loop threadsafely; WebDriver already does the same when its input stream ends. Piped input is still fully processed before the exit. Applies to both LinuxDriver and LinuxInlineDriver.
Stopping the busy-loop is the fix; exiting is a policy no other driver applies. WebDriver, the input readers and the headless driver all leave the app running when their input ends, and the existing "this can occur if the stdin is piped" comment shows carrying on was the intent. Leave the decision to exit to the app.
Owner
Author
|
Superseded by #2 and, more importantly, by Textualize#6690 (which also includes a comment about exiting on stdin EOF) |
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 receives empty input (can also be easily reproduced locally, see below), the input thread loops and cannot be cleanly exited. Only tested on macOS and Linux (i.e., using
LinuxDrivers).Fix
Check for EOF before decoding and exit cleanly on EOF.
Changes
After the fix, if you deliberately run a textual app and pass empty stdin, it will now cleanly exit rather than loop infinitely.
MRE in CI
Fix in CI
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.