69801ecc3f
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.
101 lines
2.7 KiB
Go
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
|
|
} |