Files
frpc-console/internal/process/manager.go
T

975 lines
24 KiB
Go

// internal/process/manager.go
// frpc-console 进程管理模块
// 2.6-preview: 状态机驱动 + 端口检测 + 单实例锁定
package process
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
// ================================================================
// 常量定义
// ================================================================
const (
LockFileName = ".frpc.lock"
PortCheckTimeout = 500 * time.Millisecond
StopMaxWaitTime = 5 * time.Second
LockAcquireTimeout = 30 * time.Second
LockRetryInterval = 100 * time.Millisecond
APITimeout = 2 * time.Second
StartupMaxAttempts = 50
StartupRetryDelay = 200 * time.Millisecond
StartupTimeout = 10 * time.Second
)
// ================================================================
// 进程状态定义
// ================================================================
type ProcessPhase string
const (
PhaseUnknown ProcessPhase = "UNKNOWN"
PhaseStarting ProcessPhase = "STARTING"
PhaseRunning ProcessPhase = "RUNNING"
PhaseDegraded ProcessPhase = "DEGRADED"
PhaseFailed ProcessPhase = "FAILED"
PhaseStopped ProcessPhase = "STOPPED"
)
// ================================================================
// 数据结构
// ================================================================
type PortCheckResult struct {
Ready bool
Err error
}
type ProcessState struct {
Phase ProcessPhase `json:"phase"`
PID int `json:"pid"`
Port int `json:"port"`
StartedAt time.Time `json:"started_at"`
ExitCode int `json:"exit_code,omitempty"`
Alive bool `json:"alive"`
PortReady bool `json:"port_ready"`
PortError string `json:"port_error,omitempty"`
FRPReady bool `json:"frp_ready"`
Version string `json:"version,omitempty"`
ExpectedStop bool `json:"expected_stop"`
LastOutput string `json:"last_output,omitempty"`
LastError string `json:"last_error,omitempty"`
Error string `json:"error,omitempty"`
}
type PortStatus struct {
Port int `json:"port"`
Occupied bool `json:"occupied"`
PID int `json:"pid"`
IsFRPC bool `json:"is_frpc"`
ProcessCmd string `json:"process_cmd,omitempty"`
}
type FRPCStatus struct {
Version string `json:"version"`
RunID string `json:"run_id"`
Proxies []struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
LocalAddr string `json:"local_addr"`
} `json:"proxies"`
}
// ================================================================
// ProcessManager 主结构
// ================================================================
type ProcessManager struct {
mu sync.Mutex
dataDir string
configPath string
frpcBinPath string
adminPort int
startTime time.Time
expectedStop bool
exitCode int
exitMu sync.Mutex
lastOutput string
lastError string
outputMu sync.Mutex
lockFile *os.File
locked bool
}
var (
globalManager *ProcessManager
globalMu sync.Mutex
)
// ================================================================
// 构造函数
// ================================================================
func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager {
return &ProcessManager{
dataDir: dataDir,
configPath: configPath,
frpcBinPath: frpcBinPath,
adminPort: 0,
}
}
func SetGlobalManager(pm *ProcessManager) {
globalMu.Lock()
defer globalMu.Unlock()
globalManager = pm
}
func GetGlobalManager() *ProcessManager {
globalMu.Lock()
defer globalMu.Unlock()
return globalManager
}
func (pm *ProcessManager) AdminPort() int {
return pm.adminPort
}
// ================================================================
// 配置读取
// ================================================================
func (pm *ProcessManager) LoadConfig() error {
content, err := os.ReadFile(pm.configPath)
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
log.Printf("[DEBUG] LoadConfig 读取到文件,长度: %d 字节", len(content))
preview := string(content)
if len(preview) > 600 {
preview = preview[:600] + "\n... (截断)"
}
log.Printf("[DEBUG] 文件内容预览:\n%s", preview)
if port := extractIntValue(string(content), "admin_port"); port > 0 {
pm.adminPort = port
log.Printf("[DEBUG] ✅ 从 admin_port 解析到端口: %d", port)
return nil
}
log.Printf("[DEBUG] ❌ admin_port 未找到")
if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 {
pm.adminPort = port
log.Printf("[DEBUG] ✅ 从 webServer.port 解析到端口: %d", port)
return nil
}
log.Printf("[DEBUG] ❌ webServer.port 未找到")
if port := extractPortFromAddrSection(string(content), "webServer", "addr"); port > 0 {
pm.adminPort = port
log.Printf("[DEBUG] ✅ 从 webServer.addr 解析到端口: %d", port)
return nil
}
log.Printf("[DEBUG] ❌ webServer.addr 未找到或解析失败")
return fmt.Errorf("未找到 admin_port 或 webServer.port/addr 配置")
}
func extractIntValue(content, key string) int {
lines := strings.Split(content, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, key) {
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) == 2 {
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, `"`)
if port, err := strconv.Atoi(val); err == nil && port > 0 {
return port
}
}
}
}
return 0
}
func extractIntValueFromSection(content, section, key string) int {
lines := strings.Split(content, "\n")
inSection := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
sectionName := strings.TrimSpace(strings.Trim(trimmed, "[]"))
inSection = strings.EqualFold(sectionName, section)
continue
}
if inSection && strings.HasPrefix(trimmed, key) {
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) == 2 {
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, `"`)
if port, err := strconv.Atoi(val); err == nil && port > 0 {
return port
}
}
}
}
return 0
}
func extractPortFromAddrSection(content, section, key string) int {
lines := strings.Split(content, "\n")
inSection := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
sectionName := strings.TrimSpace(strings.Trim(trimmed, "[]"))
inSection = strings.EqualFold(sectionName, section)
continue
}
if inSection && strings.HasPrefix(trimmed, key) {
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) == 2 {
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, `"`)
if idx := strings.LastIndex(val, ":"); idx != -1 {
portStr := val[idx+1:]
if port, err := strconv.Atoi(portStr); err == nil && port > 0 {
return port
}
}
}
}
}
return 0
}
// ================================================================
// 端口检测
// ================================================================
func (pm *ProcessManager) CheckPort() PortCheckResult {
if pm.adminPort <= 0 {
return PortCheckResult{Ready: false, Err: fmt.Errorf("admin_port 未配置")}
}
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout)
if err != nil {
return PortCheckResult{Ready: false, Err: err}
}
conn.Close()
return PortCheckResult{Ready: true, Err: nil}
}
func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) {
status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false}
result := pm.CheckPort()
if result.Err != nil {
return status, result.Err
}
if !result.Ready {
return status, nil
}
status.Occupied = true
pid, err := pm.getPIDByPort(pm.adminPort)
if err != nil {
if pidFromFile := pm.readPIDFile(); pidFromFile > 0 {
if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) {
status.PID = pidFromFile
}
}
} else {
status.PID = pid
}
if status.PID == 0 {
return status, nil
}
isFRPC, cmd := pm.isFRPCProcess(status.PID)
status.IsFRPC = isFRPC
status.ProcessCmd = cmd
return status, nil
}
func (pm *ProcessManager) getPIDByPort(port int) (int, error) {
if pid, err := pm.getPIDBySS(port); err == nil && pid > 0 {
return pid, nil
}
if pid, err := pm.getPIDByNetstat(port); err == nil && pid > 0 {
return pid, nil
}
if pid, err := pm.getPIDByLsof(port); err == nil && pid > 0 {
return pid, nil
}
return 0, fmt.Errorf("无法通过端口反查 PID")
}
func (pm *ProcessManager) getPIDBySS(port int) (int, error) {
cmd := exec.Command("ss", "-lpn", "state", "listening")
out, err := cmd.Output()
if err != nil {
return 0, err
}
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, fmt.Sprintf(":%d", port)) {
continue
}
if idx := strings.Index(line, "pid="); idx != -1 {
end := strings.Index(line[idx:], ",")
if end == -1 {
end = strings.Index(line[idx:], ")")
}
if end == -1 {
continue
}
pidStr := line[idx+4 : idx+end]
pidStr = strings.TrimSpace(pidStr)
if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 {
return pid, nil
}
}
}
return 0, fmt.Errorf("未找到监听端口 %d 的进程", port)
}
func (pm *ProcessManager) getPIDByNetstat(port int) (int, error) {
cmd := exec.Command("netstat", "-tulpn")
out, err := cmd.Output()
if err != nil {
return 0, err
}
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, fmt.Sprintf(":%d", port)) {
continue
}
parts := strings.Fields(line)
if len(parts) < 7 {
continue
}
last := parts[len(parts)-1]
if idx := strings.Index(last, "/"); idx != -1 {
pidStr := last[:idx]
if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 {
return pid, nil
}
}
}
return 0, fmt.Errorf("未找到监听端口 %d 的进程", port)
}
func (pm *ProcessManager) getPIDByLsof(port int) (int, error) {
cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN")
out, err := cmd.Output()
if err != nil {
return 0, err
}
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "frpc") {
parts := strings.Fields(line)
if len(parts) >= 2 {
if pid, err := strconv.Atoi(parts[1]); err == nil && pid > 0 {
return pid, nil
}
}
}
}
return 0, fmt.Errorf("未找到监听端口 %d 的 frpc 进程", port)
}
func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool {
cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-a", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN")
out, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(out), "LISTEN")
}
func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) {
cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid)
if data, err := os.ReadFile(cmdlinePath); err == nil {
cmd := strings.ReplaceAll(string(data), "\x00", " ")
if strings.Contains(cmd, "frpc") {
return true, cmd
}
}
cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=")
out, err := cmd.Output()
if err == nil {
args := strings.TrimSpace(string(out))
if strings.Contains(args, "frpc") {
return true, args
}
}
return false, ""
}
// ================================================================
// 进程存活检测
// ================================================================
func (pm *ProcessManager) isProcessAlive(pid int) bool {
if pid <= 0 {
return false
}
if runtime.GOOS == "windows" {
cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid))
output, err := cmd.CombinedOutput()
if err != nil {
return false
}
return strings.Contains(string(output), strconv.Itoa(pid))
}
process, err := os.FindProcess(pid)
if err != nil {
return false
}
return process.Signal(syscall.Signal(0)) == nil
}
// ================================================================
// 进程清理
// ================================================================
func (pm *ProcessManager) killProcess(pid int) error {
if pid <= 0 {
return nil
}
if runtime.GOOS == "windows" {
cmd := exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid))
return cmd.Run()
}
process, err := os.FindProcess(pid)
if err != nil {
return err
}
return process.Kill()
}
func (pm *ProcessManager) cleanupOrphans() {
result := pm.CheckPort()
if !result.Ready {
return
}
pid := pm.readPIDFile()
if pid > 0 && pm.isProcessAlive(pid) {
return
}
log.Printf("[WARN] 检测到孤儿 frpc 进程 (端口 %d 被占用但无有效 PID),正在清理...", pm.adminPort)
if runtime.GOOS == "windows" {
exec.Command("taskkill", "/F", "/IM", "frpc.exe").Run()
} else {
exec.Command("pkill", "-f", "frpc").Run()
}
pm.deletePIDFile()
time.Sleep(1 * time.Second)
}
// ================================================================
// PID 文件操作
// ================================================================
func (pm *ProcessManager) pidFilePath() string {
return filepath.Join(pm.dataDir, "frpc.pid")
}
func (pm *ProcessManager) readPIDFile() int {
data, err := os.ReadFile(pm.pidFilePath())
if err != nil {
return 0
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil || pid <= 0 {
return 0
}
return pid
}
func (pm *ProcessManager) writePIDFile(pid int) error {
return os.WriteFile(pm.pidFilePath(), []byte(strconv.Itoa(pid)), 0644)
}
func (pm *ProcessManager) deletePIDFile() error {
err := os.Remove(pm.pidFilePath())
if os.IsNotExist(err) {
return nil
}
return err
}
// ================================================================
// stdout/stderr 捕获
// ================================================================
func (pm *ProcessManager) captureOutput(reader io.ReadCloser, isError bool) {
defer reader.Close()
scanner := bufio.NewScanner(reader)
buf := make([]byte, 64*1024)
scanner.Buffer(buf, 256*1024)
var lines []string
const maxLines = 20
for scanner.Scan() {
line := scanner.Text()
pm.outputMu.Lock()
if isError {
pm.lastError = line
} else {
pm.lastOutput = line
}
pm.outputMu.Unlock()
if isError {
lines = append(lines, line)
if len(lines) > maxLines {
lines = lines[1:]
}
pm.outputMu.Lock()
pm.lastError = strings.Join(lines, "\n")
pm.outputMu.Unlock()
} else {
lines = append(lines, line)
if len(lines) > maxLines {
lines = lines[1:]
}
pm.outputMu.Lock()
pm.lastOutput = strings.Join(lines, "\n")
pm.outputMu.Unlock()
}
}
}
func (pm *ProcessManager) getLastOutput() string {
pm.outputMu.Lock()
defer pm.outputMu.Unlock()
return pm.lastOutput
}
func (pm *ProcessManager) getLastError() string {
pm.outputMu.Lock()
defer pm.outputMu.Unlock()
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
}
// ================================================================
// 状态计算
// ================================================================
func (pm *ProcessManager) computeState(pid int, result PortCheckResult) ProcessState {
alive := pm.isProcessAlive(pid)
portReady := result.Ready
portErr := result.Err
stdout := pm.getLastOutput()
stderr := pm.getLastError()
frpReady := pm.isFRPReady(stdout)
// 获取退出码
pm.exitMu.Lock()
exitCode := pm.exitCode
pm.exitMu.Unlock()
state := ProcessState{
PID: pid,
Port: pm.adminPort,
Alive: alive,
PortReady: portReady,
PortError: "",
FRPReady: frpReady,
StartedAt: pm.startTime,
ExpectedStop: pm.expectedStop,
LastOutput: stdout,
LastError: stderr,
ExitCode: exitCode,
}
if portErr != nil {
state.PortError = portErr.Error()
}
// ---- 状态判定逻辑 ----
if !alive {
if pm.expectedStop {
state.Phase = PhaseStopped
return state
}
// 异常退出
if exitCode != 0 && exitCode != -1 {
state.Error = fmt.Sprintf("进程异常退出 (exit code: %d)", exitCode)
if stderr != "" {
state.Error += ": " + stderr
}
} else {
state.Error = "进程意外退出"
if stderr != "" {
state.Error += ": " + stderr
}
}
state.Phase = PhaseFailed
return state
}
// ---- alive == true ----
if time.Since(pm.startTime) > StartupTimeout {
state.Error = fmt.Sprintf("启动超时 (超过 %v)", StartupTimeout)
if portErr != nil {
state.Error += ": " + portErr.Error()
}
state.Phase = PhaseFailed
return state
}
if portReady && frpReady {
state.Phase = PhaseRunning
return state
}
if portReady && !frpReady {
state.Phase = PhaseDegraded
state.Error = "端口已监听,但 frpc 未报告就绪"
return state
}
if !portReady {
if portErr != nil && strings.Contains(portErr.Error(), "permission denied") {
state.Phase = PhaseDegraded
state.Error = "端口检测权限不足: " + portErr.Error()
return state
}
state.Phase = PhaseStarting
if portErr != nil {
state.Error = "等待端口就绪: " + portErr.Error()
}
return state
}
state.Phase = PhaseUnknown
return state
}
// ================================================================
// 状态查询
// ================================================================
func (pm *ProcessManager) Status() (*ProcessState, error) {
pid := pm.readPIDFile()
result := pm.CheckPort()
state := pm.computeState(pid, result)
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
}
// ================================================================
// 操作执行
// ================================================================
// Lock 由平台文件 (lock_*.go) 实现
func (pm *ProcessManager) Start(ctx context.Context) error {
if err := pm.Lock(); err != nil {
return fmt.Errorf("获取锁失败: %w", err)
}
defer pm.Unlock()
pm.cleanupOrphans()
return pm.startLocked(ctx)
}
func (pm *ProcessManager) startLocked(ctx context.Context) error {
result := pm.CheckPort()
if result.Ready {
pid := pm.readPIDFile()
if pid > 0 && pm.isProcessAlive(pid) {
log.Printf("[INFO] frpc 已在运行 (PID: %d)", pid)
return nil
}
pm.cleanupOrphans()
}
pm.startTime = time.Now()
pm.expectedStop = false
// 重置退出码
pm.exitMu.Lock()
pm.exitCode = -1
pm.exitMu.Unlock()
cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath)
setProcessAttributes(cmd)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("创建 stdout pipe 失败: %w", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("创建 stderr pipe 失败: %w", err)
}
go pm.captureOutput(stdoutPipe, false)
go pm.captureOutput(stderrPipe, true)
if err := cmd.Start(); err != nil {
return fmt.Errorf("启动 frpc 失败: %w", err)
}
if err := pm.writePIDFile(cmd.Process.Pid); err != nil {
log.Printf("[WARN] 写入 PID 文件失败: %v", err)
}
log.Printf("[DEBUG] frpc 进程已启动,PID: %d", cmd.Process.Pid)
go func() {
err := cmd.Wait()
var code int
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
code = exitErr.ExitCode()
} else {
code = -1
}
} else {
code = 0
}
pm.exitMu.Lock()
pm.exitCode = code
pm.exitMu.Unlock()
log.Printf("[INFO] frpc 进程 (PID: %d) 已退出,退出码: %d", cmd.Process.Pid, code)
if !pm.expectedStop && code != 0 {
log.Printf("[WARN] frpc 进程异常退出 (exit code: %d)", code)
}
if !pm.expectedStop {
pm.deletePIDFile()
}
}()
for attempt := 0; attempt < StartupMaxAttempts; attempt++ {
r := pm.CheckPort()
if r.Ready {
stdout := pm.getLastOutput()
if pm.isFRPReady(stdout) {
log.Printf("[INFO] frpc 启动成功 (PID: %d, 端口: %d),耗时 %dms",
cmd.Process.Pid, pm.adminPort, attempt*int(StartupRetryDelay/time.Millisecond))
return nil
}
log.Printf("[DEBUG] 端口已就绪,等待 frpc 初始化...")
}
time.Sleep(StartupRetryDelay)
}
if pm.isProcessAlive(cmd.Process.Pid) {
stderr := pm.getLastError()
log.Printf("[WARN] frpc 启动超时 (PID: %d),当前 stderr: %s", cmd.Process.Pid, stderr)
// 进程还在但端口没起来,尝试获取 frpc admin API 版本信息
if info := pm.getFRPCStatus(cmd.Process.Pid); info != nil && info.Version != "" {
log.Printf("[INFO] frpc admin API 可访问,版本: %s", info.Version)
// API 可访问说明 frpc 已正常运行,只是端口检测有误
return nil
}
return fmt.Errorf("frpc 启动超时: 进程存在但端口未就绪")
}
pm.deletePIDFile()
return fmt.Errorf("frpc 启动失败: 进程已退出")
}
func (pm *ProcessManager) Stop(ctx context.Context) error {
if err := pm.Lock(); err != nil {
return fmt.Errorf("获取锁失败: %w", err)
}
defer pm.Unlock()
pm.expectedStop = true
return pm.stopLocked(ctx)
}
func (pm *ProcessManager) stopLocked(_ context.Context) error {
pid := pm.readPIDFile()
if pid <= 0 {
if runtime.GOOS == "windows" {
exec.Command("taskkill", "/F", "/IM", "frpc.exe").Run()
} else {
exec.Command("pkill", "-f", "frpc").Run()
}
pm.deletePIDFile()
return nil
}
if !pm.isProcessAlive(pid) {
pm.deletePIDFile()
return nil
}
process, err := os.FindProcess(pid)
if err != nil {
pm.deletePIDFile()
return nil
}
if err := process.Signal(syscall.SIGTERM); err != nil {
pm.deletePIDFile()
return nil
}
start := time.Now()
for time.Since(start) < StopMaxWaitTime {
if !pm.isProcessAlive(pid) {
pm.deletePIDFile()
return nil
}
time.Sleep(200 * time.Millisecond)
}
if runtime.GOOS == "windows" {
exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run()
} else {
process.Kill()
}
time.Sleep(500 * time.Millisecond)
if pm.isProcessAlive(pid) {
return fmt.Errorf("强制停止失败: 进程仍存活 (PID: %d)", pid)
}
pm.deletePIDFile()
return nil
}
func (pm *ProcessManager) Restart(ctx context.Context) error {
if err := pm.Lock(); err != nil {
return fmt.Errorf("获取锁失败: %w", err)
}
defer pm.Unlock()
if err := pm.stopLocked(ctx); err != nil {
return fmt.Errorf("停止失败: %w", err)
}
time.Sleep(1 * time.Second)
if err := pm.startLocked(ctx); err != nil {
return fmt.Errorf("启动失败: %w", err)
}
return nil
}
// ================================================================
// 健康检查
// ================================================================
func (pm *ProcessManager) HealthCheck() map[string]interface{} {
result := make(map[string]interface{})
result["admin_port"] = pm.adminPort
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["alive"] = state.Alive
result["port_ready"] = state.PortReady
result["frp_ready"] = state.FRPReady
if state.PortError != "" {
result["port_error"] = state.PortError
}
if state.Error != "" {
result["error"] = state.Error
}
if state.Version != "" {
result["version"] = state.Version
}
return result
}