Skip to content

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
mainfrom
eof-busy-loop-ci-demo
Closed

Fix app looping infinitely at 100% CPU when stdin reaches EOF, exit instead#1
m-k-l-s wants to merge 3 commits into
mainfrom
eof-busy-loop-ci-demo

Conversation

@m-k-l-s

@m-k-l-s m-k-l-s commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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

  1. New regression test / MRE showing the problem fails (on purpose): https://github.com/m-k-l-s/textual/actions/runs/31011504718/job/92324588038?pr=1

Fix in CI

  1. After adding a fix, tests pass: https://github.com/m-k-l-s/textual/actions/runs/31014147928

Reproducing manually

To test (tested with textual==8.2.8):

# how to reproduce 8.2.8 with this PR
1 python -m textual < /dev/null, then watch that process in top 100% of one core, steady state ~0%; app keeps running, and Ctrl+C still exits cleanly and restores the terminal
2 python -m textual — an ordinary run, as a control idle, ~0% idle, ~0% (unchanged)
3 python -m textual over SSH or in a VS Code terminal, then close the terminal so the connection drops without SIGHUP burns 100% of a core indefinitely ~0%; the process still lingers, since the app is left to decide when to exit
4 pytest tests/test_driver_input_eof.py 2 failed 2 passed

AI 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.

m-k-l-s added 2 commits August 5, 2026 15:37
… 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.
@m-k-l-s m-k-l-s closed this Aug 5, 2026
@m-k-l-s m-k-l-s reopened this Aug 5, 2026
@m-k-l-s m-k-l-s changed the title CI demo: regression test fails before the EOF busy-loop fix Fix app looping infinitely at 100% CPU when stdin reaches EOF Aug 5, 2026
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.
@m-k-l-s m-k-l-s changed the title Fix app looping infinitely at 100% CPU when stdin reaches EOF Fix app looping infinitely at 100% CPU when stdin reaches EOF, exit instead Aug 5, 2026
@m-k-l-s

m-k-l-s commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #2 and, more importantly, by Textualize#6690 (which also includes a comment about exiting on stdin EOF)

@m-k-l-s m-k-l-s closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant