测试新版本的进程管理

This commit is contained in:
2026-08-10 22:58:27 +08:00
parent cb752fe2c8
commit 4c6d4ea6a3
2 changed files with 225 additions and 402 deletions
+1 -1
View File
@@ -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
+224 -401
View File
@@ -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
}
// 获取 cmdlineWindows 较难获取,先用进程名匹配)
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