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
@@ -12,6 +12,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,7 +30,7 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=%s
|
||||
ExecStart=%s --serve
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StateDirectory=echolot-server
|
||||
@@ -144,3 +145,41 @@ func UninstallSystemd() error {
|
||||
fmt.Println("removed echolot-server units (state dir and env file left in place)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepairExecStart brings an already-installed unit up to date with the current invocation.
|
||||
//
|
||||
// Serving became an explicit verb (--serve), which means every unit written before that change
|
||||
// would start the binary with no arguments — and the binary now answers that with usage and a
|
||||
// non-zero exit. A self-update replaces the binary but never the unit, so without this a routine
|
||||
// update would leave a service that cannot start, discovered whenever the host next reboots.
|
||||
//
|
||||
// Only a unit this program wrote is touched, identified by its description line. Editing an
|
||||
// operator's hand-written unit would be overreach; leaving ours broken would be negligence.
|
||||
func RepairExecStart() (repaired bool, err error) {
|
||||
b, err := os.ReadFile(unitPath)
|
||||
if err != nil {
|
||||
return false, nil // no unit installed: nothing to repair, and not an error
|
||||
}
|
||||
text := string(b)
|
||||
if !strings.Contains(text, "Echolot probe server") {
|
||||
return false, nil // somebody else's unit
|
||||
}
|
||||
lines := strings.Split(text, "\n")
|
||||
changed := false
|
||||
for i, ln := range lines {
|
||||
t := strings.TrimSpace(ln)
|
||||
// Only the serving unit's ExecStart; the timer's own line already carries its verb.
|
||||
if strings.HasPrefix(t, "ExecStart=") && !strings.Contains(t, "--") {
|
||||
lines[i] = ln + " --serve"
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return false, nil
|
||||
}
|
||||
if err := os.WriteFile(unitPath, []byte(strings.Join(lines, "\n")), 0o644); err != nil {
|
||||
return false, fmt.Errorf("updating %s: %w", unitPath, err)
|
||||
}
|
||||
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user