Native Windows deployment: config file, service, signed auto-update
- internal/config: .env-style config file (gpu-turnstile.env next to the exe, -config flag or GPU_TURNSTILE_CONFIG); process env overrides file. - internal/service: Windows service via golang.org/x/sys/windows/svc — graceful SCM stop, 'service install/remove' commands, restart-on-failure recovery (also applies staged updates). First external dependency, Windows-only; Linux/Docker build unaffected (go.mod stays at 1.23). - internal/update: polls the Gitea releases API, verifies the Ed25519 signature of the downloaded binary against an embedded public key (openssl-signed by CI), swaps it in next to the running exe, and once the GPU lock is idle exits with code 3 so service recovery restarts onto the new version. Dev builds and empty pubkey never update. - CI: tag builds additionally produce gpu-turnstile.exe + .sig + .sha256 attached to a Gitea release. - LOG_FILE env var so the service has somewhere to log.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package service provides the non-Windows stubs for the Windows service
|
||||
// integration. Run falls back to plain signal handling; install/remove
|
||||
// are unsupported.
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Name matches the Windows service name.
|
||||
const Name = "gpu-turnstile"
|
||||
|
||||
var errUnsupported = errors.New("service management is only supported on Windows")
|
||||
|
||||
// IsService is always false on non-Windows platforms.
|
||||
func IsService() bool { return false }
|
||||
|
||||
// Run executes run with SIGINT/SIGTERM cancellation, mirroring the
|
||||
// interactive behavior.
|
||||
func Run(run func(ctx context.Context) error) error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
return run(ctx)
|
||||
}
|
||||
|
||||
// Install is unsupported on non-Windows platforms.
|
||||
func Install(string) error { return errUnsupported }
|
||||
|
||||
// Remove is unsupported on non-Windows platforms.
|
||||
func Remove() error { return errUnsupported }
|
||||
@@ -0,0 +1,120 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user