测试新版本的进程管理
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
|
|||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
# 先切换到国内镜像源,再安装包
|
# 先切换到国内镜像源,再安装包
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
+198
-375
@@ -1,13 +1,11 @@
|
|||||||
// internal/process/manager.go
|
// internal/process/manager.go
|
||||||
// frpc-console 进程管理模块
|
// frpc-console 进程管理模块
|
||||||
// 2.6-preview: 状态机驱动 + 冲突检测 + 生命周期管理
|
// 2.7-preview: 状态机驱动 + 端口归属检测 + admin API 健康检查
|
||||||
// 2.7-preview: 软化版 - 精确控制 + CONFLICT/STOPPING 状态
|
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -66,19 +64,24 @@ const (
|
|||||||
// 数据结构
|
// 数据结构
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
|
// PortCheckResult 端口检测结果(含归属信息)
|
||||||
type PortCheckResult struct {
|
type PortCheckResult struct {
|
||||||
Ready bool
|
Ready bool
|
||||||
Err error
|
Err error
|
||||||
|
PID int // 占用端口的进程 PID(0 表示无法获取)
|
||||||
|
Process string // 占用端口的进程名
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FrpcInstance 系统中检测到的 frpc 实例
|
||||||
type FrpcInstance struct {
|
type FrpcInstance struct {
|
||||||
PID int
|
PID int
|
||||||
ParentPID int
|
ParentPID int
|
||||||
ExecPath string
|
ExecPath string
|
||||||
CmdLine string
|
CmdLine string
|
||||||
Owned bool
|
Owned bool // 是否由当前 ProcessManager 管理
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessState 进程完整状态
|
||||||
type ProcessState struct {
|
type ProcessState struct {
|
||||||
Phase ProcessPhase `json:"phase"`
|
Phase ProcessPhase `json:"phase"`
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
@@ -88,6 +91,8 @@ type ProcessState struct {
|
|||||||
|
|
||||||
Alive bool `json:"alive"`
|
Alive bool `json:"alive"`
|
||||||
PortReady bool `json:"port_ready"`
|
PortReady bool `json:"port_ready"`
|
||||||
|
PortPID int `json:"port_pid"` // 实际占用端口的 PID
|
||||||
|
PortOwner string `json:"port_owner"` // 实际占用端口的进程名
|
||||||
PortError string `json:"port_error,omitempty"`
|
PortError string `json:"port_error,omitempty"`
|
||||||
FRPReady bool `json:"frp_ready"`
|
FRPReady bool `json:"frp_ready"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
@@ -122,6 +127,14 @@ type FRPCStatus struct {
|
|||||||
} `json:"proxies"`
|
} `json:"proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConflictInfo 冲突检测结果
|
||||||
|
type ConflictInfo struct {
|
||||||
|
HasConflict bool
|
||||||
|
Count int
|
||||||
|
Owned []FrpcInstance
|
||||||
|
Unknown []FrpcInstance
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 全局变量
|
// 全局变量
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -156,6 +169,9 @@ type ProcessManager struct {
|
|||||||
|
|
||||||
currentPhase ProcessPhase
|
currentPhase ProcessPhase
|
||||||
statusMu sync.RWMutex
|
statusMu sync.RWMutex
|
||||||
|
|
||||||
|
// 用于 admin API 检测的 HTTP 客户端
|
||||||
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -169,6 +185,9 @@ func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager {
|
|||||||
frpcBinPath: frpcBinPath,
|
frpcBinPath: frpcBinPath,
|
||||||
adminPort: 0,
|
adminPort: 0,
|
||||||
currentPhase: PhaseUnknown,
|
currentPhase: PhaseUnknown,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: APITimeout,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return pm
|
return pm
|
||||||
}
|
}
|
||||||
@@ -316,9 +335,10 @@ func extractPortFromAddrSection(content, section, key string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 端口检测
|
// 端口检测(含归属信息)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
|
// CheckPort 检测端口是否被占用,并返回占用者信息
|
||||||
func (pm *ProcessManager) CheckPort() PortCheckResult {
|
func (pm *ProcessManager) CheckPort() PortCheckResult {
|
||||||
if pm.adminPort <= 0 {
|
if pm.adminPort <= 0 {
|
||||||
return PortCheckResult{Ready: false, Err: fmt.Errorf("admin_port 未配置")}
|
return PortCheckResult{Ready: false, Err: fmt.Errorf("admin_port 未配置")}
|
||||||
@@ -329,9 +349,48 @@ func (pm *ProcessManager) CheckPort() PortCheckResult {
|
|||||||
return PortCheckResult{Ready: false, Err: err}
|
return PortCheckResult{Ready: false, Err: err}
|
||||||
}
|
}
|
||||||
conn.Close()
|
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) {
|
func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) {
|
||||||
status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false}
|
status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false}
|
||||||
result := pm.CheckPort()
|
result := pm.CheckPort()
|
||||||
@@ -342,168 +401,11 @@ func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) {
|
|||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
status.Occupied = true
|
status.Occupied = true
|
||||||
|
status.PID = result.PID
|
||||||
pid, err := pm.getPIDByPort(pm.adminPort)
|
if result.Process != "" {
|
||||||
if err != nil {
|
status.IsFRPC = strings.Contains(result.Process, "frpc")
|
||||||
if pidFromFile := pm.readPIDFile(); pidFromFile > 0 {
|
|
||||||
if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) {
|
|
||||||
status.PID = pidFromFile
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
status.PID = pid
|
|
||||||
}
|
|
||||||
if status.PID == 0 {
|
|
||||||
return status, nil
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -511,68 +413,15 @@ func (pm *ProcessManager) isProcessAlive(pid int) bool {
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
// DetectFrpcInstances 检测系统中所有 frpc 实例
|
// DetectFrpcInstances 检测系统中所有 frpc 实例
|
||||||
// 三重确认:PID存在 + 进程存活 + Executable/CmdLine匹配
|
|
||||||
func (pm *ProcessManager) DetectFrpcInstances() []FrpcInstance {
|
func (pm *ProcessManager) DetectFrpcInstances() []FrpcInstance {
|
||||||
var instances []FrpcInstance
|
var instances []FrpcInstance
|
||||||
|
|
||||||
// 获取当前 frpc 可执行文件的绝对路径
|
if runtime.GOOS == "linux" {
|
||||||
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")
|
|
||||||
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") {
|
|
||||||
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 !pm.isProcessAlive(pid) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 获取 cmdline(Windows 较难获取,先用进程名匹配)
|
|
||||||
inst := FrpcInstance{
|
|
||||||
PID: pid,
|
|
||||||
ExecPath: "frpc.exe",
|
|
||||||
CmdLine: "frpc.exe",
|
|
||||||
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")
|
cmd := exec.Command("pgrep", "-f", "frpc")
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 没有找到进程,pgrep 返回非0,属于正常情况
|
|
||||||
return instances
|
return instances
|
||||||
}
|
}
|
||||||
|
|
||||||
pids := strings.Split(strings.TrimSpace(string(out)), "\n")
|
pids := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
for _, pidStr := range pids {
|
for _, pidStr := range pids {
|
||||||
pidStr = strings.TrimSpace(pidStr)
|
pidStr = strings.TrimSpace(pidStr)
|
||||||
@@ -583,29 +432,23 @@ func (pm *ProcessManager) DetectFrpcInstances() []FrpcInstance {
|
|||||||
if err != nil || pid <= 0 {
|
if err != nil || pid <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if pid == 1 || pid == os.Getpid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !pm.isProcessAlive(pid) {
|
if !pm.isProcessAlive(pid) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取命令行
|
|
||||||
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
|
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
|
||||||
cmdLineStr := strings.ReplaceAll(string(cmdline), "\x00", " ")
|
cmdLineStr := strings.ReplaceAll(string(cmdline), "\x00", " ")
|
||||||
|
|
||||||
// 获取可执行文件路径
|
|
||||||
exePath, _ := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid))
|
exePath, _ := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid))
|
||||||
|
|
||||||
// 获取父进程 PID
|
|
||||||
stat, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
stat, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
||||||
var parentPID int
|
var parentPID int
|
||||||
if len(stat) > 0 {
|
if len(stat) > 0 {
|
||||||
// stat 格式: pid (comm) state ppid ...
|
|
||||||
parts := strings.Fields(string(stat))
|
parts := strings.Fields(string(stat))
|
||||||
if len(parts) > 3 {
|
if len(parts) > 3 {
|
||||||
parentPID, _ = strconv.Atoi(parts[3])
|
parentPID, _ = strconv.Atoi(parts[3])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inst := FrpcInstance{
|
inst := FrpcInstance{
|
||||||
PID: pid,
|
PID: pid,
|
||||||
ParentPID: parentPID,
|
ParentPID: parentPID,
|
||||||
@@ -613,27 +456,16 @@ func (pm *ProcessManager) DetectFrpcInstances() []FrpcInstance {
|
|||||||
CmdLine: cmdLineStr,
|
CmdLine: cmdLineStr,
|
||||||
Owned: false,
|
Owned: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 三重确认 Owned
|
|
||||||
ownedPid := pm.readPIDFile()
|
ownedPid := pm.readPIDFile()
|
||||||
if ownedPid == pid {
|
if ownedPid == pid {
|
||||||
// 条件1: PID 匹配
|
|
||||||
if pm.isProcessAlive(pid) {
|
|
||||||
// 条件2: 进程存在
|
|
||||||
// 条件3: 执行路径匹配
|
|
||||||
if exePath != "" && (strings.Contains(exePath, "frpc") || strings.Contains(cmdLineStr, "frpc")) {
|
|
||||||
inst.Owned = true
|
inst.Owned = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instances = append(instances, inst)
|
instances = append(instances, inst)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return instances
|
return instances
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterOwned 返回 Owned 实例
|
|
||||||
func (pm *ProcessManager) filterOwned(instances []FrpcInstance) []FrpcInstance {
|
func (pm *ProcessManager) filterOwned(instances []FrpcInstance) []FrpcInstance {
|
||||||
var result []FrpcInstance
|
var result []FrpcInstance
|
||||||
for _, inst := range instances {
|
for _, inst := range instances {
|
||||||
@@ -644,7 +476,6 @@ func (pm *ProcessManager) filterOwned(instances []FrpcInstance) []FrpcInstance {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterUnknown 返回 Unknown 实例
|
|
||||||
func (pm *ProcessManager) filterUnknown(instances []FrpcInstance) []FrpcInstance {
|
func (pm *ProcessManager) filterUnknown(instances []FrpcInstance) []FrpcInstance {
|
||||||
var result []FrpcInstance
|
var result []FrpcInstance
|
||||||
for _, inst := range instances {
|
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 {
|
func (pm *ProcessManager) DetectConflict() ConflictInfo {
|
||||||
instances := pm.DetectFrpcInstances()
|
instances := pm.DetectFrpcInstances()
|
||||||
owned := pm.filterOwned(instances)
|
owned := pm.filterOwned(instances)
|
||||||
@@ -680,29 +504,85 @@ func (pm *ProcessManager) DetectConflict() ConflictInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 实例健康检查
|
// 进程存活检测
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
func (pm *ProcessManager) isInstanceHealthy(inst FrpcInstance) bool {
|
func (pm *ProcessManager) isProcessAlive(pid int) bool {
|
||||||
// 1. 进程必须存在
|
if pid <= 0 {
|
||||||
if !pm.isProcessAlive(inst.PID) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 2. 端口必须可连接
|
if runtime.GOOS == "windows" {
|
||||||
result := pm.CheckPort()
|
cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid))
|
||||||
if !result.Ready {
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 3. FRP 就绪(通过 stdout 或 admin API)
|
return strings.Contains(string(output), strconv.Itoa(pid))
|
||||||
stdout := pm.getLastOutput()
|
|
||||||
if pm.isFRPReady(stdout) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
// 4. 尝试 admin API
|
process, err := os.FindProcess(pid)
|
||||||
if info := pm.getFRPCStatus(inst.PID); info != nil && info.Version != "" {
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
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
|
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 {
|
if pid <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
cmd := exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid))
|
cmd := exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid))
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
process, err := os.FindProcess(pid)
|
process, err := os.FindProcess(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -731,12 +609,10 @@ func (pm *ProcessManager) cleanupOrphans() {
|
|||||||
if !result.Ready {
|
if !result.Ready {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pid := pm.readPIDFile()
|
pid := pm.readPIDFile()
|
||||||
if pid > 0 && pm.isProcessAlive(pid) {
|
if pid > 0 && pm.isProcessAlive(pid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[WARN] 检测到孤儿 frpc 进程 (端口 %d 被占用但无有效 PID),正在清理...", pm.adminPort)
|
log.Printf("[WARN] 检测到孤儿 frpc 进程 (端口 %d 被占用但无有效 PID),正在清理...", pm.adminPort)
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
exec.Command("taskkill", "/F", "/IM", "frpc.exe").Run()
|
exec.Command("taskkill", "/F", "/IM", "frpc.exe").Run()
|
||||||
@@ -834,18 +710,6 @@ func (pm *ProcessManager) getLastError() string {
|
|||||||
return pm.lastError
|
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 {
|
func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessState {
|
||||||
alive := pm.isProcessAlive(pid)
|
alive := pm.isProcessAlive(pid)
|
||||||
portReady := result.Ready
|
portReady := result.Ready
|
||||||
|
portPID := result.PID
|
||||||
|
portOwner := result.Process
|
||||||
portErr := result.Err
|
portErr := result.Err
|
||||||
|
|
||||||
stdout := pm.getLastOutput()
|
|
||||||
stderr := pm.getLastError()
|
|
||||||
frpReady := pm.isFRPReady(stdout)
|
|
||||||
|
|
||||||
pm.exitMu.Lock()
|
pm.exitMu.Lock()
|
||||||
exitCode := pm.exitCode
|
exitCode := pm.exitCode
|
||||||
pm.exitMu.Unlock()
|
pm.exitMu.Unlock()
|
||||||
@@ -868,12 +730,14 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
Port: pm.adminPort,
|
Port: pm.adminPort,
|
||||||
Alive: alive,
|
Alive: alive,
|
||||||
PortReady: portReady,
|
PortReady: portReady,
|
||||||
|
PortPID: portPID,
|
||||||
|
PortOwner: portOwner,
|
||||||
PortError: "",
|
PortError: "",
|
||||||
FRPReady: frpReady,
|
FRPReady: false,
|
||||||
StartedAt: pm.startTime,
|
StartedAt: pm.startTime,
|
||||||
ExpectedStop: pm.expectedStop,
|
ExpectedStop: pm.expectedStop,
|
||||||
LastOutput: stdout,
|
LastOutput: pm.getLastOutput(),
|
||||||
LastError: stderr,
|
LastError: pm.getLastError(),
|
||||||
ExitCode: exitCode,
|
ExitCode: exitCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,11 +745,9 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
state.PortError = portErr.Error()
|
state.PortError = portErr.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ---- 状态判定逻辑 ----
|
||||||
// 状态判定逻辑
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// 1. 如果是主动停止,直接返回 STOPPING
|
// 1. 主动停止中
|
||||||
if pm.expectedStop && alive {
|
if pm.expectedStop && alive {
|
||||||
state.Phase = PhaseStopping
|
state.Phase = PhaseStopping
|
||||||
return state
|
return state
|
||||||
@@ -897,23 +759,22 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
state.Phase = PhaseStopped
|
state.Phase = PhaseStopped
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
if exitCode != 0 && exitCode != -1 {
|
if exitCode != 0 && exitCode != -1 {
|
||||||
state.Error = fmt.Sprintf("进程异常退出 (exit code: %d)", exitCode)
|
state.Error = fmt.Sprintf("进程异常退出 (exit code: %d)", exitCode)
|
||||||
if stderr != "" {
|
if state.LastError != "" {
|
||||||
state.Error += ": " + stderr
|
state.Error += ": " + state.LastError
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.Error = "进程意外退出"
|
state.Error = "进程意外退出"
|
||||||
if stderr != "" {
|
if state.LastError != "" {
|
||||||
state.Error += ": " + stderr
|
state.Error += ": " + state.LastError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.Phase = PhaseFailed
|
state.Phase = PhaseFailed
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 进程存在,检查超时
|
// 3. 启动超时
|
||||||
if time.Since(pm.startTime) > StartupTimeout {
|
if time.Since(pm.startTime) > StartupTimeout {
|
||||||
state.Error = fmt.Sprintf("启动超时 (超过 %v)", StartupTimeout)
|
state.Error = fmt.Sprintf("启动超时 (超过 %v)", StartupTimeout)
|
||||||
if portErr != nil {
|
if portErr != nil {
|
||||||
@@ -923,34 +784,7 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 冲突检测
|
// 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. 端口未就绪
|
|
||||||
if !portReady {
|
if !portReady {
|
||||||
if portErr != nil && strings.Contains(portErr.Error(), "permission denied") {
|
if portErr != nil && strings.Contains(portErr.Error(), "permission denied") {
|
||||||
state.Phase = PhaseDegraded
|
state.Phase = PhaseDegraded
|
||||||
@@ -964,7 +798,28 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
return state
|
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
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,41 +835,6 @@ func (pm *ProcessManager) Status() (*ProcessState, error) {
|
|||||||
return &state, nil
|
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 状态",
|
log.Printf("[WARN] 检测到 %d 个 frpc 实例 (Owned: %d, Unknown: %d),进入 CONFLICT 状态",
|
||||||
conflict.Count, len(conflict.Owned), len(conflict.Unknown))
|
conflict.Count, len(conflict.Owned), len(conflict.Unknown))
|
||||||
|
|
||||||
// 清理 Unknown 实例
|
|
||||||
for _, inst := range conflict.Unknown {
|
for _, inst := range conflict.Unknown {
|
||||||
log.Printf("[INFO] 清理 Unknown frpc 实例 (PID: %d)", inst.PID)
|
log.Printf("[INFO] 清理 Unknown frpc 实例 (PID: %d)", inst.PID)
|
||||||
pm.killProcess(inst.PID)
|
pm.killProcess(inst.PID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有且仅有一个 Owned 实例,保留它
|
|
||||||
if len(conflict.Owned) == 1 {
|
if len(conflict.Owned) == 1 {
|
||||||
pid := conflict.Owned[0].PID
|
pid := conflict.Owned[0].PID
|
||||||
log.Printf("[INFO] 保留 Owned frpc 实例 (PID: %d),清理完成", pid)
|
log.Printf("[INFO] 保留 Owned frpc 实例 (PID: %d),清理完成", pid)
|
||||||
@@ -1050,7 +868,6 @@ func (pm *ProcessManager) Start(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 多个 Owned 或没有 Owned:全部清理,重新启动
|
|
||||||
if len(conflict.Owned) > 1 {
|
if len(conflict.Owned) > 1 {
|
||||||
log.Printf("[WARN] 多个 Owned 实例冲突,全部清理")
|
log.Printf("[WARN] 多个 Owned 实例冲突,全部清理")
|
||||||
for _, inst := range conflict.Owned {
|
for _, inst := range conflict.Owned {
|
||||||
@@ -1068,14 +885,19 @@ func (pm *ProcessManager) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
||||||
|
// 检查端口状态
|
||||||
result := pm.CheckPort()
|
result := pm.CheckPort()
|
||||||
if result.Ready {
|
if result.Ready {
|
||||||
pid := pm.readPIDFile()
|
pid := pm.readPIDFile()
|
||||||
if pid > 0 && pm.isProcessAlive(pid) {
|
if pid > 0 && pm.isProcessAlive(pid) {
|
||||||
|
// 端口就绪且有有效 PID,检查是否匹配
|
||||||
|
if result.PID == pid {
|
||||||
log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid)
|
log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid)
|
||||||
pm.setPhase(PhaseRunning)
|
pm.setPhase(PhaseRunning)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
log.Printf("[WARN] 端口被进程 %d 占用,但 PID 文件指向 %d,可能存在冲突", result.PID, pid)
|
||||||
|
}
|
||||||
pm.cleanupOrphans()
|
pm.cleanupOrphans()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,8 +964,13 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
|||||||
for attempt := 0; attempt < StartupMaxAttempts; attempt++ {
|
for attempt := 0; attempt < StartupMaxAttempts; attempt++ {
|
||||||
r := pm.CheckPort()
|
r := pm.CheckPort()
|
||||||
if r.Ready {
|
if r.Ready {
|
||||||
stdout := pm.getLastOutput()
|
if r.PID > 0 && r.PID != cmd.Process.Pid {
|
||||||
if pm.isFRPReady(stdout) {
|
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",
|
log.Printf("[INFO] frpc 启动成功 (PID: %d, 端口: %d),耗时 %dms",
|
||||||
cmd.Process.Pid, pm.adminPort, attempt*int(StartupRetryDelay/time.Millisecond))
|
cmd.Process.Pid, pm.adminPort, attempt*int(StartupRetryDelay/time.Millisecond))
|
||||||
pm.setPhase(PhaseRunning)
|
pm.setPhase(PhaseRunning)
|
||||||
@@ -1158,9 +985,9 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
|||||||
if pm.isProcessAlive(cmd.Process.Pid) {
|
if pm.isProcessAlive(cmd.Process.Pid) {
|
||||||
stderr := pm.getLastError()
|
stderr := pm.getLastError()
|
||||||
log.Printf("[WARN] frpc 启动超时 (PID: %d),当前 stderr: %s", cmd.Process.Pid, stderr)
|
log.Printf("[WARN] frpc 启动超时 (PID: %d),当前 stderr: %s", cmd.Process.Pid, stderr)
|
||||||
if info := pm.getFRPCStatus(cmd.Process.Pid); info != nil && info.Version != "" {
|
// 最后一次尝试 admin API
|
||||||
log.Printf("[INFO] frpc admin API 可访问,版本: %s", info.Version)
|
if pm.isFRPReady() {
|
||||||
pm.writePIDFile(cmd.Process.Pid)
|
log.Printf("[INFO] frpc 实际已就绪 (admin API 可访问),超时误判,修正状态")
|
||||||
pm.setPhase(PhaseRunning)
|
pm.setPhase(PhaseRunning)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1205,7 +1032,6 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送 SIGTERM
|
|
||||||
process, err := os.FindProcess(pid)
|
process, err := os.FindProcess(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
@@ -1218,7 +1044,6 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待进程退出
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
for time.Since(start) < StopMaxWaitTime {
|
for time.Since(start) < StopMaxWaitTime {
|
||||||
if !pm.isProcessAlive(pid) {
|
if !pm.isProcessAlive(pid) {
|
||||||
@@ -1229,7 +1054,6 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error {
|
|||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制 kill
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run()
|
exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run()
|
||||||
} else {
|
} else {
|
||||||
@@ -1264,7 +1088,7 @@ func (pm *ProcessManager) Restart(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 健康检查看门狗(由 main.go 调用)
|
// 健康检查看门狗
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
func (pm *ProcessManager) StartHealthMonitor(ctx context.Context) {
|
func (pm *ProcessManager) StartHealthMonitor(ctx context.Context) {
|
||||||
@@ -1288,11 +1112,9 @@ func (pm *ProcessManager) runHealthCheck() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据状态执行自动恢复
|
|
||||||
switch state.Phase {
|
switch state.Phase {
|
||||||
case PhaseConflict:
|
case PhaseConflict:
|
||||||
log.Printf("[WARN] 检测到冲突,尝试自动恢复...")
|
log.Printf("[WARN] 检测到冲突,尝试自动恢复...")
|
||||||
// 冲突恢复:清理未知实例
|
|
||||||
for _, inst := range state.Conflicts {
|
for _, inst := range state.Conflicts {
|
||||||
if !inst.Owned {
|
if !inst.Owned {
|
||||||
pm.killProcess(inst.PID)
|
pm.killProcess(inst.PID)
|
||||||
@@ -1301,7 +1123,6 @@ func (pm *ProcessManager) runHealthCheck() {
|
|||||||
pm.setPhase(PhaseStarting)
|
pm.setPhase(PhaseStarting)
|
||||||
case PhaseDegraded:
|
case PhaseDegraded:
|
||||||
log.Printf("[WARN] frpc 处于降级状态,尝试恢复...")
|
log.Printf("[WARN] frpc 处于降级状态,尝试恢复...")
|
||||||
// 降级恢复:重启
|
|
||||||
pm.Restart(context.Background())
|
pm.Restart(context.Background())
|
||||||
case PhaseFailed:
|
case PhaseFailed:
|
||||||
if !state.ExpectedStop {
|
if !state.ExpectedStop {
|
||||||
@@ -1312,7 +1133,7 @@ func (pm *ProcessManager) runHealthCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 健康检查
|
// 健康检查(供 API 调用)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
func (pm *ProcessManager) HealthCheck() map[string]interface{} {
|
func (pm *ProcessManager) HealthCheck() map[string]interface{} {
|
||||||
@@ -1330,6 +1151,8 @@ func (pm *ProcessManager) HealthCheck() map[string]interface{} {
|
|||||||
result["port"] = state.Port
|
result["port"] = state.Port
|
||||||
result["alive"] = state.Alive
|
result["alive"] = state.Alive
|
||||||
result["port_ready"] = state.PortReady
|
result["port_ready"] = state.PortReady
|
||||||
|
result["port_pid"] = state.PortPID
|
||||||
|
result["port_owner"] = state.PortOwner
|
||||||
result["frp_ready"] = state.FRPReady
|
result["frp_ready"] = state.FRPReady
|
||||||
if state.PortError != "" {
|
if state.PortError != "" {
|
||||||
result["port_error"] = state.PortError
|
result["port_error"] = state.PortError
|
||||||
|
|||||||
Reference in New Issue
Block a user