138 lines
3.6 KiB
Go
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)
|
|
}
|