Portable install: the binary installs itself; static builds on five architectures; root via sudo, doas or uid 0; every package manager; older systemd; no lsblk

- hamlab install / uninstall (internal/setup): systemd or OpenRC, elevates through
  sudo or doas, runs as the invoking user, sets or generates the password, names only
  journal groups that exist, waits for /healthz, reports an active firewalld/ufw.
  --purge and the data chown never touch a directory that isn't Ham Lab's alone.
- install.sh: curl-able bootstrap (arch detection, SHA256SUMS, offline beside the
  binaries, noexec /tmp fallback). make release builds dist/ for amd64, arm64, arm
  (ARMv6), 386, riscv64. Every build is CGO_ENABLED=0; the native one used to link glibc.
- internal/priv replaces every literal `sudo -n`: uid 0, sudo or doas.
- Updates acts on dnf, zypper, pacman and apk as well as apt; dnf shows installed versions.
- Services falls back to the plain systemctl table before systemd 246 and says so on
  hosts without systemd. Disks falls back to /sys/block without lsblk.
- Listener binds with IP_FREEBIND, so a LAN address that isn't up at boot is no failure.
- Self-update: the new build must pass `check` against the live config, is put in
  place by atomic rename, and the daemon re-execs itself (same under any supervisor).
- State and config writes are fsynced; a corrupt state.json is set aside, not fatal.
- Terminal shutdown no longer empties the saved list, so names come back after a
  restart by design rather than by winning a race.
- Config default is ~/.config/hamlab; an existing ~/apps/hamlab is still found.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 23:00:20 -07:00
co-authored by Claude Fable 5.1
parent 91b7f4f009
commit 488d929733
26 changed files with 1617 additions and 178 deletions
+9 -3
View File
@@ -24,6 +24,8 @@ import (
"sync"
"syscall"
"time"
"hamlab/internal/priv"
)
const (
@@ -732,7 +734,10 @@ func leadingNum(s string) float64 {
func upsPwrstat() *UPS {
out, ok := runOut("pwrstat", "-status")
if !ok {
if out, ok = runOut("sudo", "-n", "pwrstat", "-status"); !ok {
if !priv.Available() {
return nil
}
if out, ok = runOut(priv.Wrap("pwrstat", "-status")...); !ok {
return nil
}
}
@@ -1106,8 +1111,9 @@ func Signal(pid int, sig syscall.Signal) error {
} else if !errors.Is(err, syscall.EPERM) {
return err
}
if lookPath("sudo") {
if err := exec.Command("sudo", "-n", "kill", "-"+strconv.Itoa(int(sig)), strconv.Itoa(pid)).Run(); err == nil {
if priv.Available() {
k := priv.Wrap("kill", "-"+strconv.Itoa(int(sig)), strconv.Itoa(pid))
if err := exec.Command(k[0], k[1:]...).Run(); err == nil {
return nil
}
}