Skip to content

fix(cli): silent SIGPIPE termination instead of BrokenPipeError traceback#13

Merged
igor-ctrl merged 1 commit into
mainfrom
fix/sigpipe-handling
May 12, 2026
Merged

fix(cli): silent SIGPIPE termination instead of BrokenPipeError traceback#13
igor-ctrl merged 1 commit into
mainfrom
fix/sigpipe-handling

Conversation

@igor-ctrl
Copy link
Copy Markdown
Owner

Summary

Piping bcli output to a truncating consumer — bcli ... | head -5, | grep -m 1, etc. — emits a Python traceback at interpreter shutdown:

Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe

Scary-looking but harmless — Python's atexit stdout-flush hits EPIPE because the downstream pipe is already closed. cat, grep, ls and other well-behaved Unix tools handle this silently via SIGPIPE.

Fix

A new console-script entry point bcli_cli.app:main installs the POSIX default SIGPIPE handler (SIG_DFL) before invoking the Typer app. On Windows (no SIGPIPE constant), a BrokenPipeError safety net closes stdout/stderr and exits 0 so the atexit flush can't re-trigger.

  • pyproject.toml — entry point changes from bcli_cli.app:app to bcli_cli.app:main. app remains exported and used directly by tests and bcli-mcp.
  • src/bcli_cli/app.py — adds the main() wrapper, ~25 lines.

Test plan

  • New tests/test_cli/test_pipe_handling.py runs the entry point in a subprocess, pipes bcli --help to head -1, asserts stderr contains no BrokenPipeError or Exception ignored markers.
  • uv run pytest tests/ — 526 pass, 5 skipped.
  • Manual: bcli --profile finance-sandbox endpoint list | head -5 shows the table and exits cleanly, no traceback.
  • Manual reproduction of the original bug confirmed via bcli-beautech-installer's install verify step.

…back

Piping bcli output to a consumer that closes early (e.g. `head -5`,
`grep -m 1`, scripted truncation) caused Python's interpreter teardown
to flush stdout, hit EPIPE, and emit:

    Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' ...>
    BrokenPipeError: [Errno 32] Broken pipe

— scary-looking noise on stderr for what's actually normal Unix
behaviour. cat, grep, ls, etc. all exit silently in this case.

Introduce a new console-script entry point `bcli_cli.app:main` that
restores the POSIX default for SIGPIPE before invoking the Typer app.
On platforms without SIGPIPE (Windows), a `BrokenPipeError` safety net
catches the same condition and closes stdout/stderr before exiting so
the atexit flush can't re-trigger it.

The pyproject `[project.scripts] bcli` entry updates to
`bcli_cli.app:main`; the `app` object remains exported and importable
for tests and `bcli-mcp`.

Adds a subprocess regression test that runs the entry point and pipes
into `head -1`, asserting the resulting stderr contains no
`BrokenPipeError` or `Exception ignored` markers.
@igor-ctrl igor-ctrl merged commit c20d483 into main May 12, 2026
igor-ctrl added a commit that referenced this pull request May 12, 2026
The Typer root-callback (introduced in #10) shared the name `main` with
the console-script entry point (added in #13), tripping ruff F811
(redefinition) when CI lint runs against the rebased history. The
callback's name is arbitrary — Typer wires it via the `@app.callback()`
decorator, not by symbol lookup — so renaming to `_root_callback`
resolves the redefinition without behavior change.
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.

1 participant