Files
frpc-client-win/internal/config/config.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

234 lines
6.7 KiB
Go

// Package config 管理 frpc-client 的运行配置与文件资源。
//
// 它负责三件事:
//
// 1. frpc.ini 的存在性检查与导入交互
// 2. frpc.exe 的释放(优先使用 EXE 同目录的本地版,其次用内嵌版)
// 3. 各种路径(日志目录、配置目录)的一致性
package config
import (
"embed"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"frpc-client-win/internal/logger"
)
// Config 持有运行期所需的全部路径与资源。
type Config struct {
ExeDir string // 程序自身所在目录
FrpcPath string // 释放后的 frpc.exe 完整路径
IniPath string // 主配置(frpc.ini)完整路径 - 优先
TomlPath string // 备选配置(frpc.toml)完整路径
LogDir string // 日志目录
Log *logger.Log
FS embed.FS // 内嵌资源
FrpcName string // 内嵌的 frpc 二进制文件名
}
// New 构造一个 Config。
func New(exeDir, frpcName string, frpcFS embed.FS, log *logger.Log) *Config {
c := &Config{
ExeDir: exeDir,
FrpcPath: filepath.Join(exeDir, frpcName),
IniPath: filepath.Join(exeDir, "frpc.ini"),
TomlPath: filepath.Join(exeDir, "frpc.toml"),
LogDir: filepath.Join(exeDir, "logs"),
Log: log,
FS: frpcFS,
FrpcName: frpcName,
}
return c
}
// EnsureFrpcBinary 确保 frpc.exe 在本地可用。
//
// 优先级:
//
// 本地已存在 -> 直接使用
// 内嵌版本 -> 释放到本地
//
// 当内嵌版本也缺失(开发期没打包),返回 ErrFrpcMissing,
// 由调用方决定是否尝试网络下载。
func (c *Config) EnsureFrpcBinary() error {
if _, err := os.Stat(c.FrpcPath); err == nil {
c.Log.Info("using local frpc binary at %s", c.FrpcPath)
return nil
}
data, err := fs.ReadFile(c.FS, "assets/"+c.FrpcName)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return ErrFrpcMissing
}
return fmt.Errorf("read embedded frpc: %w", err)
}
if err := os.WriteFile(c.FrpcPath, data, 0o755); err != nil {
return fmt.Errorf("extract frpc to %s: %w", c.FrpcPath, err)
}
c.Log.Info("extracted embedded frpc (%d bytes) to %s", len(data), c.FrpcPath)
return nil
}
// ErrFrpcMissing 表示内嵌和本地都没有 frpc.exe。
var ErrFrpcMissing = errors.New("frpc binary not found: neither local nor embedded")
// EnsureIni 检查 frpc 配置文件是否存在,并选出实际使用的路径。
//
// 优先级:
//
// 1. frpc.ini (固定名)
// 2. frpc.toml (固定名)
// 3. 同目录任意唯一的 *.ini / *.toml 文件(适用于客户用 frpc-mike.ini 这类自定义名)
//
// 注意:不是把客户的 frpc-mike.ini 改名复制,而是直接把绝对路径传给 frpc -c,
// 这样客户原始文件名保持不变(便于版本管理/多客户多份配置共存)。
func (c *Config) EnsureIni() error {
if _, _, err := c.findConfig(); err != nil {
return err
}
return nil
}
// findConfig 实现三段式查找,返回(选中的文件路径, 来源标签, error)。
//
// 来源标签:
//
// "frpc.ini" -> 固定名 frpc.ini
// "frpc.toml" -> 固定名 frpc.toml
// "scan" -> 同目录扫描到的唯一 .ini/.toml
// "" -> 未找到
func (c *Config) findConfig() (string, string, error) {
// 1. frpc.ini
if _, err := os.Stat(c.IniPath); err == nil {
return c.IniPath, "frpc.ini", nil
}
// 2. frpc.toml
if _, err := os.Stat(c.TomlPath); err == nil {
return c.TomlPath, "frpc.toml", nil
}
// 3. 同目录扫描
candidates, err := c.scanConfigs()
if err != nil {
return "", "", fmt.Errorf("scan configs: %w", err)
}
switch len(candidates) {
case 0:
return "", "", ErrIniMissing
case 1:
return candidates[0], "scan", nil
default:
// 多个 - 让用户选,而不是猜
return "", "", fmt.Errorf("%w: found multiple candidates in %s: %s",
ErrIniAmbiguous, c.ExeDir, strings.Join(candidates, ", "))
}
}
// scanConfigs 扫描 ExeDir 下所有 .ini/.toml 文件,返回绝对路径列表。
func (c *Config) scanConfigs() ([]string, error) {
entries, err := os.ReadDir(c.ExeDir)
if err != nil {
return nil, err
}
var found []string
for _, e := range entries {
if e.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(e.Name()))
if ext == ".ini" || ext == ".toml" {
found = append(found, filepath.Join(c.ExeDir, e.Name()))
}
}
return found, nil
}
// EffectiveConfigPath 返回实际可用的配置文件路径。
// 由 Start 在 EnsureIni 之后调用。
func (c *Config) EffectiveConfigPath() string {
p, _, _ := c.findConfig()
return p
}
// ConfigSource 返回当前配置的来源标签(用于日志/排错)。
func (c *Config) ConfigSource() string {
_, src, _ := c.findConfig()
return src
}
// ErrIniMissing 表示 frpc 配置不存在。
var ErrIniMissing = errors.New("frpc config not found (expected frpc.ini / frpc.toml / or any *.ini *.toml in the same directory as frpc-client.exe)")
// ErrIniAmbiguous 表示找到多个配置文件且无法判定使用哪个。
var ErrIniAmbiguous = errors.New("ambiguous frpc config")
// ImportInteractive 交互式导入 frpc 配置。
//
// 服务模式下不应该调用,所以这里只支持控制台提示;
// 用户拖入一个 .ini / .toml / .yaml 文件,我们复制到 EXE 同目录。
func ImportInteractive(c *Config) error {
fmt.Println("== frpc config import ==")
fmt.Printf("当前目录: %s\n", c.ExeDir)
fmt.Printf("目标文件: %s (或 frpc.toml)\n", c.IniPath)
if _, err := os.Stat(c.IniPath); err == nil {
fmt.Println("frpc.ini 已存在,无需导入")
return nil
}
if _, err := os.Stat(c.TomlPath); err == nil {
fmt.Println("frpc.toml 已存在,无需导入")
return nil
}
fmt.Print("请拖入 frpc.ini / frpc.toml 文件(或粘贴完整路径),回车确认:\n> ")
var src string
if _, err := fmt.Scanln(&src); err != nil {
return fmt.Errorf("read input: %w", err)
}
src = filepath.Clean(stripQuotes(src))
// 根据扩展名决定目标文件
dst := c.IniPath
switch strings.ToLower(filepath.Ext(src)) {
case ".toml":
dst = c.TomlPath
case ".yaml", ".yml":
// frpc 不直接支持 yaml 配置文件 - 提示用户转换
return fmt.Errorf("frpc 不直接支持 yaml 配置;请改用 .ini 或 .toml 格式")
}
return copyFile(src, dst)
}
// stripQuotes 去掉用户拖入文件路径时常见的包裹引号。
func stripQuotes(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return fmt.Errorf("create dest: %w", err)
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return fmt.Errorf("copy: %w", err)
}
return nil
}