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) Waiting to run
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
// process exits; the wait is bounded by shutdownTimeout. report is
// 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 returned; RunOperation returns errInterrupted instead.
// it becomes errReported.
//
// 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(
ctx context.Context, opts AppOptions,
op func(v *vaultik.Vaultik) error, report func(err error),
) error {
var (
mu sync.Mutex
failed bool
interrupted bool
mu sync.Mutex
finished bool // op returned before any interrupt cancelled it
failed bool // op finished with an error
)
opts.Invokes = append(opts.Invokes,
@@ -247,21 +251,19 @@ func RunOperation(
err := op(v)
// Only stop, called from OnStop below, cancels the
// Vaultik context; before op has returned, that
// happens only on an interrupt. Check the context,
// not err: an interrupted op need not return
// Vaultik context, so a live context means no
// interrupt cancelled op. Check the context, not
// err: an interrupted op need not return
// context.Canceled (`snapshot verify --json`
// returns a verification failure).
switch {
case v.Context().Err() != nil:
mu.Lock()
interrupted = true
mu.Unlock()
case err != nil:
report(err)
if v.Context().Err() == nil {
if err != nil {
report(err)
}
mu.Lock()
failed = true
finished = true
failed = err != nil
mu.Unlock()
}
@@ -282,12 +284,6 @@ func RunOperation(
if !stop(ctx) {
log.Warn("Shutdown timed out before the operation " +
"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
@@ -300,16 +296,17 @@ func RunOperation(
return err
}
// RunWithApp returns only after OnStop has waited for the goroutine
// to return, whether the shutdown came from op finishing or from an
// interrupt, so the goroutine's write to failed or interrupted is in
// place by the time we read it. If OnStop timed out, the goroutine
// may still be running, and OnStop has set interrupted itself.
// RunWithApp returns only after the app was asked to stop, either by
// an interrupt or by the goroutine's Shutdown call. When op finished
// without being cancelled, the goroutine set finished before that
// call. So if finished is unset here, an interrupt stopped the app,
// and op either returned after it was cancelled or is still running
// because the shutdown timed out.
mu.Lock()
defer mu.Unlock()
switch {
case interrupted:
case !finished:
return errInterrupted
case failed:
return errReported