package main import ( "bytes" "embed" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" "syscall" "text/template" ) //go:embed bin/* var embeddedFrpc embed.FS //go:embed frpc.tmpl var FrpcTemplateContent string var ( cachedFrpcPath string frpcPathMutex sync.Mutex ) // getFrpcPath 获取 frpc 路径,优先级: // 1. 当前目录 ./bin/ (便于用户手动管理 frpc 版本) // 2. embed 解压到 /tmp (便于开箱即用) // 3. 系统 PATH 中的 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" case runtime.GOOS == "linux" && runtime.GOARCH == "arm": fileName = "frpc_linux_arm_hf" default: path, err := exec.LookPath("frpc") if err == nil { cachedFrpcPath = path return path, nil } return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH) } // 优先级1:本地 ./bin/ localPath := filepath.Join(".", "bin", fileName) if _, err := os.Stat(localPath); err == nil { cachedFrpcPath = localPath return localPath, nil } // 优先级2:embed 解压到临时目录 data, err := embeddedFrpc.ReadFile("bin/" + fileName) if err == nil { tmpPath := filepath.Join(os.TempDir(), "frpc") if runtime.GOOS == "windows" { tmpPath += ".exe" } if err := os.WriteFile(tmpPath, data, 0755); err == nil { cachedFrpcPath = tmpPath return tmpPath, nil } if _, statErr := os.Stat(tmpPath); statErr == nil { cachedFrpcPath = tmpPath return tmpPath, nil } } // 优先级3:系统 PATH path, err := exec.LookPath("frpc") if err == nil { cachedFrpcPath = path return path, nil } return "", fmt.Errorf("未找到 frpc 文件 (本地 bin/、embed、PATH 均无)") } // 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, } var tmplContent string if _, err := os.Stat("frpc.tmpl"); err == nil { content, readErr := os.ReadFile("frpc.tmpl") if readErr == nil { tmplContent = string(content) } else { tmplContent = FrpcTemplateContent } } else { 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 } // isFrpcRunning 通过 PID 文件检查 frpc 是否在运行 func isFrpcRunning() bool { pidData, err := os.ReadFile("./frpc.pid") if err != nil { return false } pid, err := strconv.Atoi(strings.TrimSpace(string(pidData))) if err != nil { return false } if runtime.GOOS == "windows" { // Windows 用 tasklist 检查 cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid)) output, err := cmd.CombinedOutput() if err != nil { return false } return strings.Contains(string(output), strconv.Itoa(pid)) } // Linux/Unix: 用 kill 0 检查进程是否存在 process, err := os.FindProcess(pid) if err != nil { return false } return process.Signal(syscall.Signal(0)) == 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) } } if isFrpcRunning() { return nil } // 清理旧的 PID 文件 os.Remove("./frpc.pid") cmd := exec.Command(frpcPath, "-c", "./frpc.toml") setWindowHide(cmd) // ✅ 设置进程属性(Unix: Setsid 脱离父进程,Windows: 不处理) setSysProcAttr(cmd) // 把输出重定向到日志文件 logFile, err := os.OpenFile("./frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return fmt.Errorf("打开日志文件失败: %w", err) } cmd.Stdout = logFile cmd.Stderr = logFile if err := cmd.Start(); err != nil { return fmt.Errorf("启动 frpc 失败: %w", err) } // ✅ 保存 PID 到文件 if err := os.WriteFile("./frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil { return fmt.Errorf("保存 PID 失败: %w", err) } return nil } // StopFrpc 停止 frpc(通过 PID 文件精准杀进程) 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) } os.Remove("./frpc.pid") return nil } // Linux: 读取 PID 文件精准杀进程 pidData, err := os.ReadFile("./frpc.pid") if err != nil { // 降级到 pkill 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 } pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData))) process, err := os.FindProcess(pid) if err != nil { os.Remove("./frpc.pid") return nil } if err := process.Kill(); err != nil { return fmt.Errorf("杀死进程失败: %w", err) } os.Remove("./frpc.pid") return nil } // GetFrpcStatus 获取 frpc 运行状态(通过 PID 文件) func GetFrpcStatus() (bool, error) { return isFrpcRunning(), nil } // ReloadFrpc 热加载,失败时自动降级为重启 func ReloadFrpc() error { running := isFrpcRunning() if !running { return StartFrpc() } frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } cmd := exec.Command(frpcPath, "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 }