测试新版本的进程管理
This commit is contained in:
+1
-1
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user