Compare commits

..
1 Commits
Author SHA1 Message Date
sneak afc07c5ac6 Exit 130 and say so when a command is interrupted (closes #267)
check / check (push) Canceled after 0s
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
counts an op as interrupted when the Vaultik context was cancelled
before it returned, rather than when its error wraps context.Canceled,
which verify --json does not. 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 reach the branch for an op still running when the
30s shutdown timeout ends.

Model: opus-5-5
2026-10-07 18:01:01 +00:00
+26 -23
View File
@@ -225,20 +225,16 @@ var errInterrupted = errors.New("interrupted before the command finished")
// op's cleanup (removing decrypted scratch files) runs before the // op's cleanup (removing decrypted scratch files) runs before the
// process exits; the wait is bounded by shutdownTimeout. report is // process exits; the wait is bounded by shutdownTimeout. report is
// called with a failure so the caller can show it to the user before // called with a failure so the caller can show it to the user before
// it becomes errReported. // it becomes errReported. An interrupted op is not reported, whatever
// // it returned; RunOperation returns errInterrupted instead.
// The run counts as interrupted unless op returned, without an
// interrupt having cancelled it, before RunWithApp returned. An
// interrupted op is not reported, whatever it returned; RunOperation
// returns errInterrupted instead.
func RunOperation( func RunOperation(
ctx context.Context, opts AppOptions, ctx context.Context, opts AppOptions,
op func(v *vaultik.Vaultik) error, report func(err error), op func(v *vaultik.Vaultik) error, report func(err error),
) error { ) error {
var ( var (
mu sync.Mutex mu sync.Mutex
finished bool // op returned before any interrupt cancelled it failed bool
failed bool // op finished with an error interrupted bool
) )
opts.Invokes = append(opts.Invokes, opts.Invokes = append(opts.Invokes,
@@ -251,19 +247,21 @@ func RunOperation(
err := op(v) err := op(v)
// Only stop, called from OnStop below, cancels the // Only stop, called from OnStop below, cancels the
// Vaultik context, so a live context means no // Vaultik context; before op has returned, that
// interrupt cancelled op. Check the context, not // happens only on an interrupt. Check the context,
// err: an interrupted op need not return // not err: an interrupted op need not return
// context.Canceled (`snapshot verify --json` // context.Canceled (`snapshot verify --json`
// returns a verification failure). // returns a verification failure).
if v.Context().Err() == nil { switch {
if err != nil { case v.Context().Err() != nil:
mu.Lock()
interrupted = true
mu.Unlock()
case err != nil:
report(err) report(err)
}
mu.Lock() mu.Lock()
finished = true failed = true
failed = err != nil
mu.Unlock() mu.Unlock()
} }
@@ -284,6 +282,12 @@ func RunOperation(
if !stop(ctx) { if !stop(ctx) {
log.Warn("Shutdown timed out before the operation " + log.Warn("Shutdown timed out before the operation " +
"finished; decrypted temporary files may remain") "finished; decrypted temporary files may remain")
// op has not returned, so the app is stopping on
// an interrupt.
mu.Lock()
interrupted = true
mu.Unlock()
} }
return nil return nil
@@ -296,17 +300,16 @@ func RunOperation(
return err return err
} }
// RunWithApp returns only after the app was asked to stop, either by // RunWithApp returns only after OnStop has waited for the goroutine
// an interrupt or by the goroutine's Shutdown call. When op finished // to return, whether the shutdown came from op finishing or from an
// without being cancelled, the goroutine set finished before that // interrupt, so the goroutine's write to failed or interrupted is in
// call. So if finished is unset here, an interrupt stopped the app, // place by the time we read it. If OnStop timed out, the goroutine
// and op either returned after it was cancelled or is still running // may still be running, and OnStop has set interrupted itself.
// because the shutdown timed out.
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
switch { switch {
case !finished: case interrupted:
return errInterrupted return errInterrupted
case failed: case failed:
return errReported return errReported