package main import ( "bytes" "embed" "fmt" "log" "os" "os/exec" "runtime" "strings" "sync" "text/template" ) //go:embed bin/* var embeddedFrpc embed.FS //go:embed frpc.tmpl var FrpcTemplateContent string var ( cachedFrpcPath string frpcPathMutex sync.Mutex ) // getFrpcPath 获取当前平台对应的 frpc 路径(优先使用缓存) func getFrpcPath() (string, error) { frpcPathMutex.Lock() defer frpcPathMutex.Unlock() // 如果缓存有效,直接返回 if cachedFrpcPath != "" { if _, err := os.Stat(cachedFrpcPath); err == nil { return cachedFrpcPath, nil } // 缓存失效,清空 cachedFrpcPath = "" } // 确定文件名 var fileName string switch { case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": fileName = "frpc_windows_amd64.exe" case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": fileName = "frpc_linux_amd64" case runtime.GOOS == "linux" && runtime.GOARCH == "arm64": fileName = "frpc_linux_arm64" default: // 降级:尝试从系统 PATH 查找 frpc path, err := exec.LookPath("frpc") if err == nil { cachedFrpcPath = path return path, nil } return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH) } // 从 embed 读取 data, err := embeddedFrpc.ReadFile("bin/" + fileName) if err != nil { // 降级到 PATH path, lookErr := exec.LookPath("frpc") if lookErr == nil { cachedFrpcPath = path return path, nil } return "", fmt.Errorf("未找到 frpc 文件 (embed 和 PATH 均无): %w", err) } // 写入临时目录 tmpPath := os.TempDir() + "/frpc" if runtime.GOOS == "windows" { tmpPath += ".exe" } // 尝试写入(如果文件已存在且正在使用,忽略错误,直接使用已有文件) if err := os.WriteFile(tmpPath, data, 0755); err != nil { // 如果写入失败,检查是否已有可用的 frpc if _, statErr := os.Stat(tmpPath); statErr == nil { cachedFrpcPath = tmpPath return tmpPath, nil } return "", fmt.Errorf("写入临时文件失败: %w", err) } cachedFrpcPath = tmpPath return tmpPath, nil } // GenerateFrpcConfig 生成 frpc.toml 配置文件 func GenerateFrpcConfig() error { cfg, err := GetGlobalConfig() if err != nil { return fmt.Errorf("读取全局配置失败: %w", err) } proxies, err := GetProxies() if err != nil { return fmt.Errorf("读取隧道列表失败: %w", err) } var activeProxies []Proxy for _, p := range proxies { if p.Enabled { activeProxies = append(activeProxies, p) } } data := struct { *GlobalConfig Proxies []Proxy }{ GlobalConfig: cfg, Proxies: activeProxies, } // ===== 核心改动 ===== // 优先使用外部模板文件 frpc.tmpl,如果不存在则使用 embed 中的模板 var tmplContent string if _, err := os.Stat("frpc.tmpl"); err == nil { // 外部文件存在,读取它 content, readErr := os.ReadFile("frpc.tmpl") if readErr == nil { tmplContent = string(content) } else { // 读取失败时回退到 embed tmplContent = FrpcTemplateContent } } else { // 外部文件不存在,使用 embed 中的模板 tmplContent = FrpcTemplateContent } // ===== 核心改动结束 ===== tmpl, err := template.New("frpc").Parse(tmplContent) if err != nil { return fmt.Errorf("解析模板失败: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { return fmt.Errorf("渲染模板失败: %w", err) } if err := os.WriteFile("./frpc.toml", buf.Bytes(), 0644); err != nil { return fmt.Errorf("写入配置文件失败: %w", err) } return nil } // StartFrpc 启动 frpc(后台运行) func StartFrpc() error { frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } // 确保配置文件存在 if _, err := os.Stat("./frpc.toml"); os.IsNotExist(err) { if err := GenerateFrpcConfig(); err != nil { return fmt.Errorf("生成配置文件失败: %w", err) } } // 检查是否已在运行 running, _ := GetFrpcStatus() if running { return nil } cmd := exec.Command(frpcPath, "-c", "./frpc.toml") if runtime.GOOS == "windows" { setWindowHide(cmd) } if err := cmd.Start(); err != nil { return fmt.Errorf("启动 frpc 失败: %w", err) } return nil } // ReloadFrpc 热加载 frpc 配置(仅发信号,不重复写入二进制) // ReloadFrpc 热加载 frpc 配置,失败时自动降级为重启 func ReloadFrpc() error { running, err := GetFrpcStatus() if err != nil { return fmt.Errorf("检查 frpc 状态失败: %w", err) } if !running { return StartFrpc() } if cachedFrpcPath == "" { if _, err := getFrpcPath(); err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } } cmd := exec.Command(cachedFrpcPath, "reload", "-c", "./frpc.toml") _, err = cmd.CombinedOutput() // ← 这里改了 if err != nil { log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err) if stopErr := StopFrpc(); stopErr != nil { return fmt.Errorf("停止 frpc 失败: %w", stopErr) } if startErr := StartFrpc(); startErr != nil { return fmt.Errorf("启动 frpc 失败: %w", startErr) } return nil } return nil } // StopFrpc 停止 frpc func StopFrpc() error { if runtime.GOOS == "windows" { cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe") if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") { return fmt.Errorf("停止 frpc 失败: %w", err) } return nil } cmd := exec.Command("pkill", "-f", "frpc") if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") { return fmt.Errorf("停止 frpc 失败: %w", err) } return nil } // GetFrpcStatus 检查 frpc 是否在运行 func GetFrpcStatus() (bool, error) { if runtime.GOOS == "windows" { cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq frpc.exe") output, err := cmd.CombinedOutput() if err != nil { return false, nil } return strings.Contains(string(output), "frpc.exe"), nil } cmd := exec.Command("pgrep", "-f", "frpc") output, err := cmd.Output() if err != nil { return false, nil } return len(output) > 0, nil }