Files
ss/internal/service/wrapper_windows.go
2026-07-19 21:23:44 +08:00

138 lines
3.6 KiB
Go

//go:build windows
package service
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"golang.org/x/sys/windows/svc"
)
// windowsServiceHandler implements svc.Handler, hosting the mihomo
// subprocess directly inside the ssm binary. Replaces
// windows_service_wrapper.py + pywin32 + the per-service JSON config file:
// the equivalent configuration (bin path + args) is now passed as command
// line flags to `ssm service run-windows`, and this same binary registers
// itself as the service via golang.org/x/sys/windows/svc.
type windowsServiceHandler struct {
name string
binPath string
args string
logger *log.Logger
}
func (h *windowsServiceHandler) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
const accepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending}
var cmdArgs []string
if h.args != "" {
split, err := shlexSplit(h.args)
if err != nil {
h.logger.Printf("failed to parse service args %q: %v", h.args, err)
return false, 1
}
cmdArgs = split
}
cmd := exec.Command(h.binPath, cmdArgs...)
cmd.Dir = filepath.Dir(h.binPath)
cmd.Stdout = &logWriter{h.logger, "stdout"}
cmd.Stderr = &logWriter{h.logger, "stderr"}
if err := cmd.Start(); err != nil {
h.logger.Printf("failed to launch %s: %v", h.binPath, err)
return false, 1
}
h.logger.Printf("started process PID %d", cmd.Process.Pid)
changes <- svc.Status{State: svc.Running, Accepts: accepted}
exited := make(chan error, 1)
go func() { exited <- cmd.Wait() }()
loop:
for {
select {
case err := <-exited:
h.logger.Printf("process exited: %v", err)
break loop
case req := <-r:
switch req.Cmd {
case svc.Interrogate:
changes <- req.CurrentStatus
case svc.Stop, svc.Shutdown:
h.logger.Printf("stop requested")
changes <- svc.Status{State: svc.StopPending}
h.terminate(cmd)
<-exited
break loop
}
}
}
changes <- svc.Status{State: svc.Stopped}
return false, 0
}
func (h *windowsServiceHandler) terminate(cmd *exec.Cmd) {
if cmd.Process == nil {
return
}
if err := cmd.Process.Kill(); err != nil {
h.logger.Printf("error terminating process: %v", err)
return
}
done := make(chan struct{})
go func() {
cmd.Wait()
close(done)
}()
select {
case <-done:
h.logger.Printf("process terminated")
case <-time.After(30 * time.Second):
h.logger.Printf("process did not terminate within timeout")
}
}
type logWriter struct {
logger *log.Logger
stream string
}
func (w *logWriter) Write(p []byte) (int, error) {
for _, line := range strings.Split(strings.TrimRight(string(p), "\r\n"), "\n") {
if strings.TrimSpace(line) != "" {
w.logger.Printf("[%s] %s", w.stream, line)
}
}
return len(p), nil
}
// RunWindowsService registers this process as the named Windows service and
// blocks until the service control manager stops it. Invoked via the hidden
// `ssm service run-windows --name <name> --bin <binPath> --args <args>`
// command that Install() points sc.exe's binPath= at.
func RunWindowsService(name, binPath, args string) error {
logPath := filepath.Join(filepath.Dir(binPath), name+"_service.log")
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return fmt.Errorf("failed to open service log file %s: %w", logPath, err)
}
defer logFile.Close()
logger := log.New(logFile, "", log.LstdFlags)
logger.Printf("service %q starting, bin=%s args=%q", name, binPath, args)
handler := &windowsServiceHandler{name: name, binPath: binPath, args: args, logger: logger}
return svc.Run(name, handler)
}