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.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user