Skip to content

fix(main): exit with code 130 on SIGINT (#615)#863

Open
gustcol wants to merge 2 commits intopeak:masterfrom
gustcol:fix/issue-615-sigint-exit-130
Open

fix(main): exit with code 130 on SIGINT (#615)#863
gustcol wants to merge 2 commits intopeak:masterfrom
gustcol:fix/issue-615-sigint-exit-130

Conversation

@gustcol
Copy link
Copy Markdown

@gustcol gustcol commented Apr 12, 2026

Context

Fixes #615

Root cause

Different commands returned different exit codes on Ctrl-C:

  • ls returned 0 — the context-cancel path swallowed the error
  • cp returned 1 — its context-cancel path propagated an error
  • Neither returned 130, the POSIX standard (128 + signal 2)

This made it impossible to reliably detect Ctrl-C in scripts vs real failures.

Fix

Register a dedicated os.Interrupt notification channel (intCh) in
main(), alongside the existing signal.NotifyContext. After
command.Main returns, a non-blocking receive on intCh tells us
whether SIGINT was the cause. If yes, we call os.Exit(130) before
evaluating the command's own error return.

intCh := make(chan os.Signal, 1)
signal.Notify(intCh, os.Interrupt)
defer signal.Stop(intCh)

err := command.Main(ctx, os.Args)

select {
case <-intCh:
    os.Exit(130)
default:
}

Files changed: main.go

Tests

  • Added TestSIGINTExitCode in main_test.go: builds the binary, runs
    a command against an unreachable endpoint so it blocks, sends SIGINT,
    and asserts exit code 130.

Manual verification

s5cmd ls s3://bucket/ &
sleep 0.2
kill -INT %1
wait %1
echo $?  # 130

Notes

  • Exit code 130 is the POSIX standard for SIGINT-terminated processes.
  • This is technically a breaking change for scripts that checked for
    exit code 0 or 1 specifically from Ctrl-C interrupts.

gustcol added 2 commits April 12, 2026 15:41
Fixes peak#615

When Ctrl-C was pressed, different s5cmd commands returned different
exit codes: 'ls' returned 0 (the context-cancel error was swallowed),
'cp' returned 1 (its context-cancel path returned an error). Neither
matched the POSIX convention of 130 (128 + SIGINT=2), making it hard
for scripts to distinguish Ctrl-C from actual failures.

Fix: register a second os.Interrupt notification channel (intCh) alongside
the signal.NotifyContext cancel mechanism. After command.Main returns, a
non-blocking receive on intCh tells us whether SIGINT was the cause; if so
we call os.Exit(130) before evaluating the command error.

Reproducer:
  s5cmd ls s3://bucket/ &; sleep 0.1; kill -INT $!; echo $?
  # Before: 0 (ls) or 1 (cp)

After this change:
  echo $?  # 130 for any command interrupted by SIGINT
The original test could be flaky when run alongside other packages
because credential/region lookup sometimes finished before the 500 ms
sleep elapsed, causing the binary to exit before SIGINT was sent.

Use --no-sign-request + AWS_DEFAULT_REGION=us-east-1 to skip both
credential and region-discovery requests so the binary blocks
immediately on the black-hole TCP dial.  Increase the pre-signal
sleep to 3 s to absorb build-time variation under parallel test load.
@gustcol gustcol marked this pull request as ready for review April 12, 2026 15:35
@gustcol gustcol requested a review from a team as a code owner April 12, 2026 15:35
@gustcol gustcol requested review from igungor and sonmezonur and removed request for a team April 12, 2026 15:35
@gustcol
Copy link
Copy Markdown
Author

gustcol commented Apr 12, 2026

Test results

Unit tests (go test -race ./...): PASS (after hardening the test)

Changes included beyond the original fix:

  • main_test.go: Added --no-sign-request and AWS_DEFAULT_REGION=us-east-1 to skip credential/region discovery so the binary blocks immediately on the TCP dial; increased pre-signal sleep to 3 s to absorb build-time variance under parallel test load. The 500 ms sleep was insufficient when running alongside the full ./... suite.

Specific tests (run 3 times to confirm stable):

go test -race -count=1 ./...
ok  github.com/peak/s5cmd/v2   # includes TestSIGINTExitCode

Wasabi smoke test:

$ ./s5cmd cp /tmp/largefile.bin s3://bucket/large-upload-test &
PID=$!
sleep 2
kill -INT $PID
wait $PID
echo $?
130

SIGINT exits with code 130. SIGTERM or error exit remains 1.

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.

Correct exit codes for SIGINT signals

1 participant