cli: serving is an explicit verb; no arguments prints usage
Running an unfamiliar binary by name should tell you what it does, not bind a dozen ports and start answering the internet. --serve (or --daemon) now does that, and a bare invocation prints usage and exits 2 - non-zero on purpose, so a service manager sees a failure rather than concluding the server ran and finished cleanly. The hazard this creates is worth spelling out, because it bites once and silently: three places started the binary with no arguments - the systemd unit, the unit template, and the Dockerfile - and --self-update replaces the binary but never the unit. A routine update would therefore leave a service that cannot start, discovered whenever the host next rebooted. So the updater repairs it: after replacing the binary it appends --serve to an ExecStart that has no flags, but only in a unit this program wrote (identified by its description). Editing an operator's hand-written unit would be overreach; leaving ours broken would be negligence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
80d2092f1b
commit
3cdbccee18
@@ -10,6 +10,7 @@ package config
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -136,6 +137,9 @@ func Load(args []string) (*Config, *Actions, error) {
|
||||
|
||||
fs.BoolVar(&a.InstallSystemd, "install-systemd", false, "install a systemd unit for this binary and exit")
|
||||
fs.BoolVar(&a.UninstallSystemd, "uninstall-systemd", false, "remove the systemd unit and exit")
|
||||
var daemon bool
|
||||
fs.BoolVar(&a.Serve, "serve", false, "run the server (bind listeners and answer requests)")
|
||||
fs.BoolVar(&daemon, "daemon", false, "alias for --serve")
|
||||
fs.BoolVar(&a.SetAdminPassword, "set-admin-password", false,
|
||||
"set the break-glass admin password (username as --admin-user, password read from stdin) and exit")
|
||||
fs.StringVar(&c.AdminUser, "admin-user", envOr("ADMIN_USER", "admin"), "username for the break-glass admin")
|
||||
@@ -148,12 +152,42 @@ func Load(args []string) (*Config, *Actions, error) {
|
||||
if !c.Docker {
|
||||
c.Docker = inContainer()
|
||||
}
|
||||
a.Serve = a.Serve || daemon
|
||||
// No verb at all means the caller has not said what they want. Usage is the answer, and it
|
||||
// is a usage error rather than success — otherwise a service manager sees a clean exit and
|
||||
// concludes the server ran and finished.
|
||||
if !a.Serve && !a.InstallSystemd && !a.UninstallSystemd && !a.SelfUpdate &&
|
||||
!a.SetAdminPassword && !a.Version {
|
||||
a.Help = true
|
||||
}
|
||||
if c.Docker && (a.InstallSystemd || a.UninstallSystemd || a.SelfUpdate) {
|
||||
return nil, nil, fmt.Errorf("systemd/self-update actions are native-mode only (container detected; override with ECHOLOT_DOCKER=0 if this is wrong)")
|
||||
}
|
||||
return c, a, nil
|
||||
}
|
||||
|
||||
// Usage prints the verbs first and the tuning flags second, because the question someone has
|
||||
// when they run this by name is "what does it do", not "what can I set".
|
||||
func Usage(w io.Writer) {
|
||||
fmt.Fprint(w, `echolot-server — the Echolot probe server
|
||||
|
||||
USAGE
|
||||
echolot-server --serve run the server
|
||||
echolot-server --version print the version
|
||||
echolot-server --install-systemd install and enable a systemd unit
|
||||
echolot-server --uninstall-systemd remove it
|
||||
echolot-server --self-update replace this binary with the latest release
|
||||
echolot-server --set-admin-password set the break-glass admin password (stdin)
|
||||
echolot-server --help full flag list
|
||||
|
||||
Every flag can also be set as an environment variable: --control-listen becomes
|
||||
ECHOLOT_CONTROL_LISTEN. In a container, configuration comes from the environment.
|
||||
|
||||
Running with no verb prints this and exits non-zero: starting to serve the internet
|
||||
should be something you asked for.
|
||||
`)
|
||||
}
|
||||
|
||||
// Addrs splits a comma-separated listen spec into individual addresses.
|
||||
// Explicit per-address binds matter on multi-IP hosts: a wildcard bind
|
||||
// (":8443") would also claim addresses reserved for other purposes (e.g. an
|
||||
@@ -168,8 +202,15 @@ func Addrs(spec string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// Actions are one-shot verbs that exit instead of serving.
|
||||
// Actions are the verbs. Serving is one of them, and it is explicit: running the binary with no
|
||||
// arguments prints usage rather than binding a dozen ports and starting to answer the internet.
|
||||
// Someone typing the name of an unfamiliar program on a terminal should be told what it does, not
|
||||
// have it start doing it.
|
||||
type Actions struct {
|
||||
Serve bool
|
||||
// Help is set when there is nothing to do: no verb was given.
|
||||
Help bool
|
||||
|
||||
InstallSystemd bool
|
||||
UninstallSystemd bool
|
||||
SelfUpdate bool
|
||||
|
||||
Reference in New Issue
Block a user