//go:build linux // Package service integrates gpu-turnstile with systemd on Linux: running // under a unit with readiness notification and watchdog, plus // install/remove helpers that manage a system unit. package service import ( "context" "fmt" "os" "os/exec" "os/signal" "path/filepath" "syscall" ) // Name matches the Windows service name; the systemd unit is Name + ".service". const Name = "gpu-turnstile" // unitPath is where Install writes the unit file. const unitPath = "/etc/systemd/system/" + Name + ".service" // IsService reports whether the process was started by systemd. func IsService() bool { return os.Getenv("INVOCATION_ID") != "" } // Run executes run with SIGINT/SIGTERM cancellation (which is how systemctl // stop signals the process) and tells systemd when the shutdown begins. func Run(run func(ctx context.Context) error) error { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() defer NotifyStopping() return run(ctx) } // renderUnit builds the systemd unit: Type=notify so systemctl start blocks // until the listeners are bound, a 30s watchdog, and restart-on-failure // with a 5s delay — which is also what brings up a staged update after the // updater exits with a non-zero code. func renderUnit(exePath, configPath string) string { return fmt.Sprintf(`[Unit] Description=gpu-turnstile GPU arbitration proxy for Ollama and ComfyUI After=network-online.target Wants=network-online.target [Service] Type=notify WatchdogSec=30s ExecStart=%q -config %q Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target `, exePath, configPath) } // Install writes the unit for the current executable and the given config // file, then enables and starts it. Needs root. func Install(configPath string) error { exe, err := os.Executable() if err != nil { return err } if abs, absErr := filepath.Abs(exe); absErr == nil { exe = abs } if err := os.WriteFile(unitPath, []byte(renderUnit(exe, configPath)), 0o644); err != nil { return fmt.Errorf("write %s (run as root): %w", unitPath, err) } if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil { return fmt.Errorf("systemctl daemon-reload: %w (%s)", err, out) } if out, err := exec.Command("systemctl", "enable", "--now", Name+".service").CombinedOutput(); err != nil { return fmt.Errorf("systemctl enable --now: %w (%s)", err, out) } return nil } // Remove stops and disables the service and deletes the unit file. func Remove() error { exec.Command("systemctl", "disable", "--now", Name+".service").Run() // ignore: may not exist if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove %s: %w", unitPath, err) } if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil { return fmt.Errorf("systemctl daemon-reload: %w (%s)", err, out) } return nil }