Files
steve 8062db1f2f agent: P2-16 Windows service (SCM) integration
internal/agent/service: build-tagged into service_windows.go (svc.Handler
that listens for Stop/Shutdown + delegates to the agent loop) and
service_other.go (foreground stub for Linux/macOS). install_windows.go
wraps mgr.Connect+CreateService/Delete/Start/Stop for the new
'restic-manager-agent install|uninstall|start|stop' subcommands.

Cross-compile verified: GOOS=windows GOARCH=amd64 go build ./cmd/agent
succeeds. UNTESTED on Windows itself — the SCM round-trip can't be
exercised from Linux CI; treat as a starting point for the first
real Windows install.
2026-05-04 11:13:56 +01:00

104 lines
2.6 KiB
Go

//go:build windows
// install_windows.go — thin wrappers around the Service Control
// Manager via golang.org/x/sys/windows/svc/mgr. Used by the agent's
// `install` / `uninstall` / `start` / `stop` subcommands.
//
// UNTESTED in CI. Mirrors the canonical example shape; if you need
// to extend this, prefer copying from x/sys/windows/svc/example
// over inventing new patterns.
package service
import (
"fmt"
"os"
"path/filepath"
"golang.org/x/sys/windows/svc/mgr"
)
// Install registers the service with the SCM, pointing it at the
// currently-running binary. The service starts on every boot and
// runs as LocalSystem (default).
func Install() error {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("install: locate executable: %w", err)
}
exe, err = filepath.Abs(exe)
if err != nil {
return fmt.Errorf("install: absolutise path: %w", err)
}
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("install: connect SCM: %w", err)
}
defer m.Disconnect()
if existing, err := m.OpenService(ServiceName); err == nil {
_ = existing.Close()
return fmt.Errorf("service %q already installed; uninstall first", ServiceName)
}
s, err := m.CreateService(ServiceName, exe, mgr.Config{
StartType: mgr.StartAutomatic,
DisplayName: "Restic-manager agent",
Description: "Backs up this host on the schedule the central restic-manager dictates.",
}, "run")
if err != nil {
return fmt.Errorf("install: create service: %w", err)
}
defer s.Close()
return nil
}
// Uninstall removes the service from the SCM. Caller is expected to
// stop the service first; this returns the SCM's error if it's
// still running.
func Uninstall() error {
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("uninstall: connect SCM: %w", err)
}
defer m.Disconnect()
s, err := m.OpenService(ServiceName)
if err != nil {
return fmt.Errorf("uninstall: open service: %w", err)
}
defer s.Close()
if err := s.Delete(); err != nil {
return fmt.Errorf("uninstall: delete service: %w", err)
}
return nil
}
// Start asks the SCM to start the installed service. No-op if it's
// already running (the SCM returns an error which we surface).
func Start() error {
m, err := mgr.Connect()
if err != nil {
return err
}
defer m.Disconnect()
s, err := m.OpenService(ServiceName)
if err != nil {
return err
}
defer s.Close()
return s.Start()
}
// Stop sends a stop control to the service.
func Stop() error {
m, err := mgr.Connect()
if err != nil {
return err
}
defer m.Disconnect()
s, err := m.OpenService(ServiceName)
if err != nil {
return err
}
defer s.Close()
_, err = s.Control(0x00000001) // SERVICE_CONTROL_STOP
return err
}