d29475560d
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.
104 lines
2.6 KiB
Go
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
|
|
}
|