Skip to content

LinuxDriver input thread busy-loops at 100%+ CPU forever when stdin reaches EOF (terminal gone) #6737

Open

Description

@p4norman

Have you checked closed issues? Yes (#3831 is related but is a crash on piped stdin, not a spin).

Have you checked against the most recent version of Textual? Yes, 8.2.8, and the code below is unchanged on main.

The bug

When the terminal an app is attached to disappears (SSH drop, killed tmux pane) and the process survives (SIGHUP not delivered or ignored, e.g. nohup), the app never exits and the input thread spins, consuming a full core indefinitely. On a shared host we have seen these run for days.

In drivers/linux_driver.py, run_input_thread / process_selector_events:

unicode_data = decode(read(fileno, 1024 * 4), final=final and last)
if not unicode_data:
    # This can occur if the stdin is piped
    break

At EOF os.read returns b"" and break leaves only the inner for. The outer loop while not self.exit_event.is_set(): process_selector_events(selector.select(0.1)) then calls select again. EOF is permanently "readable", so select returns immediately and the loop never blocks.

Observed with strace on a real process (a Textual-based app detached from its terminal for weeks), repeating ~13,000 times per second:

read(0, "", 4096)                                      = 0
select(1, [0], NULL, NULL, {tv_sec=0, tv_usec=100000}) = 1 (in [0], left {tv_sec=0, tv_usec=99999})

That thread is in R state; the other threads are idle in futex_wait.

Reproduction

app.py:

import signal
from textual.app import App

signal.signal(signal.SIGHUP, signal.SIG_IGN)  # simulates SIGHUP not reaching the process
App().run()

repro.py (runs the app in a pty, then closes the master end):

import os, pty, sys, time

pid, master = pty.fork()
if pid == 0:
    os.execv(sys.executable, [sys.executable, "app.py"])
time.sleep(3)

def cpu(p):
    f = open(f"/proc/{p}/stat").read().rsplit(")", 1)[1].split()
    return (int(f[11]) + int(f[12])) / os.sysconf("SC_CLK_TCK")

os.close(master)  # terminal goes away
time.sleep(1); a = cpu(pid); time.sleep(5); b = cpu(pid)
print(f"{(b - a) / 5 * 100:.0f}% CPU; alive={os.path.exists(f'/proc/{pid}')}")
os.kill(pid, 9)

Result on Textual 8.2.8, Python 3.12, Linux 5.4: 127% CPU; alive=True. Before closing the master, the app uses ~0% CPU.

Expected

The app exits (or at minimum stops polling) when stdin reaches EOF on a terminal, rather than looping forever.

Suggested fix

Distinguish a piped stdin from a lost terminal. When read returns empty and os.isatty(fileno) was true at startup, treat it as a hangup: set exit_event / call app.exit(). For the piped case keep the current behaviour but stop polling that fd (e.g. selector.unregister(fileno) and wait on exit_event.wait(0.1)). Happy to send a PR if you'd like one.

Related

#5140 (graceful handling of termination signals) covers the signal side. This issue is the case where no signal is delivered and the EOF is the only indication.

Textual Diagnostics

Versions

Name Value
Textual 8.2.8
Rich 15.0.0

Python

Name Value
Version 3.12.11
Implementation CPython
Compiler Clang 20.1.4

Operating System

Name Value
System Linux
Release 5.4.0-216-generic
Version #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025

Terminal

Name Value
Terminal Application vscode (1.141.0)
TERM xterm-256color
COLORTERM truecolor

Activity

  1. github-actions commented on Oct 13, 2026

    @github-actions

    We found the following entries in the FAQ which you may find helpful:

    Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

    This project is developed and maintained by Will McGugan. Consider sponsoring Will's work on this project (and others).

    This is an automated reply, generated by FAQtory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions