check / check (push) Failing after 3s
SIGINT, SIGTERM and SIGHUP were caught for the whole run, but only the ssh and sftp children acted on them: the mnemonic prompt waited for Enter, and an interrupted `age encrypt -o` put the encryption of the cut-off input in place. Now they end the tool at once, except where a command cleans up first: `ssh to` and `ssh install` while ssh or sftp runs, and `age encrypt -o` and `age decrypt -o` while they write, where a signal up to a tenth of a second after the input ends still removes the unfinished file and exits 1. Those commands catch only the signals the tool was not started ignoring, so a run under nohup survives a hangup. Model: opus-5-5
91 lines
2.7 KiB
Go
91 lines
2.7 KiB
Go
// Package cli builds the command tree and runs it.
|
|
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"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
|
|
}
|
|
|
|
// 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
|
|
}
|