device/monitor: fix pio monitor crash when stdin is a pipe#5477
Open
Socialpranker wants to merge 1 commit into
Open
device/monitor: fix pio monitor crash when stdin is a pipe#5477Socialpranker wants to merge 1 commit into
Socialpranker wants to merge 1 commit into
Conversation
`Terminal.__init__` (via `miniterm.Miniterm.__init__`) unconditionally constructed `miniterm.Console`, which calls `termios.tcgetattr(stdin)` to switch the terminal into raw mode. When stdin isn't a real TTY (e.g. `echo "" | pio run -t monitor`), this raises `termios.error: (25, 'Inappropriate ioctl for device')` before the monitor can even start. Add `NonInteractiveConsole`, based on pyserial's `ConsoleBase` (the same base miniterm uses for non-POSIX platforms), which reads bytes from stdin directly instead of reconfiguring a terminal that doesn't exist. `Terminal.__init__` now swaps in this console before calling the parent constructor whenever `sys.stdin.isatty()` is false, since the crash happens inside that constructor call itself. Fixes platformio#5113
|
|
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.
AI disclosure
Written by Claude (Anthropic, Sonnet 5). The entire patch, root-cause analysis, and verification below were produced by an AI agent. I (the human account owner) reviewed the diff and the verification steps before submitting.
Problem
pio device monitor(andpio run -t monitor) crashes withtermios.error: (25, 'Inappropriate ioctl for device')whenever stdin isn't a real TTY, e.g.:Root cause
Terminal.__init__inplatformio/device/monitor/terminal.pycallssuper().__init__(), which isminiterm.Miniterm.__init__(from pyserial). That constructor unconditionally doesself.console = Console(), and pyserial's POSIXConsole.__init__immediately callstermios.tcgetattr(sys.stdin.fileno())to put the terminal into raw mode. There is no TTY to reconfigure when stdin is a pipe, so this raises beforeTerminal.__init__'s own body (or evennew_terminal(), its caller) gets a chance to react.Fix
Added
NonInteractiveConsole, a small subclass of pyserial's ownConsoleBase(the same base class miniterm itself falls back to on non-POSIX platforms that don't havetermios). It reads bytes from stdin directly withsys.stdin.read(1)— no raw-mode reconfiguration needed, since there's no interactive terminal involved — and reports EOF as the monitor's exit character so the writer thread stops cleanly instead of spinning on empty reads forever.Because the crash happens inside the parent constructor call,
Terminal.__init__temporarily swapsminiterm.Consolefor a factory that returnsNonInteractiveConsolebefore callingsuper().__init__(), only whensys.stdin.isatty()is false, restoring it immediately after (viatry/finally). This is the narrowest fix I found: it changes nothing when stdin is a real terminal (the reported, working case), and the substitution window is a single synchronous constructor call, not global state kept around afterward.Testing
I confirmed the crash first, then verified the fix, both against a real
serial.serial_for_url("loop://")instance (pyserial's built-in loopback transport — no physical hardware needed) with stdin replaced by a pipe:Before the fix (
git stashon this change):Terminal(ser, ...)raisestermios.error: (25, 'Inappropriate ioctl for device')— the exact error from the issue.After the fix:
Terminal(ser, ...)succeeds, usesNonInteractiveConsole; a fullstart()/join()cycle correctly reads piped bytes and stops cleanly on EOF with no exception recorded inpio_unexpected_exception.Non-regression: when stdin is a real TTY,
Terminalstill uses pyserial's realConsole— verified with a dedicated test.Added
tests/commands/test_device_monitor_terminal.py(3 tests, all passing) covering: construction doesn't crash when stdin isn't a TTY, the writer loop reads piped data and exits cleanly on EOF, and the TTY path is unchanged. Ran the existingtests/commands/test_device_monitor.pyalongside it — all pass, no regressions. Also ranblack,isort, andpylint --rcfile=./.pylintrcon both changed files — clean (10.00/10on pylint).I don't have physical hardware to test against
pio run -t monitorend-to-end on a real board; the fix and tests operate at theTerminal/minitermlayer where the actual bug lives, using pyserial's loopback URL as a stand-in serial device.Fixes #5113