diff --git a/frp.go b/frp.go index aef4884..b5c2066 100644 --- a/frp.go +++ b/frp.go @@ -29,7 +29,7 @@ var ( ) // ================================================================ -// frpc 二进制提取 (保持不变) +// frpc 二进制提取 // ================================================================ func getFrpcPath() (string, error) { @@ -94,7 +94,7 @@ func getFrpcPath() (string, error) { } // ================================================================ -// 配置生成 (保持不变) +// 配置生成 // ================================================================ func GenerateFrpcConfig() error { @@ -164,14 +164,10 @@ func GenerateFrpcConfig() error { } // ================================================================ -// 进程管理 (兼容层,实际调用 ProcessManager) -// 2.6-preview: 所有进程管理逻辑迁移到 process_manager.go +// 进程管理兼容层 // ================================================================ -// isFrpcRunning 检查 frpc 是否在运行 -// 优先使用 ProcessManager,降级到 PID 文件 func isFrpcRunning() bool { - // 如果 ProcessManager 已初始化,使用它 if processManager != nil { status, err := processManager.Status() if err != nil { @@ -183,7 +179,6 @@ func isFrpcRunning() bool { return isFrpcRunningLegacy() } -// isFrpcRunningLegacy 旧版 PID 文件检测 (降级方案) func isFrpcRunningLegacy() bool { pidData, err := os.ReadFile("./data/frpc.pid") if err != nil { @@ -193,7 +188,6 @@ func isFrpcRunningLegacy() bool { if err != nil { return false } - if runtime.GOOS == "windows" { cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid)) output, err := cmd.CombinedOutput() @@ -202,7 +196,6 @@ func isFrpcRunningLegacy() bool { } return strings.Contains(string(output), strconv.Itoa(pid)) } - process, err := os.FindProcess(pid) if err != nil { return false @@ -210,8 +203,6 @@ func isFrpcRunningLegacy() bool { return process.Signal(syscall.Signal(0)) == nil } -// StartFrpc 启动 frpc (幂等) -// 优先使用 ProcessManager,降级到旧逻辑 func StartFrpc() error { if processManager != nil { log.Println("[INFO] 使用 ProcessManager 启动 frpc") @@ -221,60 +212,47 @@ func StartFrpc() error { return startFrpcLegacy() } -// startFrpcLegacy 旧版启动逻辑 (降级方案) func startFrpcLegacy() error { frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } - if err := os.MkdirAll("./data", 0755); err != nil { return fmt.Errorf("创建 data 目录失败: %w", err) } - if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) { if err := GenerateFrpcConfig(); err != nil { return fmt.Errorf("生成配置文件失败: %w", err) } } - if isFrpcRunningLegacy() { return nil } - os.Remove("./data/frpc.pid") - cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml") setWindowHide(cmd) setSysProcAttr(cmd) - logFile, err := os.OpenFile("./data/frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return fmt.Errorf("打开日志文件失败: %w", err) } cmd.Stdout = logFile cmd.Stderr = logFile - if err := cmd.Start(); err != nil { return fmt.Errorf("启动 frpc 失败: %w", err) } - go func() { if err := cmd.Wait(); err != nil { log.Printf("frpc 子进程退出: %v", err) } os.Remove("./data/frpc.pid") }() - if err := os.WriteFile("./data/frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil { return fmt.Errorf("保存 PID 失败: %w", err) } - return nil } -// StopFrpc 停止 frpc (幂等) -// 优先使用 ProcessManager,降级到旧逻辑 func StopFrpc() error { if processManager != nil { log.Println("[INFO] 使用 ProcessManager 停止 frpc") @@ -284,7 +262,6 @@ func StopFrpc() error { return stopFrpcLegacy() } -// stopFrpcLegacy 旧版停止逻辑 (降级方案) func stopFrpcLegacy() error { if runtime.GOOS == "windows" { cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe") @@ -294,7 +271,6 @@ func stopFrpcLegacy() error { os.Remove("./data/frpc.pid") return nil } - pidData, err := os.ReadFile("./data/frpc.pid") if err != nil { cmd := exec.Command("pkill", "-f", "frpc") @@ -303,24 +279,19 @@ func stopFrpcLegacy() error { } return nil } - pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData))) process, err := os.FindProcess(pid) if err != nil { os.Remove("./data/frpc.pid") return nil } - if err := process.Kill(); err != nil { return fmt.Errorf("杀死进程失败: %w", err) } - os.Remove("./data/frpc.pid") return nil } -// RestartFrpc 重启 frpc (原子操作) -// 优先使用 ProcessManager,降级到旧逻辑 func RestartFrpc() error { if processManager != nil { log.Println("[INFO] 使用 ProcessManager 重启 frpc") @@ -333,8 +304,6 @@ func RestartFrpc() error { return startFrpcLegacy() } -// GetFrpcStatus 获取 frpc 详细状态 (供 API 调用) -// 优先使用 ProcessManager,降级到旧逻辑 func GetFrpcStatus() (map[string]interface{}, error) { if processManager != nil { status, err := processManager.Status() @@ -347,73 +316,57 @@ func GetFrpcStatus() (map[string]interface{}, error) { "port": status.Port, }, nil } - - // 降级: 返回简单布尔值 running := isFrpcRunningLegacy() return map[string]interface{}{ - "state": map[bool]string{true: "running", false: "stopped"}[running], - "pid": 0, - "port": 0, - "legacy": true, + "state": map[bool]string{true: "running", false: "stopped"}[running], + "pid": 0, + "port": 0, + "legacy": true, }, nil } -// ReloadFrpc 热加载 frpc 配置 -// 优先尝试 reload,失败则降级到重启 func ReloadFrpc() error { - // 如果 ProcessManager 未初始化,降级到旧逻辑 if processManager == nil { return reloadFrpcLegacy() } - - // 检查 frpc 是否在运行 status, err := processManager.Status() if err != nil { return fmt.Errorf("获取 frpc 状态失败: %w", err) } - if status.State != "running" { - // 未运行,直接启动 return processManager.Start(nil) } - - // 尝试热加载 (使用 frpc reload 命令) frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } - cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") output, err := cmd.CombinedOutput() if err != nil { log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) log.Printf(" reload 输出: %s", string(output)) - // 降级: 重启 if err := processManager.Restart(nil); err != nil { return fmt.Errorf("重启 frpc 失败: %w", err) } return nil } - - log.Printf("✅ frpc 热加载成功") + log.Printf("✅ frpc 热加载成功: %s", string(output)) return nil } -// reloadFrpcLegacy 旧版热加载 (降级方案) func reloadFrpcLegacy() error { if !isFrpcRunningLegacy() { return startFrpcLegacy() } - frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } - cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") output, err := cmd.CombinedOutput() if err != nil { log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) + log.Printf(" reload 输出: %s", string(output)) if stopErr := stopFrpcLegacy(); stopErr != nil { return fmt.Errorf("停止 frpc 失败: %w", stopErr) } @@ -422,15 +375,14 @@ func reloadFrpcLegacy() error { } return nil } - log.Printf("✅ frpc 热加载成功 (兼容模式)") + log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output)) return nil } // ================================================================ -// 日志读取 (保持不变) +// 日志读取 // ================================================================ -// readTailLog 读取文件末尾 n 行 func readTailLog(filePath string, n int) ([]string, error) { file, err := os.Open(filePath) if err != nil { @@ -502,4 +454,4 @@ func readTailLog(filePath string, n int) ([]string, error) { } return lines, nil -} \ No newline at end of file +} diff --git a/frp_windows.go b/frp_windows.go index b776362..51a045d 100644 --- a/frp_windows.go +++ b/frp_windows.go @@ -7,15 +7,12 @@ import ( "syscall" ) -// setWindowHide Windows 隐藏窗口 func setWindowHide(cmd *exec.Cmd) { - if cmd.SysProcAttr == nil { - cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, } - cmd.SysProcAttr.HideWindow = true } -// setSysProcAttr Windows 不需要 Setsid,空操作 func setSysProcAttr(cmd *exec.Cmd) { - // Windows 不需要 Setsid,什么都不做 + // Windows 不支持 Setpgid,不需要额外设置 } diff --git a/go.mod b/go.mod index 17847e1..a17a9cb 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module frpc-console -go 1.25.0 +go 1.26.5 require ( github.com/gin-gonic/gin v1.12.0 github.com/golang-jwt/jwt/v5 v5.3.1 golang.org/x/crypto v0.54.0 + golang.org/x/sys v0.47.0 modernc.org/sqlite v1.54.0 ) @@ -39,7 +40,6 @@ require ( go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/net v0.56.0 // indirect - golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.40.0 // indirect google.golang.org/protobuf v1.36.10 // indirect modernc.org/libc v1.74.1 // indirect diff --git a/main.go b/main.go index eadd338..e5970ce 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,7 @@ func main() { // 2.6-preview: 使用 ProcessManager 管理 frpc 进程 // ============================================================ processManager = NewProcessManager( - "./data", // 数据目录 + "./data", // 数据目录 "./data/frpc.toml", // 配置文件路径 "./data/frpc", // frpc 二进制路径 ) @@ -106,4 +106,4 @@ func startWatchdog() { } } } -} \ No newline at end of file +} diff --git a/process_manager.go b/process_manager.go index 2139c69..aa6dd9a 100644 --- a/process_manager.go +++ b/process_manager.go @@ -1,12 +1,6 @@ // process_manager.go // frpc-console 进程管理模块 // 2.6-preview: 端口检测 + 单实例锁定 + 状态自述 -// -// 设计原则: -// 1. 端口是事实来源,PID 文件是缓存 -// 2. 所有操作幂等 (多次调用结果一致) -// 3. 通过文件锁 (flock) 实现进程间互斥 -// 4. 通过 admin_port API 获取 frpc 真实状态 package main @@ -18,6 +12,7 @@ import ( "fmt" "io" "net" + "net/http" "os" "os/exec" "path/filepath" @@ -26,6 +21,8 @@ import ( "sync" "syscall" "time" + + "golang.org/x/sys/unix" ) // ================================================================ @@ -33,57 +30,40 @@ import ( // ================================================================ const ( - // 锁文件路径 (相对于 DataDir) - LockFileName = ".frpc.lock" - - // 端口检测超时 - PortCheckTimeout = 500 * time.Millisecond - - // 启动后等待端口就绪的时间 - StartWaitTime = 500 * time.Millisecond - - // 停止时等待端口释放的最大时间 - StopMaxWaitTime = 5 * time.Second - - // 锁获取超时 + LockFileName = ".frpc.lock" + PortCheckTimeout = 500 * time.Millisecond + StartWaitTime = 500 * time.Millisecond + StopMaxWaitTime = 5 * time.Second LockAcquireTimeout = 30 * time.Second - - // 锁重试间隔 - LockRetryInterval = 100 * time.Millisecond - LockMaxRetries = 5 - - // API 请求超时 - APITimeout = 2 * time.Second + LockRetryInterval = 100 * time.Millisecond + APITimeout = 2 * time.Second ) // ================================================================ // 数据结构 // ================================================================ -// PortStatus 端口检测结果 type PortStatus struct { - Port int `json:"port"` - Occupied bool `json:"occupied"` - PID int `json:"pid"` - IsFRPC bool `json:"is_frpc"` + Port int `json:"port"` + Occupied bool `json:"occupied"` + PID int `json:"pid"` + IsFRPC bool `json:"is_frpc"` ProcessCmd string `json:"process_cmd,omitempty"` } -// ProcessStatus frpc 进程状态 type ProcessStatus struct { - State string `json:"state"` // "running" | "stopped" | "unknown" | "conflict" - PID int `json:"pid"` // 进程 PID (如果运行中) - Port int `json:"port"` // 监听的端口 - Uptime string `json:"uptime"` // 运行时长 (可选) - Version string `json:"version"` // frpc 版本 (如果 API 可访问) + State string `json:"state"` + PID int `json:"pid"` + Port int `json:"port"` + Uptime string `json:"uptime,omitempty"` + Version string `json:"version,omitempty"` Error string `json:"error,omitempty"` } -// FRPCStatus 来自 frpc admin API 的状态响应 type FRPCStatus struct { - Version string `json:"version"` - RunID string `json:"run_id"` - Proxies []struct { + Version string `json:"version"` + RunID string `json:"run_id"` + Proxies []struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` @@ -96,24 +76,21 @@ type FRPCStatus struct { // ================================================================ type ProcessManager struct { - mu sync.Mutex - - dataDir string // 数据目录 (存放 lock 和 pid 文件) - configPath string // frpc.toml 路径 - frpcBinPath string // frpc 二进制路径 - adminPort int // admin_port (从配置读取) - - lockFile *os.File // flock 文件句柄 - locked bool // 是否持有锁 + mu sync.Mutex + dataDir string + configPath string + frpcBinPath string + adminPort int + lockFile *os.File + locked bool } -// NewProcessManager 创建进程管理器 func NewProcessManager(dataDir, configPath, frpcBinPath string) *ProcessManager { return &ProcessManager{ dataDir: dataDir, configPath: configPath, frpcBinPath: frpcBinPath, - adminPort: 0, // 需要调用 LoadConfig 后确定 + adminPort: 0, } } @@ -121,35 +98,22 @@ func NewProcessManager(dataDir, configPath, frpcBinPath string) *ProcessManager // 配置读取 // ================================================================ -// LoadConfig 从 frpc.toml 读取 admin_port 配置 -// 兼容 frp 0.52.0 前后的配置格式 func (pm *ProcessManager) LoadConfig() error { - // 读取 frpc.toml 内容 content, err := os.ReadFile(pm.configPath) if err != nil { return fmt.Errorf("读取配置文件失败: %w", err) } - - // 尝试解析 admin_port (旧格式) - // admin_port = 7400 if port := extractIntValue(string(content), "admin_port"); port > 0 { pm.adminPort = port return nil } - - // 尝试解析 webServer.port (新格式, 0.52.0+) - // [webServer] - // port = 7400 if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 { pm.adminPort = port return nil } - - // 如果都找不到,说明 frpc 配置没有启用 admin 端口 return fmt.Errorf("未找到 admin_port 或 webServer.port 配置") } -// extractIntValue 从配置中提取 key = value 格式的值 func extractIntValue(content, key string) int { lines := strings.Split(content, "\n") for _, line := range lines { @@ -168,7 +132,6 @@ func extractIntValue(content, key string) int { return 0 } -// extractIntValueFromSection 从指定 section 中提取 key = value func extractIntValueFromSection(content, section, key string) int { lines := strings.Split(content, "\n") inSection := false @@ -193,110 +156,76 @@ func extractIntValueFromSection(content, section, key string) int { } // ================================================================ -// 端口检测器 (PortDetector) +// 端口检测 // ================================================================ -// CheckPort 检测端口是否被占用 func (pm *ProcessManager) CheckPort() (bool, error) { if pm.adminPort <= 0 { return false, fmt.Errorf("admin_port 未配置") } - conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout) if err != nil { - // 连接失败 = 端口未被占用 return false, nil } conn.Close() return true, nil } -// GetPortStatus 获取端口完整状态 (占用 + PID + 进程类型) func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) { - status := &PortStatus{ - Port: pm.adminPort, - Occupied: false, - PID: 0, - IsFRPC: false, - } - - // 1. 检测端口是否被占用 + status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false} occupied, err := pm.CheckPort() if err != nil { return status, err } status.Occupied = occupied - if !occupied { return status, nil } - // 2. 反查 PID pid, err := pm.getPIDByPort(pm.adminPort) if err != nil { - // 反查失败,尝试从 PID 文件读取 if pidFromFile := pm.readPIDFile(); pidFromFile > 0 { - status.PID = pidFromFile - // 验证这个 PID 是否真的在监听端口 if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) { status.PID = pidFromFile - } else { - status.PID = 0 } } } else { status.PID = pid } - if status.PID == 0 { return status, nil } - - // 3. 验证进程是否是 frpc isFRPC, cmd := pm.isFRPCProcess(status.PID) status.IsFRPC = isFRPC status.ProcessCmd = cmd - return status, nil } -// getPIDByPort 通过端口反查 PID -// 优先级: ss > netstat > lsof func (pm *ProcessManager) getPIDByPort(port int) (int, error) { - // 方法 1: ss -lpn (最可靠) if pid, err := pm.getPIDBySS(port); err == nil && pid > 0 { return pid, nil } - - // 方法 2: netstat -tulpn (兼容性广) if pid, err := pm.getPIDByNetstat(port); err == nil && pid > 0 { return pid, nil } - - // 方法 3: lsof -i :port (最后的备选) if pid, err := pm.getPIDByLsof(port); err == nil && pid > 0 { return pid, nil } - return 0, fmt.Errorf("无法通过端口反查 PID") } -// getPIDBySS 通过 ss 命令反查 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 } - // ss 输出格式: tcp LISTEN 0 128 0.0.0.0:7400 0.0.0.0:* users:(("frpc",pid=12345,fd=3)) - // 提取 pid=12345 if idx := strings.Index(line, "pid="); idx != -1 { end := strings.Index(line[idx:], ",") if end == -1 { @@ -315,27 +244,23 @@ func (pm *ProcessManager) getPIDBySS(port int) (int, error) { return 0, fmt.Errorf("未找到监听端口 %d 的进程", port) } -// getPIDByNetstat 通过 netstat 命令反查 PID 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 } - // netstat 输出格式: tcp 0 0 0.0.0.0:7400 0.0.0.0:* LISTEN 12345/frpc parts := strings.Fields(line) if len(parts) < 7 { continue } last := parts[len(parts)-1] - // 提取 PID: 12345/frpc if idx := strings.Index(last, "/"); idx != -1 { pidStr := last[:idx] if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 { @@ -346,19 +271,16 @@ func (pm *ProcessManager) getPIDByNetstat(port int) (int, error) { return 0, fmt.Errorf("未找到监听端口 %d 的进程", port) } -// getPIDByLsof 通过 lsof 命令反查 PID 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") { - // lsof 输出格式: frpc 12345 root 3u IPv4 123456 0t0 TCP *:7400 (LISTEN) parts := strings.Fields(line) if len(parts) >= 2 { if pid, err := strconv.Atoi(parts[1]); err == nil && pid > 0 { @@ -370,9 +292,7 @@ func (pm *ProcessManager) getPIDByLsof(port int) (int, error) { return 0, fmt.Errorf("未找到监听端口 %d 的 frpc 进程", port) } -// isProcessListeningOnPort 验证 PID 是否在监听指定端口 func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool { - // 通过 /proc 验证 cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-a", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN") out, err := cmd.Output() if err != nil { @@ -381,9 +301,7 @@ func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool { return strings.Contains(string(out), "LISTEN") } -// isFRPCProcess 验证进程是否是 frpc func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) { - // 方法 1: 读取 /proc//cmdline cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid) if data, err := os.ReadFile(cmdlinePath); err == nil { cmd := strings.ReplaceAll(string(data), "\x00", " ") @@ -391,8 +309,6 @@ func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) { return true, cmd } } - - // 方法 2: ps -p cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=") out, err := cmd.Output() if err == nil { @@ -401,7 +317,6 @@ func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) { return true, args } } - return false, "" } @@ -409,68 +324,51 @@ func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) { // 互斥锁 (flock) // ================================================================ -// Lock 获取进程间互斥锁 func (pm *ProcessManager) Lock() error { pm.mu.Lock() defer pm.mu.Unlock() - if pm.locked { - return nil // 已持有锁 + return nil } - lockPath := filepath.Join(pm.dataDir, LockFileName) - - // 确保数据目录存在 if err := os.MkdirAll(pm.dataDir, 0755); err != nil { return fmt.Errorf("创建数据目录失败: %w", err) } - file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644) if err != nil { return fmt.Errorf("打开锁文件失败: %w", err) } - - // 尝试获取排他锁 (阻塞) - // 使用 LOCK_EX | LOCK_NB 实现非阻塞尝试,然后手动重试 start := time.Now() for { - err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB) if err == nil { pm.lockFile = file pm.locked = true return nil } - if err != syscall.EWOULDBLOCK { file.Close() return fmt.Errorf("获取锁失败: %w", err) } - - // 检查超时 if time.Since(start) > LockAcquireTimeout { file.Close() return fmt.Errorf("获取锁超时 (超过 %v)", LockAcquireTimeout) } - time.Sleep(LockRetryInterval) } } -// Unlock 释放互斥锁 func (pm *ProcessManager) Unlock() error { pm.mu.Lock() defer pm.mu.Unlock() - if !pm.locked { return nil } - if pm.lockFile != nil { - syscall.Flock(int(pm.lockFile.Fd()), syscall.LOCK_UN) + unix.Flock(int(pm.lockFile.Fd()), unix.LOCK_UN) pm.lockFile.Close() pm.lockFile = nil } - pm.locked = false return nil } @@ -508,54 +406,37 @@ func (pm *ProcessManager) deletePIDFile() error { } // ================================================================ -// 状态查询 (Status) +// 状态查询 // ================================================================ -// Status 获取 frpc 进程实时状态 func (pm *ProcessManager) Status() (*ProcessStatus, error) { - status := &ProcessStatus{ - State: "unknown", - PID: 0, - Port: pm.adminPort, - } - + status := &ProcessStatus{State: "unknown", PID: 0, Port: pm.adminPort} if pm.adminPort <= 0 { - status.State = "unknown" status.Error = "admin_port 未配置" return status, nil } - - // 1. 获取端口状态 portStatus, err := pm.GetPortStatus() if err != nil { status.Error = err.Error() return status, nil } - if !portStatus.Occupied { - // 端口未被占用: 清理过期的 PID 文件 pm.deletePIDFile() status.State = "stopped" return status, nil } - if !portStatus.IsFRPC { status.State = "conflict" status.PID = portStatus.PID status.Error = fmt.Sprintf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID) return status, nil } - - // 2. 端口被 frpc 占用: 更新 PID 文件 status.State = "running" status.PID = portStatus.PID pm.writePIDFile(portStatus.PID) - - // 3. 尝试通过 API 获取更多信息 if info := pm.getFRPCStatus(portStatus.PID); info != nil { status.Version = info.Version } - return status, nil } @@ -563,42 +444,34 @@ func (pm *ProcessManager) Status() (*ProcessStatus, error) { // API 回源 (2.7-preview 预留) // ================================================================ -// getFRPCStatus 通过 admin API 获取 frpc 状态 (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 } @@ -606,38 +479,39 @@ func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus { // 操作执行 (Start / Stop / Restart) // ================================================================ -// Start 启动 frpc (幂等) func (pm *ProcessManager) Start(ctx context.Context) error { - // 1. 获取锁 if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) } defer pm.Unlock() + return pm.startLocked(ctx) +} - // 2. 双重检查: 端口是否已被占用 +func (pm *ProcessManager) startLocked(ctx context.Context) error { portStatus, err := pm.GetPortStatus() if err != nil { return fmt.Errorf("检测端口状态失败: %w", err) } - if portStatus.Occupied { if portStatus.IsFRPC { - // 已启动, 幂等返回 pm.writePIDFile(portStatus.PID) return nil } return fmt.Errorf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID) } - // 3. 启动 frpc - if err := pm.startFRPC(ctx); err != nil { - return err + cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath) + + // 根据平台设置 SysProcAttr + setProcessAttributes(cmd) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return fmt.Errorf("启动 frpc 失败: %w", err) } - - // 4. 等待端口就绪 time.Sleep(StartWaitTime) - - // 5. 验证启动成功 occupied, err := pm.CheckPort() if err != nil { return fmt.Errorf("验证启动状态失败: %w", err) @@ -645,88 +519,51 @@ func (pm *ProcessManager) Start(ctx context.Context) error { if !occupied { return fmt.Errorf("frpc 启动失败: 端口未监听") } - - // 6. 写入 PID 文件 pid, _ := pm.getPIDByPort(pm.adminPort) if pid > 0 { pm.writePIDFile(pid) } - return nil } -// startFRPC 实际执行 frpc 启动 (Setsid) -func (pm *ProcessManager) startFRPC(ctx context.Context) error { - // 构建启动命令 - cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath) - - // Setsid: 创建独立会话, 使 frpc 脱离 console 生命周期 - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } - - // 重定向输出 (可选) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - // 启动 - if err := cmd.Start(); err != nil { - return fmt.Errorf("启动 frpc 失败: %w", err) - } - - // 注意: 这里不 wait, 让 frpc 独立运行 - return nil -} - -// Stop 停止 frpc (幂等) func (pm *ProcessManager) Stop(ctx context.Context) error { - // 1. 获取锁 if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) } defer pm.Unlock() + return pm.stopLocked(ctx) +} - // 2. 检查端口状态 +func (pm *ProcessManager) stopLocked(ctx context.Context) error { portStatus, err := pm.GetPortStatus() if err != nil { return fmt.Errorf("检测端口状态失败: %w", err) } - if !portStatus.Occupied { - // 已停止, 幂等返回 pm.deletePIDFile() return nil } - var pid int if portStatus.IsFRPC { pid = portStatus.PID } else { - // 端口被非 frpc 占用, 不能强制停止 return fmt.Errorf("端口 %d 被非 frpc 进程占用, 无法安全停止", pm.adminPort) } - if pid <= 0 { - // 尝试从 PID 文件读取 pid = pm.readPIDFile() if pid <= 0 { return fmt.Errorf("无法确定 frpc 进程 PID") } } - - // 3. 发送 SIGTERM (优雅停止) proc, err := os.FindProcess(pid) if err != nil { - return fmt.Errorf("查找进程失败: %w", err) - } - - if err := proc.Signal(syscall.SIGTERM); err != nil { - // 可能进程已退出 pm.deletePIDFile() return nil } - - // 4. 等待端口释放 + if err := proc.Signal(syscall.SIGTERM); err != nil { + pm.deletePIDFile() + return nil + } start := time.Now() for time.Since(start) < StopMaxWaitTime { occupied, _ := pm.CheckPort() @@ -736,88 +573,41 @@ func (pm *ProcessManager) Stop(ctx context.Context) error { } time.Sleep(200 * time.Millisecond) } - - // 5. 端口未释放, 强制 kill proc.Kill() time.Sleep(500 * time.Millisecond) - - // 再次检查 - occupied, _ := pm.CheckPort() - if occupied { + if occupied, _ := pm.CheckPort(); occupied { return fmt.Errorf("强制停止失败: 端口仍被占用") } - pm.deletePIDFile() return nil } -// Restart 重启 frpc (原子操作) func (pm *ProcessManager) Restart(ctx context.Context) error { - // 1. 获取锁 (整个操作持有锁) if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) } defer pm.Unlock() - - // 2. 停止 if err := pm.stopLocked(ctx); err != nil { return fmt.Errorf("停止失败: %w", err) } - - // 3. 启动 if err := pm.startLocked(ctx); err != nil { return fmt.Errorf("启动失败: %w", err) } - - return nil -} - -// stopLocked 内部停止 (调用者必须持有锁) -func (pm *ProcessManager) stopLocked(ctx context.Context) error { - // 同 Stop 逻辑, 但跳过锁获取 - portStatus, err := pm.GetPortStatus() - if err != nil { - return err - } - if !portStatus.Occupied { - pm.deletePIDFile() - return nil - } - // ... 其余逻辑与 Stop 相同 - // (为节省篇幅, 这里省略重复代码, 实际实现可复用) - return nil -} - -// startLocked 内部启动 (调用者必须持有锁) -func (pm *ProcessManager) startLocked(ctx context.Context) error { - // 同 Start 逻辑, 但跳过锁获取 - portStatus, err := pm.GetPortStatus() - if err != nil { - return err - } - if portStatus.Occupied && portStatus.IsFRPC { - return nil - } - // ... 其余逻辑与 Start 相同 return nil } // ================================================================ -// 健康检查 (用于 WebUI 展示) +// 健康检查 // ================================================================ -// HealthCheck 返回简要健康状态 func (pm *ProcessManager) HealthCheck() map[string]interface{} { - result := make(map[string]interface{}) - result["admin_port"] = pm.adminPort - + result := map[string]interface{}{"admin_port": pm.adminPort} status, err := pm.Status() if err != nil { result["state"] = "error" result["error"] = err.Error() return result } - result["state"] = status.State result["pid"] = status.PID if status.Version != "" { @@ -826,6 +616,5 @@ func (pm *ProcessManager) HealthCheck() map[string]interface{} { if status.Error != "" { result["error"] = status.Error } - return result -} \ No newline at end of file +} diff --git a/process_manager_linux.go b/process_manager_linux.go new file mode 100644 index 0000000..b5c8d4e --- /dev/null +++ b/process_manager_linux.go @@ -0,0 +1,15 @@ +//go:build linux + +package main + +import ( + "os/exec" + "syscall" +) + +// setProcessAttributes 设置 Linux 进程属性 (Setpgid) +func setProcessAttributes(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } +} diff --git a/process_manager_lock_linux.go b/process_manager_lock_linux.go new file mode 100644 index 0000000..99ac018 --- /dev/null +++ b/process_manager_lock_linux.go @@ -0,0 +1,65 @@ +//go:build linux + +package main + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "golang.org/x/sys/unix" +) + +func (pm *ProcessManager) Lock() error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if pm.locked { + return nil + } + + lockPath := filepath.Join(pm.dataDir, LockFileName) + if err := os.MkdirAll(pm.dataDir, 0755); err != nil { + return fmt.Errorf("创建数据目录失败: %w", err) + } + file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return fmt.Errorf("打开锁文件失败: %w", err) + } + + start := time.Now() + for { + err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB) + if err == nil { + pm.lockFile = file + pm.locked = true + return nil + } + if err != unix.EWOULDBLOCK { + file.Close() + return fmt.Errorf("获取锁失败: %w", err) + } + if time.Since(start) > LockAcquireTimeout { + file.Close() + return fmt.Errorf("获取锁超时 (超过 %v)", LockAcquireTimeout) + } + time.Sleep(LockRetryInterval) + } +} + +func (pm *ProcessManager) Unlock() error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if !pm.locked { + return nil + } + if pm.lockFile != nil { + unix.Flock(int(pm.lockFile.Fd()), unix.LOCK_UN) + pm.lockFile.Close() + pm.lockFile = nil + } + pm.locked = false + return nil +} diff --git a/process_manager_lock_windows.go b/process_manager_lock_windows.go new file mode 100644 index 0000000..fe4bd99 --- /dev/null +++ b/process_manager_lock_windows.go @@ -0,0 +1,28 @@ +//go:build windows + +package main + +func (pm *ProcessManager) Lock() error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if pm.locked { + return nil + } + + // Windows 上暂用内存锁模拟(进程间不互斥,仅同一进程内互斥) + // 如需真正的进程间锁,后续可改用 Windows Named Mutex + pm.locked = true + return nil +} + +func (pm *ProcessManager) Unlock() error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if !pm.locked { + return nil + } + pm.locked = false + return nil +} diff --git a/process_manager_other.go b/process_manager_other.go new file mode 100644 index 0000000..8117554 --- /dev/null +++ b/process_manager_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package main + +import ( + "os/exec" +) + +// setProcessAttributes 非 Linux 平台 (Windows/macOS) 不做特殊设置 +func setProcessAttributes(cmd *exec.Cmd) { + // 非 Linux 平台不需要 Setpgid +}