diff --git a/Dockerfile b/Dockerfile index 0a619e3..19c5d83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \ FROM alpine:latest # 先切换到国内镜像源,再安装包 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories -RUN apk --no-cache add ca-certificates tzdata sqlite bash +RUN apk --no-cache add ca-certificates tzdata sqlite bash netstat curl WORKDIR /app diff --git a/internal/process/manager.go b/internal/process/manager.go index 28f68c8..f170b00 100644 --- a/internal/process/manager.go +++ b/internal/process/manager.go @@ -1,13 +1,11 @@ // internal/process/manager.go // frpc-console 进程管理模块 -// 2.6-preview: 状态机驱动 + 冲突检测 + 生命周期管理 -// 2.7-preview: 软化版 - 精确控制 + CONFLICT/STOPPING 状态 +// 2.7-preview: 状态机驱动 + 端口归属检测 + admin API 健康检查 package process import ( "bufio" - "bytes" "context" "encoding/json" "fmt" @@ -66,19 +64,24 @@ const ( // 数据结构 // ================================================================ +// PortCheckResult 端口检测结果(含归属信息) type PortCheckResult struct { - Ready bool - Err error + Ready bool + Err error + PID int // 占用端口的进程 PID(0 表示无法获取) + Process string // 占用端口的进程名 } +// FrpcInstance 系统中检测到的 frpc 实例 type FrpcInstance struct { PID int ParentPID int ExecPath string CmdLine string - Owned bool + Owned bool // 是否由当前 ProcessManager 管理 } +// ProcessState 进程完整状态 type ProcessState struct { Phase ProcessPhase `json:"phase"` PID int `json:"pid"` @@ -88,6 +91,8 @@ type ProcessState struct { Alive bool `json:"alive"` PortReady bool `json:"port_ready"` + PortPID int `json:"port_pid"` // 实际占用端口的 PID + PortOwner string `json:"port_owner"` // 实际占用端口的进程名 PortError string `json:"port_error,omitempty"` FRPReady bool `json:"frp_ready"` Version string `json:"version,omitempty"` @@ -122,6 +127,14 @@ type FRPCStatus struct { } `json:"proxies"` } +// ConflictInfo 冲突检测结果 +type ConflictInfo struct { + HasConflict bool + Count int + Owned []FrpcInstance + Unknown []FrpcInstance +} + // ================================================================ // 全局变量 // ================================================================ @@ -156,6 +169,9 @@ type ProcessManager struct { currentPhase ProcessPhase statusMu sync.RWMutex + + // 用于 admin API 检测的 HTTP 客户端 + httpClient *http.Client } // ================================================================ @@ -169,6 +185,9 @@ func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager { frpcBinPath: frpcBinPath, adminPort: 0, currentPhase: PhaseUnknown, + httpClient: &http.Client{ + Timeout: APITimeout, + }, } return pm } @@ -316,9 +335,10 @@ func extractPortFromAddrSection(content, section, key string) int { } // ================================================================ -// 端口检测 +// 端口检测(含归属信息) // ================================================================ +// CheckPort 检测端口是否被占用,并返回占用者信息 func (pm *ProcessManager) CheckPort() PortCheckResult { if pm.adminPort <= 0 { return PortCheckResult{Ready: false, Err: fmt.Errorf("admin_port 未配置")} @@ -329,9 +349,48 @@ func (pm *ProcessManager) CheckPort() PortCheckResult { return PortCheckResult{Ready: false, Err: err} } conn.Close() - return PortCheckResult{Ready: true, Err: nil} + + // 端口已就绪,获取占用者信息 + pid, process := pm.getPortOwner(pm.adminPort) + return PortCheckResult{ + Ready: true, + PID: pid, + Process: process, + } } +// getPortOwner 获取占用端口的进程 PID 和进程名 +func (pm *ProcessManager) getPortOwner(port int) (int, string) { + if runtime.GOOS != "linux" { + return 0, "" + } + + // 使用 ss 获取占用端口的 PID + 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 { + log.Printf("[DEBUG] getPortOwner: ss 执行失败: %v", err) + return 0, "" + } + pidStr := strings.TrimSpace(string(out)) + if pidStr == "" { + return 0, "" + } + pid, err := strconv.Atoi(pidStr) + if err != nil || pid <= 0 { + return 0, "" + } + + // 获取进程名 + cmd2 := exec.Command("sh", "-c", fmt.Sprintf("ps -p %d -o comm= 2>/dev/null | head -1", pid)) + out2, err2 := cmd2.Output() + if err2 != nil { + return pid, "" + } + return pid, strings.TrimSpace(string(out2)) +} + +// GetPortStatus 兼容旧接口 func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) { status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false} result := pm.CheckPort() @@ -342,298 +401,71 @@ func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) { return status, nil } status.Occupied = true - - pid, err := pm.getPIDByPort(pm.adminPort) - if err != nil { - if pidFromFile := pm.readPIDFile(); pidFromFile > 0 { - if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) { - status.PID = pidFromFile - } - } - } else { - status.PID = pid + status.PID = result.PID + if result.Process != "" { + status.IsFRPC = strings.Contains(result.Process, "frpc") } - if status.PID == 0 { - return status, nil - } - isFRPC, cmd := pm.isFRPCProcess(status.PID) - status.IsFRPC = isFRPC - status.ProcessCmd = cmd return status, nil } -func (pm *ProcessManager) getPIDByPort(port int) (int, error) { - if pid, err := pm.getPIDBySS(port); err == nil && pid > 0 { - return pid, nil - } - if pid, err := pm.getPIDByNetstat(port); err == nil && pid > 0 { - return pid, nil - } - if pid, err := pm.getPIDByLsof(port); err == nil && pid > 0 { - return pid, nil - } - return 0, fmt.Errorf("无法通过端口反查 PID") -} - -func (pm *ProcessManager) getPIDBySS(port int) (int, error) { - cmd := exec.Command("ss", "-lpn", "state", "listening") - out, err := cmd.Output() - if err != nil { - return 0, err - } - scanner := bufio.NewScanner(bytes.NewReader(out)) - for scanner.Scan() { - line := scanner.Text() - if !strings.Contains(line, fmt.Sprintf(":%d", port)) { - continue - } - if idx := strings.Index(line, "pid="); idx != -1 { - end := strings.Index(line[idx:], ",") - if end == -1 { - end = strings.Index(line[idx:], ")") - } - if end == -1 { - continue - } - pidStr := line[idx+4 : idx+end] - pidStr = strings.TrimSpace(pidStr) - if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 { - return pid, nil - } - } - } - return 0, fmt.Errorf("未找到监听端口 %d 的进程", port) -} - -func (pm *ProcessManager) getPIDByNetstat(port int) (int, error) { - cmd := exec.Command("netstat", "-tulpn") - out, err := cmd.Output() - if err != nil { - return 0, err - } - scanner := bufio.NewScanner(bytes.NewReader(out)) - for scanner.Scan() { - line := scanner.Text() - if !strings.Contains(line, fmt.Sprintf(":%d", port)) { - continue - } - parts := strings.Fields(line) - if len(parts) < 7 { - continue - } - last := parts[len(parts)-1] - if idx := strings.Index(last, "/"); idx != -1 { - pidStr := last[:idx] - if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 { - return pid, nil - } - } - } - return 0, fmt.Errorf("未找到监听端口 %d 的进程", port) -} - -func (pm *ProcessManager) getPIDByLsof(port int) (int, error) { - cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN") - out, err := cmd.Output() - if err != nil { - return 0, err - } - scanner := bufio.NewScanner(bytes.NewReader(out)) - for scanner.Scan() { - line := scanner.Text() - if strings.Contains(line, "frpc") { - parts := strings.Fields(line) - if len(parts) >= 2 { - if pid, err := strconv.Atoi(parts[1]); err == nil && pid > 0 { - return pid, nil - } - } - } - } - return 0, fmt.Errorf("未找到监听端口 %d 的 frpc 进程", port) -} - -func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool { - cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-a", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN") - out, err := cmd.Output() - if err != nil { - return false - } - return strings.Contains(string(out), "LISTEN") -} - -func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) { - cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid) - if data, err := os.ReadFile(cmdlinePath); err == nil { - cmd := strings.ReplaceAll(string(data), "\x00", " ") - if strings.Contains(cmd, "frpc") { - return true, cmd - } - } - cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=") - out, err := cmd.Output() - if err == nil { - args := strings.TrimSpace(string(out)) - if strings.Contains(args, "frpc") { - return true, args - } - } - return false, "" -} - -// ================================================================ -// 进程存活检测 -// ================================================================ - -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 -} - // ================================================================ // 实例检测(精确扫描) // ================================================================ // DetectFrpcInstances 检测系统中所有 frpc 实例 -// 三重确认:PID存在 + 进程存活 + Executable/CmdLine匹配 func (pm *ProcessManager) DetectFrpcInstances() []FrpcInstance { var instances []FrpcInstance - // 获取当前 frpc 可执行文件的绝对路径 - targetExe := pm.frpcBinPath - if !filepath.IsAbs(targetExe) { - if abs, err := filepath.Abs(targetExe); err == nil { - targetExe = abs - } - } - - if runtime.GOOS == "windows" { - // Windows: 使用 tasklist + wmic 获取进程信息 - cmd := exec.Command("tasklist", "/FO", "CSV", "/FI", "IMAGENAME eq frpc.exe") + if runtime.GOOS == "linux" { + cmd := exec.Command("pgrep", "-f", "frpc") out, err := cmd.Output() if err != nil { - log.Printf("[WARN] 检测 frpc 实例失败: %v", err) return instances } - lines := strings.Split(string(out), "\n") - for _, line := range lines { - if !strings.Contains(line, "frpc.exe") { + pids := strings.Split(strings.TrimSpace(string(out)), "\n") + for _, pidStr := range pids { + pidStr = strings.TrimSpace(pidStr) + if pidStr == "" { continue } - parts := strings.Split(line, ",") - if len(parts) < 2 { - continue - } - pidStr := strings.Trim(parts[1], `"`) pid, err := strconv.Atoi(pidStr) if err != nil || pid <= 0 { continue } + if pid == 1 || pid == os.Getpid() { + continue + } if !pm.isProcessAlive(pid) { continue } - // 获取 cmdline(Windows 较难获取,先用进程名匹配) + 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, - ExecPath: "frpc.exe", - CmdLine: "frpc.exe", - Owned: false, + PID: pid, + ParentPID: parentPID, + ExecPath: exePath, + CmdLine: cmdLineStr, + Owned: false, } - // 检查是否 Owned ownedPid := pm.readPIDFile() if ownedPid == pid { inst.Owned = true } instances = append(instances, inst) } - return instances } - - // Linux: 使用 pgrep + /proc 精确匹配 - cmd := exec.Command("pgrep", "-f", "frpc") - out, err := cmd.Output() - if err != nil { - // 没有找到进程,pgrep 返回非0,属于正常情况 - 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 !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)) - - // 获取父进程 PID - stat, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) - var parentPID int - if len(stat) > 0 { - // stat 格式: pid (comm) state ppid ... - 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, - } - - // 三重确认 Owned - ownedPid := pm.readPIDFile() - if ownedPid == pid { - // 条件1: PID 匹配 - if pm.isProcessAlive(pid) { - // 条件2: 进程存在 - // 条件3: 执行路径匹配 - if exePath != "" && (strings.Contains(exePath, "frpc") || strings.Contains(cmdLineStr, "frpc")) { - inst.Owned = true - } - } - } - - instances = append(instances, inst) - } - return instances } -// filterOwned 返回 Owned 实例 func (pm *ProcessManager) filterOwned(instances []FrpcInstance) []FrpcInstance { var result []FrpcInstance for _, inst := range instances { @@ -644,7 +476,6 @@ func (pm *ProcessManager) filterOwned(instances []FrpcInstance) []FrpcInstance { return result } -// filterUnknown 返回 Unknown 实例 func (pm *ProcessManager) filterUnknown(instances []FrpcInstance) []FrpcInstance { var result []FrpcInstance for _, inst := range instances { @@ -659,13 +490,6 @@ func (pm *ProcessManager) filterUnknown(instances []FrpcInstance) []FrpcInstance // 冲突检测 // ================================================================ -type ConflictInfo struct { - HasConflict bool - Count int - Owned []FrpcInstance - Unknown []FrpcInstance -} - func (pm *ProcessManager) DetectConflict() ConflictInfo { instances := pm.DetectFrpcInstances() owned := pm.filterOwned(instances) @@ -680,29 +504,85 @@ func (pm *ProcessManager) DetectConflict() ConflictInfo { } // ================================================================ -// 实例健康检查 +// 进程存活检测 // ================================================================ -func (pm *ProcessManager) isInstanceHealthy(inst FrpcInstance) bool { - // 1. 进程必须存在 - if !pm.isProcessAlive(inst.PID) { +func (pm *ProcessManager) isProcessAlive(pid int) bool { + if pid <= 0 { return false } - // 2. 端口必须可连接 - result := pm.CheckPort() - if !result.Ready { + 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 } - // 3. FRP 就绪(通过 stdout 或 admin API) - stdout := pm.getLastOutput() - if pm.isFRPReady(stdout) { + return process.Signal(syscall.Signal(0)) == nil +} + +// ================================================================ +// FRPReady 检测(使用 admin API) +// ================================================================ + +// isFRPReady 检测 frpc 是否已完全就绪(通过 admin API) +func (pm *ProcessManager) isFRPReady() bool { + if pm.adminPort <= 0 { + log.Printf("[DEBUG] FRPReady=false: admin_port 未配置") + return false + } + + // 方式1: stdout 检测(快速通道) + if strings.Contains(pm.getLastOutput(), "start proxy success") || + strings.Contains(pm.getLastOutput(), "login to server success") { + log.Printf("[DEBUG] FRPReady=true: 检测到 stdout 关键字") return true } - // 4. 尝试 admin API - if info := pm.getFRPCStatus(inst.PID); info != nil && info.Version != "" { - return true + + // 方式2: 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=false: 创建请求失败: %v", err) + return false } - return false + resp, err := pm.httpClient.Do(req) + if err != nil { + log.Printf("[DEBUG] FRPReady=false: admin API 请求失败: %v", err) + return false + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Printf("[DEBUG] FRPReady=false: admin API 返回状态码 %d", resp.StatusCode) + return false + } + + // 解析 JSON 确认返回有效 + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("[DEBUG] FRPReady=false: 读取响应失败: %v", err) + return false + } + var status FRPCStatus + if err := json.Unmarshal(body, &status); err != nil { + log.Printf("[DEBUG] FRPReady=false: 解析 JSON 失败: %v", err) + return false + } + if status.Version == "" { + log.Printf("[DEBUG] FRPReady=false: admin API 返回空版本") + return false + } + log.Printf("[DEBUG] FRPReady=true: admin API 可访问,版本: %s", status.Version) + return true } // ================================================================ @@ -713,12 +593,10 @@ 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 @@ -731,12 +609,10 @@ func (pm *ProcessManager) cleanupOrphans() { 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() @@ -834,18 +710,6 @@ func (pm *ProcessManager) getLastError() string { return pm.lastError } -// ================================================================ -// FRPReady 检测 -// ================================================================ - -func (pm *ProcessManager) isFRPReady(stdout string) bool { - if strings.Contains(stdout, "start proxy success") || - strings.Contains(stdout, "login to server success") { - return true - } - return false -} - // ================================================================ // 状态计算 // ================================================================ @@ -853,12 +717,10 @@ func (pm *ProcessManager) isFRPReady(stdout string) bool { 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 - stdout := pm.getLastOutput() - stderr := pm.getLastError() - frpReady := pm.isFRPReady(stdout) - pm.exitMu.Lock() exitCode := pm.exitCode pm.exitMu.Unlock() @@ -868,12 +730,14 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS Port: pm.adminPort, Alive: alive, PortReady: portReady, + PortPID: portPID, + PortOwner: portOwner, PortError: "", - FRPReady: frpReady, + FRPReady: false, StartedAt: pm.startTime, ExpectedStop: pm.expectedStop, - LastOutput: stdout, - LastError: stderr, + LastOutput: pm.getLastOutput(), + LastError: pm.getLastError(), ExitCode: exitCode, } @@ -881,11 +745,9 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS state.PortError = portErr.Error() } - // ============================================================ - // 状态判定逻辑 - // ============================================================ + // ---- 状态判定逻辑 ---- - // 1. 如果是主动停止,直接返回 STOPPING + // 1. 主动停止中 if pm.expectedStop && alive { state.Phase = PhaseStopping return state @@ -897,23 +759,22 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS state.Phase = PhaseStopped return state } - if exitCode != 0 && exitCode != -1 { state.Error = fmt.Sprintf("进程异常退出 (exit code: %d)", exitCode) - if stderr != "" { - state.Error += ": " + stderr + if state.LastError != "" { + state.Error += ": " + state.LastError } } else { state.Error = "进程意外退出" - if stderr != "" { - state.Error += ": " + stderr + if state.LastError != "" { + state.Error += ": " + state.LastError } } state.Phase = PhaseFailed return state } - // 3. 进程存在,检查超时 + // 3. 启动超时 if time.Since(pm.startTime) > StartupTimeout { state.Error = fmt.Sprintf("启动超时 (超过 %v)", StartupTimeout) if portErr != nil { @@ -923,34 +784,7 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS return state } - // 4. 冲突检测 - conflict := pm.DetectConflict() - if conflict.HasConflict { - state.Phase = PhaseConflict - state.Conflicts = append(conflict.Owned, conflict.Unknown...) - if len(conflict.Unknown) > 0 { - state.Error = fmt.Sprintf("检测到 %d 个未知 frpc 实例", len(conflict.Unknown)) - } - if len(conflict.Owned) > 1 { - state.Error = fmt.Sprintf("检测到 %d 个 Owned frpc 实例(异常)", len(conflict.Owned)) - } - return state - } - - // 5. 正常运行状态 - if portReady && frpReady { - state.Phase = PhaseRunning - return state - } - - // 6. 端口就绪但 FRP 未就绪 - if portReady && !frpReady { - state.Phase = PhaseDegraded - state.Error = "端口已监听,但 frpc 未报告就绪" - return state - } - - // 7. 端口未就绪 + // 4. 端口检测结果 if !portReady { if portErr != nil && strings.Contains(portErr.Error(), "permission denied") { state.Phase = PhaseDegraded @@ -964,7 +798,28 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS return state } - state.Phase = PhaseUnknown + // 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 + frpReady := pm.isFRPReady() + state.FRPReady = frpReady + + if frpReady { + state.Phase = PhaseRunning + return state + } + + // 7. 端口就绪但 FRP 未就绪 + state.Phase = PhaseDegraded + state.Error = "端口已监听,但 admin API 未就绪" return state } @@ -980,41 +835,6 @@ func (pm *ProcessManager) Status() (*ProcessState, error) { return &state, nil } -// ================================================================ -// API 回源 (2.7-preview 预留) -// ================================================================ - -func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus { - if pid <= 0 || pm.adminPort <= 0 { - return nil - } - 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 { - return nil - } - client := &http.Client{Timeout: APITimeout} - resp, err := client.Do(req) - if err != nil { - return nil - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - return nil - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil - } - var status FRPCStatus - if err := json.Unmarshal(body, &status); err != nil { - return nil - } - return &status -} - // ================================================================ // 操作执行 // ================================================================ @@ -1035,13 +855,11 @@ func (pm *ProcessManager) Start(ctx context.Context) error { log.Printf("[WARN] 检测到 %d 个 frpc 实例 (Owned: %d, Unknown: %d),进入 CONFLICT 状态", conflict.Count, len(conflict.Owned), len(conflict.Unknown)) - // 清理 Unknown 实例 for _, inst := range conflict.Unknown { log.Printf("[INFO] 清理 Unknown frpc 实例 (PID: %d)", inst.PID) pm.killProcess(inst.PID) } - // 如果有且仅有一个 Owned 实例,保留它 if len(conflict.Owned) == 1 { pid := conflict.Owned[0].PID log.Printf("[INFO] 保留 Owned frpc 实例 (PID: %d),清理完成", pid) @@ -1050,7 +868,6 @@ func (pm *ProcessManager) Start(ctx context.Context) error { return nil } - // 多个 Owned 或没有 Owned:全部清理,重新启动 if len(conflict.Owned) > 1 { log.Printf("[WARN] 多个 Owned 实例冲突,全部清理") for _, inst := range conflict.Owned { @@ -1068,13 +885,18 @@ func (pm *ProcessManager) Start(ctx context.Context) error { } func (pm *ProcessManager) startLocked(ctx context.Context) error { + // 检查端口状态 result := pm.CheckPort() if result.Ready { pid := pm.readPIDFile() if pid > 0 && pm.isProcessAlive(pid) { - log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid) - pm.setPhase(PhaseRunning) - return nil + // 端口就绪且有有效 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() } @@ -1142,8 +964,13 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error { for attempt := 0; attempt < StartupMaxAttempts; attempt++ { r := pm.CheckPort() if r.Ready { - stdout := pm.getLastOutput() - if pm.isFRPReady(stdout) { + 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() { log.Printf("[INFO] frpc 启动成功 (PID: %d, 端口: %d),耗时 %dms", cmd.Process.Pid, pm.adminPort, attempt*int(StartupRetryDelay/time.Millisecond)) pm.setPhase(PhaseRunning) @@ -1158,9 +985,9 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error { if pm.isProcessAlive(cmd.Process.Pid) { stderr := pm.getLastError() log.Printf("[WARN] frpc 启动超时 (PID: %d),当前 stderr: %s", cmd.Process.Pid, stderr) - if info := pm.getFRPCStatus(cmd.Process.Pid); info != nil && info.Version != "" { - log.Printf("[INFO] frpc admin API 可访问,版本: %s", info.Version) - pm.writePIDFile(cmd.Process.Pid) + // 最后一次尝试 admin API + if pm.isFRPReady() { + log.Printf("[INFO] frpc 实际已就绪 (admin API 可访问),超时误判,修正状态") pm.setPhase(PhaseRunning) return nil } @@ -1205,7 +1032,6 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error { return nil } - // 发送 SIGTERM process, err := os.FindProcess(pid) if err != nil { pm.deletePIDFile() @@ -1218,7 +1044,6 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error { return nil } - // 等待进程退出 start := time.Now() for time.Since(start) < StopMaxWaitTime { if !pm.isProcessAlive(pid) { @@ -1229,7 +1054,6 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error { time.Sleep(200 * time.Millisecond) } - // 强制 kill if runtime.GOOS == "windows" { exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run() } else { @@ -1264,7 +1088,7 @@ func (pm *ProcessManager) Restart(ctx context.Context) error { } // ================================================================ -// 健康检查看门狗(由 main.go 调用) +// 健康检查看门狗 // ================================================================ func (pm *ProcessManager) StartHealthMonitor(ctx context.Context) { @@ -1288,11 +1112,9 @@ func (pm *ProcessManager) runHealthCheck() { return } - // 根据状态执行自动恢复 switch state.Phase { case PhaseConflict: log.Printf("[WARN] 检测到冲突,尝试自动恢复...") - // 冲突恢复:清理未知实例 for _, inst := range state.Conflicts { if !inst.Owned { pm.killProcess(inst.PID) @@ -1301,7 +1123,6 @@ func (pm *ProcessManager) runHealthCheck() { pm.setPhase(PhaseStarting) case PhaseDegraded: log.Printf("[WARN] frpc 处于降级状态,尝试恢复...") - // 降级恢复:重启 pm.Restart(context.Background()) case PhaseFailed: if !state.ExpectedStop { @@ -1312,7 +1133,7 @@ func (pm *ProcessManager) runHealthCheck() { } // ================================================================ -// 健康检查 +// 健康检查(供 API 调用) // ================================================================ func (pm *ProcessManager) HealthCheck() map[string]interface{} { @@ -1330,6 +1151,8 @@ func (pm *ProcessManager) HealthCheck() map[string]interface{} { 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