Add Linux systemd support and --install-service/--remove-service flags

The service package now has a Linux implementation alongside the Windows
one: systemd unit install/remove (/etc/systemd/system), readiness
notification (READY=1 via go-systemd) sent only after the listeners are
bound, a 30s watchdog, and STOPPING on shutdown. Listeners are pre-bound
so port conflicts fail fast and the readiness signal is truthful. The
notify calls are no-ops without NOTIFY_SOCKET (containers, shells) and
on non-Linux builds. --install-service/--remove-service work on both
platforms; the 'service install|remove' subcommand remains as an alias.
This commit is contained in:
mram
2026-09-20 22:35:59 +02:00
parent 30a1f55aae
commit 83812cebf3
10 changed files with 288 additions and 30 deletions
+48 -13
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
@@ -34,12 +35,22 @@ var version = "dev"
const exitCodeUpdate = 3
func main() {
configPath, args := splitConfigFlag(os.Args[1:])
configPath, install, remove, args := parseFlags(os.Args[1:])
switch {
case install && remove:
fmt.Fprintf(os.Stderr, "gpu-turnstile: --install-service and --remove-service are mutually exclusive\n")
os.Exit(2)
case install:
os.Exit(serviceCommand(configPath, []string{"install"}))
case remove:
os.Exit(serviceCommand(configPath, []string{"remove"}))
}
if len(args) > 0 && args[0] == "service" {
os.Exit(serviceCommand(configPath, args[1:]))
}
if len(args) > 0 {
fmt.Fprintf(os.Stderr, "usage: gpu-turnstile [-config path] | gpu-turnstile service install|remove [-config path]\n")
fmt.Fprintf(os.Stderr, "usage: gpu-turnstile [-config path] [--install-service | --remove-service]\n")
fmt.Fprintf(os.Stderr, " gpu-turnstile service install|remove [-config path]\n")
os.Exit(2)
}
@@ -70,10 +81,10 @@ func main() {
}
}
// splitConfigFlag extracts -config <path> (or -config=<path>) from args.
func splitConfigFlag(args []string) (string, []string) {
var configPath string
rest := args[:0]
// parseFlags extracts -config <path> (or -config=<path>) and the
// --install-service / --remove-service switches from args.
func parseFlags(args []string) (configPath string, install, remove bool, rest []string) {
rest = args[:0]
for i := 0; i < len(args); i++ {
switch {
case args[i] == "-config" && i+1 < len(args):
@@ -81,11 +92,15 @@ func splitConfigFlag(args []string) (string, []string) {
i++
case strings.HasPrefix(args[i], "-config="):
configPath = strings.TrimPrefix(args[i], "-config=")
case args[i] == "--install-service" || args[i] == "-install-service":
install = true
case args[i] == "--remove-service" || args[i] == "-remove-service":
remove = true
default:
rest = append(rest, args[i])
}
}
return configPath, rest
return configPath, install, remove, rest
}
// defaultConfigPath returns gpu-turnstile.env next to the executable.
@@ -282,21 +297,41 @@ func run(ctx context.Context, cfg config.Config, log *slog.Logger, logOut io.Wri
}
probeCancel()
// Bind the listeners up front so a port conflict fails fast and the
// readiness notification below really means "accepting connections".
var servers []*http.Server
var listeners []net.Listener
bind := func(addr string, handler http.Handler, consumer string) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("listen %s on %s: %w", consumer, addr, err)
}
servers = append(servers, &http.Server{Addr: addr, Handler: handler})
listeners = append(listeners, ln)
log.Warn("listening", "consumer", consumer, "addr", addr)
return nil
}
if ollamaClient != nil {
servers = append(servers, &http.Server{Addr: cfg.ListenOllama, Handler: srv.OllamaHandler()})
log.Warn("listening", "consumer", "ollama", "addr", cfg.ListenOllama)
if err := bind(cfg.ListenOllama, srv.OllamaHandler(), "ollama"); err != nil {
return err
}
}
if comfyClient != nil {
servers = append(servers, &http.Server{Addr: cfg.ListenComfy, Handler: srv.ComfyHandler()})
log.Warn("listening", "consumer", "comfy", "addr", cfg.ListenComfy)
if err := bind(cfg.ListenComfy, srv.ComfyHandler(), "comfy"); err != nil {
return err
}
}
errCh := make(chan error, len(servers))
for _, s := range servers {
go func(s *http.Server) { errCh <- s.ListenAndServe() }(s)
for i := range servers {
go func(s *http.Server, ln net.Listener) { errCh <- s.Serve(ln) }(servers[i], listeners[i])
}
// Tell systemd we are up and start the watchdog pings; both are no-ops
// when not running under a notify/watchdog unit.
service.NotifyReady()
service.StartWatchdog(ctx)
if cfg.AutoUpdate {
go updateLoop(ctx, cfg, log, lk, isService)
}