//go:build windows package winsvc import ( "context" "fmt" "os" "path/filepath" "time" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/mgr" ) func IsService() bool { ok, err := svc.IsWindowsService() return err == nil && ok } 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, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { changes <- svc.Status{State: svc.StartPending} ctx, cancel := context.WithCancel(context.Background()) defer cancel() errCh := make(chan error, 1) go func() { errCh <- h.run(ctx) }() select { case err := <-errCh: if err != nil { return true, 1 } return false, 0 case <-time.After(800 * time.Millisecond): changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} } for { select { case err := <-errCh: if err != nil { return true, 1 } return false, 0 case c := <-r: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: changes <- svc.Status{State: svc.StopPending} cancel() select { case <-errCh: case <-time.After(8 * time.Second): } return false, 0 default: changes <- c.CurrentStatus } } } } func Install(exePath, configPath string) error { m, err := mgr.Connect() if err != nil { return wrapAccess(err, "connect to service manager") } defer m.Disconnect() if s, err := m.OpenService(Name); err == nil { s.Close() return fmt.Errorf("service %s already exists; run --uninstall first", Name) } s, err := m.CreateService(Name, exePath, mgr.Config{ DisplayName: DisplayName, Description: Description, StartType: mgr.StartAutomatic, ErrorControl: mgr.ErrorNormal, }, "--config", configPath) if err != nil { return wrapAccess(err, "create service") } defer s.Close() return nil } func Uninstall() error { m, err := mgr.Connect() if err != nil { return wrapAccess(err, "connect to service manager") } defer m.Disconnect() s, err := m.OpenService(Name) if err != nil { return fmt.Errorf("service %s not installed", Name) } defer s.Close() st, err := s.Query() if err == nil && st.State != svc.Stopped { _ = control(s, svc.Stop, svc.Stopped) } if err := s.Delete(); err != nil { return wrapAccess(err, "delete service") } return nil } func Start() error { m, err := mgr.Connect() if err != nil { return wrapAccess(err, "connect to service manager") } defer m.Disconnect() s, err := m.OpenService(Name) if err != nil { return fmt.Errorf("service %s not installed", Name) } defer s.Close() if err := s.Start(); err != nil { if err == windows.ERROR_SERVICE_ALREADY_RUNNING { return nil } return wrapAccess(err, "start service") } return nil } func Stop() error { m, err := mgr.Connect() if err != nil { return wrapAccess(err, "connect to service manager") } defer m.Disconnect() s, err := m.OpenService(Name) if err != nil { return fmt.Errorf("service %s not installed", Name) } defer s.Close() st, err := s.Query() if err != nil { return err } if st.State == svc.Stopped { return nil } return control(s, svc.Stop, svc.Stopped) } func control(s *mgr.Service, c svc.Cmd, to svc.State) error { status, err := s.Control(c) if err != nil { return err } deadline := time.Now().Add(10 * time.Second) for status.State != to { if time.Now().After(deadline) { return fmt.Errorf("timeout waiting for service state %v", to) } time.Sleep(300 * time.Millisecond) status, err = s.Query() if err != nil { return err } } return nil } func wrapAccess(err error, op string) error { if err == nil { return nil } if err == windows.ERROR_ACCESS_DENIED { return fmt.Errorf("%s: access denied; run as Administrator", op) } return fmt.Errorf("%s: %w", op, err) } func ExePath() (string, error) { p, err := os.Executable() if err != nil { return "", err } if r, err := filepath.EvalSymlinks(p); err == nil { return r, nil } return p, nil } func CopyFile(src, dst string) error { in, err := os.ReadFile(src) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(dst), 0o750); err != nil { return err } return os.WriteFile(dst, in, 0o600) }