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:
@@ -0,0 +1,90 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user