fix(cli): suppress OSError EIO on interrupt shutdown
When the user interrupts a long-running task, prompt_toolkit tries to
flush stdout during emergency shutdown. If stdout is in a broken state
(redirected to /dev/null, pipe closed, terminal gone), the flush raises
`OSError: [Errno 5] Input/output error` which propagates unhandled and
crashes the CLI.
Two defense layers:
1. `_suppress_closed_loop_errors`: add `OSError` with `errno.EIO` to
the asyncio exception handler, matching the existing pattern for
`RuntimeError("Event loop is closed")` and `KeyError("is not
registered")`.
2. Outer `except (KeyError, OSError)` block: add `errno.EIO` check
before the existing string-match guards, silently suppressing the
error instead of printing a misleading stdin-related message.
Fixes #13710.
This commit is contained in:
11
cli.py
11
cli.py
@@ -22,6 +22,7 @@ import re
|
||||
import concurrent.futures
|
||||
import base64
|
||||
import atexit
|
||||
import errno
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
@@ -10729,6 +10730,8 @@ class HermesCLI:
|
||||
return # silently suppress
|
||||
if isinstance(exc, KeyError) and "is not registered" in str(exc):
|
||||
return # suppress selector registration failures (#6393)
|
||||
if isinstance(exc, OSError) and getattr(exc, "errno", None) == errno.EIO:
|
||||
return # suppress I/O errors from broken stdout on interrupt (#13710)
|
||||
# Fall back to default handler for everything else
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
@@ -10761,9 +10764,11 @@ class HermesCLI:
|
||||
except (EOFError, KeyboardInterrupt, BrokenPipeError):
|
||||
pass
|
||||
except (KeyError, OSError) as _stdin_err:
|
||||
# Catch selector registration failures from broken stdin (#6393).
|
||||
# This is the fallback for cases that slip past the fstat() guard.
|
||||
if "is not registered" in str(_stdin_err) or "Bad file descriptor" in str(_stdin_err):
|
||||
# Catch selector registration failures from broken stdin (#6393)
|
||||
# and I/O errors from broken stdout during interrupt (#13710).
|
||||
if isinstance(_stdin_err, OSError) and getattr(_stdin_err, "errno", None) == errno.EIO:
|
||||
pass # suppress broken-stdout I/O errors on interrupt (#13710)
|
||||
elif "is not registered" in str(_stdin_err) or "Bad file descriptor" in str(_stdin_err):
|
||||
print(
|
||||
f"\nError: stdin is not usable ({_stdin_err}).\n"
|
||||
"This can happen with certain Python installations (e.g. uv-managed cPython on macOS).\n"
|
||||
|
||||
Reference in New Issue
Block a user