Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 7d8ef03247 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
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
2026-10-07 19:24:12 +00:00
+25 -28
View File
@@ -225,16 +225,20 @@ 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. An interrupted op is not reported, whatever // it becomes errReported.
// 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
failed bool finished bool // op returned before any interrupt cancelled it
interrupted bool failed bool // op finished with an error
) )
opts.Invokes = append(opts.Invokes, opts.Invokes = append(opts.Invokes,
@@ -247,21 +251,19 @@ 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; before op has returned, that // Vaultik context, so a live context means no
// happens only on an interrupt. Check the context, // interrupt cancelled op. Check the context, not
// not err: an interrupted op need not return // 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).
switch { if v.Context().Err() == nil {
case v.Context().Err() != nil: if err != nil {
mu.Lock() report(err)
interrupted = true }
mu.Unlock()
case err != nil:
report(err)
mu.Lock() mu.Lock()
failed = true finished = true
failed = err != nil
mu.Unlock() mu.Unlock()
} }
@@ -282,12 +284,6 @@ 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
@@ -300,16 +296,17 @@ func RunOperation(
return err return err
} }
// RunWithApp returns only after OnStop has waited for the goroutine // RunWithApp returns only after the app was asked to stop, either by
// to return, whether the shutdown came from op finishing or from an // an interrupt or by the goroutine's Shutdown call. When op finished
// interrupt, so the goroutine's write to failed or interrupted is in // without being cancelled, the goroutine set finished before that
// place by the time we read it. If OnStop timed out, the goroutine // call. So if finished is unset here, an interrupt stopped the app,
// may still be running, and OnStop has set interrupted itself. // and op either returned after it was cancelled or is still running
// because the shutdown timed out.
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
switch { switch {
case interrupted: case !finished:
return errInterrupted return errInterrupted
case failed: case failed:
return errReported return errReported