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.
91 lines
2.9 KiB
Go
91 lines
2.9 KiB
Go
//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
|
|
}
|