// internal/process/manager.go // 新增 PhaseReloading 状态 + FRPReady 绑定 PID package process import ( "bufio" "context" "encoding/json" "fmt" "io" "log" "net" "net/http" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" "syscall" "time" ) // ================================================================ // 常量定义 // ================================================================ const ( LockFileName = ".frpc.lock" PortCheckTimeout = 500 * time.Millisecond StopMaxWaitTime = 5 * time.Second LockAcquireTimeout = 30 * time.Second LockRetryInterval = 100 * time.Millisecond APITimeout = 2 * time.Second StartupMaxAttempts = 50 StartupRetryDelay = 200 * time.Millisecond StartupTimeout = 10 * time.Second HealthCheckInterval = 15 * time.Second ) // ================================================================ // 进程状态定义 // ================================================================ type ProcessPhase string const ( PhaseUnknown ProcessPhase = "UNKNOWN" PhaseStarting ProcessPhase = "STARTING" PhaseRunning ProcessPhase = "RUNNING" PhaseDegraded ProcessPhase = "DEGRADED" PhaseFailed ProcessPhase = "FAILED" PhaseStopped ProcessPhase = "STOPPED" PhaseStopping ProcessPhase = "STOPPING" PhaseConflict ProcessPhase = "CONFLICT" PhaseReloading ProcessPhase = "RELOADING" // 新增:reload 中间态 ) // ================================================================ // 数据结构 // ================================================================ type PortCheckResult struct { Ready bool Err error PID int Process string } type FrpcInstance struct { PID int ParentPID int ExecPath string CmdLine string Owned bool } type ProcessState struct { Phase ProcessPhase `json:"phase"` PID int `json:"pid"` Port int `json:"port"` StartedAt time.Time `json:"started_at"` ExitCode int `json:"exit_code,omitempty"` Alive bool `json:"alive"` PortReady bool `json:"port_ready"` PortPID int `json:"port_pid"` PortOwner string `json:"port_owner"` PortError string `json:"port_error,omitempty"` FRPReady bool `json:"frp_ready"` Version string `json:"version,omitempty"` ExpectedStop bool `json:"expected_stop"` LastOutput string `json:"last_output,omitempty"` LastError string `json:"last_error,omitempty"` Error string `json:"error,omitempty"` Conflicts []FrpcInstance `json:"conflicts,omitempty"` } type PortStatus struct { Port int `json:"port"` Occupied bool `json:"occupied"` PID int `json:"pid"` IsFRPC bool `json:"is_frpc"` ProcessCmd string `json:"process_cmd,omitempty"` } // FRPCStatus 匹配 frpc admin API /api/status 的实际返回结构 type FRPCStatus struct { TCP []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"tcp"` UDP []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"udp"` HTTP []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"http"` HTTPS []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"https"` STCP []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"stcp"` XTCP []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"xtcp"` SUDP []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"sudp"` } type ConflictInfo struct { HasConflict bool Count int Owned []FrpcInstance Unknown []FrpcInstance } // ================================================================ // 全局变量 // ================================================================ var ( globalManager *ProcessManager globalMu sync.Mutex ) // ================================================================ // ProcessManager 主结构 // ================================================================ type ProcessManager struct { mu sync.Mutex dataDir string configPath string frpcBinPath string adminPort int startTime time.Time expectedStop bool exitCode int exitMu sync.Mutex lastOutput string lastError string outputMu sync.Mutex lockFile *os.File locked bool currentPhase ProcessPhase statusMu sync.RWMutex // 用于 admin API 检测的 HTTP 客户端 httpClient *http.Client // 当前实例的 run_id(从 admin API 获取) runID string } // ================================================================ // 构造函数 // ================================================================ func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager { pm := &ProcessManager{ dataDir: dataDir, configPath: configPath, frpcBinPath: frpcBinPath, adminPort: 0, currentPhase: PhaseUnknown, httpClient: &http.Client{ Timeout: APITimeout, }, } return pm } func SetGlobalManager(pm *ProcessManager) { globalMu.Lock() defer globalMu.Unlock() globalManager = pm } func GetGlobalManager() *ProcessManager { globalMu.Lock() defer globalMu.Unlock() return globalManager } func (pm *ProcessManager) AdminPort() int { return pm.adminPort } func (pm *ProcessManager) CurrentPhase() ProcessPhase { pm.statusMu.RLock() defer pm.statusMu.RUnlock() return pm.currentPhase } func (pm *ProcessManager) CurrentPID() int { pm.statusMu.RLock() defer pm.statusMu.RUnlock() return pm.readPIDFile() } func (pm *ProcessManager) setPhase(phase ProcessPhase) { pm.statusMu.Lock() defer pm.statusMu.Unlock() if pm.currentPhase != phase { log.Printf("[STATUS] %s → %s", pm.currentPhase, phase) pm.currentPhase = phase } } // ================================================================ // 配置读取 // ================================================================ func (pm *ProcessManager) LoadConfig() error { content, err := os.ReadFile(pm.configPath) if err != nil { return fmt.Errorf("读取配置文件失败: %w", err) } log.Printf("[DEBUG] LoadConfig 读取到文件,长度: %d 字节", len(content)) preview := string(content) if len(preview) > 600 { preview = preview[:600] + "\n... (截断)" } log.Printf("[DEBUG] 文件内容预览:\n%s", preview) if port := extractIntValue(string(content), "admin_port"); port > 0 { pm.adminPort = port log.Printf("[DEBUG] ✅ 从 admin_port 解析到端口: %d", port) return nil } log.Printf("[DEBUG] admin_port 未找到,尝试解析 webServer") if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 { pm.adminPort = port log.Printf("[DEBUG] ✅ 从 webServer.port 解析到端口: %d", port) return nil } log.Printf("[DEBUG] webServer.port 未找到") if port := extractPortFromAddrSection(string(content), "webServer", "addr"); port > 0 { pm.adminPort = port log.Printf("[DEBUG] ✅ 从 webServer.addr 解析到端口: %d", port) return nil } log.Printf("[DEBUG] webServer.addr 未找到或解析失败") return fmt.Errorf("未找到 admin_port 或 webServer.port/addr 配置") } func extractIntValue(content, key string) int { lines := strings.Split(content, "\n") for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, key) { parts := strings.SplitN(trimmed, "=", 2) if len(parts) == 2 { val := strings.TrimSpace(parts[1]) val = strings.Trim(val, `"`) if port, err := strconv.Atoi(val); err == nil && port > 0 { return port } } } } return 0 } func extractIntValueFromSection(content, section, key string) int { lines := strings.Split(content, "\n") inSection := false for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") { sectionName := strings.TrimSpace(strings.Trim(trimmed, "[]")) inSection = strings.EqualFold(sectionName, section) continue } if inSection && strings.HasPrefix(trimmed, key) { parts := strings.SplitN(trimmed, "=", 2) if len(parts) == 2 { val := strings.TrimSpace(parts[1]) val = strings.Trim(val, `"`) if port, err := strconv.Atoi(val); err == nil && port > 0 { return port } } } } return 0 } func extractPortFromAddrSection(content, section, key string) int { lines := strings.Split(content, "\n") inSection := false for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") { sectionName := strings.TrimSpace(strings.Trim(trimmed, "[]")) inSection = strings.EqualFold(sectionName, section) continue } if inSection && strings.HasPrefix(trimmed, key) { parts := strings.SplitN(trimmed, "=", 2) if len(parts) == 2 { val := strings.TrimSpace(parts[1]) val = strings.Trim(val, `"`) if idx := strings.LastIndex(val, ":"); idx != -1 { portStr := val[idx+1:] if port, err := strconv.Atoi(portStr); err == nil && port > 0 { return port } } } } } return 0 } // ================================================================ // 端口检测(含归属信息) // ================================================================ func (pm *ProcessManager) CheckPort() PortCheckResult { if pm.adminPort <= 0 { return PortCheckResult{Ready: false, Err: fmt.Errorf("admin_port 未配置")} } conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout) if err != nil { return PortCheckResult{Ready: false, Err: err} } conn.Close() // 获取端口占用者信息 pid, process := pm.getPortOwner(pm.adminPort) // 即使 pid=0,也返回 Ready=true,让调用方决定如何处理 return PortCheckResult{ Ready: true, PID: pid, Process: process, } } func (pm *ProcessManager) getPortOwner(port int) (int, string) { // 方法1: ss if pid, name := pm.getPortOwnerBySS(port); pid > 0 { return pid, name } // 方法2: netstat if pid, name := pm.getPortOwnerByNetstat(port); pid > 0 { return pid, name } return 0, "" } func (pm *ProcessManager) getPortOwnerBySS(port int) (int, string) { // ss -lntp | grep ':7400 ' | grep -oP 'pid=\K[0-9]+' | head -1 cmd := exec.Command("sh", "-c", fmt.Sprintf("ss -lntp | grep ':%d ' | grep -oP 'pid=\\K[0-9]+' | head -1", port)) out, err := cmd.Output() if err != nil { return 0, "" } pidStr := strings.TrimSpace(string(out)) if pidStr == "" { return 0, "" } pid, err := strconv.Atoi(pidStr) if err != nil || pid <= 0 { return 0, "" } return pid, "" } func (pm *ProcessManager) getPortOwnerByNetstat(port int) (int, string) { // netstat -tlnp | grep ':7400 ' | awk '{print $7}' | cut -d'/' -f1 | head -1 cmd := exec.Command("sh", "-c", fmt.Sprintf("netstat -tlnp 2>/dev/null | grep ':%d ' | awk '{print $7}' | cut -d'/' -f1 | head -1", port)) out, err := cmd.Output() if err != nil { return 0, "" } pidStr := strings.TrimSpace(string(out)) if pidStr == "" { return 0, "" } pid, err := strconv.Atoi(pidStr) if err != nil || pid <= 0 { return 0, "" } return pid, "" } // ================================================================ // 实例检测 // ================================================================ func (pm *ProcessManager) DetectFrpcInstances() []FrpcInstance { var instances []FrpcInstance if runtime.GOOS == "linux" { cmd := exec.Command("pgrep", "-f", "frpc") out, err := cmd.Output() if err != nil { return instances } pids := strings.Split(strings.TrimSpace(string(out)), "\n") for _, pidStr := range pids { pidStr = strings.TrimSpace(pidStr) if pidStr == "" { continue } pid, err := strconv.Atoi(pidStr) if err != nil || pid <= 0 { continue } if pid == 1 || pid == os.Getpid() { continue } if !pm.isProcessAlive(pid) { continue } cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) cmdLineStr := strings.ReplaceAll(string(cmdline), "\x00", " ") exePath, _ := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid)) stat, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) var parentPID int if len(stat) > 0 { parts := strings.Fields(string(stat)) if len(parts) > 3 { parentPID, _ = strconv.Atoi(parts[3]) } } inst := FrpcInstance{ PID: pid, ParentPID: parentPID, ExecPath: exePath, CmdLine: cmdLineStr, Owned: false, } ownedPid := pm.readPIDFile() if ownedPid == pid { inst.Owned = true } instances = append(instances, inst) } } return instances } func (pm *ProcessManager) filterOwned(instances []FrpcInstance) []FrpcInstance { var result []FrpcInstance for _, inst := range instances { if inst.Owned { result = append(result, inst) } } return result } func (pm *ProcessManager) filterUnknown(instances []FrpcInstance) []FrpcInstance { var result []FrpcInstance for _, inst := range instances { if !inst.Owned { result = append(result, inst) } } return result } func (pm *ProcessManager) DetectConflict() ConflictInfo { instances := pm.DetectFrpcInstances() owned := pm.filterOwned(instances) unknown := pm.filterUnknown(instances) return ConflictInfo{ HasConflict: len(unknown) > 0 || len(owned) > 1, Count: len(instances), Owned: owned, Unknown: unknown, } } // ================================================================ // 进程存活检测 // ================================================================ func (pm *ProcessManager) isProcessAlive(pid int) bool { if pid <= 0 { 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 } // ================================================================ // FRPReady 检测(绑定 PID) // ================================================================ func (pm *ProcessManager) isFRPReady(pid int) bool { if pid <= 0 || pm.adminPort <= 0 { log.Printf("[DEBUG] FRPReady(pid=%d): pid 无效或 admin_port 未配置", pid) return false } // stdout 快速通道 if strings.Contains(pm.getLastOutput(), "start proxy success") || strings.Contains(pm.getLastOutput(), "login to server success") { log.Printf("[DEBUG] FRPReady(pid=%d): 检测到 stdout 关键字", pid) return true } // 端口归属检测 portResult := pm.CheckPort() if !portResult.Ready { log.Printf("[DEBUG] FRPReady(pid=%d): 端口 %d 未就绪", pid, pm.adminPort) return false } // 关键修复:端口被占用但无法识别归属 → 保守返回 false if portResult.PID == 0 { log.Printf("[DEBUG] FRPReady(pid=%d): 端口 %d 被占用但无法识别归属进程,保守返回 false", pid, pm.adminPort) return false } if portResult.PID != pid { log.Printf("[DEBUG] FRPReady(pid=%d): 端口 %d 被进程 %d 占用,与期望 PID %d 不一致", pid, pm.adminPort, portResult.PID, pid) return false } // admin API 检测 url := fmt.Sprintf("http://127.0.0.1:%d/api/status", pm.adminPort) ctx, cancel := context.WithTimeout(context.Background(), APITimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { log.Printf("[DEBUG] FRPReady(pid=%d): 创建请求失败: %v", pid, err) return false } resp, err := pm.httpClient.Do(req) if err != nil { log.Printf("[DEBUG] FRPReady(pid=%d): admin API 请求失败: %v", pid, err) return false } defer resp.Body.Close() if resp.StatusCode != 200 { log.Printf("[DEBUG] FRPReady(pid=%d): admin API 返回状态码 %d", pid, resp.StatusCode) return false } body, err := io.ReadAll(resp.Body) if err != nil { log.Printf("[DEBUG] FRPReady(pid=%d): 读取响应失败: %v", pid, err) return false } var status FRPCStatus if err := json.Unmarshal(body, &status); err != nil { log.Printf("[DEBUG] FRPReady(pid=%d): 解析 JSON 失败: %v", pid, err) return false } // 检查代理状态 proxyLists := [][]struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Err string `json:"err"` LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` }{ status.TCP, status.UDP, status.HTTP, status.HTTPS, status.STCP, status.XTCP, status.SUDP, } for _, proxies := range proxyLists { for _, p := range proxies { if p.Status == "running" { log.Printf("[DEBUG] FRPReady(pid=%d): 代理 %s 状态为 running", pid, p.Name) return true } } } log.Printf("[DEBUG] FRPReady(pid=%d): 没有代理处于 running 状态", pid) return false } // ================================================================ // 进程清理 // ================================================================ func (pm *ProcessManager) killProcess(pid int) error { if pid <= 0 { return nil } if runtime.GOOS == "windows" { cmd := exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)) return cmd.Run() } process, err := os.FindProcess(pid) if err != nil { return err } return process.Kill() } func (pm *ProcessManager) cleanupOrphans() { result := pm.CheckPort() if !result.Ready { return } pid := pm.readPIDFile() if pid > 0 && pm.isProcessAlive(pid) { return } log.Printf("[WARN] 检测到孤儿 frpc 进程 (端口 %d 被占用但无有效 PID),正在清理...", pm.adminPort) if runtime.GOOS == "windows" { exec.Command("taskkill", "/F", "/IM", "frpc.exe").Run() } else { exec.Command("pkill", "-f", "frpc").Run() } pm.deletePIDFile() time.Sleep(1 * time.Second) } // ================================================================ // PID 文件操作 // ================================================================ func (pm *ProcessManager) pidFilePath() string { return filepath.Join(pm.dataDir, "frpc.pid") } func (pm *ProcessManager) readPIDFile() int { data, err := os.ReadFile(pm.pidFilePath()) if err != nil { return 0 } pid, err := strconv.Atoi(strings.TrimSpace(string(data))) if err != nil || pid <= 0 { return 0 } return pid } func (pm *ProcessManager) writePIDFile(pid int) error { return os.WriteFile(pm.pidFilePath(), []byte(strconv.Itoa(pid)), 0644) } func (pm *ProcessManager) deletePIDFile() error { err := os.Remove(pm.pidFilePath()) if os.IsNotExist(err) { return nil } return err } // ================================================================ // stdout/stderr 捕获 // ================================================================ func (pm *ProcessManager) captureOutput(reader io.ReadCloser, isError bool) { defer reader.Close() scanner := bufio.NewScanner(reader) buf := make([]byte, 64*1024) scanner.Buffer(buf, 256*1024) var lines []string const maxLines = 20 for scanner.Scan() { line := scanner.Text() pm.outputMu.Lock() if isError { pm.lastError = line } else { pm.lastOutput = line } pm.outputMu.Unlock() if isError { lines = append(lines, line) if len(lines) > maxLines { lines = lines[1:] } pm.outputMu.Lock() pm.lastError = strings.Join(lines, "\n") pm.outputMu.Unlock() } else { lines = append(lines, line) if len(lines) > maxLines { lines = lines[1:] } pm.outputMu.Lock() pm.lastOutput = strings.Join(lines, "\n") pm.outputMu.Unlock() } } } func (pm *ProcessManager) getLastOutput() string { pm.outputMu.Lock() defer pm.outputMu.Unlock() return pm.lastOutput } func (pm *ProcessManager) getLastError() string { pm.outputMu.Lock() defer pm.outputMu.Unlock() return pm.lastError } // ================================================================ // 状态计算 // ================================================================ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessState { alive := pm.isProcessAlive(pid) portReady := result.Ready portPID := result.PID portOwner := result.Process portErr := result.Err pm.exitMu.Lock() exitCode := pm.exitCode pm.exitMu.Unlock() state := ProcessState{ PID: pid, Port: pm.adminPort, Alive: alive, PortReady: portReady, PortPID: portPID, PortOwner: portOwner, PortError: "", FRPReady: false, StartedAt: pm.startTime, ExpectedStop: pm.expectedStop, LastOutput: pm.getLastOutput(), LastError: pm.getLastError(), ExitCode: exitCode, } if portErr != nil { state.PortError = portErr.Error() } // ---- 状态判定逻辑 ---- // 1. 主动停止中 if pm.expectedStop && alive { state.Phase = PhaseStopping return state } // 2. 进程不存在 if !alive { if pm.expectedStop { state.Phase = PhaseStopped return state } if exitCode != 0 && exitCode != -1 { state.Error = fmt.Sprintf("进程异常退出 (exit code: %d)", exitCode) if state.LastError != "" { state.Error += ": " + state.LastError } } else { state.Error = "进程意外退出" if state.LastError != "" { state.Error += ": " + state.LastError } } state.Phase = PhaseFailed return state } // 3. 启动超时 if time.Since(pm.startTime) > StartupTimeout && pm.currentPhase != PhaseReloading { state.Error = fmt.Sprintf("启动超时 (超过 %v)", StartupTimeout) if portErr != nil { state.Error += ": " + portErr.Error() } state.Phase = PhaseFailed return state } // 4. 端口检测结果 if !portReady { if portErr != nil && strings.Contains(portErr.Error(), "permission denied") { state.Phase = PhaseDegraded state.Error = "端口检测权限不足: " + portErr.Error() return state } state.Phase = PhaseStarting if portErr != nil { state.Error = "等待端口就绪: " + portErr.Error() } return state } // 5. 端口就绪,检查归属 if portPID > 0 && pid > 0 && portPID != pid { // 端口被其他进程占用 → CONFLICT state.Phase = PhaseConflict state.Error = fmt.Sprintf("端口 %d 被进程 %d (%s) 占用,与 PID 文件 %d 不一致", pm.adminPort, portPID, portOwner, pid) state.FRPReady = false return state } // 6. 端口被自己的进程占用,检查 FRPReady state.FRPReady = pm.isFRPReady(pid) if state.FRPReady { state.Phase = PhaseRunning return state } // 7. 端口就绪但 FRP 未就绪 state.Phase = PhaseDegraded state.Error = "端口已监听,但 admin API 未就绪或代理未运行" return state } // ================================================================ // 状态查询 // ================================================================ func (pm *ProcessManager) Status() (*ProcessState, error) { pid := pm.readPIDFile() result := pm.CheckPort() state := pm.computeState(pid, result) pm.setPhase(state.Phase) return &state, nil } // ================================================================ // Reload 操作(P0 核心) // ================================================================ // ReloadConfig 执行 frpc 配置热加载,带状态管理 func (pm *ProcessManager) ReloadConfig(ctx context.Context) error { if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) } defer pm.Unlock() // 获取当前状态 pid := pm.readPIDFile() if pid <= 0 || !pm.isProcessAlive(pid) { // 进程不存在,直接启动 log.Printf("[INFO] Reload: frpc 未运行,执行启动") return pm.startLocked(ctx) } // 记录 reload 前的 PID 和 run_id oldPID := pid log.Printf("[INFO] Reload: 开始热加载 (当前 PID: %d)", oldPID) // 进入 RELOADING 状态 pm.setPhase(PhaseReloading) // 执行 frpc reload 命令 frpcPath, err := pm.getFrpcPath() if err != nil { pm.setPhase(PhaseDegraded) return fmt.Errorf("获取 frpc 路径失败: %w", err) } cmd := exec.Command(frpcPath, "reload", "-c", pm.configPath) output, err := cmd.CombinedOutput() // 检查 reload 执行结果 if err != nil { // reload 命令失败,检查进程是否还在 if !pm.isProcessAlive(oldPID) { // 进程已退出,reload 失败且进程丢失 log.Printf("[WARN] Reload: frpc 进程在 reload 期间退出 (PID: %d)", oldPID) pm.setPhase(PhaseFailed) pm.deletePIDFile() return fmt.Errorf("reload 失败,frpc 进程已退出: %w", err) } // 进程还在,但 reload 命令失败,可能是配置问题 log.Printf("[WARN] Reload: 命令失败但进程仍在运行 (PID: %d), 输出: %s", oldPID, string(output)) pm.setPhase(PhaseDegraded) return fmt.Errorf("reload 命令执行失败: %w", err) } log.Printf("[INFO] Reload: 命令执行成功,输出: %s", string(output)) // 等待新进程就绪 time.Sleep(1 * time.Second) // 获取新进程的 PID newPID := pm.readPIDFile() if newPID <= 0 || newPID == oldPID { // PID 没变化,可能是 reload 没有触发进程切换 log.Printf("[INFO] Reload: PID 未变化 (PID: %d),验证服务状态...", oldPID) if pm.isFRPReady(oldPID) { pm.setPhase(PhaseRunning) log.Printf("[INFO] Reload: 服务仍健康,保持运行 (PID: %d)", oldPID) return nil } pm.setPhase(PhaseDegraded) return fmt.Errorf("reload 后服务未就绪 (PID: %d)", oldPID) } // PID 已变化,验证新进程 log.Printf("[INFO] Reload: PID 从 %d 变为 %d", oldPID, newPID) // 等待新进程的 FRPReady for attempt := 0; attempt < 20; attempt++ { if pm.isFRPReady(newPID) { pm.setPhase(PhaseRunning) log.Printf("[INFO] Reload: 成功切换到新进程 (PID: %d)", newPID) return nil } time.Sleep(200 * time.Millisecond) } // 新进程未就绪,回退状态 pm.setPhase(PhaseDegraded) return fmt.Errorf("reload 后新进程未就绪 (PID: %d)", newPID) } // getFrpcPath 获取 frpc 二进制路径 func (pm *ProcessManager) getFrpcPath() (string, error) { // 如果 frpcBinPath 有效,直接返回 if pm.frpcBinPath != "" { if _, err := os.Stat(pm.frpcBinPath); err == nil { return pm.frpcBinPath, nil } } // 否则使用 frp 模块的 GetFrpcPath // 避免循环引用,从外部传入 return pm.frpcBinPath, nil } // ================================================================ // 操作执行 // ================================================================ // Lock 由平台文件 (lock_*.go) 实现 func (pm *ProcessManager) Start(ctx context.Context) error { if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) } defer pm.Unlock() pm.expectedStop = false conflict := pm.DetectConflict() if conflict.HasConflict { log.Printf("[WARN] 检测到 %d 个 frpc 实例 (Owned: %d, Unknown: %d),进入 CONFLICT 状态", conflict.Count, len(conflict.Owned), len(conflict.Unknown)) for _, inst := range conflict.Unknown { log.Printf("[INFO] 清理 Unknown frpc 实例 (PID: %d)", inst.PID) pm.killProcess(inst.PID) } if len(conflict.Owned) == 1 { pid := conflict.Owned[0].PID log.Printf("[INFO] 保留 Owned frpc 实例 (PID: %d),清理完成", pid) pm.writePIDFile(pid) pm.setPhase(PhaseRunning) return nil } if len(conflict.Owned) > 1 { log.Printf("[WARN] 多个 Owned 实例冲突,全部清理") for _, inst := range conflict.Owned { pm.killProcess(inst.PID) } } pm.deletePIDFile() } pm.cleanupOrphans() return pm.startLocked(ctx) } func (pm *ProcessManager) startLocked(ctx context.Context) error { result := pm.CheckPort() if result.Ready { pid := pm.readPIDFile() if pid > 0 && pm.isProcessAlive(pid) { if result.PID == pid { log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid) pm.setPhase(PhaseRunning) return nil } log.Printf("[WARN] 端口被进程 %d 占用,但 PID 文件指向 %d,可能存在冲突", result.PID, pid) } pm.cleanupOrphans() } pm.startTime = time.Now() pm.expectedStop = false pm.runID = "" pm.exitMu.Lock() pm.exitCode = -1 pm.exitMu.Unlock() cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath) setProcessAttributes(cmd) stdoutPipe, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("创建 stdout pipe 失败: %w", err) } stderrPipe, err := cmd.StderrPipe() if err != nil { return fmt.Errorf("创建 stderr pipe 失败: %w", err) } go pm.captureOutput(stdoutPipe, false) go pm.captureOutput(stderrPipe, true) if err := cmd.Start(); err != nil { return fmt.Errorf("启动 frpc 失败: %w", err) } if err := pm.writePIDFile(cmd.Process.Pid); err != nil { log.Printf("[WARN] 写入 PID 文件失败: %v", err) } log.Printf("[DEBUG] frpc 进程已启动,PID: %d", cmd.Process.Pid) pm.setPhase(PhaseStarting) go func() { err := cmd.Wait() var code int if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { code = exitErr.ExitCode() } else { code = -1 } } else { code = 0 } pm.exitMu.Lock() pm.exitCode = code pm.exitMu.Unlock() log.Printf("[INFO] frpc 进程 (PID: %d) 已退出,退出码: %d", cmd.Process.Pid, code) if !pm.expectedStop && code != 0 { log.Printf("[WARN] frpc 进程异常退出 (exit code: %d)", code) if pm.currentPhase != PhaseReloading { pm.setPhase(PhaseFailed) } } if !pm.expectedStop { pm.deletePIDFile() } }() for attempt := 0; attempt < StartupMaxAttempts; attempt++ { r := pm.CheckPort() if r.Ready { if r.PID > 0 && r.PID != cmd.Process.Pid { log.Printf("[WARN] 端口被进程 %d 占用,与当前进程 %d 不一致,可能存在冲突", r.PID, cmd.Process.Pid) time.Sleep(StartupRetryDelay) continue } if pm.isFRPReady(cmd.Process.Pid) { log.Printf("[INFO] frpc 启动成功 (PID: %d, 端口: %d),耗时 %dms", cmd.Process.Pid, pm.adminPort, attempt*int(StartupRetryDelay/time.Millisecond)) pm.setPhase(PhaseRunning) return nil } log.Printf("[DEBUG] 端口已就绪,等待 frpc 初始化...") } time.Sleep(StartupRetryDelay) } if pm.isProcessAlive(cmd.Process.Pid) { stderr := pm.getLastError() log.Printf("[WARN] frpc 启动超时 (PID: %d),当前 stderr: %s", cmd.Process.Pid, stderr) if pm.isFRPReady(cmd.Process.Pid) { log.Printf("[INFO] frpc 实际已就绪 (admin API 可访问),超时误判,修正状态") pm.setPhase(PhaseRunning) return nil } pm.killProcess(cmd.Process.Pid) pm.deletePIDFile() pm.setPhase(PhaseFailed) return fmt.Errorf("frpc 启动超时: 进程存在但端口未就绪") } pm.deletePIDFile() pm.setPhase(PhaseFailed) return fmt.Errorf("frpc 启动失败: 进程已退出") } func (pm *ProcessManager) Stop(ctx context.Context) error { if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) } defer pm.Unlock() pm.expectedStop = true pm.setPhase(PhaseStopping) return pm.stopLocked(ctx) } func (pm *ProcessManager) stopLocked(_ context.Context) error { pid := pm.readPIDFile() if pid <= 0 { if runtime.GOOS == "windows" { exec.Command("taskkill", "/F", "/IM", "frpc.exe").Run() } else { exec.Command("pkill", "-f", "frpc").Run() } pm.deletePIDFile() pm.setPhase(PhaseStopped) return nil } if !pm.isProcessAlive(pid) { pm.deletePIDFile() pm.setPhase(PhaseStopped) return nil } process, err := os.FindProcess(pid) if err != nil { pm.deletePIDFile() pm.setPhase(PhaseStopped) return nil } if err := process.Signal(syscall.SIGTERM); err != nil { pm.deletePIDFile() pm.setPhase(PhaseStopped) return nil } start := time.Now() for time.Since(start) < StopMaxWaitTime { if !pm.isProcessAlive(pid) { pm.deletePIDFile() pm.setPhase(PhaseStopped) return nil } time.Sleep(200 * time.Millisecond) } if runtime.GOOS == "windows" { exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run() } else { process.Kill() } time.Sleep(500 * time.Millisecond) if pm.isProcessAlive(pid) { pm.setPhase(PhaseFailed) return fmt.Errorf("强制停止失败: 进程仍存活 (PID: %d)", pid) } pm.deletePIDFile() pm.setPhase(PhaseStopped) return nil } func (pm *ProcessManager) Restart(ctx context.Context) error { if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) } defer pm.Unlock() if err := pm.stopLocked(ctx); err != nil { return fmt.Errorf("停止失败: %w", err) } time.Sleep(1 * time.Second) if err := pm.startLocked(ctx); err != nil { return fmt.Errorf("启动失败: %w", err) } return nil } // ================================================================ // 健康检查看门狗 // ================================================================ func (pm *ProcessManager) StartHealthMonitor(ctx context.Context) { ticker := time.NewTicker(HealthCheckInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: pm.runHealthCheck() } } } func (pm *ProcessManager) runHealthCheck() { // 如果正在 reloading,跳过健康检查 if pm.currentPhase == PhaseReloading { log.Printf("[DEBUG] 健康检查跳过: 正在 RELOADING") return } state, err := pm.Status() if err != nil { log.Printf("[WARN] 健康检查失败: %v", err) return } switch state.Phase { case PhaseConflict: log.Printf("[WARN] 检测到冲突,尝试自动恢复...") for _, inst := range state.Conflicts { if !inst.Owned { pm.killProcess(inst.PID) } } pm.setPhase(PhaseStarting) case PhaseDegraded: log.Printf("[WARN] frpc 处于降级状态,尝试恢复...") pm.Restart(context.Background()) case PhaseFailed: if !state.ExpectedStop { log.Printf("[WARN] frpc 已失败,自动重启...") pm.Start(context.Background()) } } } // ================================================================ // 健康检查(供 API 调用) // ================================================================ func (pm *ProcessManager) HealthCheck() map[string]interface{} { result := make(map[string]interface{}) result["admin_port"] = pm.adminPort result["phase"] = pm.CurrentPhase() state, err := pm.Status() if err != nil { result["error"] = err.Error() return result } result["pid"] = state.PID result["port"] = state.Port result["alive"] = state.Alive result["port_ready"] = state.PortReady result["port_pid"] = state.PortPID result["port_owner"] = state.PortOwner result["frp_ready"] = state.FRPReady if state.PortError != "" { result["port_error"] = state.PortError } if state.Error != "" { result["error"] = state.Error } if state.Version != "" { result["version"] = state.Version } if len(state.Conflicts) > 0 { result["conflicts"] = state.Conflicts } return result }