Skip to content

fix(no-ticket): exit quietly when Ctrl+C meets a closed pipe - #567

Open
cloudsmith-iduffy wants to merge 4 commits into
masterfrom
iduffy/fix-interrupt-broken-pipe
Open

cloudsmith-iduffy wants to merge 4 commits into
masterfrom
iduffy/fix-interrupt-broken-pipe

Conversation

@cloudsmith-iduffy

@cloudsmith-iduffy cloudsmith-iduffy commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Sentry CLOUDSMITH-CLI-1R is a BrokenPipeError from cloudsmith whoami on 1.30.0. The user, or a wrapper process, sent Ctrl+C while the CLI was still starting up. The reader of stderr had already closed the pipe.

On Ctrl+C, Click writes a newline to stderr before it raises Abort. Here that write raised BrokenPipeError inside Click's KeyboardInterrupt handler. Click has an EPIPE handler, but it only catches errors from the command, not from another except block, so the error reached AliasGroup.main. Our catch-all sent it to Sentry as a crash. The process then exited with status 120, because Python flushed the failed newline again at exit and that flush failed too.

AliasGroup.main now catches BrokenPipeError, prints nothing and exits with status 1. Before the exit it wraps stdout and stderr in Click's _PacifyFlushWrapper, as Click's own EPIPE handler does, so the flush at exit can't change the status. When standalone_mode=False it re-raises, like the other branches.

The Sentry event names #374 (lazy command imports) as the suspect commit. That commit is not the cause. The interrupt landed during the cloudsmith_api import only because that import is the slowest part of start-up.

Type of Change

  • Bug fix

Additional Notes

There are two tests. The CliRunner test checks that nothing is reported and nothing is printed. The subprocess test runs the CLI with stderr on a closed pipe and checks that the exit status is 1. CliRunner never reaches interpreter shutdown, so it can't see the status 120. The subprocess test turns off PYTHONUNBUFFERED, because unbuffered stderr hides the failure.

_PacifyFlushWrapper is a private Click name. It is in Click 8.5, and the CLI has no upper bound on Click.

A BrokenPipeError raised inside our other handlers, such as the JSON error output or Aborted!, still escapes. That was already the case before this change, and I'll fix it separately.

🤖 Generated with Claude Code

On Ctrl+C, Click writes a newline to stderr before it raises Abort.
When the reader of stderr is gone, that write raises BrokenPipeError
inside Click's KeyboardInterrupt handler. Click's own EPIPE handler
does not catch it, so AliasGroup.main reported it to Sentry as a
crash and wrote an error line to the closed pipe.

Catch BrokenPipeError in AliasGroup.main and exit with status 1.

Fixes CLOUDSMITH-CLI-1R

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A closed stderr pipe can leave the Ctrl+C newline buffered, causing Python’s shutdown flush to override exit status 1 with 120; the stream needs safe cleanup and a real-pipe subprocess regression test.

1 open issue
What changed in this PR

Handles broken pipes during Ctrl+C in the CLI’s central error handler, avoiding misleading crash messages and Sentry reports.

Changes:

  • Catches BrokenPipeError, requesting exit status 1 in standalone mode and re-raising for embedded callers.
  • Adds regression coverage for an interrupt with closed stderr.
  • Documents the behavior in the changelog.
File Description
cloudsmith_cli/​cli/​tests/​test_unexpected_errors.py Tests silent handling without Sentry reporting using a mocked closed pipe.
cloudsmith_cli/​cli/​command.py Adds a dedicated BrokenPipeError handler.
CHANGELOG.md Describes the intended quiet exit on interruption with a closed pipe.

Review effort: Balanced

Comment thread cloudsmith_cli/cli/command.py Resolved
cloudsmith-iduffy and others added 2 commits October 28, 2026 14:25
The newline that Click failed to write stays in the stderr buffer.
Python flushes stderr again at exit, the flush fails, and the process
exits with status 120. Wrap stdout and stderr in Click's
_PacifyFlushWrapper before the exit, as Click does for a closed pipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudsmith-iduffy
cloudsmith-iduffy marked this pull request as ready for review October 28, 2026 14:40
@cloudsmith-iduffy
cloudsmith-iduffy requested a review from a team as a code owner October 28, 2026 14:40
@cloudsmith-iduffy

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts on this branch.

…t-broken-pipe

# Conflicts:
#	CHANGELOG.md

Co-authored-by: cloudsmith-iduffy <178375997+cloudsmith-iduffy@users.noreply.github.com>
auto-merge was automatically disabled October 29, 2026 11:02

Head branch was pushed to by a user without write access

Copilot AI commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts on this branch.

Resolved in 528f53f by merging master and retaining both branches’ changelog entries. The closed-pipe fix and regression tests are preserved. All 51 targeted tests and pre-commit checks passed. CodeQL flagged only existing credential-file storage inherited unchanged from master.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants