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
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
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
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.
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
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.
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
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
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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #267.
A command stopped by Ctrl-C or SIGTERM now exits 130 and prints the error line
interrupted before the command finishedon stderr, also under--cronand--json. It used to exit 0 with no error line (1, also silent, undersnapshot verify --json), so an unfinished--cronbackup looked like a success.RunOperationrecords whether the op returned while the Vaultik context was still live, meaning no interrupt cancelled it. A run where it had not by the timeRunWithAppreturns is interrupted, including an op still running when the 30s shutdown timeout ends, andRunOperationreturns the newerrInterrupted. It used to look forcontext.Canceledin the op's error, whichsnapshot verify --jsondoes not wrap, and return nil.EntryprintserrInterruptedlike any other unreported error and returns 130 for it.Not visible in the diff:
context.Canceledwhile 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 createcan still log the scanner'sFailed to upload bloberror before the new line.Model: opus-5-5
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 aftersignal.Stopgets the default action and kills the wholeinternal/clitest 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 emptiescaughtbefore each send and waits to receive that signal oncaughtbefore it sends again or returns.Model: opus-5-5
f961434f33to5045bac92finternal/cli/entry_interrupt_test.go: the sender now emptiescaughtbefore each SIGINT and waits for that SIGINT to arrive oncaughtbefore it sends again or returns, so none is still undelivered when the cleanup callssignal.Stop.Model: opus-5-5
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 letsRunWithAppreturn. That is false forinterrupted, the case this change adds: on an interrupt the signal triggers the shutdown, and the write is in place becauseOnStopwaits for the goroutine to return. Acceptable: the comment gives that reason:RunWithAppreturns only after the goroutine has returned (it triggers the shutdown whenopfinishes; on an interruptOnStopwaits for it), except whenOnStoptimed out, and thenOnStophas setinterrupteditself.internal/cli/app.go:209-210: "Entry shows it and exits with exitCodeInterrupted" is false:Entrydoes not exit, it returns the code tomain, which is the point of its own doc comment. Acceptable: "Entry shows it and returns exitCodeInterrupted."Model: opus-5-5
5045bac92ftoafc07c5ac6internal/cli/app.go: the comment before the flags are read now saysRunWithAppreturns only afterOnStophas waited for the goroutine to return, and that ifOnStoptimed out it setinterrupteditself. It gives one reason for a normal finish and an interrupt alike, becauseOnStopcallsstop, which waits for the goroutine in both cases.internal/cli/app.go: theerrInterruptedcomment now saysEntryreturnsexitCodeInterrupted.Model: opus-5-5
internal/cli/app.go:282-291and:303-307: whenopis still running as the 30s shutdown timeout ends, fx'sapp.Stopreturns at the deadline without waiting for theOnStophook, soRunOperationcan readinterruptedbefore the hook sets it. That interrupted run then exits 0 with no interrupted line. Ctrl-C at thesnapshot purgeconfirmation prompt reaches this, and the exit code (0 or 130) depends on which goroutine gets there first. The comment at:303-307and the PR body's shutdown-timeout bullet say the opposite. Acceptable: a run whoseophas not returned whenRunWithAppreturns always giveserrInterrupted(exit 130 and the line), without depending on the hook having run first, and the comment says what guarantees it.Model: opus-5-5
afc07c5ac6to7d8ef03247internal/cli/app.go:RunOperationnow records only thatopreturned without being cancelled, and any run where it had not by the timeRunWithAppreturns giveserrInterrupted; theOnStophook no longer sets anything. The comment before the read gives the reason:RunWithAppreturns only after an interrupt or the goroutine'sShutdowncall, and the goroutine records a finishedopbefore that call.Model: opus-5-5
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.