Skip to content

device/monitor: fix pio monitor crash when stdin is a pipe#5477

Open
Socialpranker wants to merge 1 commit into
platformio:developfrom
Socialpranker:fix/5113-monitor-piped-stdin
Open

device/monitor: fix pio monitor crash when stdin is a pipe#5477
Socialpranker wants to merge 1 commit into
platformio:developfrom
Socialpranker:fix/5113-monitor-piped-stdin

Conversation

@Socialpranker

Copy link
Copy Markdown

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 (and pio run -t monitor) crashes with termios.error: (25, 'Inappropriate ioctl for device') whenever stdin isn't a real TTY, e.g.:

$ echo "" | pio run -t monitor

Root cause

Terminal.__init__ in platformio/device/monitor/terminal.py calls super().__init__(), which is miniterm.Miniterm.__init__ (from pyserial). That constructor unconditionally does self.console = Console(), and pyserial's POSIX Console.__init__ immediately calls termios.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 before Terminal.__init__'s own body (or even new_terminal(), its caller) gets a chance to react.

Fix

Added NonInteractiveConsole, a small subclass of pyserial's own ConsoleBase (the same base class miniterm itself falls back to on non-POSIX platforms that don't have termios). It reads bytes from stdin directly with sys.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 swaps miniterm.Console for a factory that returns NonInteractiveConsole before calling super().__init__(), only when sys.stdin.isatty() is false, restoring it immediately after (via try/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 stash on this change): Terminal(ser, ...) raises termios.error: (25, 'Inappropriate ioctl for device') — the exact error from the issue.

After the fix: Terminal(ser, ...) succeeds, uses NonInteractiveConsole; a full start()/join() cycle correctly reads piped bytes and stops cleanly on EOF with no exception recorded in pio_unexpected_exception.

Non-regression: when stdin is a real TTY, Terminal still uses pyserial's real Console — 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 existing tests/commands/test_device_monitor.py alongside it — all pass, no regressions. Also ran black, isort, and pylint --rcfile=./.pylintrc on both changed files — clean (10.00/10 on pylint).

I don't have physical hardware to test against pio run -t monitor end-to-end on a real board; the fix and tests operate at the Terminal/miniterm layer where the actual bug lives, using pyserial's loopback URL as a stand-in serial device.

Fixes #5113

`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
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

pio monitor fails when stdin is a pipe

2 participants