Skip to content

burn -t: Windows drops redirected stdin, so piped commands never reach the board #139

Description

@openipc-ai

burn -t forwards keystrokes on every platform except one combination: Windows
with stdin redirected from a pipe or a file. There, nothing typed is ever sent
to the board, and nothing says so.

Why

read_available_keys() dispatches on platform before it looks at anything else:

def read_available_keys() -> bytes:
    if sys.platform == "win32":
        return _read_windows()
    return _read_posix()

_read_windows() only ever asks the console:

while len(out) < _MAX_BYTES_PER_POLL and msvcrt.kbhit():

msvcrt.kbhit() reports console keyboard input. With stdin redirected it is
always false, so the loop never runs and the function returns b"" forever.
_read_posix() would handle a pipe fine — it selects on the descriptor — but
it is unreachable on Windows, and deliberately so: tests/test_terminal_keyboard.py
skips TestPosixReading there with the note that "Windows select() accepts
only sockets, and this path never runs there".

POSIX is unaffected: select() on a pipe works, and raw_terminal() is a no-op
when there is no terminal to configure.

Reproducing

On Windows, against any board parked at a U-Boot prompt:

echo printenv | uv run python -m defib burn -c <chip> -p COM15 -t

The banner appears and serial output streams, but printenv never reaches the
board. The same command on Linux or macOS works.

Suggested shape

Keep the invariant the current implementation was built around — no thread, no
executor, nothing that can stall the loop while it is also holding the serial
link to the board. So not a blocking stdin.read() handed to asyncio.to_thread.

PeekNamedPipe gives the available byte count on a Windows pipe without
blocking, which fits the existing poll-between-serial-reads structure:

def _read_windows() -> bytes:
    if sys.stdin.isatty():
        return _read_windows_console()   # what exists today
    return _read_windows_stream()        # PeekNamedPipe, then os.read that many

via msvcrt.get_osfhandle(sys.stdin.fileno()) and ctypes.windll.kernel32.
A redirected regular file needs no peek — a read returns immediately — so the
count can come from the peek for pipes and a plain bounded os.read otherwise.
Whatever the mechanism, it should stay bounded by _MAX_BYTES_PER_POLL for the
reason already documented there: a producer that never pauses must not starve
the board's output or the stop flag.

Credit

Found while reviewing #131, which had this case covered before #136 shipped —
its third branch handled any non-tty stream on either platform. That PR is
closed as superseded by #136, but this part of it was not wrong, and #136 did
not carry it across. #131 solved it with a blocking read in a thread, which is
the approach #136 deliberately avoided, so this wants a fresh implementation
rather than a straight port.

Activity

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