Files
runner/internal/app/cmd/service_windows.go
T
silverwind d6f5b68a0d enhance: test on Windows and fix what it found (#1348)
Adds a weekly and on-demand workflow that runs the cross-compiled tests on Windows Server 2025 Core in a [dockur/windows](https://github.com/dockur/windows) QEMU VM, and fixes the Windows bugs it found in host and UNC paths, env handling, shell quoting, post-task scripts, health checks, checkout, action symlinks, step summaries, Git client certificates and docker mode from a Windows host.

Host-mode steps now emit UTF-8, `bash` falls back to Git's bash instead of the WSL launcher, the runner can run as a native Windows service, steps can keep background helpers with `RUNNER_TRACKING_ID`, and Windows arm64 binaries are released.

Fixes: https://gitea.com/gitea/runner/issues/1339
Fixes: https://gitea.com/gitea/runner/issues/637
Fixes: https://gitea.com/gitea/runner/issues/1172
Fixes: https://gitea.com/gitea/runner/issues/347
Reviewed-on: https://gitea.com/gitea/runner/pulls/1348
Reviewed-by: bircni <bircni@icloud.com>
2026-11-03 01:26:10 +00:00

108 lines
2.7 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"gitea.com/gitea/runner/act/common/logging"
"golang.org/x/sys/windows/svc"
)
func runService(ctx context.Context, run func(context.Context) error) error {
isService, err := svc.IsWindowsService()
if err != nil {
return fmt.Errorf("detect Windows service: %w", err)
}
if !isService {
return run(ctx)
}
handler := &daemonService{ctx: ctx, run: func(ctx context.Context) error {
executable, err := os.Executable()
if err != nil {
return err
}
if err := os.Chdir(filepath.Dir(executable)); err != nil {
return err
}
logFile, err := os.OpenFile("gitea-runner.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("open service log: %w", err)
}
defer func() { _ = logFile.Close() }()
previousOutput := logging.Output()
logging.SetOutput(logFile)
defer logging.SetOutput(previousOutput)
err = run(ctx)
if errors.Is(err, context.Canceled) && ctx.Err() != nil {
return nil
}
if err != nil {
logging.WithError(err).Error("runner daemon failed")
}
return err
}}
if err := svc.Run("", handler); err != nil { // SCM supplies the name for an own-process service.
return fmt.Errorf("run Windows service: %w", err)
}
return handler.err
}
type daemonService struct {
ctx context.Context
run func(context.Context) error
err error
}
func (s *daemonService) Execute(_args []string, requests <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
return s.execute(requests, changes, ticker.C)
}
func (s *daemonService) execute(requests <-chan svc.ChangeRequest, changes chan<- svc.Status, ticks <-chan time.Time) (bool, uint32) {
changes <- svc.Status{State: svc.StartPending}
ctx, cancel := context.WithCancel(s.ctx)
defer cancel()
done := make(chan error, 1)
go func() { done <- s.run(ctx) }()
status := svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPreShutdown}
changes <- status
for {
select {
case s.err = <-done:
if status.State != svc.StopPending {
changes <- svc.Status{State: svc.StopPending}
}
if s.err != nil {
return true, 1
}
return false, 0
case <-ticks:
if status.State == svc.StopPending {
status.CheckPoint++
changes <- status
}
case request := <-requests:
switch request.Cmd {
case svc.Interrogate:
changes <- status
case svc.Stop, svc.Shutdown, svc.PreShutdown:
if status.State != svc.StopPending {
status = svc.Status{State: svc.StopPending, CheckPoint: 1, WaitHint: 10000}
changes <- status
cancel()
}
}
}
}
}