//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 ss 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 `ss 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 // `ss service run-windows --name --bin --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) }