package main import ( "bytes" "embed" "fmt" "io" "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 ) // ================================================================ // 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) } localPath := filepath.Join(".", "bin", fileName) if _, err := os.Stat(localPath); err == nil { cachedFrpcPath = localPath return localPath, nil } 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 } } path, err := exec.LookPath("frpc") if err == nil { cachedFrpcPath = path return path, nil } return "", fmt.Errorf("未找到 frpc 文件") } // ================================================================ // 配置生成 (保持不变) // ================================================================ 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 WireProtocolLine string }{ GlobalConfig: cfg, Proxies: activeProxies, } if cfg.WireProtocolV2 { data.WireProtocolLine = `wireProtocol = "v2"` } else { data.WireProtocolLine = "" } 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.MkdirAll("./data", 0755); err != nil { return fmt.Errorf("创建 data 目录失败: %w", err) } if err := os.WriteFile("./data/frpc.toml", buf.Bytes(), 0644); err != nil { return fmt.Errorf("写入配置文件失败: %w", err) } return nil } // ================================================================ // 进程管理 (兼容层,实际调用 ProcessManager) // 2.6-preview: 所有进程管理逻辑迁移到 process_manager.go // ================================================================ // isFrpcRunning 检查 frpc 是否在运行 // 优先使用 ProcessManager,降级到 PID 文件 func isFrpcRunning() bool { // 如果 ProcessManager 已初始化,使用它 if processManager != nil { status, err := processManager.Status() if err != nil { log.Printf("[WARN] ProcessManager.Status() 失败: %v,降级到 PID 文件", err) return isFrpcRunningLegacy() } return status.State == "running" } return isFrpcRunningLegacy() } // isFrpcRunningLegacy 旧版 PID 文件检测 (降级方案) func isFrpcRunningLegacy() bool { pidData, err := os.ReadFile("./data/frpc.pid") if err != nil { return false } pid, err := strconv.Atoi(strings.TrimSpace(string(pidData))) if err != nil { return false } if runtime.GOOS == "windows" { 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)) } process, err := os.FindProcess(pid) if err != nil { return false } return process.Signal(syscall.Signal(0)) == nil } // StartFrpc 启动 frpc (幂等) // 优先使用 ProcessManager,降级到旧逻辑 func StartFrpc() error { if processManager != nil { log.Println("[INFO] 使用 ProcessManager 启动 frpc") return processManager.Start(nil) } log.Println("[WARN] ProcessManager 未初始化,使用兼容模式启动 frpc") return startFrpcLegacy() } // startFrpcLegacy 旧版启动逻辑 (降级方案) func startFrpcLegacy() error { frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } if err := os.MkdirAll("./data", 0755); err != nil { return fmt.Errorf("创建 data 目录失败: %w", err) } if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) { if err := GenerateFrpcConfig(); err != nil { return fmt.Errorf("生成配置文件失败: %w", err) } } if isFrpcRunningLegacy() { return nil } os.Remove("./data/frpc.pid") cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml") setWindowHide(cmd) setSysProcAttr(cmd) logFile, err := os.OpenFile("./data/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) } go func() { if err := cmd.Wait(); err != nil { log.Printf("frpc 子进程退出: %v", err) } os.Remove("./data/frpc.pid") }() if err := os.WriteFile("./data/frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil { return fmt.Errorf("保存 PID 失败: %w", err) } return nil } // StopFrpc 停止 frpc (幂等) // 优先使用 ProcessManager,降级到旧逻辑 func StopFrpc() error { if processManager != nil { log.Println("[INFO] 使用 ProcessManager 停止 frpc") return processManager.Stop(nil) } log.Println("[WARN] ProcessManager 未初始化,使用兼容模式停止 frpc") return stopFrpcLegacy() } // stopFrpcLegacy 旧版停止逻辑 (降级方案) func stopFrpcLegacy() 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("./data/frpc.pid") return nil } pidData, err := os.ReadFile("./data/frpc.pid") if err != 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 } pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData))) process, err := os.FindProcess(pid) if err != nil { os.Remove("./data/frpc.pid") return nil } if err := process.Kill(); err != nil { return fmt.Errorf("杀死进程失败: %w", err) } os.Remove("./data/frpc.pid") return nil } // RestartFrpc 重启 frpc (原子操作) // 优先使用 ProcessManager,降级到旧逻辑 func RestartFrpc() error { if processManager != nil { log.Println("[INFO] 使用 ProcessManager 重启 frpc") return processManager.Restart(nil) } log.Println("[WARN] ProcessManager 未初始化,使用兼容模式重启 frpc") if err := stopFrpcLegacy(); err != nil { return err } return startFrpcLegacy() } // GetFrpcStatus 获取 frpc 详细状态 (供 API 调用) // 优先使用 ProcessManager,降级到旧逻辑 func GetFrpcStatus() (map[string]interface{}, error) { if processManager != nil { status, err := processManager.Status() if err != nil { return nil, err } return map[string]interface{}{ "state": status.State, "pid": status.PID, "port": status.Port, }, nil } // 降级: 返回简单布尔值 running := isFrpcRunningLegacy() return map[string]interface{}{ "state": map[bool]string{true: "running", false: "stopped"}[running], "pid": 0, "port": 0, "legacy": true, }, nil } // ReloadFrpc 热加载 frpc 配置 // 优先尝试 reload,失败则降级到重启 func ReloadFrpc() error { // 如果 ProcessManager 未初始化,降级到旧逻辑 if processManager == nil { return reloadFrpcLegacy() } // 检查 frpc 是否在运行 status, err := processManager.Status() if err != nil { return fmt.Errorf("获取 frpc 状态失败: %w", err) } if status.State != "running" { // 未运行,直接启动 return processManager.Start(nil) } // 尝试热加载 (使用 frpc reload 命令) frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") output, err := cmd.CombinedOutput() if err != nil { log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) log.Printf(" reload 输出: %s", string(output)) // 降级: 重启 if err := processManager.Restart(nil); err != nil { return fmt.Errorf("重启 frpc 失败: %w", err) } return nil } log.Printf("✅ frpc 热加载成功") return nil } // reloadFrpcLegacy 旧版热加载 (降级方案) func reloadFrpcLegacy() error { if !isFrpcRunningLegacy() { return startFrpcLegacy() } frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") output, err := cmd.CombinedOutput() if err != nil { log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) if stopErr := stopFrpcLegacy(); stopErr != nil { return fmt.Errorf("停止 frpc 失败: %w", stopErr) } if startErr := startFrpcLegacy(); startErr != nil { return fmt.Errorf("启动 frpc 失败: %w", startErr) } return nil } log.Printf("✅ frpc 热加载成功 (兼容模式)") return nil } // ================================================================ // 日志读取 (保持不变) // ================================================================ // readTailLog 读取文件末尾 n 行 func readTailLog(filePath string, n int) ([]string, error) { file, err := os.Open(filePath) if err != nil { return nil, err } defer file.Close() info, err := file.Stat() if err != nil { return nil, err } fileSize := info.Size() if fileSize == 0 { return []string{}, nil } const chunkSize = 4096 var lines []string var leftover []byte offset := fileSize for len(lines) < n && offset > 0 { readSize := chunkSize if offset < int64(chunkSize) { readSize = int(offset) } offset -= int64(readSize) buf := make([]byte, readSize) _, err := file.ReadAt(buf, offset) if err != nil && err != io.EOF { return nil, err } data := append(buf, leftover...) leftover = nil start := 0 for i := len(data) - 1; i >= 0; i-- { if data[i] == '\n' { if i+1 < len(data) { line := string(data[i+1:]) if line != "" { lines = append([]string{line}, lines...) if len(lines) >= n { break } } } start = i } } if len(lines) < n && start > 0 { leftover = data[:start] } } if len(lines) < n && len(leftover) > 0 { parts := strings.Split(string(leftover), "\n") for i := len(parts) - 1; i >= 0; i-- { if parts[i] != "" { lines = append([]string{parts[i]}, lines...) if len(lines) >= n { break } } } } return lines, nil }