//go:build windows // +build windows // Package service 负责 Windows 服务生命周期 + frpc 子进程管理。 // // 设计概览: // // ┌─────────────────────────────────────────────────────────────┐ // │ Windows Service Control Manager (SCM) │ // │ │ Start/Stop/Control │ // │ ▼ │ // │ Run() -> svc.Run("frpc-client", &handler{cfg}) │ // │ │ │ // │ ▼ │ // │ handler.Execute() -> supervise frpc 子进程 │ // └─────────────────────────────────────────────────────────────┘ // // 关键点: // // - svc.Handler.Execute 是阻塞的,直到我们 return 才视为服务停止 // - frpc 以子进程方式运行,不用 syscall 0x08000000 隐藏窗口 // - SCM 发 Stop -> 我们杀 frpc -> return,服务停止 // - frpc 异常退出 -> 我们也 return,SCM 会重试或上报 package service import ( "context" "errors" "fmt" "os" "os/exec" "sync" "syscall" "time" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" "frpc-client-win/internal/config" "frpc-client-win/internal/logger" ) // kernel32 是 win32 调用封装,用于隐藏 SCM 启动时附带的 console 窗口。 var ( modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procFreeConsole = modkernel32.NewProc("FreeConsole") ) // freeConsole 解除当前进程与 console 的关联,避免 SCM 启动时闪黑窗。 // // 仅在以 console 子系统编译、且当前已被 SCM 启动时调用。 func freeConsole() { procFreeConsole.Call() } // ServiceName 注册到 SCM 的服务名。改这个会破坏已部署客户机,慎改。 const ServiceName = "frpc-client" // DisplayName SCM 中显示名。 const DisplayName = "FRPC Client Service" // Description 服务描述。 const Description = "frpc 内网穿透客户端,后台常驻服务。" // elog 是写到 Windows Event Log 的 logger。 // debug 模式和服务模式下都用 debug.Log,以便 -d 时也能看到事件日志格式。 var elog debug.Log // Run 是服务模式入口。 // // 如果当前是 SCM 启动 -> svc.Run;否则 -> debug.Run(日志到控制台)。 // // 注意:用 console 子系统编译是为了让 install/start 等 CLI 子命令有输出; // SCM 启动时这里会短暂附一个 console(可能闪黑窗),所以立刻 FreeConsole 脱离。 func Run(cfg *config.Config) error { elog = debug.New(ServiceName) defer elog.Close() isSvc, err := svc.IsWindowsService() if err != nil { return fmt.Errorf("query SCM: %w", err) } if isSvc { // 服务模式:脱离 console,避免 SCM 启动时闪黑窗 freeConsole() } h := &handler{cfg: cfg, log: cfg.Log} if isSvc { elog.Info(1, "starting in SCM service mode") return svc.Run(ServiceName, h) } elog.Info(1, "starting in debug mode (console)") return debug.Run(ServiceName, h) } // RunForeground 控制台前台运行,Ctrl+C 退出。 // // 适合临时调试或在没有 SCM 的环境(Linux 交叉编译产物)验证。 func RunForeground(cfg *config.Config) error { cfg.Log.Info("starting in foreground mode") ctx, cancel := context.WithCancel(context.Background()) defer cancel() sup := newSupervisor(cfg) // Run 是阻塞的,自愈重启循环也在里面;Ctrl+C 通过 ctx 取消让它退出。 go func() { sigCh := make(chan os.Signal, 1) signalNotify(sigCh) <-sigCh cfg.Log.Info("foreground: signal received, exiting") cancel() sup.Stop() }() if err := sup.Run(ctx); err != nil { return err } cfg.Log.Info("foreground: supervisor exited") return nil } // supervisor 持有 frpc 子进程 + 自愈逻辑。 // // 设计:frpc 进程意外退出时,supervisor 不会让整个服务跟着挂, // 而是按退避策略自动重启,直到: // // - 服务收到 SCM Stop/Shutdown 信号 // - 连续重启次数达到上限(MaxRestarts) - 防止配错时无限刷日志 // - 配置/二进制缺失等不可恢复错误 type supervisor struct { cfg *config.Config log *logger.Log cmd *exec.Cmd cmdMu sync.Mutex stopped bool } func newSupervisor(cfg *config.Config) *supervisor { return &supervisor{cfg: cfg, log: cfg.Log} } // 重启策略常量。 const ( // MaxRestarts 是 supervisor 在放弃前的最大连续重启次数。 // 超过此数说明配置/环境有问题,服务主动停止,让 SCM 接管。 MaxRestarts = 20 // InitialBackoff 是第一次重启前的等待。 InitialBackoff = 2 * time.Second // MaxBackoff 是单次重启等待的上限(指数退避)。 MaxBackoff = 60 * time.Second ) // Run 启动并监督 frpc 进程,frpc 死了自动重启。 // // 返回: // // nil - 收到停止信号,正常退出 // error - 不可恢复错误(配置缺失、二进制缺失等)或重启次数耗尽 func (s *supervisor) Run(ctx context.Context) error { // 启动前的环境检查 if err := s.cfg.EnsureFrpcBinary(); err != nil { if errors.Is(err, config.ErrFrpcMissing) { return fmt.Errorf("no frpc binary; place frpc.exe in %s or rebuild with embedded binary", s.cfg.ExeDir) } return err } if err := s.cfg.EnsureIni(); err != nil { return fmt.Errorf("no frpc config in %s; run `frpc-client.exe import` to import one, or just place any *.ini/*.toml in that directory", s.cfg.ExeDir) } s.log.Info("config source=%s, path=%s", s.cfg.ConfigSource(), s.cfg.EffectiveConfigPath()) backoff := InitialBackoff restarts := 0 for { if err := ctx.Err(); err != nil { // 上下文被取消(服务停止) s.log.Info("supervisor: context cancelled, exiting") return nil } cmd, err := s.startOnce(ctx) if err != nil { // 启动命令本身失败(例如二进制权限)——不可恢复,立即退出 return err } // 等子进程退出 waitErr := cmd.Wait() s.cmdMu.Lock() s.cmd = nil s.cmdMu.Unlock() if s.stopped || ctx.Err() != nil { s.log.Info("frpc exited due to supervisor stop") if waitErr != nil { s.log.Info("frpc wait err (expected on stop): %v", waitErr) } return nil } restarts++ if restarts > MaxRestarts { s.log.Error("frpc died %d times in a row, giving up; check frpc config and frps reachability", MaxRestarts) return fmt.Errorf("frpc restart budget exhausted (%d restarts)", MaxRestarts) } s.log.Warn("frpc exited unexpectedly (err=%v); restarting in %s (attempt %d/%d)", waitErr, backoff, restarts, MaxRestarts) // 退避等待,带取消感知 select { case <-ctx.Done(): return nil case <-time.After(backoff): } // 指数退避,封顶 MaxBackoff backoff *= 2 if backoff > MaxBackoff { backoff = MaxBackoff } } } // startOnce 启动一次 frpc 并返回 *exec.Cmd。 // 由 Run() 在循环里反复调用。 func (s *supervisor) startOnce(ctx context.Context) (*exec.Cmd, error) { cfgPath := s.cfg.EffectiveConfigPath() cmd := exec.CommandContext(ctx, s.cfg.FrpcPath, "-c", cfgPath) cmd.Stdout = s.log.FrpcRedirect() cmd.Stderr = s.log.FrpcRedirect() // Windows 上不弹黑窗口:CREATE_NO_WINDOW=0x08000000 cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000} s.log.Info("launching frpc: %s -c %s", s.cfg.FrpcPath, cfgPath) if err := cmd.Start(); err != nil { return nil, fmt.Errorf("start frpc: %w", err) } s.cmdMu.Lock() s.cmd = cmd s.cmdMu.Unlock() return cmd, nil } // Stop 终止 frpc 子进程,最多等 5s。 // // 调用方应该先取消 supervisor 的 ctx,本方法会把 stopped 标记置上, // 防止 Run() 循环把它当作"意外退出"而触发重启。 func (s *supervisor) Stop() { s.cmdMu.Lock() defer s.cmdMu.Unlock() if s.cmd == nil || s.cmd.Process == nil { s.stopped = true return } if s.stopped { return } s.stopped = true s.log.Info("stopping frpc (pid=%d)...", s.cmd.Process.Pid) _ = s.cmd.Process.Kill() // cmd.Wait() 在 Run() 里已经异步执行,这里不能再 Wait。 // 用轮询 ProcessState 代替。 deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if s.cmd.ProcessState != nil { break } time.Sleep(100 * time.Millisecond) } if s.cmd.ProcessState == nil { s.log.Warn("frpc did not exit in 5s, force killing") _ = s.cmd.Process.Kill() } else { s.log.Info("frpc stopped") } } // alive 检查 frpc 子进程是否还在。 // 仅用于早期(StartOnce 之后第一次心跳),大部分时间 Run 自己在监听 cmd.Wait。 func (s *supervisor) alive() bool { s.cmdMu.Lock() defer s.cmdMu.Unlock() if s.cmd == nil || s.cmd.Process == nil { return false } return s.cmd.ProcessState == nil } // ---------- handler implements svc.Handler ---------- type handler struct { cfg *config.Config log *logger.Log sup *supervisor } func (h *handler) 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} ctx, cancel := context.WithCancel(context.Background()) defer cancel() h.sup = newSupervisor(h.cfg) // supervisor.Run() 是阻塞的,自带自愈重启循环。 // 我们把它丢到 goroutine 里,通过 done channel 观察它什么时候退。 done := make(chan error, 1) go func() { done <- h.sup.Run(ctx) }() changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} heartbeat := time.NewTicker(1 * time.Second) defer heartbeat.Stop() for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: h.log.Info("SCM requests %v, stopping supervisor", c.Cmd) elog.Info(1, fmt.Sprintf("SCM requests %v, stopping supervisor", c.Cmd)) cancel() // 通知 supervisor.Run 退出 h.sup.Stop() // 兜底:再强杀一次 <-done // 等 Run 退出干净 changes <- svc.Status{State: svc.StopPending} return false, 0 default: h.log.Warn("unknown SCM request cmd=%d", c.Cmd) elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) } case <-heartbeat.C: // 心跳仅用于确认 SCM 还能联系上我们;supervisor 自己管 frpc 死活 _ = h.sup.alive() case err := <-done: // supervisor 自己退出了 —— 可能是重启次数耗尽或不可恢复错误 if err != nil { h.log.Error("supervisor exited with error: %v", err) elog.Error(1, fmt.Sprintf("supervisor exited with error: %v", err)) return false, 3 } h.log.Info("supervisor exited cleanly") return false, 0 } } } // ---------- 服务安装/卸载/启停 ---------- // Install 注册本程序为 Windows 服务,启动类型为自动。 func Install() error { exe, err := os.Executable() if err != nil { return err } m, err := mgr.Connect() if err != nil { return fmt.Errorf("connect SCM: %w", err) } defer m.Disconnect() // 检查服务是否已存在 if existing, err := m.OpenService(ServiceName); err == nil { existing.Close() return fmt.Errorf("service %q already exists; run `remove` first if you want to reinstall", ServiceName) } // 注册 Event Log 源(失败不致命,只影响事件日志可见性) _ = eventlog.InstallAsEventCreate(ServiceName, eventlog.Error|eventlog.Warning|eventlog.Info) conf := mgr.Config{ DisplayName: DisplayName, Description: Description, StartType: mgr.StartAutomatic, } s, err := m.CreateService(ServiceName, exe, conf) if err != nil { // 失败时回滚 eventlog _ = eventlog.Remove(ServiceName) return fmt.Errorf("create service: %w", err) } defer s.Close() fmt.Printf("✔ service installed: %s\n", ServiceName) fmt.Printf(" binary: %s\n", exe) fmt.Println(" next: run `frpc-client.exe start` to start it now") return nil } // Remove 停止并删除服务。 func Remove() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ServiceName) if err != nil { return fmt.Errorf("open service: %w", err) } defer s.Close() // 先尝试停止(忽略 "not started" 错误) _, _ = s.Control(svc.Stop) // 等待它真的停掉 for { status, err := s.Query() if err != nil { break } if status.State == svc.Stopped { break } time.Sleep(500 * time.Millisecond) } if err := s.Delete(); err != nil { return fmt.Errorf("delete service: %w", err) } _ = eventlog.Remove(ServiceName) fmt.Println("✔ service removed:", ServiceName) return nil } // Start 启动服务。 func Start() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ServiceName) if err != nil { return err } defer s.Close() if err := s.Start(); err != nil { return fmt.Errorf("start service: %w", err) } fmt.Println("✔ service started:", ServiceName) return nil } // Stop 停止服务(不卸载)。 // // 如果服务当前已经是 Stopped 状态,返回 nil 而不是 error - // 这样 `stop && remove` 这种链式调用不会因为前置 stop 没必要而中断。 func Stop() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ServiceName) if err != nil { return err } defer s.Close() // 先查询状态:已停止就跳过 status, err := s.Query() if err != nil { return fmt.Errorf("query service: %w", err) } if status.State == svc.Stopped { fmt.Println("service already stopped:", ServiceName) return nil } if _, err := s.Control(svc.Stop); err != nil { return fmt.Errorf("stop service: %w", err) } fmt.Println("✔ service stopped:", ServiceName) return nil }