From aebc4f4e5849df40bf9bcaee69ecb4328fe532d1 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Tue, 11 Aug 2026 21:22:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E6=9D=A5=E6=9B=B4=E6=96=B0=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E8=BF=9B=E7=A8=8B=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=BA=86=EF=BC=9AReload=20=E7=AB=9E=E6=80=81=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/frp/legacy.go | 94 ++++---------- internal/process/manager.go | 251 +++++++++++++++++++++++++----------- 2 files changed, 196 insertions(+), 149 deletions(-) diff --git a/internal/frp/legacy.go b/internal/frp/legacy.go index 9ea1c33..8415db8 100644 --- a/internal/frp/legacy.go +++ b/internal/frp/legacy.go @@ -1,8 +1,10 @@ +// internal/frp/legacy.go +// Reload 函数适配 P0 改动 + package frp import ( "context" - "fmt" "log" "os" "os/exec" @@ -16,10 +18,8 @@ import ( // ================================================================ // 兼容层:保持对外接口不变 -// 这些函数供 api 调用,实际委托给 process.Manager // ================================================================ -// IsRunning 检查 frpc 是否在运行 func IsRunning() bool { pm := process.GetGlobalManager() if pm != nil { @@ -33,7 +33,6 @@ func IsRunning() bool { return isRunningLegacy() } -// Start 启动 frpc (幂等) func Start() error { pm := process.GetGlobalManager() if pm != nil { @@ -44,7 +43,6 @@ func Start() error { return startLegacy() } -// Stop 停止 frpc (幂等) func Stop() error { pm := process.GetGlobalManager() if pm != nil { @@ -55,7 +53,6 @@ func Stop() error { return stopLegacy() } -// Restart 重启 frpc (原子操作) func Restart() error { pm := process.GetGlobalManager() if pm != nil { @@ -69,72 +66,45 @@ func Restart() error { return startLegacy() } -// GetStatus 获取 frpc 详细状态 (供 API 调用) func GetStatus() (map[string]interface{}, error) { pm := process.GetGlobalManager() if pm != nil { - status, err := pm.Status() + state, err := pm.Status() if err != nil { return nil, err } return map[string]interface{}{ - "state": status.Phase, - "pid": status.PID, - "port": status.Port, + "phase": state.Phase, + "pid": state.PID, + "port": state.Port, + "alive": state.Alive, + "frp_ready": state.FRPReady, }, nil } - running := isRunningLegacy() return map[string]interface{}{ - "state": map[bool]string{true: "running", false: "stopped"}[running], + "phase": map[bool]string{true: "RUNNING", false: "STOPPED"}[running], "pid": 0, "port": 0, "legacy": true, }, nil } -// Reload 热加载 frpc 配置 +// ================================================================ +// Reload 热加载 frpc 配置(P0 核心) +// ================================================================ + func Reload() error { pm := process.GetGlobalManager() if pm == nil { + log.Println("[WARN] ProcessManager 未初始化,使用兼容模式 reload") return reloadLegacy() } - status, err := pm.Status() - if err != nil { - return err - } - - // 只有在 FAILED 或 STOPPED 时才启动(恢复) - // 其他状态(RUNNING、DEGRADED、STARTING)应该执行热加载而不是启动 - if status.Phase == "FAILED" || status.Phase == "STOPPED" { - log.Printf("[DEBUG] Reload: 状态 %s,执行启动恢复", status.Phase) - return pm.Start(context.Background()) - } - - // RUNNING / DEGRADED / STARTING 状态:执行真正的热加载 - log.Printf("[DEBUG] Reload: 状态 %s,执行配置热加载", status.Phase) - - // 执行 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 := pm.Restart(context.Background()); err != nil { - return fmt.Errorf("重启 frpc 失败: %w", err) - } - return nil - } - - log.Printf("✅ frpc 热加载成功: %s", string(output)) - return nil + // 使用 ProcessManager 的 ReloadConfig 方法 + // 该方法内部处理了 RELOADING 状态和 PID 归属验证 + log.Println("[INFO] 使用 ProcessManager 执行热加载") + return pm.ReloadConfig(context.Background()) } // ================================================================ @@ -150,7 +120,6 @@ func isRunningLegacy() bool { if err != nil { return false } - if runtime.GOOS == "windows" { cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid)) output, err := cmd.CombinedOutput() @@ -159,12 +128,11 @@ func isRunningLegacy() bool { } return strings.Contains(string(output), strconv.Itoa(pid)) } - - proc, err := os.FindProcess(pid) + process, err := os.FindProcess(pid) if err != nil { return false } - return proc.Signal(syscall.Signal(0)) == nil + return process.Signal(syscall.Signal(0)) == nil } func startLegacy() error { @@ -172,45 +140,36 @@ func startLegacy() error { if err != nil { return err } - if err := os.MkdirAll("./data", 0755); err != nil { return err } - if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) { if err := GenerateConfig(); err != nil { return err } } - if isRunningLegacy() { 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 err } cmd.Stdout = logFile cmd.Stderr = logFile - if err := cmd.Start(); err != nil { return err } - go func() { if err := cmd.Wait(); err != nil { log.Printf("frpc 子进程退出: %v", err) } os.Remove("./data/frpc.pid") }() - return os.WriteFile("./data/frpc.pid", []byte(strconv.Itoa(cmd.Process.Pid)), 0644) } @@ -223,7 +182,6 @@ func stopLegacy() error { os.Remove("./data/frpc.pid") return nil } - pidData, err := os.ReadFile("./data/frpc.pid") if err != nil { cmd := exec.Command("pkill", "-f", "frpc") @@ -232,18 +190,15 @@ func stopLegacy() error { } return nil } - pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData))) - proc, err := os.FindProcess(pid) + process, err := os.FindProcess(pid) if err != nil { os.Remove("./data/frpc.pid") return nil } - - if err := proc.Kill(); err != nil { + if err := process.Kill(); err != nil { return err } - os.Remove("./data/frpc.pid") return nil } @@ -252,12 +207,10 @@ func reloadLegacy() error { if !isRunningLegacy() { return startLegacy() } - frpcPath, err := GetFrpcPath() if err != nil { return err } - cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") output, err := cmd.CombinedOutput() if err != nil { @@ -268,7 +221,6 @@ func reloadLegacy() error { } return startLegacy() } - log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output)) return nil } diff --git a/internal/process/manager.go b/internal/process/manager.go index 1b27580..4fa4299 100644 --- a/internal/process/manager.go +++ b/internal/process/manager.go @@ -1,11 +1,5 @@ // internal/process/manager.go -// frpc-console 进程管理模块 -// 2.7-preview: 状态机驱动 + 端口归属检测 + admin API 健康检查 - -// 260810-作者留:现在的进程管理模块的框架其实已经基本成型了 -// 但是manager与frpc本身的进程生命周期对不上是个大问题呢 -// 现阶段已经不是猫猫自己能扛得住的了,接下来会试着能不能再收敛一下测试信息完成后续的状态机设计 -// 或许依旧是持久战呢喵 +// 新增 PhaseReloading 状态 + FRPReady 绑定 PID package process @@ -55,38 +49,36 @@ const ( 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" + 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 中间态 ) // ================================================================ // 数据结构 // ================================================================ -// PortCheckResult 端口检测结果(含归属信息) type PortCheckResult struct { Ready bool Err error - PID int // 占用端口的进程 PID(0 表示无法获取) - Process string // 占用端口的进程名 + PID int + Process string } -// FrpcInstance 系统中检测到的 frpc 实例 type FrpcInstance struct { PID int ParentPID int ExecPath string CmdLine string - Owned bool // 是否由当前 ProcessManager 管理 + Owned bool } -// ProcessState 进程完整状态 type ProcessState struct { Phase ProcessPhase `json:"phase"` PID int `json:"pid"` @@ -96,8 +88,8 @@ type ProcessState struct { Alive bool `json:"alive"` PortReady bool `json:"port_ready"` - PortPID int `json:"port_pid"` // 实际占用端口的 PID - PortOwner string `json:"port_owner"` // 实际占用端口的进程名 + 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"` @@ -111,7 +103,6 @@ type ProcessState struct { Conflicts []FrpcInstance `json:"conflicts,omitempty"` } -// PortStatus 兼容旧接口 type PortStatus struct { Port int `json:"port"` Occupied bool `json:"occupied"` @@ -120,7 +111,7 @@ type PortStatus struct { ProcessCmd string `json:"process_cmd,omitempty"` } -// FRPCStatus 来自 frpc admin API 的状态响应 +// FRPCStatus 匹配 frpc admin API /api/status 的实际返回结构 type FRPCStatus struct { TCP []struct { Name string `json:"name"` @@ -138,7 +129,6 @@ type FRPCStatus struct { LocalAddr string `json:"local_addr"` RemoteAddr string `json:"remote_addr"` } `json:"udp"` - // frp 0.70.0 还支持其他协议类型,可根据需要扩展 HTTP []struct { Name string `json:"name"` Type string `json:"type"` @@ -181,7 +171,6 @@ type FRPCStatus struct { } `json:"sudp"` } -// ConflictInfo 冲突检测结果 type ConflictInfo struct { HasConflict bool Count int @@ -226,6 +215,9 @@ type ProcessManager struct { // 用于 admin API 检测的 HTTP 客户端 httpClient *http.Client + + // 当前实例的 run_id(从 admin API 获取) + runID string } // ================================================================ @@ -268,6 +260,12 @@ func (pm *ProcessManager) CurrentPhase() ProcessPhase { 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() @@ -300,21 +298,21 @@ func (pm *ProcessManager) LoadConfig() error { log.Printf("[DEBUG] ✅ 从 admin_port 解析到端口: %d", port) return nil } - log.Printf("[DEBUG] ❌ admin_port 未找到") + 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 未找到") + 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 未找到或解析失败") + log.Printf("[DEBUG] webServer.addr 未找到或解析失败") return fmt.Errorf("未找到 admin_port 或 webServer.port/addr 配置") } @@ -392,7 +390,6 @@ 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 未配置")} @@ -404,7 +401,6 @@ func (pm *ProcessManager) CheckPort() PortCheckResult { } conn.Close() - // 端口已就绪,获取占用者信息 pid, process := pm.getPortOwner(pm.adminPort) return PortCheckResult{ Ready: true, @@ -413,17 +409,14 @@ func (pm *ProcessManager) CheckPort() PortCheckResult { } } -// 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)) @@ -435,7 +428,6 @@ func (pm *ProcessManager) getPortOwner(port int) (int, string) { 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 { @@ -444,7 +436,6 @@ func (pm *ProcessManager) getPortOwner(port int) (int, string) { 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() @@ -463,10 +454,9 @@ func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) { } // ================================================================ -// 实例检测(精确扫描) +// 实例检测 // ================================================================ -// DetectFrpcInstances 检测系统中所有 frpc 实例 func (pm *ProcessManager) DetectFrpcInstances() []FrpcInstance { var instances []FrpcInstance @@ -540,10 +530,6 @@ func (pm *ProcessManager) filterUnknown(instances []FrpcInstance) []FrpcInstance return result } -// ================================================================ -// 冲突检测 -// ================================================================ - func (pm *ProcessManager) DetectConflict() ConflictInfo { instances := pm.DetectFrpcInstances() owned := pm.filterOwned(instances) @@ -581,60 +567,68 @@ func (pm *ProcessManager) isProcessAlive(pid int) bool { } // ================================================================ -// FRPReady 检测(使用 admin API) +// FRPReady 检测(绑定 PID) // ================================================================ -// isFRPReady 检测 frpc 是否已完全就绪 -// 判定标准:至少有一个代理处于 running 状态 -func (pm *ProcessManager) isFRPReady() bool { - if pm.adminPort <= 0 { - log.Printf("[DEBUG] FRPReady=false: admin_port 未配置") +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 } // 方式1: stdout 检测(快速通道) if strings.Contains(pm.getLastOutput(), "start proxy success") || strings.Contains(pm.getLastOutput(), "login to server success") { - log.Printf("[DEBUG] FRPReady=true: 检测到 stdout 关键字") + log.Printf("[DEBUG] FRPReady(pid=%d): 检测到 stdout 关键字", pid) return true } - // 方式2: admin API 检测 + // 方式2: admin API 检测(绑定 PID) + // 首先确认端口是否被当前 PID 占用 + portResult := pm.CheckPort() + if !portResult.Ready { + log.Printf("[DEBUG] FRPReady(pid=%d): 端口 %d 未就绪", pid, pm.adminPort) + return false + } + if portResult.PID > 0 && portResult.PID != pid { + log.Printf("[DEBUG] FRPReady(pid=%d): 端口被进程 %d 占用,与期望 PID %d 不一致", pid, portResult.PID, pid) + return false + } + 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) + log.Printf("[DEBUG] FRPReady(pid=%d): 创建请求失败: %v", pid, err) return false } resp, err := pm.httpClient.Do(req) if err != nil { - log.Printf("[DEBUG] FRPReady=false: admin API 请求失败: %v", err) + 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=false: admin API 返回状态码 %d", resp.StatusCode) + 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=false: 读取响应失败: %v", err) + 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=false: 解析 JSON 失败: %v", err) + log.Printf("[DEBUG] FRPReady(pid=%d): 解析 JSON 失败: %v", pid, err) return false } - // 检查是否有任何代理处于 running 状态 - // 遍历所有已知的代理类型 + // 检查是否有代理处于 running 状态 proxyLists := [][]struct { Name string `json:"name"` Type string `json:"type"` @@ -655,13 +649,13 @@ func (pm *ProcessManager) isFRPReady() bool { for _, proxies := range proxyLists { for _, p := range proxies { if p.Status == "running" { - log.Printf("[DEBUG] FRPReady=true: 代理 %s 状态为 running", p.Name) + log.Printf("[DEBUG] FRPReady(pid=%d): 代理 %s 状态为 running", pid, p.Name) return true } } } - log.Printf("[DEBUG] FRPReady=false: 没有代理处于 running 状态") + log.Printf("[DEBUG] FRPReady(pid=%d): 没有代理处于 running 状态", pid) return false } @@ -855,7 +849,7 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS } // 3. 启动超时 - if time.Since(pm.startTime) > StartupTimeout { + if time.Since(pm.startTime) > StartupTimeout && pm.currentPhase != PhaseReloading { state.Error = fmt.Sprintf("启动超时 (超过 %v)", StartupTimeout) if portErr != nil { state.Error += ": " + portErr.Error() @@ -889,17 +883,16 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS } // 6. 端口被自己的进程占用,检查 FRPReady - frpReady := pm.isFRPReady() - state.FRPReady = frpReady + state.FRPReady = pm.isFRPReady(pid) - if frpReady { + if state.FRPReady { state.Phase = PhaseRunning return state } // 7. 端口就绪但 FRP 未就绪 state.Phase = PhaseDegraded - state.Error = "端口已监听,但 admin API 未就绪" + state.Error = "端口已监听,但 admin API 未就绪或代理未运行" return state } @@ -915,6 +908,109 @@ func (pm *ProcessManager) Status() (*ProcessState, error) { 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 +} + // ================================================================ // 操作执行 // ================================================================ @@ -929,7 +1025,6 @@ func (pm *ProcessManager) Start(ctx context.Context) error { pm.expectedStop = false - // 1. 检测冲突 conflict := pm.DetectConflict() if conflict.HasConflict { log.Printf("[WARN] 检测到 %d 个 frpc 实例 (Owned: %d, Unknown: %d),进入 CONFLICT 状态", @@ -957,20 +1052,15 @@ func (pm *ProcessManager) Start(ctx context.Context) error { pm.deletePIDFile() } - // 2. 清理孤儿 pm.cleanupOrphans() - - // 3. 正常启动 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) { - // 端口就绪且有有效 PID,检查是否匹配 if result.PID == pid { log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid) pm.setPhase(PhaseRunning) @@ -983,6 +1073,7 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error { pm.startTime = time.Now() pm.expectedStop = false + pm.runID = "" pm.exitMu.Lock() pm.exitCode = -1 @@ -1033,24 +1124,24 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error { if !pm.expectedStop && code != 0 { log.Printf("[WARN] frpc 进程异常退出 (exit code: %d)", code) - pm.setPhase(PhaseFailed) + 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() { + 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) @@ -1061,12 +1152,10 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error { time.Sleep(StartupRetryDelay) } - // 超时:检查进程状态 if pm.isProcessAlive(cmd.Process.Pid) { stderr := pm.getLastError() log.Printf("[WARN] frpc 启动超时 (PID: %d),当前 stderr: %s", cmd.Process.Pid, stderr) - // 最后一次尝试 admin API - if pm.isFRPReady() { + if pm.isFRPReady(cmd.Process.Pid) { log.Printf("[INFO] frpc 实际已就绪 (admin API 可访问),超时误判,修正状态") pm.setPhase(PhaseRunning) return nil @@ -1186,6 +1275,12 @@ func (pm *ProcessManager) StartHealthMonitor(ctx context.Context) { } 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)