Files
frpc-client-win/main.go
T
cnbugs 69801ecc3f Initial commit: frpc-client-win v1.0
Single-file Windows frpc service wrapper.

Features:
- Embeds frpc v0.70.1 (16.7MB) into one 18.9MB EXE
- Registers as Windows Service (auto-start, no login required)
- Auto-detects frpc config by three-tier search:
  1. frpc.ini  2. frpc.toml  3. any *.ini/*.toml in exe dir
- Self-healing supervisor: frpc crashes are restarted with
  exponential backoff (2s..60s), capped to avoid log spam
- Console subsystem so install/start show feedback
- FreeConsole on SCM start to suppress console window
- Logs to <exe-dir>/logs/service.log

Subcommands:
  install  start  stop  remove  import  -d

Smoke test (smoke_linux.go) covers all config detection paths
including the custom-name (e.g. frpc-mike.ini) use case.
2026-08-10 22:11:43 +08:00

101 lines
2.7 KiB
Go

//go:build windows
// +build windows
// Package main 是 frpc-client-win 的入口。
//
// 双模式入口:
//
// 带子命令 -> 服务管理 (install/remove/start/stop/run/import)
// 不带子命令 -> 服务模式运行(已注册为服务时)
// 不带子命令 -d -> 控制台前台运行(调试用)
//
// frpc.exe 通过 embed.FS 内嵌到 binary 中,部署只需一个 EXE。
//
// 编译说明:
//
// 默认用 CONSOLE 子系统,这样 install/start/import 等 CLI 子命令能输出反馈。
// 服务运行时 (svc.Run) 即使从 console EXE 启动,SCM 也会自动脱离控制台窗口,
// 不会出现黑窗。
// 不要加 -H windowsgui,否则 CLI 子命令的 stdout 会被静默吃掉(踩过的坑)。
package main
import (
"embed"
"fmt"
"os"
"path/filepath"
"frpc-client-win/internal/config"
"frpc-client-win/internal/logger"
"frpc-client-win/internal/service"
)
//go:embed assets/frpc.exe
var frpcFS embed.FS
// frpcBinName 是嵌入的 frpc 二进制文件名(同目录会优先使用本地版)。
const frpcBinName = "frpc.exe"
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func run() error {
exeDir, err := exeDir()
if err != nil {
return err
}
// 注意:日志路径必须是绝对的!
// SCM 启动的服务默认工作目录是 C:\Windows\System32,
// 用相对路径会把日志写到 System32 下,而不是 EXE 同目录。
logDir := filepath.Join(exeDir, "logs")
log, err := logger.New(logDir)
if err != nil {
return fmt.Errorf("init logger at %s: %w", logDir, err)
}
defer log.Close()
cfg := config.New(exeDir, frpcBinName, frpcFS, log)
// 没有参数 -> 直接以服务/前台模式启动
if len(os.Args) < 2 {
return service.Run(cfg)
}
switch os.Args[1] {
case "install":
return service.Install()
case "remove", "uninstall":
return service.Remove()
case "start":
return service.Start()
case "stop":
return service.Stop()
case "run":
// 'run' 子命令强制以服务模式运行(SCM 调用此子命令)
return service.Run(cfg)
case "import":
return config.ImportInteractive(cfg)
case "-d", "console":
// 控制台前台模式,直接启动 frpc 看日志
return service.RunForeground(cfg)
default:
return fmt.Errorf("unknown command: %s\n\nUsage:\n install | remove | start | stop | import | -d", os.Args[1])
}
}
// exeDir 返回当前可执行文件所在目录。
//
// 当作为 Windows 服务运行时,os.Getwd() 指向 System32,
// 所以必须用 Module 路径而不是当前工作目录。
func exeDir() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", fmt.Errorf("locate executable: %w", err)
}
return filepath.Dir(exe), nil
}