首次优化的进程管理工具
This commit is contained in:
+387
-57
@@ -1,10 +1,7 @@
|
||||
// internal/process/manager.go
|
||||
// frpc-console 进程管理模块
|
||||
// 2.6-preview: 状态机驱动 + 端口检测 + 单实例锁定
|
||||
|
||||
// 作者留:这个版本可以用了,已经明显看到进程出来发现端口不对立马被杀掉的状态了
|
||||
// 但是稍显粗暴,差点意思,到时候让Deepseek稍微软化下就好了
|
||||
// 但是现阶段Preview还不能用,再等等
|
||||
// 2.6-preview: 状态机驱动 + 冲突检测 + 生命周期管理
|
||||
// 2.7-preview: 软化版 - 精确控制 + CONFLICT/STOPPING 状态
|
||||
|
||||
package process
|
||||
|
||||
@@ -44,6 +41,8 @@ const (
|
||||
StartupMaxAttempts = 50
|
||||
StartupRetryDelay = 200 * time.Millisecond
|
||||
StartupTimeout = 10 * time.Second
|
||||
|
||||
HealthCheckInterval = 15 * time.Second
|
||||
)
|
||||
|
||||
// ================================================================
|
||||
@@ -59,6 +58,8 @@ const (
|
||||
PhaseDegraded ProcessPhase = "DEGRADED"
|
||||
PhaseFailed ProcessPhase = "FAILED"
|
||||
PhaseStopped ProcessPhase = "STOPPED"
|
||||
PhaseStopping ProcessPhase = "STOPPING" // 正在停止中
|
||||
PhaseConflict ProcessPhase = "CONFLICT" // 检测到多实例冲突
|
||||
)
|
||||
|
||||
// ================================================================
|
||||
@@ -70,6 +71,14 @@ type PortCheckResult struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
type FrpcInstance struct {
|
||||
PID int
|
||||
ParentPID int
|
||||
ExecPath string
|
||||
CmdLine string
|
||||
Owned bool // 是否由当前 ProcessManager 管理
|
||||
}
|
||||
|
||||
type ProcessState struct {
|
||||
Phase ProcessPhase `json:"phase"`
|
||||
PID int `json:"pid"`
|
||||
@@ -88,8 +97,16 @@ type ProcessState struct {
|
||||
LastOutput string `json:"last_output,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
|
||||
// 冲突检测信息
|
||||
Conflicts []FrpcInstance `json:"conflicts,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
globalManager *ProcessManager
|
||||
globalMu sync.Mutex
|
||||
)
|
||||
|
||||
type PortStatus struct {
|
||||
Port int `json:"port"`
|
||||
Occupied bool `json:"occupied"`
|
||||
@@ -131,24 +148,28 @@ type ProcessManager struct {
|
||||
|
||||
lockFile *os.File
|
||||
locked bool
|
||||
}
|
||||
|
||||
var (
|
||||
globalManager *ProcessManager
|
||||
globalMu sync.Mutex
|
||||
)
|
||||
// 当前状态缓存
|
||||
currentPhase ProcessPhase
|
||||
statusMu sync.RWMutex
|
||||
|
||||
// 取消函数(用于停止健康检查)
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 构造函数
|
||||
// ================================================================
|
||||
|
||||
func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager {
|
||||
return &ProcessManager{
|
||||
dataDir: dataDir,
|
||||
configPath: configPath,
|
||||
frpcBinPath: frpcBinPath,
|
||||
adminPort: 0,
|
||||
pm := &ProcessManager{
|
||||
dataDir: dataDir,
|
||||
configPath: configPath,
|
||||
frpcBinPath: frpcBinPath,
|
||||
adminPort: 0,
|
||||
currentPhase: PhaseUnknown,
|
||||
}
|
||||
return pm
|
||||
}
|
||||
|
||||
func SetGlobalManager(pm *ProcessManager) {
|
||||
@@ -167,6 +188,21 @@ func (pm *ProcessManager) AdminPort() int {
|
||||
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
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 实例检测(精确扫描)
|
||||
// ================================================================
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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() {
|
||||
result := pm.CheckPort()
|
||||
if !result.Ready {
|
||||
@@ -673,7 +880,17 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
||||
state.PortError = portErr.Error()
|
||||
}
|
||||
|
||||
// ---- 状态判定逻辑 ----
|
||||
// ============================================================
|
||||
// 状态判定逻辑
|
||||
// ============================================================
|
||||
|
||||
// 1. 如果是主动停止,直接返回 STOPPING
|
||||
if pm.expectedStop && alive {
|
||||
state.Phase = PhaseStopping
|
||||
return state
|
||||
}
|
||||
|
||||
// 2. 进程不存在
|
||||
if !alive {
|
||||
if pm.expectedStop {
|
||||
state.Phase = PhaseStopped
|
||||
@@ -695,7 +912,7 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
||||
return state
|
||||
}
|
||||
|
||||
// ---- alive == true ----
|
||||
// 3. 进程存在,检查超时
|
||||
if time.Since(pm.startTime) > StartupTimeout {
|
||||
state.Error = fmt.Sprintf("启动超时 (超过 %v)", StartupTimeout)
|
||||
if portErr != nil {
|
||||
@@ -705,22 +922,34 @@ func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessS
|
||||
return state
|
||||
}
|
||||
|
||||
if portReady && frpReady {
|
||||
state.Phase = PhaseRunning
|
||||
// 检测多进程
|
||||
if count := pm.countFrpcProcesses(); count > 1 {
|
||||
state.Phase = PhaseDegraded
|
||||
state.Error = fmt.Sprintf("检测到 %d 个 frpc 进程,可能存在多开", count)
|
||||
// 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 portErr != nil && strings.Contains(portErr.Error(), "permission denied") {
|
||||
state.Phase = PhaseDegraded
|
||||
@@ -746,6 +975,7 @@ func (pm *ProcessManager) Status() (*ProcessState, error) {
|
||||
pid := pm.readPIDFile()
|
||||
result := pm.CheckPort()
|
||||
state := pm.computeState(pid, result)
|
||||
pm.setPhase(state.Phase)
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
@@ -796,12 +1026,43 @@ func (pm *ProcessManager) Start(ctx context.Context) error {
|
||||
}
|
||||
defer pm.Unlock()
|
||||
|
||||
// 先杀所有 frpc 进程(避免多开)
|
||||
pm.killAllFrpc()
|
||||
pm.expectedStop = false
|
||||
|
||||
// 清理孤儿
|
||||
// 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()
|
||||
|
||||
// 3. 正常启动
|
||||
return pm.startLocked(ctx)
|
||||
}
|
||||
|
||||
@@ -811,6 +1072,7 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
||||
pid := pm.readPIDFile()
|
||||
if pid > 0 && pm.isProcessAlive(pid) {
|
||||
log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid)
|
||||
pm.setPhase(PhaseRunning)
|
||||
return nil
|
||||
}
|
||||
pm.cleanupOrphans()
|
||||
@@ -847,6 +1109,8 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
||||
}
|
||||
log.Printf("[DEBUG] frpc 进程已启动,PID: %d", cmd.Process.Pid)
|
||||
|
||||
pm.setPhase(PhaseStarting)
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
var code int
|
||||
@@ -866,12 +1130,14 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
||||
|
||||
if !pm.expectedStop && code != 0 {
|
||||
log.Printf("[WARN] frpc 进程异常退出 (exit code: %d)", code)
|
||||
pm.setPhase(PhaseFailed)
|
||||
}
|
||||
if !pm.expectedStop {
|
||||
pm.deletePIDFile()
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待端口就绪
|
||||
for attempt := 0; attempt < StartupMaxAttempts; attempt++ {
|
||||
r := pm.CheckPort()
|
||||
if r.Ready {
|
||||
@@ -879,6 +1145,7 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
||||
if pm.isFRPReady(stdout) {
|
||||
log.Printf("[INFO] frpc 启动成功 (PID: %d, 端口: %d),耗时 %dms",
|
||||
cmd.Process.Pid, pm.adminPort, attempt*int(StartupRetryDelay/time.Millisecond))
|
||||
pm.setPhase(PhaseRunning)
|
||||
return nil
|
||||
}
|
||||
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 != "" {
|
||||
log.Printf("[INFO] frpc admin API 可访问,版本: %s", info.Version)
|
||||
pm.writePIDFile(cmd.Process.Pid)
|
||||
pm.setPhase(PhaseRunning)
|
||||
return nil
|
||||
}
|
||||
// 启动失败:杀掉进程
|
||||
pm.killProcess(cmd.Process.Pid)
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseFailed)
|
||||
return fmt.Errorf("frpc 启动超时: 进程存在但端口未就绪")
|
||||
}
|
||||
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseFailed)
|
||||
return fmt.Errorf("frpc 启动失败: 进程已退出")
|
||||
}
|
||||
|
||||
@@ -912,6 +1181,7 @@ func (pm *ProcessManager) Stop(ctx context.Context) error {
|
||||
defer pm.Unlock()
|
||||
|
||||
pm.expectedStop = true
|
||||
pm.setPhase(PhaseStopping)
|
||||
return pm.stopLocked(ctx)
|
||||
}
|
||||
|
||||
@@ -924,33 +1194,41 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error {
|
||||
exec.Command("pkill", "-f", "frpc").Run()
|
||||
}
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseStopped)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !pm.isProcessAlive(pid) {
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseStopped)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 发送 SIGTERM
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseStopped)
|
||||
return nil
|
||||
}
|
||||
if err := process.Signal(syscall.SIGTERM); err != nil {
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseStopped)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 等待进程退出
|
||||
start := time.Now()
|
||||
for time.Since(start) < StopMaxWaitTime {
|
||||
if !pm.isProcessAlive(pid) {
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseStopped)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 强制 kill
|
||||
if runtime.GOOS == "windows" {
|
||||
exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run()
|
||||
} else {
|
||||
@@ -959,10 +1237,12 @@ func (pm *ProcessManager) stopLocked(_ context.Context) error {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if pm.isProcessAlive(pid) {
|
||||
pm.setPhase(PhaseFailed)
|
||||
return fmt.Errorf("强制停止失败: 进程仍存活 (PID: %d)", pid)
|
||||
}
|
||||
|
||||
pm.deletePIDFile()
|
||||
pm.setPhase(PhaseStopped)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -982,6 +1262,54 @@ func (pm *ProcessManager) Restart(ctx context.Context) error {
|
||||
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{} {
|
||||
result := make(map[string]interface{})
|
||||
result["admin_port"] = pm.adminPort
|
||||
result["phase"] = pm.CurrentPhase()
|
||||
|
||||
state, err := pm.Status()
|
||||
if err != nil {
|
||||
result["phase"] = "ERROR"
|
||||
result["error"] = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
result["phase"] = state.Phase
|
||||
result["pid"] = state.PID
|
||||
result["port"] = state.Port
|
||||
result["alive"] = state.Alive
|
||||
@@ -1012,6 +1339,9 @@ func (pm *ProcessManager) HealthCheck() map[string]interface{} {
|
||||
if state.Version != "" {
|
||||
result["version"] = state.Version
|
||||
}
|
||||
if len(state.Conflicts) > 0 {
|
||||
result["conflicts"] = state.Conflicts
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user