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 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
131 lines
4.1 KiB
Go
131 lines
4.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"sneak.berlin/go/vaultik/internal/globals"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
)
|
|
|
|
// shortCommitLen is the number of git commit hash characters shown in
|
|
// the startup banner.
|
|
const shortCommitLen = 12
|
|
|
|
// exitCodeInterrupted is the exit status of a command that SIGINT or
|
|
// SIGTERM stopped. It is 128 plus SIGINT's number, 2, which is what a
|
|
// shell reports for a command stopped by Ctrl-C.
|
|
const exitCodeInterrupted = 130
|
|
|
|
// Entry is the main entry point for the CLI application.
|
|
// It prints the startup banner to stderr (unless a banner-suppressing
|
|
// flag is present in os.Args — see bannerSuppressedInArgs), executes the
|
|
// root cobra command, and routes any returned error through the
|
|
// ui.Writer so the user sees a properly formatted "🛑 ERROR:" line.
|
|
// The banner goes to stderr because stdout carries only the output the
|
|
// user asked for, such as a completion script or a `config get` value.
|
|
//
|
|
// It returns the process exit code (0 on success, 130 when interrupted,
|
|
// 1 on any other error) rather than calling os.Exit, so that main's
|
|
// deferred profile writers run before the process ends. See run in
|
|
// cmd/vaultik/main.go.
|
|
func Entry() int {
|
|
emitStartupBanner(os.Args[1:], os.Stderr)
|
|
|
|
rootCmd := NewRootCommand()
|
|
rootCmd.SilenceErrors = true
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
// An operation that ran inside the fx app has already reported
|
|
// its own failure (`snapshot verify --json` puts it in the
|
|
// document instead); errReported says so. Printing it again
|
|
// here would double the error line.
|
|
// Every other error — bad arguments, a config that would not
|
|
// load, an interrupt — reaches Entry unreported, so it is shown
|
|
// here.
|
|
if !errors.Is(err, errReported) {
|
|
ReportErrorf("%s", err.Error())
|
|
}
|
|
|
|
if errors.Is(err, errInterrupted) {
|
|
return exitCodeInterrupted
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// emitStartupBanner writes the startup banner to w unless args (the
|
|
// argument vector with the program name already stripped) contains a
|
|
// flag that suppresses it. Split out of Entry so that the decision is
|
|
// reachable from a test without running the whole CLI.
|
|
func emitStartupBanner(args []string, w io.Writer) {
|
|
if bannerSuppressedInArgs(args) {
|
|
return
|
|
}
|
|
|
|
short := globals.Commit
|
|
if len(short) > shortCommitLen {
|
|
short = short[:shortCommitLen]
|
|
}
|
|
|
|
writeStartupBanner(ui.New(w), time.Now().UTC(), short)
|
|
}
|
|
|
|
// ReportErrorf emits a user-facing error to stderr in the standard
|
|
// 🛑 ERROR: format. Use it from goroutine error paths (where returning
|
|
// an error to cobra isn't an option) and anywhere else a CLI command
|
|
// must surface a failure outside the normal RunE return path.
|
|
func ReportErrorf(format string, args ...any) {
|
|
ui.New(os.Stderr).Errorf(format, args...)
|
|
}
|
|
|
|
// bannerSuppressedInArgs reports whether any of args is a flag that
|
|
// should suppress the startup banner (--quiet/-q/--cron/--json). Stops
|
|
// at the "--" argument terminator. Recognizes both long forms and short
|
|
// -q, including combined short flags like "-qv".
|
|
//
|
|
// This scans the raw argument vector because the banner is printed
|
|
// before cobra parses anything — deliberately, so that it still appears
|
|
// when cobra rejects the arguments and on --help. The consequence is
|
|
// that a flag is matched wherever it occurs in the vector, including
|
|
// positions where the command it belongs to would not accept it.
|
|
// --json is a subcommand flag rather than a persistent one, but so is
|
|
// --cron (it exists only on `snapshot create`), so this adds no new
|
|
// class of imprecision. The only cost of a false positive is a missing
|
|
// decorative banner.
|
|
func bannerSuppressedInArgs(args []string) bool {
|
|
for _, a := range args {
|
|
if a == "--" {
|
|
return false
|
|
}
|
|
|
|
switch a {
|
|
case "--quiet", "-q", "--cron", "--json":
|
|
return true
|
|
}
|
|
|
|
if strings.HasPrefix(a, "--quiet=") ||
|
|
strings.HasPrefix(a, "--cron=") ||
|
|
strings.HasPrefix(a, "--json=") {
|
|
return true
|
|
}
|
|
// Combined short flags like -qv or -vq.
|
|
if len(a) > 1 && a[0] == '-' && a[1] != '-' {
|
|
for _, c := range a[1:] {
|
|
if c == 'q' {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|