Files
2026-09-21 16:46:55 +08:00

213 lines
4.3 KiB
Go

//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)
}