首次优化的进程管理工具
This commit is contained in:
+383
-53
@@ -1,10 +1,7 @@
|
|||||||
// internal/process/manager.go
|
// internal/process/manager.go
|
||||||
// frpc-console 进程管理模块
|
// frpc-console 进程管理模块
|
||||||
// 2.6-preview: 状态机驱动 + 端口检测 + 单实例锁定
|
// 2.6-preview: 状态机驱动 + 冲突检测 + 生命周期管理
|
||||||
|
// 2.7-preview: 软化版 - 精确控制 + CONFLICT/STOPPING 状态
|
||||||
// 作者留:这个版本可以用了,已经明显看到进程出来发现端口不对立马被杀掉的状态了
|
|
||||||
// 但是稍显粗暴,差点意思,到时候让Deepseek稍微软化下就好了
|
|
||||||
// 但是现阶段Preview还不能用,再等等
|
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
@@ -44,6 +41,8 @@ const (
|
|||||||
StartupMaxAttempts = 50
|
StartupMaxAttempts = 50
|
||||||
StartupRetryDelay = 200 * time.Millisecond
|
StartupRetryDelay = 200 * time.Millisecond
|
||||||
StartupTimeout = 10 * time.Second
|
StartupTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
HealthCheckInterval = 15 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -59,6 +58,8 @@ const (
|
|||||||
PhaseDegraded ProcessPhase = "DEGRADED"
|
PhaseDegraded ProcessPhase = "DEGRADED"
|
||||||
PhaseFailed ProcessPhase = "FAILED"
|
PhaseFailed ProcessPhase = "FAILED"
|
||||||
PhaseStopped ProcessPhase = "STOPPED"
|
PhaseStopped ProcessPhase = "STOPPED"
|
||||||
|
PhaseStopping ProcessPhase = "STOPPING" // 正在停止中
|
||||||
|
PhaseConflict ProcessPhase = "CONFLICT" // 检测到多实例冲突
|
||||||
)
|
)
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -70,6 +71,14 @@ type PortCheckResult struct {
|
|||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FrpcInstance struct {
|
||||||
|
PID int
|
||||||
|
ParentPID int
|
||||||
|
ExecPath string
|
||||||
|
CmdLine string
|
||||||
|
Owned bool // 是否由当前 ProcessManager 管理
|
||||||
|
}
|
||||||
|
|
||||||
type ProcessState struct {
|
type ProcessState struct {
|
||||||
Phase ProcessPhase `json:"phase"`
|
Phase ProcessPhase `json:"phase"`
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
@@ -88,8 +97,16 @@ type ProcessState struct {
|
|||||||
LastOutput string `json:"last_output,omitempty"`
|
LastOutput string `json:"last_output,omitempty"`
|
||||||
LastError string `json:"last_error,omitempty"`
|
LastError string `json:"last_error,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
|
||||||
|
// 冲突检测信息
|
||||||
|
Conflicts []FrpcInstance `json:"conflicts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalManager *ProcessManager
|
||||||
|
globalMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
type PortStatus struct {
|
type PortStatus struct {
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Occupied bool `json:"occupied"`
|
Occupied bool `json:"occupied"`
|
||||||
@@ -131,24 +148,28 @@ type ProcessManager struct {
|
|||||||
|
|
||||||
lockFile *os.File
|
lockFile *os.File
|
||||||
locked bool
|
locked bool
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
// 当前状态缓存
|
||||||
globalManager *ProcessManager
|
currentPhase ProcessPhase
|
||||||
globalMu sync.Mutex
|
statusMu sync.RWMutex
|
||||||
)
|
|
||||||
|
// 取消函数(用于停止健康检查)
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 构造函数
|
// 构造函数
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager {
|
func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager {
|
||||||
return &ProcessManager{
|
pm := &ProcessManager{
|
||||||
dataDir: dataDir,
|
dataDir: dataDir,
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
frpcBinPath: frpcBinPath,
|
frpcBinPath: frpcBinPath,
|
||||||
adminPort: 0,
|
adminPort: 0,
|
||||||
|
currentPhase: PhaseUnknown,
|
||||||
}
|
}
|
||||||
|
return pm
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetGlobalManager(pm *ProcessManager) {
|
func SetGlobalManager(pm *ProcessManager) {
|
||||||
@@ -167,6 +188,21 @@ func (pm *ProcessManager) AdminPort() int {
|
|||||||
return pm.adminPort
|
return pm.adminPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) CurrentPhase() ProcessPhase {
|
||||||
|
pm.statusMu.RLock()
|
||||||
|
defer pm.statusMu.RUnlock()
|
||||||
|
return pm.currentPhase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) setPhase(phase ProcessPhase) {
|
||||||
|
pm.statusMu.Lock()
|
||||||
|
defer pm.statusMu.Unlock()
|
||||||
|
if pm.currentPhase != phase {
|
||||||
|
log.Printf("[STATUS] %s → %s", pm.currentPhase, phase)
|
||||||
|
pm.currentPhase = phase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 配置读取
|
// 配置读取
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -469,6 +505,205 @@ func (pm *ProcessManager) isProcessAlive(pid int) bool {
|
|||||||
return process.Signal(syscall.Signal(0)) == nil
|
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")
|
||||||
|
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")
|
||||||
|
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 {
|
||||||
|
if inst.Owned {
|
||||||
|
result = append(result, inst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterUnknown 返回 Unknown 实例
|
||||||
|
func (pm *ProcessManager) filterUnknown(instances []FrpcInstance) []FrpcInstance {
|
||||||
|
var result []FrpcInstance
|
||||||
|
for _, inst := range instances {
|
||||||
|
if !inst.Owned {
|
||||||
|
result = append(result, inst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 冲突检测
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
type ConflictInfo struct {
|
||||||
|
HasConflict bool
|
||||||
|
Count int
|
||||||
|
Owned []FrpcInstance
|
||||||
|
Unknown []FrpcInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) DetectConflict() ConflictInfo {
|
||||||
|
instances := pm.DetectFrpcInstances()
|
||||||
|
owned := pm.filterOwned(instances)
|
||||||
|
unknown := pm.filterUnknown(instances)
|
||||||
|
|
||||||
|
return ConflictInfo{
|
||||||
|
HasConflict: len(unknown) > 0 || len(owned) > 1,
|
||||||
|
Count: len(instances),
|
||||||
|
Owned: owned,
|
||||||
|
Unknown: unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 实例健康检查
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func (pm *ProcessManager) isInstanceHealthy(inst FrpcInstance) bool {
|
||||||
|
// 1. 进程必须存在
|
||||||
|
if !pm.isProcessAlive(inst.PID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 2. 端口必须可连接
|
||||||
|
result := pm.CheckPort()
|
||||||
|
if !result.Ready {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 3. FRP 就绪(通过 stdout 或 admin API)
|
||||||
|
stdout := pm.getLastOutput()
|
||||||
|
if pm.isFRPReady(stdout) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 4. 尝试 admin API
|
||||||
|
if info := pm.getFRPCStatus(inst.PID); info != nil && info.Version != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 进程清理
|
// 进程清理
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -490,34 +725,6 @@ func (pm *ProcessManager) killProcess(pid int) error {
|
|||||||
return process.Kill()
|
return process.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
// killAllFrpc 强制杀死所有 frpc 进程(包括孤儿)
|
|
||||||
func (pm *ProcessManager) killAllFrpc() {
|
|
||||||
log.Printf("[INFO] 清理所有 frpc 进程...")
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
exec.Command("taskkill", "/F", "/IM", "frpc.exe").Run()
|
|
||||||
} else {
|
|
||||||
exec.Command("pkill", "-9", "-f", "frpc").Run()
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
pm.deletePIDFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
// countFrpcProcesses 统计当前 frpc 进程数量
|
|
||||||
func (pm *ProcessManager) countFrpcProcesses() int {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq frpc.exe")
|
|
||||||
out, _ := cmd.CombinedOutput()
|
|
||||||
return strings.Count(string(out), "frpc.exe")
|
|
||||||
}
|
|
||||||
cmd := exec.Command("pgrep", "-c", "-f", "frpc")
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
count, _ := strconv.Atoi(strings.TrimSpace(string(out)))
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) cleanupOrphans() {
|
func (pm *ProcessManager) cleanupOrphans() {
|
||||||
result := pm.CheckPort()
|
result := pm.CheckPort()
|
||||||
if !result.Ready {
|
if !result.Ready {
|
||||||
@@ -673,7 +880,17 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
state.PortError = portErr.Error()
|
state.PortError = portErr.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 状态判定逻辑 ----
|
// ============================================================
|
||||||
|
// 状态判定逻辑
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// 1. 如果是主动停止,直接返回 STOPPING
|
||||||
|
if pm.expectedStop && alive {
|
||||||
|
state.Phase = PhaseStopping
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 进程不存在
|
||||||
if !alive {
|
if !alive {
|
||||||
if pm.expectedStop {
|
if pm.expectedStop {
|
||||||
state.Phase = PhaseStopped
|
state.Phase = PhaseStopped
|
||||||
@@ -695,7 +912,7 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- alive == true ----
|
// 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 {
|
||||||
@@ -705,22 +922,34 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
if portReady && frpReady {
|
// 4. 冲突检测
|
||||||
state.Phase = PhaseRunning
|
conflict := pm.DetectConflict()
|
||||||
// 检测多进程
|
if conflict.HasConflict {
|
||||||
if count := pm.countFrpcProcesses(); count > 1 {
|
state.Phase = PhaseConflict
|
||||||
state.Phase = PhaseDegraded
|
state.Conflicts = append(conflict.Owned, conflict.Unknown...)
|
||||||
state.Error = fmt.Sprintf("检测到 %d 个 frpc 进程,可能存在多开", count)
|
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
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. 正常运行状态
|
||||||
|
if portReady && frpReady {
|
||||||
|
state.Phase = PhaseRunning
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 端口就绪但 FRP 未就绪
|
||||||
if portReady && !frpReady {
|
if portReady && !frpReady {
|
||||||
state.Phase = PhaseDegraded
|
state.Phase = PhaseDegraded
|
||||||
state.Error = "端口已监听,但 frpc 未报告就绪"
|
state.Error = "端口已监听,但 frpc 未报告就绪"
|
||||||
return state
|
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
|
||||||
@@ -746,6 +975,7 @@ func (pm *ProcessManager) Status() (*ProcessState, error) {
|
|||||||
pid := pm.readPIDFile()
|
pid := pm.readPIDFile()
|
||||||
result := pm.CheckPort()
|
result := pm.CheckPort()
|
||||||
state := pm.computeState(pid, result)
|
state := pm.computeState(pid, result)
|
||||||
|
pm.setPhase(state.Phase)
|
||||||
return &state, nil
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,12 +1026,43 @@ func (pm *ProcessManager) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
defer pm.Unlock()
|
defer pm.Unlock()
|
||||||
|
|
||||||
// 先杀所有 frpc 进程(避免多开)
|
pm.expectedStop = false
|
||||||
pm.killAllFrpc()
|
|
||||||
|
|
||||||
// 清理孤儿
|
// 1. 检测冲突
|
||||||
|
conflict := pm.DetectConflict()
|
||||||
|
if conflict.HasConflict {
|
||||||
|
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)
|
||||||
|
pm.writePIDFile(pid)
|
||||||
|
pm.setPhase(PhaseRunning)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多个 Owned 或没有 Owned:全部清理,重新启动
|
||||||
|
if len(conflict.Owned) > 1 {
|
||||||
|
log.Printf("[WARN] 多个 Owned 实例冲突,全部清理")
|
||||||
|
for _, inst := range conflict.Owned {
|
||||||
|
pm.killProcess(inst.PID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pm.deletePIDFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 清理孤儿
|
||||||
pm.cleanupOrphans()
|
pm.cleanupOrphans()
|
||||||
|
|
||||||
|
// 3. 正常启动
|
||||||
return pm.startLocked(ctx)
|
return pm.startLocked(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,6 +1072,7 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
|||||||
pid := pm.readPIDFile()
|
pid := pm.readPIDFile()
|
||||||
if pid > 0 && pm.isProcessAlive(pid) {
|
if pid > 0 && pm.isProcessAlive(pid) {
|
||||||
log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid)
|
log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid)
|
||||||
|
pm.setPhase(PhaseRunning)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
pm.cleanupOrphans()
|
pm.cleanupOrphans()
|
||||||
@@ -847,6 +1109,8 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
log.Printf("[DEBUG] frpc 进程已启动,PID: %d", cmd.Process.Pid)
|
log.Printf("[DEBUG] frpc 进程已启动,PID: %d", cmd.Process.Pid)
|
||||||
|
|
||||||
|
pm.setPhase(PhaseStarting)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
var code int
|
var code int
|
||||||
@@ -866,12 +1130,14 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
|||||||
|
|
||||||
if !pm.expectedStop && code != 0 {
|
if !pm.expectedStop && code != 0 {
|
||||||
log.Printf("[WARN] frpc 进程异常退出 (exit code: %d)", code)
|
log.Printf("[WARN] frpc 进程异常退出 (exit code: %d)", code)
|
||||||
|
pm.setPhase(PhaseFailed)
|
||||||
}
|
}
|
||||||
if !pm.expectedStop {
|
if !pm.expectedStop {
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 等待端口就绪
|
||||||
for attempt := 0; attempt < StartupMaxAttempts; attempt++ {
|
for attempt := 0; attempt < StartupMaxAttempts; attempt++ {
|
||||||
r := pm.CheckPort()
|
r := pm.CheckPort()
|
||||||
if r.Ready {
|
if r.Ready {
|
||||||
@@ -879,6 +1145,7 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
|||||||
if pm.isFRPReady(stdout) {
|
if pm.isFRPReady(stdout) {
|
||||||
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)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("[DEBUG] 端口已就绪,等待 frpc 初始化...")
|
log.Printf("[DEBUG] 端口已就绪,等待 frpc 初始化...")
|
||||||
@@ -893,15 +1160,17 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
|||||||
if info := pm.getFRPCStatus(cmd.Process.Pid); info != nil && info.Version != "" {
|
if info := pm.getFRPCStatus(cmd.Process.Pid); info != nil && info.Version != "" {
|
||||||
log.Printf("[INFO] frpc admin API 可访问,版本: %s", info.Version)
|
log.Printf("[INFO] frpc admin API 可访问,版本: %s", info.Version)
|
||||||
pm.writePIDFile(cmd.Process.Pid)
|
pm.writePIDFile(cmd.Process.Pid)
|
||||||
|
pm.setPhase(PhaseRunning)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// 启动失败:杀掉进程
|
|
||||||
pm.killProcess(cmd.Process.Pid)
|
pm.killProcess(cmd.Process.Pid)
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
|
pm.setPhase(PhaseFailed)
|
||||||
return fmt.Errorf("frpc 启动超时: 进程存在但端口未就绪")
|
return fmt.Errorf("frpc 启动超时: 进程存在但端口未就绪")
|
||||||
}
|
}
|
||||||
|
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
|
pm.setPhase(PhaseFailed)
|
||||||
return fmt.Errorf("frpc 启动失败: 进程已退出")
|
return fmt.Errorf("frpc 启动失败: 进程已退出")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,6 +1181,7 @@ func (pm *ProcessManager) Stop(ctx context.Context) error {
|
|||||||
defer pm.Unlock()
|
defer pm.Unlock()
|
||||||
|
|
||||||
pm.expectedStop = true
|
pm.expectedStop = true
|
||||||
|
pm.setPhase(PhaseStopping)
|
||||||
return pm.stopLocked(ctx)
|
return pm.stopLocked(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,33 +1194,41 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error {
|
|||||||
exec.Command("pkill", "-f", "frpc").Run()
|
exec.Command("pkill", "-f", "frpc").Run()
|
||||||
}
|
}
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
|
pm.setPhase(PhaseStopped)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pm.isProcessAlive(pid) {
|
if !pm.isProcessAlive(pid) {
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
|
pm.setPhase(PhaseStopped)
|
||||||
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()
|
||||||
|
pm.setPhase(PhaseStopped)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := process.Signal(syscall.SIGTERM); err != nil {
|
if err := process.Signal(syscall.SIGTERM); err != nil {
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
|
pm.setPhase(PhaseStopped)
|
||||||
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) {
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
|
pm.setPhase(PhaseStopped)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
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 {
|
||||||
@@ -959,10 +1237,12 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
if pm.isProcessAlive(pid) {
|
if pm.isProcessAlive(pid) {
|
||||||
|
pm.setPhase(PhaseFailed)
|
||||||
return fmt.Errorf("强制停止失败: 进程仍存活 (PID: %d)", pid)
|
return fmt.Errorf("强制停止失败: 进程仍存活 (PID: %d)", pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
pm.deletePIDFile()
|
pm.deletePIDFile()
|
||||||
|
pm.setPhase(PhaseStopped)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,6 +1262,54 @@ func (pm *ProcessManager) Restart(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 健康检查看门狗(由 main.go 调用)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func (pm *ProcessManager) StartHealthMonitor(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(HealthCheckInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
pm.runHealthCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) runHealthCheck() {
|
||||||
|
state, err := pm.Status()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] 健康检查失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据状态执行自动恢复
|
||||||
|
switch state.Phase {
|
||||||
|
case PhaseConflict:
|
||||||
|
log.Printf("[WARN] 检测到冲突,尝试自动恢复...")
|
||||||
|
// 冲突恢复:清理未知实例
|
||||||
|
for _, inst := range state.Conflicts {
|
||||||
|
if !inst.Owned {
|
||||||
|
pm.killProcess(inst.PID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pm.setPhase(PhaseStarting)
|
||||||
|
case PhaseDegraded:
|
||||||
|
log.Printf("[WARN] frpc 处于降级状态,尝试恢复...")
|
||||||
|
// 降级恢复:重启
|
||||||
|
pm.Restart(context.Background())
|
||||||
|
case PhaseFailed:
|
||||||
|
if !state.ExpectedStop {
|
||||||
|
log.Printf("[WARN] frpc 已失败,自动重启...")
|
||||||
|
pm.Start(context.Background())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 健康检查
|
// 健康检查
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -989,15 +1317,14 @@ func (pm *ProcessManager) Restart(ctx context.Context) error {
|
|||||||
func (pm *ProcessManager) HealthCheck() map[string]interface{} {
|
func (pm *ProcessManager) HealthCheck() map[string]interface{} {
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]interface{})
|
||||||
result["admin_port"] = pm.adminPort
|
result["admin_port"] = pm.adminPort
|
||||||
|
result["phase"] = pm.CurrentPhase()
|
||||||
|
|
||||||
state, err := pm.Status()
|
state, err := pm.Status()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result["phase"] = "ERROR"
|
|
||||||
result["error"] = err.Error()
|
result["error"] = err.Error()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
result["phase"] = state.Phase
|
|
||||||
result["pid"] = state.PID
|
result["pid"] = state.PID
|
||||||
result["port"] = state.Port
|
result["port"] = state.Port
|
||||||
result["alive"] = state.Alive
|
result["alive"] = state.Alive
|
||||||
@@ -1012,6 +1339,9 @@ func (pm *ProcessManager) HealthCheck() map[string]interface{} {
|
|||||||
if state.Version != "" {
|
if state.Version != "" {
|
||||||
result["version"] = state.Version
|
result["version"] = state.Version
|
||||||
}
|
}
|
||||||
|
if len(state.Conflicts) > 0 {
|
||||||
|
result["conflicts"] = state.Conflicts
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user