//go:build windows // Package service integrates gpu-turnstile with the Windows Service // Control Manager: running as a service with graceful stop, plus // install/remove helpers. package service import ( "context" "fmt" "os" "time" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/mgr" ) // Name is the Windows service name. const Name = "gpu-turnstile" // IsService reports whether the process is running as a Windows service. func IsService() bool { isSvc, err := svc.IsWindowsService() return err == nil && isSvc } // Run executes run as a Windows service. SCM Stop and Shutdown cancel the // context passed to run, triggering the same graceful shutdown as SIGTERM // in interactive mode. func Run(run func(ctx context.Context) error) error { return svc.Run(Name, &handler{run: run}) } type handler struct { run func(ctx context.Context) error } func (h *handler) Execute(_ []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() status <- svc.Status{State: svc.StartPending} errCh := make(chan error, 1) go func() { errCh <- h.run(ctx) }() status <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} for { select { case err := <-errCh: status <- svc.Status{State: svc.Stopped} if err != nil { return true, 1 } return false, 0 case c := <-requests: switch c.Cmd { case svc.Interrogate: status <- c.CurrentStatus case svc.Stop, svc.Shutdown: status <- svc.Status{State: svc.StopPending} cancel() } } } } // Install registers gpu-turnstile as an auto-start Windows service whose // binPath loads the given config file. Recovery actions restart the // service after 5s on failure — this is also what brings up a staged // update after the updater exits with a non-zero code. func Install(configPath string) error { exe, err := os.Executable() if err != nil { return err } m, err := mgr.Connect() if err != nil { return fmt.Errorf("connect to service manager (run as administrator): %w", err) } defer m.Disconnect() binPath := fmt.Sprintf(`"%s" -config "%s"`, exe, configPath) s, err := m.CreateService(Name, binPath, mgr.Config{ StartType: mgr.StartAutomatic, DisplayName: "gpu-turnstile", Description: "GPU arbitration proxy for Ollama and ComfyUI", }) if err != nil { return fmt.Errorf("create service: %w", err) } defer s.Close() restart := mgr.RecoveryAction{Type: mgr.ServiceRestart, Delay: 5 * time.Second} if err := s.SetRecoveryActions([]mgr.RecoveryAction{restart, restart, restart}, 24*60*60); err != nil { return fmt.Errorf("set recovery actions: %w", err) } if err := s.SetRecoveryActionsOnNonCrashFailures(true); err != nil { return fmt.Errorf("set failure actions flag: %w", err) } return nil } // Remove stops (if running) and unregisters the service. func Remove() error { m, err := mgr.Connect() if err != nil { return fmt.Errorf("connect to service manager (run as administrator): %w", err) } defer m.Disconnect() s, err := m.OpenService(Name) if err != nil { return fmt.Errorf("open service: %w", err) } defer s.Close() s.Control(svc.Stop) // ignore error: may already be stopped if err := s.Delete(); err != nil { return fmt.Errorf("delete service: %w", err) } return nil }