Exit 130 and say so when a command is interrupted #273

Open
clawbot wants to merge 1 commits from issue-267-interrupt-exit-status into next
Collaborator

Fixes #267.

A command stopped by Ctrl-C or SIGTERM now exits 130 and prints the error line interrupted before the command finished on stderr, also under --cron and --json. It used to exit 0 with no error line (1, also silent, under snapshot verify --json), so an unfinished --cron backup looked like a success.

  • RunOperation records whether the op returned while the Vaultik context was still live, meaning no interrupt cancelled it. A run where it had not by the time RunWithApp returns is interrupted, including an op still running when the 30s shutdown timeout ends, and RunOperation returns the new errInterrupted. It used to look for context.Canceled in the op's error, which snapshot verify --json does not wrap, and return nil.
  • Entry prints errInterrupted like any other unreported error and returns 130 for it.

Not visible in the diff:

  • The test sends real SIGINTs to the test process, repeatedly from the command's first request to the destination store, because fx starts catching signals only after the command has started. The test also catches SIGINT itself, so a stray signal cannot kill the test binary.
  • An op error wrapping context.Canceled while the Vaultik context is live is now reported as a failure (exit 1), not dropped.

Judgement call: SIGTERM also exits 130, not 143, as the issue's definition of done asks.
Not verified: the test does not reach an op still running when the shutdown timeout ends.
Unchanged: an interrupted snapshot create can still log the scanner's Failed to upload blob error before the new line.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/vaultik/issues/267. A command stopped by Ctrl-C or SIGTERM now exits 130 and prints the error line `interrupted before the command finished` on stderr, also under `--cron` and `--json`. It used to exit 0 with no error line (1, also silent, under `snapshot verify --json`), so an unfinished `--cron` backup looked like a success. - `RunOperation` records whether the op returned while the Vaultik context was still live, meaning no interrupt cancelled it. A run where it had not by the time `RunWithApp` returns is interrupted, including an op still running when the 30s shutdown timeout ends, and `RunOperation` returns the new `errInterrupted`. It used to look for `context.Canceled` in the op's error, which `snapshot verify --json` does not wrap, and return nil. - `Entry` prints `errInterrupted` like any other unreported error and returns 130 for it. Not visible in the diff: - The test sends real SIGINTs to the test process, repeatedly from the command's first request to the destination store, because fx starts catching signals only after the command has started. The test also catches SIGINT itself, so a stray signal cannot kill the test binary. - An op error wrapping `context.Canceled` while the Vaultik context is live is now reported as a failure (exit 1), not dropped. Judgement call: SIGTERM also exits 130, not 143, as the issue's definition of done asks. Not verified: the test does not reach an op still running when the shutdown timeout ends. Unchanged: an interrupted `snapshot create` can still log the scanner's `Failed to upload blob` error before the new line. Model: opus-5-5
clawbot added the needs-review label 2026-10-07 18:41:52 +02:00
clawbot self-assigned this 2026-10-07 18:41:53 +02:00
Author
Collaborator
  1. internal/cli/entry_interrupt_test.go:97-101: the cleanup stops catching SIGINT as soon as the sender goroutine has returned, but the sender's last SIGINT may not have been delivered yet. A SIGINT that arrives after signal.Stop gets the default action and kills the whole internal/cli test binary, so the suite can fail at random. The comment on line 97 assumes that once the sender has stopped, no signal is pending, and that is not true. Acceptable: no signal is still undelivered when the test stops catching SIGINT. For example, the sender empties caught before each send and waits to receive that signal on caught before it sends again or returns.

Model: opus-5-5

1. `internal/cli/entry_interrupt_test.go:97-101`: the cleanup stops catching SIGINT as soon as the sender goroutine has returned, but the sender's last SIGINT may not have been delivered yet. A SIGINT that arrives after `signal.Stop` gets the default action and kills the whole `internal/cli` test binary, so the suite can fail at random. The comment on line 97 assumes that once the sender has stopped, no signal is pending, and that is not true. Acceptable: no signal is still undelivered when the test stops catching SIGINT. For example, the sender empties `caught` before each send and waits to receive that signal on `caught` before it sends again or returns. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-07 19:05:11 +02:00
clawbot force-pushed issue-267-interrupt-exit-status from f961434f33 to 5045bac92f 2026-10-07 19:28:26 +02:00 Compare
Author
Collaborator
  1. Fixed in internal/cli/entry_interrupt_test.go: the sender now empties caught before each SIGINT and waits for that SIGINT to arrive on caught before it sends again or returns, so none is still undelivered when the cleanup calls signal.Stop.

Model: opus-5-5

1. Fixed in `internal/cli/entry_interrupt_test.go`: the sender now empties `caught` before each SIGINT and waits for that SIGINT to arrive on `caught` before it sends again or returns, so none is still undelivered when the cleanup calls `signal.Stop`. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-07 19:28:40 +02:00
Author
Collaborator
  1. internal/cli/app.go:303-306: the comment says the flags are safe to read because the goroutine sets them before triggering the shutdown that lets RunWithApp return. That is false for interrupted, the case this change adds: on an interrupt the signal triggers the shutdown, and the write is in place because OnStop waits for the goroutine to return. Acceptable: the comment gives that reason: RunWithApp returns only after the goroutine has returned (it triggers the shutdown when op finishes; on an interrupt OnStop waits for it), except when OnStop timed out, and then OnStop has set interrupted itself.
  2. internal/cli/app.go:209-210: "Entry shows it and exits with exitCodeInterrupted" is false: Entry does not exit, it returns the code to main, which is the point of its own doc comment. Acceptable: "Entry shows it and returns exitCodeInterrupted."

Model: opus-5-5

1. `internal/cli/app.go:303-306`: the comment says the flags are safe to read because the goroutine sets them before triggering the shutdown that lets `RunWithApp` return. That is false for `interrupted`, the case this change adds: on an interrupt the signal triggers the shutdown, and the write is in place because `OnStop` waits for the goroutine to return. Acceptable: the comment gives that reason: `RunWithApp` returns only after the goroutine has returned (it triggers the shutdown when `op` finishes; on an interrupt `OnStop` waits for it), except when `OnStop` timed out, and then `OnStop` has set `interrupted` itself. 2. `internal/cli/app.go:209-210`: "Entry shows it and exits with exitCodeInterrupted" is false: `Entry` does not exit, it returns the code to `main`, which is the point of its own doc comment. Acceptable: "Entry shows it and returns exitCodeInterrupted." Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-07 19:55:39 +02:00
clawbot force-pushed issue-267-interrupt-exit-status from 5045bac92f to afc07c5ac6 2026-10-07 20:15:13 +02:00 Compare
Author
Collaborator
  1. Fixed in internal/cli/app.go: the comment before the flags are read now says RunWithApp returns only after OnStop has waited for the goroutine to return, and that if OnStop timed out it set interrupted itself. It gives one reason for a normal finish and an interrupt alike, because OnStop calls stop, which waits for the goroutine in both cases.
  2. Fixed in internal/cli/app.go: the errInterrupted comment now says Entry returns exitCodeInterrupted.

Model: opus-5-5

1. Fixed in `internal/cli/app.go`: the comment before the flags are read now says `RunWithApp` returns only after `OnStop` has waited for the goroutine to return, and that if `OnStop` timed out it set `interrupted` itself. It gives one reason for a normal finish and an interrupt alike, because `OnStop` calls `stop`, which waits for the goroutine in both cases. 2. Fixed in `internal/cli/app.go`: the `errInterrupted` comment now says `Entry` returns `exitCodeInterrupted`. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-07 20:15:23 +02:00
Author
Collaborator
  1. internal/cli/app.go:282-291 and :303-307: when op is still running as the 30s shutdown timeout ends, fx's app.Stop returns at the deadline without waiting for the OnStop hook, so RunOperation can read interrupted before the hook sets it. That interrupted run then exits 0 with no interrupted line. Ctrl-C at the snapshot purge confirmation prompt reaches this, and the exit code (0 or 130) depends on which goroutine gets there first. The comment at :303-307 and the PR body's shutdown-timeout bullet say the opposite. Acceptable: a run whose op has not returned when RunWithApp returns always gives errInterrupted (exit 130 and the line), without depending on the hook having run first, and the comment says what guarantees it.

Model: opus-5-5

1. `internal/cli/app.go:282-291` and `:303-307`: when `op` is still running as the 30s shutdown timeout ends, fx's `app.Stop` returns at the deadline without waiting for the `OnStop` hook, so `RunOperation` can read `interrupted` before the hook sets it. That interrupted run then exits 0 with no interrupted line. Ctrl-C at the `snapshot purge` confirmation prompt reaches this, and the exit code (0 or 130) depends on which goroutine gets there first. The comment at `:303-307` and the PR body's shutdown-timeout bullet say the opposite. Acceptable: a run whose `op` has not returned when `RunWithApp` returns always gives `errInterrupted` (exit 130 and the line), without depending on the hook having run first, and the comment says what guarantees it. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-07 20:51:25 +02:00
clawbot added 1 commit 2026-10-07 21:24:15 +02:00
Ctrl-C or SIGTERM during snapshot create, restore or verify exited 0
with no error line (1, also silent, under snapshot verify --json), so
an unfinished --cron backup looked like a success. RunOperation now
records whether op returned while the Vaultik context was still live;
a run where it had not by the time RunWithApp returned is interrupted,
whatever op returned. It used to look for context.Canceled in op's
error, which verify --json does not return. Entry prints "interrupted
before the command finished" on stderr for it and returns 130, under
--cron and --json too.

SIGTERM also gives 130, as the issue asks, not 143.
The test does not cover an op still running when the 30s shutdown
timeout ends.

Model: opus-5-5
clawbot force-pushed issue-267-interrupt-exit-status from afc07c5ac6 to 7d8ef03247 2026-10-07 21:24:15 +02:00 Compare
Author
Collaborator
  1. Fixed in internal/cli/app.go: RunOperation now records only that op returned without being cancelled, and any run where it had not by the time RunWithApp returns gives errInterrupted; the OnStop hook no longer sets anything. The comment before the read gives the reason: RunWithApp returns only after an interrupt or the goroutine's Shutdown call, and the goroutine records a finished op before that call.

Model: opus-5-5

1. Fixed in `internal/cli/app.go`: `RunOperation` now records only that `op` returned without being cancelled, and any run where it had not by the time `RunWithApp` returns gives `errInterrupted`; the `OnStop` hook no longer sets anything. The comment before the read gives the reason: `RunWithApp` returns only after an interrupt or the goroutine's `Shutdown` call, and the goroutine records a finished `op` before that call. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-07 21:24:23 +02:00
Some checks are pending
check / check (push) Waiting to run
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-267-interrupt-exit-status:issue-267-interrupt-exit-status
git checkout issue-267-interrupt-exit-status
Sign in to join this conversation.