Files
keyfunc/internal/cli/cli.go
T
clawbot c96b77dd67
check / check (push) Failing after 1s
Every command ends on SIGINT, SIGTERM or SIGHUP; an interrupted age -o leaves no file (closes #48)
SIGINT, SIGTERM and SIGHUP are no longer caught for the whole run, so they end any command at once, the mnemonic prompt included. One package, internal/cli/signals, catches them only where cleanup is needed, and only those not ignored at start, so nohup still works: ssh to and ssh install while their child runs, and age encrypt -o and age decrypt -o while they write. A signal received by the time the input ends leaves no new file and exits 1; otherwise the whole file is put in place, never an unfinished one.

Judgement call: the guarantee is stated for a signal keyfunc has received, as Go cannot promise more; the main goroutine stays on the main thread so a Ctrl-C on a pipeline is seen first on Linux. Unverified on macOS.

Model: opus-5-5 (implementation); fable-5-1 (design)
2026-10-04 13:42:49 +02:00

103 lines
3.1 KiB
Go

// Package cli builds the command tree and runs it.
package cli
import (
"errors"
"fmt"
"os"
"runtime"
"runtime/debug"
"github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/cli/age"
"sneak.berlin/go/keyfunc/internal/cli/mnemonic"
"sneak.berlin/go/keyfunc/internal/cli/options"
"sneak.berlin/go/keyfunc/internal/cli/ssh"
)
// devVersion is what Version holds until a build stamps a real one.
const devVersion = "dev"
// Version is what --version prints. make build stamps it with -ldflags.
//
//nolint:gochecknoglobals // set at build time with -ldflags
var Version = devVersion
// resolveVersion chooses what --version reports. A value stamped at
// build time wins. Otherwise, for a binary from go install, the module
// version recorded in the build info is used, unless that is empty or
// the "(devel)" of a local build. When neither names a version, the
// "dev" fallback stays.
func resolveVersion(stamped string, info *debug.BuildInfo) string {
if stamped != devVersion {
return stamped
}
if info != nil && info.Main.Version != "" &&
info.Main.Version != "(devel)" {
return info.Main.Version
}
return devVersion
}
// Root returns the whole command tree.
func Root() *cobra.Command {
info, _ := debug.ReadBuildInfo()
root := &cobra.Command{
Use: "keyfunc",
Short: "derive key pairs from a BIP-39 mnemonic",
Long: "keyfunc turns a BIP-39 mnemonic into key pairs that can " +
"be recreated from that mnemonic at any time. The same " +
"mnemonic, key type and index always give the same key.",
Version: resolveVersion(Version, info),
SilenceUsage: true,
SilenceErrors: true,
}
options.Add(root)
root.AddCommand(ssh.Command(), age.Command(), mnemonic.Command())
return root
}
// init keeps the command on the main thread. Linux hands a signal sent
// to the tool to that thread first, and a thread runs a pending signal
// handler before its own code, so when "age encrypt -o" or "age
// decrypt -o" checks for a signal as its input ends, one sent before
// then, as by Ctrl-C on a pipeline, has been received.
//
//nolint:gochecknoinits // only an init can keep main on the main thread
func init() {
runtime.LockOSThread()
}
// Main runs the tool and returns the status the process should exit
// with. An error ends the tool with status 1, except when it carries a
// status of its own, which "ssh to" uses to hand on the status ssh
// ended with. ssh has already said whatever it had to say in that
// case, so nothing more is printed.
//
// SIGINT, SIGTERM and SIGHUP end the tool at once, as they end any Go
// program, so a command waiting at the mnemonic prompt or reading what
// it encrypts or decrypts goes no further. The exceptions catch the
// signals to clean up first: "ssh to" and "ssh install" while they
// have ssh or sftp running, so the child ends and their own cleanup
// still runs, and "age encrypt -o" and "age decrypt -o" while they
// write, so the unfinished file is removed.
func Main() int {
err := Root().Execute()
if err == nil {
return 0
}
if passed, ok := errors.AsType[ssh.StatusError](err); ok {
return passed.Status
}
fmt.Fprintln(os.Stderr, "keyfunc: "+err.Error())
return 1
}