Bug: RuntimeError: reentrant call inside <_io.BufferedWriter> when stopping the server via Ctrl+C on Windows
Summary
On Windows, when the Streamlit server is stopped via Ctrl+C (SIGINT), the signal handler calls server.stop() which tries to write a "Stopping..." message to the console via click.secho → colorama's AnsiToWin32. This can race with the asyncio event loop's select.select() → buffered write to the same console handle, causing a RuntimeError: reentrant call inside <_io.BufferedWriter>.
Error
RuntimeError: reentrant call inside <_io.BufferedWriter>
Full traceback
Traceback (most recent call last):
File "bootstrap.py", line 43, in signal_handler
server.stop()
File "server.py", line 529, in stop
cli_util.print_to_cli(" Stopping...", fg="blue")
File "cli_util.py", line 34, in print_to_cli
click.secho(message, **kwargs)
File "termui.py", line 690, in secho
return echo(message, file=file, nl=nl, err=err, color=color)
File "utils.py", line 321, in echo
file.write(out) # type: ignore
File "_compat.py", line 543, in _safe_write
return _write(s)
File "ansitowin32.py", line 47, in write
self.__convertor.write(text)
File "ansitowin32.py", line 177, in write
self.write_and_convert(text)
File "ansitowin32.py", line 202, in write_and_convert
self.write_plain_text(text, cursor, start)
File "ansitowin32.py", line 211, in write_plain_text
self.wrapped.flush()
File "_winconsole.py", line 176, in write
buf = get_buffer(b)
File "_winconsole.py", line 112, in get_buffer
out: Array[c_char] = buffer_type.from_address(buf.buf)
RuntimeError: reentrant call inside <_io.BufferedWriter>
Note: the signal handler at bootstrap.py:43 is invoked inside the asyncio event loop's _run_once → select.select() call, so the reentrant write happens while the loop is already inside a buffered I/O operation.
Environment
- OS: Windows 10/11 (10.0.26200)
- Python: 3.11.3
- Streamlit: latest (reproduced via
aider --gui which launches Streamlit)
- Virtual environment: No
Root cause
The signal handler (signal_handler in bootstrap.py) runs synchronously inside the asyncio event loop's select.select() call. It calls server.stop() → cli_util.print_to_cli(" Stopping...", fg="blue") → click.secho → colorama AnsiToWin32.write → wrapped.flush() → _winconsole.py get_buffer. This writes to the same _io.BufferedWriter that the event loop may already be writing to, triggering CPython's reentrancy guard: RuntimeError: reentrant call inside <_io.BufferedWriter>.
This is a race condition — it doesn't happen every time, only when the signal arrives while the event loop is mid-write to the console buffer.
Suggested fix
Avoid writing to the console from inside the signal handler. Options:
-
Set a flag in the signal handler and do the actual stop + console output from the event loop. E.g.:
def signal_handler(signum, frame):
# Don't do I/O inside the signal handler; schedule the stop
loop.call_soon_threadsafe(server.stop)
-
Wrap the cli_util.print_to_cli call in a try/except to swallow RuntimeError during shutdown (less clean, but minimal change).
-
Use signal.signal() with a coroutine-aware handler that doesn't write to the console synchronously.
Option 1 is the cleanest — signal handlers should never do I/O.
Bug:
RuntimeError: reentrant call inside <_io.BufferedWriter>when stopping the server via Ctrl+C on WindowsSummary
On Windows, when the Streamlit server is stopped via Ctrl+C (SIGINT), the signal handler calls
server.stop()which tries to write a "Stopping..." message to the console viaclick.secho→ colorama'sAnsiToWin32. This can race with the asyncio event loop'sselect.select()→ buffered write to the same console handle, causing aRuntimeError: reentrant call inside <_io.BufferedWriter>.Error
Full traceback
Note: the signal handler at
bootstrap.py:43is invoked inside the asyncio event loop's_run_once→select.select()call, so the reentrant write happens while the loop is already inside a buffered I/O operation.Environment
aider --guiwhich launches Streamlit)Root cause
The signal handler (
signal_handlerinbootstrap.py) runs synchronously inside the asyncio event loop'sselect.select()call. It callsserver.stop()→cli_util.print_to_cli(" Stopping...", fg="blue")→click.secho→ coloramaAnsiToWin32.write→wrapped.flush()→_winconsole.pyget_buffer. This writes to the same_io.BufferedWriterthat the event loop may already be writing to, triggering CPython's reentrancy guard:RuntimeError: reentrant call inside <_io.BufferedWriter>.This is a race condition — it doesn't happen every time, only when the signal arrives while the event loop is mid-write to the console buffer.
Suggested fix
Avoid writing to the console from inside the signal handler. Options:
Set a flag in the signal handler and do the actual stop + console output from the event loop. E.g.:
Wrap the
cli_util.print_to_clicall in a try/except to swallowRuntimeErrorduring shutdown (less clean, but minimal change).Use
signal.signal()with a coroutine-aware handler that doesn't write to the console synchronously.Option 1 is the cleanest — signal handlers should never do I/O.