- Add native Windows service detection and signal handling - Implement configurable shutdown timeout for graceful job completion - Improve HTTP client with connection pooling and timeouts - Propagate context through poller for proper shutdown coordination - Add documentation for Windows service installation (NSSM and sc.exe) - Add *.exe to .gitignore for Windows builds
103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build windows
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/windows/svc"
|
|
)
|
|
|
|
// runnerService implements svc.Handler for Windows service management
|
|
type runnerService struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// Execute is called by the Windows Service Control Manager
|
|
func (s *runnerService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
|
|
|
changes <- svc.Status{State: svc.StartPending}
|
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
|
|
|
log.Info("Windows service started")
|
|
|
|
loop:
|
|
for {
|
|
select {
|
|
case c := <-r:
|
|
switch c.Cmd {
|
|
case svc.Interrogate:
|
|
changes <- c.CurrentStatus
|
|
// Windows wants two responses for interrogate
|
|
time.Sleep(100 * time.Millisecond)
|
|
changes <- c.CurrentStatus
|
|
case svc.Stop, svc.Shutdown:
|
|
log.Info("Windows service stop/shutdown requested")
|
|
s.cancel()
|
|
break loop
|
|
default:
|
|
log.Warnf("unexpected control request #%d", c)
|
|
}
|
|
case <-s.ctx.Done():
|
|
break loop
|
|
}
|
|
}
|
|
|
|
changes <- svc.Status{State: svc.StopPending}
|
|
return
|
|
}
|
|
|
|
// IsWindowsService returns true if the process is running as a Windows service
|
|
func IsWindowsService() bool {
|
|
// Check if we're running interactively
|
|
isInteractive, err := svc.IsWindowsService()
|
|
if err != nil {
|
|
log.WithError(err).Debug("failed to detect if running as Windows service")
|
|
return false
|
|
}
|
|
return isInteractive
|
|
}
|
|
|
|
// RunAsService runs the application as a Windows service
|
|
func RunAsService(serviceName string, run func(ctx context.Context)) error {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Start the actual runner in a goroutine
|
|
go run(ctx)
|
|
|
|
// Run the service handler - this blocks until service stops
|
|
err := svc.Run(serviceName, &runnerService{ctx: ctx, cancel: cancel})
|
|
if err != nil {
|
|
log.WithError(err).Error("Windows service run failed")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetServiceName returns the service name from environment or default
|
|
func GetServiceName() string {
|
|
if name := os.Getenv("GITEA_RUNNER_SERVICE_NAME"); name != "" {
|
|
return name
|
|
}
|
|
// Try to detect from executable name
|
|
exe, err := os.Executable()
|
|
if err == nil {
|
|
base := strings.TrimSuffix(exe, ".exe")
|
|
if idx := strings.LastIndex(base, string(os.PathSeparator)); idx >= 0 {
|
|
return base[idx+1:]
|
|
}
|
|
return base
|
|
}
|
|
return "GiteaRunnerSvc"
|
|
}
|