重构前最后一份代码(bug较多,请勿使用)

This commit is contained in:
2026-08-07 19:02:44 +08:00
parent fafe07baf8
commit 5bce105bd6
9 changed files with 199 additions and 341 deletions
+7 -55
View File
@@ -29,7 +29,7 @@ var (
) )
// ================================================================ // ================================================================
// frpc 二进制提取 (保持不变) // frpc 二进制提取
// ================================================================ // ================================================================
func getFrpcPath() (string, error) { func getFrpcPath() (string, error) {
@@ -94,7 +94,7 @@ func getFrpcPath() (string, error) {
} }
// ================================================================ // ================================================================
// 配置生成 (保持不变) // 配置生成
// ================================================================ // ================================================================
func GenerateFrpcConfig() 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 { func isFrpcRunning() bool {
// 如果 ProcessManager 已初始化,使用它
if processManager != nil { if processManager != nil {
status, err := processManager.Status() status, err := processManager.Status()
if err != nil { if err != nil {
@@ -183,7 +179,6 @@ func isFrpcRunning() bool {
return isFrpcRunningLegacy() return isFrpcRunningLegacy()
} }
// isFrpcRunningLegacy 旧版 PID 文件检测 (降级方案)
func isFrpcRunningLegacy() bool { func isFrpcRunningLegacy() bool {
pidData, err := os.ReadFile("./data/frpc.pid") pidData, err := os.ReadFile("./data/frpc.pid")
if err != nil { if err != nil {
@@ -193,7 +188,6 @@ func isFrpcRunningLegacy() bool {
if err != nil { if err != nil {
return false return false
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid)) cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
@@ -202,7 +196,6 @@ func isFrpcRunningLegacy() bool {
} }
return strings.Contains(string(output), strconv.Itoa(pid)) return strings.Contains(string(output), strconv.Itoa(pid))
} }
process, err := os.FindProcess(pid) process, err := os.FindProcess(pid)
if err != nil { if err != nil {
return false return false
@@ -210,8 +203,6 @@ func isFrpcRunningLegacy() bool {
return process.Signal(syscall.Signal(0)) == nil return process.Signal(syscall.Signal(0)) == nil
} }
// StartFrpc 启动 frpc (幂等)
// 优先使用 ProcessManager,降级到旧逻辑
func StartFrpc() error { func StartFrpc() error {
if processManager != nil { if processManager != nil {
log.Println("[INFO] 使用 ProcessManager 启动 frpc") log.Println("[INFO] 使用 ProcessManager 启动 frpc")
@@ -221,60 +212,47 @@ func StartFrpc() error {
return startFrpcLegacy() return startFrpcLegacy()
} }
// startFrpcLegacy 旧版启动逻辑 (降级方案)
func startFrpcLegacy() error { func startFrpcLegacy() error {
frpcPath, err := getFrpcPath() frpcPath, err := getFrpcPath()
if err != nil { if err != nil {
return fmt.Errorf("获取 frpc 路径失败: %w", err) return fmt.Errorf("获取 frpc 路径失败: %w", err)
} }
if err := os.MkdirAll("./data", 0755); err != nil { if err := os.MkdirAll("./data", 0755); err != nil {
return fmt.Errorf("创建 data 目录失败: %w", err) return fmt.Errorf("创建 data 目录失败: %w", err)
} }
if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) { if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) {
if err := GenerateFrpcConfig(); err != nil { if err := GenerateFrpcConfig(); err != nil {
return fmt.Errorf("生成配置文件失败: %w", err) return fmt.Errorf("生成配置文件失败: %w", err)
} }
} }
if isFrpcRunningLegacy() { if isFrpcRunningLegacy() {
return nil return nil
} }
os.Remove("./data/frpc.pid") os.Remove("./data/frpc.pid")
cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml") cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml")
setWindowHide(cmd) setWindowHide(cmd)
setSysProcAttr(cmd) setSysProcAttr(cmd)
logFile, err := os.OpenFile("./data/frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) logFile, err := os.OpenFile("./data/frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil { if err != nil {
return fmt.Errorf("打开日志文件失败: %w", err) return fmt.Errorf("打开日志文件失败: %w", err)
} }
cmd.Stdout = logFile cmd.Stdout = logFile
cmd.Stderr = logFile cmd.Stderr = logFile
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return fmt.Errorf("启动 frpc 失败: %w", err) return fmt.Errorf("启动 frpc 失败: %w", err)
} }
go func() { go func() {
if err := cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {
log.Printf("frpc 子进程退出: %v", err) log.Printf("frpc 子进程退出: %v", err)
} }
os.Remove("./data/frpc.pid") os.Remove("./data/frpc.pid")
}() }()
if err := os.WriteFile("./data/frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil { if err := os.WriteFile("./data/frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
return fmt.Errorf("保存 PID 失败: %w", err) return fmt.Errorf("保存 PID 失败: %w", err)
} }
return nil return nil
} }
// StopFrpc 停止 frpc (幂等)
// 优先使用 ProcessManager,降级到旧逻辑
func StopFrpc() error { func StopFrpc() error {
if processManager != nil { if processManager != nil {
log.Println("[INFO] 使用 ProcessManager 停止 frpc") log.Println("[INFO] 使用 ProcessManager 停止 frpc")
@@ -284,7 +262,6 @@ func StopFrpc() error {
return stopFrpcLegacy() return stopFrpcLegacy()
} }
// stopFrpcLegacy 旧版停止逻辑 (降级方案)
func stopFrpcLegacy() error { func stopFrpcLegacy() error {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe") cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe")
@@ -294,7 +271,6 @@ func stopFrpcLegacy() error {
os.Remove("./data/frpc.pid") os.Remove("./data/frpc.pid")
return nil return nil
} }
pidData, err := os.ReadFile("./data/frpc.pid") pidData, err := os.ReadFile("./data/frpc.pid")
if err != nil { if err != nil {
cmd := exec.Command("pkill", "-f", "frpc") cmd := exec.Command("pkill", "-f", "frpc")
@@ -303,24 +279,19 @@ func stopFrpcLegacy() error {
} }
return nil return nil
} }
pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData))) pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData)))
process, err := os.FindProcess(pid) process, err := os.FindProcess(pid)
if err != nil { if err != nil {
os.Remove("./data/frpc.pid") os.Remove("./data/frpc.pid")
return nil return nil
} }
if err := process.Kill(); err != nil { if err := process.Kill(); err != nil {
return fmt.Errorf("杀死进程失败: %w", err) return fmt.Errorf("杀死进程失败: %w", err)
} }
os.Remove("./data/frpc.pid") os.Remove("./data/frpc.pid")
return nil return nil
} }
// RestartFrpc 重启 frpc (原子操作)
// 优先使用 ProcessManager,降级到旧逻辑
func RestartFrpc() error { func RestartFrpc() error {
if processManager != nil { if processManager != nil {
log.Println("[INFO] 使用 ProcessManager 重启 frpc") log.Println("[INFO] 使用 ProcessManager 重启 frpc")
@@ -333,8 +304,6 @@ func RestartFrpc() error {
return startFrpcLegacy() return startFrpcLegacy()
} }
// GetFrpcStatus 获取 frpc 详细状态 (供 API 调用)
// 优先使用 ProcessManager,降级到旧逻辑
func GetFrpcStatus() (map[string]interface{}, error) { func GetFrpcStatus() (map[string]interface{}, error) {
if processManager != nil { if processManager != nil {
status, err := processManager.Status() status, err := processManager.Status()
@@ -347,8 +316,6 @@ func GetFrpcStatus() (map[string]interface{}, error) {
"port": status.Port, "port": status.Port,
}, nil }, nil
} }
// 降级: 返回简单布尔值
running := isFrpcRunningLegacy() running := isFrpcRunningLegacy()
return map[string]interface{}{ return map[string]interface{}{
"state": map[bool]string{true: "running", false: "stopped"}[running], "state": map[bool]string{true: "running", false: "stopped"}[running],
@@ -358,62 +325,48 @@ func GetFrpcStatus() (map[string]interface{}, error) {
}, nil }, nil
} }
// ReloadFrpc 热加载 frpc 配置
// 优先尝试 reload,失败则降级到重启
func ReloadFrpc() error { func ReloadFrpc() error {
// 如果 ProcessManager 未初始化,降级到旧逻辑
if processManager == nil { if processManager == nil {
return reloadFrpcLegacy() return reloadFrpcLegacy()
} }
// 检查 frpc 是否在运行
status, err := processManager.Status() status, err := processManager.Status()
if err != nil { if err != nil {
return fmt.Errorf("获取 frpc 状态失败: %w", err) return fmt.Errorf("获取 frpc 状态失败: %w", err)
} }
if status.State != "running" { if status.State != "running" {
// 未运行,直接启动
return processManager.Start(nil) return processManager.Start(nil)
} }
// 尝试热加载 (使用 frpc reload 命令)
frpcPath, err := getFrpcPath() frpcPath, err := getFrpcPath()
if err != nil { if err != nil {
return fmt.Errorf("获取 frpc 路径失败: %w", err) return fmt.Errorf("获取 frpc 路径失败: %w", err)
} }
cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err)
log.Printf(" reload 输出: %s", string(output)) log.Printf(" reload 输出: %s", string(output))
// 降级: 重启
if err := processManager.Restart(nil); err != nil { if err := processManager.Restart(nil); err != nil {
return fmt.Errorf("重启 frpc 失败: %w", err) return fmt.Errorf("重启 frpc 失败: %w", err)
} }
return nil return nil
} }
log.Printf("✅ frpc 热加载成功: %s", string(output))
log.Printf("✅ frpc 热加载成功")
return nil return nil
} }
// reloadFrpcLegacy 旧版热加载 (降级方案)
func reloadFrpcLegacy() error { func reloadFrpcLegacy() error {
if !isFrpcRunningLegacy() { if !isFrpcRunningLegacy() {
return startFrpcLegacy() return startFrpcLegacy()
} }
frpcPath, err := getFrpcPath() frpcPath, err := getFrpcPath()
if err != nil { if err != nil {
return fmt.Errorf("获取 frpc 路径失败: %w", err) return fmt.Errorf("获取 frpc 路径失败: %w", err)
} }
cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err)
log.Printf(" reload 输出: %s", string(output))
if stopErr := stopFrpcLegacy(); stopErr != nil { if stopErr := stopFrpcLegacy(); stopErr != nil {
return fmt.Errorf("停止 frpc 失败: %w", stopErr) return fmt.Errorf("停止 frpc 失败: %w", stopErr)
} }
@@ -422,15 +375,14 @@ func reloadFrpcLegacy() error {
} }
return nil return nil
} }
log.Printf("✅ frpc 热加载成功 (兼容模式)") log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output))
return nil return nil
} }
// ================================================================ // ================================================================
// 日志读取 (保持不变) // 日志读取
// ================================================================ // ================================================================
// readTailLog 读取文件末尾 n 行
func readTailLog(filePath string, n int) ([]string, error) { func readTailLog(filePath string, n int) ([]string, error) {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
+3 -6
View File
@@ -7,15 +7,12 @@ import (
"syscall" "syscall"
) )
// setWindowHide Windows 隐藏窗口
func setWindowHide(cmd *exec.Cmd) { 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) { func setSysProcAttr(cmd *exec.Cmd) {
// Windows 不需要 Setsid什么都不做 // Windows 不支持 Setpgid不需要额外设置
} }
+2 -2
View File
@@ -1,11 +1,12 @@
module frpc-console module frpc-console
go 1.25.0 go 1.26.5
require ( require (
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
golang.org/x/crypto v0.54.0 golang.org/x/crypto v0.54.0
golang.org/x/sys v0.47.0
modernc.org/sqlite v1.54.0 modernc.org/sqlite v1.54.0
) )
@@ -39,7 +40,6 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.56.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 golang.org/x/text v0.40.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.74.1 // indirect modernc.org/libc v1.74.1 // indirect
+45 -256
View File
@@ -1,12 +1,6 @@
// process_manager.go // process_manager.go
// frpc-console 进程管理模块 // frpc-console 进程管理模块
// 2.6-preview: 端口检测 + 单实例锁定 + 状态自述 // 2.6-preview: 端口检测 + 单实例锁定 + 状态自述
//
// 设计原则:
// 1. 端口是事实来源,PID 文件是缓存
// 2. 所有操作幂等 (多次调用结果一致)
// 3. 通过文件锁 (flock) 实现进程间互斥
// 4. 通过 admin_port API 获取 frpc 真实状态
package main package main
@@ -18,6 +12,7 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -26,6 +21,8 @@ import (
"sync" "sync"
"syscall" "syscall"
"time" "time"
"golang.org/x/sys/unix"
) )
// ================================================================ // ================================================================
@@ -33,26 +30,12 @@ import (
// ================================================================ // ================================================================
const ( const (
// 锁文件路径 (相对于 DataDir)
LockFileName = ".frpc.lock" LockFileName = ".frpc.lock"
// 端口检测超时
PortCheckTimeout = 500 * time.Millisecond PortCheckTimeout = 500 * time.Millisecond
// 启动后等待端口就绪的时间
StartWaitTime = 500 * time.Millisecond StartWaitTime = 500 * time.Millisecond
// 停止时等待端口释放的最大时间
StopMaxWaitTime = 5 * time.Second StopMaxWaitTime = 5 * time.Second
// 锁获取超时
LockAcquireTimeout = 30 * time.Second LockAcquireTimeout = 30 * time.Second
// 锁重试间隔
LockRetryInterval = 100 * time.Millisecond LockRetryInterval = 100 * time.Millisecond
LockMaxRetries = 5
// API 请求超时
APITimeout = 2 * time.Second APITimeout = 2 * time.Second
) )
@@ -60,7 +43,6 @@ const (
// 数据结构 // 数据结构
// ================================================================ // ================================================================
// PortStatus 端口检测结果
type PortStatus struct { type PortStatus struct {
Port int `json:"port"` Port int `json:"port"`
Occupied bool `json:"occupied"` Occupied bool `json:"occupied"`
@@ -69,17 +51,15 @@ type PortStatus struct {
ProcessCmd string `json:"process_cmd,omitempty"` ProcessCmd string `json:"process_cmd,omitempty"`
} }
// ProcessStatus frpc 进程状态
type ProcessStatus struct { type ProcessStatus struct {
State string `json:"state"` // "running" | "stopped" | "unknown" | "conflict" State string `json:"state"`
PID int `json:"pid"` // 进程 PID (如果运行中) PID int `json:"pid"`
Port int `json:"port"` // 监听的端口 Port int `json:"port"`
Uptime string `json:"uptime"` // 运行时长 (可选) Uptime string `json:"uptime,omitempty"`
Version string `json:"version"` // frpc 版本 (如果 API 可访问) Version string `json:"version,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// FRPCStatus 来自 frpc admin API 的状态响应
type FRPCStatus struct { type FRPCStatus struct {
Version string `json:"version"` Version string `json:"version"`
RunID string `json:"run_id"` RunID string `json:"run_id"`
@@ -97,23 +77,20 @@ type FRPCStatus struct {
type ProcessManager struct { type ProcessManager struct {
mu sync.Mutex mu sync.Mutex
dataDir string
dataDir string // 数据目录 (存放 lock 和 pid 文件) configPath string
configPath string // frpc.toml 路径 frpcBinPath string
frpcBinPath string // frpc 二进制路径 adminPort int
adminPort int // admin_port (从配置读取) lockFile *os.File
locked bool
lockFile *os.File // flock 文件句柄
locked bool // 是否持有锁
} }
// NewProcessManager 创建进程管理器
func NewProcessManager(dataDir, configPath, frpcBinPath string) *ProcessManager { func NewProcessManager(dataDir, configPath, frpcBinPath string) *ProcessManager {
return &ProcessManager{ return &ProcessManager{
dataDir: dataDir, dataDir: dataDir,
configPath: configPath, configPath: configPath,
frpcBinPath: frpcBinPath, 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 { func (pm *ProcessManager) LoadConfig() error {
// 读取 frpc.toml 内容
content, err := os.ReadFile(pm.configPath) content, err := os.ReadFile(pm.configPath)
if err != nil { if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err) return fmt.Errorf("读取配置文件失败: %w", err)
} }
// 尝试解析 admin_port (旧格式)
// admin_port = 7400
if port := extractIntValue(string(content), "admin_port"); port > 0 { if port := extractIntValue(string(content), "admin_port"); port > 0 {
pm.adminPort = port pm.adminPort = port
return nil return nil
} }
// 尝试解析 webServer.port (新格式, 0.52.0+)
// [webServer]
// port = 7400
if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 { if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 {
pm.adminPort = port pm.adminPort = port
return nil return nil
} }
// 如果都找不到,说明 frpc 配置没有启用 admin 端口
return fmt.Errorf("未找到 admin_port 或 webServer.port 配置") return fmt.Errorf("未找到 admin_port 或 webServer.port 配置")
} }
// extractIntValue 从配置中提取 key = value 格式的值
func extractIntValue(content, key string) int { func extractIntValue(content, key string) int {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
for _, line := range lines { for _, line := range lines {
@@ -168,7 +132,6 @@ func extractIntValue(content, key string) int {
return 0 return 0
} }
// extractIntValueFromSection 从指定 section 中提取 key = value
func extractIntValueFromSection(content, section, key string) int { func extractIntValueFromSection(content, section, key string) int {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
inSection := false inSection := false
@@ -193,110 +156,76 @@ func extractIntValueFromSection(content, section, key string) int {
} }
// ================================================================ // ================================================================
// 端口检测器 (PortDetector) // 端口检测
// ================================================================ // ================================================================
// CheckPort 检测端口是否被占用
func (pm *ProcessManager) CheckPort() (bool, error) { func (pm *ProcessManager) CheckPort() (bool, error) {
if pm.adminPort <= 0 { if pm.adminPort <= 0 {
return false, fmt.Errorf("admin_port 未配置") return false, fmt.Errorf("admin_port 未配置")
} }
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout) conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout)
if err != nil { if err != nil {
// 连接失败 = 端口未被占用
return false, nil return false, nil
} }
conn.Close() conn.Close()
return true, nil return true, nil
} }
// GetPortStatus 获取端口完整状态 (占用 + PID + 进程类型)
func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) { func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) {
status := &PortStatus{ status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false}
Port: pm.adminPort,
Occupied: false,
PID: 0,
IsFRPC: false,
}
// 1. 检测端口是否被占用
occupied, err := pm.CheckPort() occupied, err := pm.CheckPort()
if err != nil { if err != nil {
return status, err return status, err
} }
status.Occupied = occupied status.Occupied = occupied
if !occupied { if !occupied {
return status, nil return status, nil
} }
// 2. 反查 PID
pid, err := pm.getPIDByPort(pm.adminPort) pid, err := pm.getPIDByPort(pm.adminPort)
if err != nil { if err != nil {
// 反查失败,尝试从 PID 文件读取
if pidFromFile := pm.readPIDFile(); pidFromFile > 0 { if pidFromFile := pm.readPIDFile(); pidFromFile > 0 {
status.PID = pidFromFile
// 验证这个 PID 是否真的在监听端口
if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) { if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) {
status.PID = pidFromFile status.PID = pidFromFile
} else {
status.PID = 0
} }
} }
} else { } else {
status.PID = pid status.PID = pid
} }
if status.PID == 0 { if status.PID == 0 {
return status, nil return status, nil
} }
// 3. 验证进程是否是 frpc
isFRPC, cmd := pm.isFRPCProcess(status.PID) isFRPC, cmd := pm.isFRPCProcess(status.PID)
status.IsFRPC = isFRPC status.IsFRPC = isFRPC
status.ProcessCmd = cmd status.ProcessCmd = cmd
return status, nil return status, nil
} }
// getPIDByPort 通过端口反查 PID
// 优先级: ss > netstat > lsof
func (pm *ProcessManager) getPIDByPort(port int) (int, error) { func (pm *ProcessManager) getPIDByPort(port int) (int, error) {
// 方法 1: ss -lpn (最可靠)
if pid, err := pm.getPIDBySS(port); err == nil && pid > 0 { if pid, err := pm.getPIDBySS(port); err == nil && pid > 0 {
return pid, nil return pid, nil
} }
// 方法 2: netstat -tulpn (兼容性广)
if pid, err := pm.getPIDByNetstat(port); err == nil && pid > 0 { if pid, err := pm.getPIDByNetstat(port); err == nil && pid > 0 {
return pid, nil return pid, nil
} }
// 方法 3: lsof -i :port (最后的备选)
if pid, err := pm.getPIDByLsof(port); err == nil && pid > 0 { if pid, err := pm.getPIDByLsof(port); err == nil && pid > 0 {
return pid, nil return pid, nil
} }
return 0, fmt.Errorf("无法通过端口反查 PID") return 0, fmt.Errorf("无法通过端口反查 PID")
} }
// getPIDBySS 通过 ss 命令反查 PID
func (pm *ProcessManager) getPIDBySS(port int) (int, error) { func (pm *ProcessManager) getPIDBySS(port int) (int, error) {
cmd := exec.Command("ss", "-lpn", "state", "listening") cmd := exec.Command("ss", "-lpn", "state", "listening")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
return 0, err return 0, err
} }
scanner := bufio.NewScanner(bytes.NewReader(out)) scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if !strings.Contains(line, fmt.Sprintf(":%d", port)) { if !strings.Contains(line, fmt.Sprintf(":%d", port)) {
continue 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 { if idx := strings.Index(line, "pid="); idx != -1 {
end := strings.Index(line[idx:], ",") end := strings.Index(line[idx:], ",")
if end == -1 { if end == -1 {
@@ -315,27 +244,23 @@ func (pm *ProcessManager) getPIDBySS(port int) (int, error) {
return 0, fmt.Errorf("未找到监听端口 %d 的进程", port) return 0, fmt.Errorf("未找到监听端口 %d 的进程", port)
} }
// getPIDByNetstat 通过 netstat 命令反查 PID
func (pm *ProcessManager) getPIDByNetstat(port int) (int, error) { func (pm *ProcessManager) getPIDByNetstat(port int) (int, error) {
cmd := exec.Command("netstat", "-tulpn") cmd := exec.Command("netstat", "-tulpn")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
return 0, err return 0, err
} }
scanner := bufio.NewScanner(bytes.NewReader(out)) scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if !strings.Contains(line, fmt.Sprintf(":%d", port)) { if !strings.Contains(line, fmt.Sprintf(":%d", port)) {
continue continue
} }
// netstat 输出格式: tcp 0 0 0.0.0.0:7400 0.0.0.0:* LISTEN 12345/frpc
parts := strings.Fields(line) parts := strings.Fields(line)
if len(parts) < 7 { if len(parts) < 7 {
continue continue
} }
last := parts[len(parts)-1] last := parts[len(parts)-1]
// 提取 PID: 12345/frpc
if idx := strings.Index(last, "/"); idx != -1 { if idx := strings.Index(last, "/"); idx != -1 {
pidStr := last[:idx] pidStr := last[:idx]
if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 { 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) return 0, fmt.Errorf("未找到监听端口 %d 的进程", port)
} }
// getPIDByLsof 通过 lsof 命令反查 PID
func (pm *ProcessManager) getPIDByLsof(port int) (int, error) { func (pm *ProcessManager) getPIDByLsof(port int) (int, error) {
cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN") cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
return 0, err return 0, err
} }
scanner := bufio.NewScanner(bytes.NewReader(out)) scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if strings.Contains(line, "frpc") { if strings.Contains(line, "frpc") {
// lsof 输出格式: frpc 12345 root 3u IPv4 123456 0t0 TCP *:7400 (LISTEN)
parts := strings.Fields(line) parts := strings.Fields(line)
if len(parts) >= 2 { if len(parts) >= 2 {
if pid, err := strconv.Atoi(parts[1]); err == nil && pid > 0 { 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) return 0, fmt.Errorf("未找到监听端口 %d 的 frpc 进程", port)
} }
// isProcessListeningOnPort 验证 PID 是否在监听指定端口
func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool { 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") cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-a", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
@@ -381,9 +301,7 @@ func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool {
return strings.Contains(string(out), "LISTEN") return strings.Contains(string(out), "LISTEN")
} }
// isFRPCProcess 验证进程是否是 frpc
func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) { func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) {
// 方法 1: 读取 /proc/<pid>/cmdline
cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid) cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid)
if data, err := os.ReadFile(cmdlinePath); err == nil { if data, err := os.ReadFile(cmdlinePath); err == nil {
cmd := strings.ReplaceAll(string(data), "\x00", " ") cmd := strings.ReplaceAll(string(data), "\x00", " ")
@@ -391,8 +309,6 @@ func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) {
return true, cmd return true, cmd
} }
} }
// 方法 2: ps -p
cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=") cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=")
out, err := cmd.Output() out, err := cmd.Output()
if err == nil { if err == nil {
@@ -401,7 +317,6 @@ func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) {
return true, args return true, args
} }
} }
return false, "" return false, ""
} }
@@ -409,68 +324,51 @@ func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) {
// 互斥锁 (flock) // 互斥锁 (flock)
// ================================================================ // ================================================================
// Lock 获取进程间互斥锁
func (pm *ProcessManager) Lock() error { func (pm *ProcessManager) Lock() error {
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
if pm.locked { if pm.locked {
return nil // 已持有锁 return nil
} }
lockPath := filepath.Join(pm.dataDir, LockFileName) lockPath := filepath.Join(pm.dataDir, LockFileName)
// 确保数据目录存在
if err := os.MkdirAll(pm.dataDir, 0755); err != nil { if err := os.MkdirAll(pm.dataDir, 0755); err != nil {
return fmt.Errorf("创建数据目录失败: %w", err) return fmt.Errorf("创建数据目录失败: %w", err)
} }
file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644) file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
if err != nil { if err != nil {
return fmt.Errorf("打开锁文件失败: %w", err) return fmt.Errorf("打开锁文件失败: %w", err)
} }
// 尝试获取排他锁 (阻塞)
// 使用 LOCK_EX | LOCK_NB 实现非阻塞尝试,然后手动重试
start := time.Now() start := time.Now()
for { 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 { if err == nil {
pm.lockFile = file pm.lockFile = file
pm.locked = true pm.locked = true
return nil return nil
} }
if err != syscall.EWOULDBLOCK { if err != syscall.EWOULDBLOCK {
file.Close() file.Close()
return fmt.Errorf("获取锁失败: %w", err) return fmt.Errorf("获取锁失败: %w", err)
} }
// 检查超时
if time.Since(start) > LockAcquireTimeout { if time.Since(start) > LockAcquireTimeout {
file.Close() file.Close()
return fmt.Errorf("获取锁超时 (超过 %v)", LockAcquireTimeout) return fmt.Errorf("获取锁超时 (超过 %v)", LockAcquireTimeout)
} }
time.Sleep(LockRetryInterval) time.Sleep(LockRetryInterval)
} }
} }
// Unlock 释放互斥锁
func (pm *ProcessManager) Unlock() error { func (pm *ProcessManager) Unlock() error {
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
if !pm.locked { if !pm.locked {
return nil return nil
} }
if pm.lockFile != 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.Close()
pm.lockFile = nil pm.lockFile = nil
} }
pm.locked = false pm.locked = false
return nil return nil
} }
@@ -508,54 +406,37 @@ func (pm *ProcessManager) deletePIDFile() error {
} }
// ================================================================ // ================================================================
// 状态查询 (Status) // 状态查询
// ================================================================ // ================================================================
// Status 获取 frpc 进程实时状态
func (pm *ProcessManager) Status() (*ProcessStatus, error) { func (pm *ProcessManager) Status() (*ProcessStatus, error) {
status := &ProcessStatus{ status := &ProcessStatus{State: "unknown", PID: 0, Port: pm.adminPort}
State: "unknown",
PID: 0,
Port: pm.adminPort,
}
if pm.adminPort <= 0 { if pm.adminPort <= 0 {
status.State = "unknown"
status.Error = "admin_port 未配置" status.Error = "admin_port 未配置"
return status, nil return status, nil
} }
// 1. 获取端口状态
portStatus, err := pm.GetPortStatus() portStatus, err := pm.GetPortStatus()
if err != nil { if err != nil {
status.Error = err.Error() status.Error = err.Error()
return status, nil return status, nil
} }
if !portStatus.Occupied { if !portStatus.Occupied {
// 端口未被占用: 清理过期的 PID 文件
pm.deletePIDFile() pm.deletePIDFile()
status.State = "stopped" status.State = "stopped"
return status, nil return status, nil
} }
if !portStatus.IsFRPC { if !portStatus.IsFRPC {
status.State = "conflict" status.State = "conflict"
status.PID = portStatus.PID status.PID = portStatus.PID
status.Error = fmt.Sprintf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID) status.Error = fmt.Sprintf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID)
return status, nil return status, nil
} }
// 2. 端口被 frpc 占用: 更新 PID 文件
status.State = "running" status.State = "running"
status.PID = portStatus.PID status.PID = portStatus.PID
pm.writePIDFile(portStatus.PID) pm.writePIDFile(portStatus.PID)
// 3. 尝试通过 API 获取更多信息
if info := pm.getFRPCStatus(portStatus.PID); info != nil { if info := pm.getFRPCStatus(portStatus.PID); info != nil {
status.Version = info.Version status.Version = info.Version
} }
return status, nil return status, nil
} }
@@ -563,42 +444,34 @@ func (pm *ProcessManager) Status() (*ProcessStatus, error) {
// API 回源 (2.7-preview 预留) // API 回源 (2.7-preview 预留)
// ================================================================ // ================================================================
// getFRPCStatus 通过 admin API 获取 frpc 状态 (2.7-preview 启用)
func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus { func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus {
if pid <= 0 || pm.adminPort <= 0 { if pid <= 0 || pm.adminPort <= 0 {
return nil return nil
} }
url := fmt.Sprintf("http://127.0.0.1:%d/api/status", pm.adminPort) url := fmt.Sprintf("http://127.0.0.1:%d/api/status", pm.adminPort)
ctx, cancel := context.WithTimeout(context.Background(), APITimeout) ctx, cancel := context.WithTimeout(context.Background(), APITimeout)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
return nil return nil
} }
client := &http.Client{Timeout: APITimeout} client := &http.Client{Timeout: APITimeout}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil return nil
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return nil return nil
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil return nil
} }
var status FRPCStatus var status FRPCStatus
if err := json.Unmarshal(body, &status); err != nil { if err := json.Unmarshal(body, &status); err != nil {
return nil return nil
} }
return &status return &status
} }
@@ -606,38 +479,39 @@ func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus {
// 操作执行 (Start / Stop / Restart) // 操作执行 (Start / Stop / Restart)
// ================================================================ // ================================================================
// Start 启动 frpc (幂等)
func (pm *ProcessManager) Start(ctx context.Context) error { func (pm *ProcessManager) Start(ctx context.Context) error {
// 1. 获取锁
if err := pm.Lock(); err != nil { if err := pm.Lock(); err != nil {
return fmt.Errorf("获取锁失败: %w", err) return fmt.Errorf("获取锁失败: %w", err)
} }
defer pm.Unlock() defer pm.Unlock()
return pm.startLocked(ctx)
}
// 2. 双重检查: 端口是否已被占用 func (pm *ProcessManager) startLocked(ctx context.Context) error {
portStatus, err := pm.GetPortStatus() portStatus, err := pm.GetPortStatus()
if err != nil { if err != nil {
return fmt.Errorf("检测端口状态失败: %w", err) return fmt.Errorf("检测端口状态失败: %w", err)
} }
if portStatus.Occupied { if portStatus.Occupied {
if portStatus.IsFRPC { if portStatus.IsFRPC {
// 已启动, 幂等返回
pm.writePIDFile(portStatus.PID) pm.writePIDFile(portStatus.PID)
return nil return nil
} }
return fmt.Errorf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID) return fmt.Errorf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID)
} }
// 3. 启动 frpc cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath)
if err := pm.startFRPC(ctx); err != nil {
return err // 根据平台设置 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) time.Sleep(StartWaitTime)
// 5. 验证启动成功
occupied, err := pm.CheckPort() occupied, err := pm.CheckPort()
if err != nil { if err != nil {
return fmt.Errorf("验证启动状态失败: %w", err) return fmt.Errorf("验证启动状态失败: %w", err)
@@ -645,88 +519,51 @@ func (pm *ProcessManager) Start(ctx context.Context) error {
if !occupied { if !occupied {
return fmt.Errorf("frpc 启动失败: 端口未监听") return fmt.Errorf("frpc 启动失败: 端口未监听")
} }
// 6. 写入 PID 文件
pid, _ := pm.getPIDByPort(pm.adminPort) pid, _ := pm.getPIDByPort(pm.adminPort)
if pid > 0 { if pid > 0 {
pm.writePIDFile(pid) pm.writePIDFile(pid)
} }
return nil 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 { func (pm *ProcessManager) Stop(ctx context.Context) error {
// 1. 获取锁
if err := pm.Lock(); err != nil { if err := pm.Lock(); err != nil {
return fmt.Errorf("获取锁失败: %w", err) return fmt.Errorf("获取锁失败: %w", err)
} }
defer pm.Unlock() defer pm.Unlock()
return pm.stopLocked(ctx)
}
// 2. 检查端口状态 func (pm *ProcessManager) stopLocked(ctx context.Context) error {
portStatus, err := pm.GetPortStatus() portStatus, err := pm.GetPortStatus()
if err != nil { if err != nil {
return fmt.Errorf("检测端口状态失败: %w", err) return fmt.Errorf("检测端口状态失败: %w", err)
} }
if !portStatus.Occupied { if !portStatus.Occupied {
// 已停止, 幂等返回
pm.deletePIDFile() pm.deletePIDFile()
return nil return nil
} }
var pid int var pid int
if portStatus.IsFRPC { if portStatus.IsFRPC {
pid = portStatus.PID pid = portStatus.PID
} else { } else {
// 端口被非 frpc 占用, 不能强制停止
return fmt.Errorf("端口 %d 被非 frpc 进程占用, 无法安全停止", pm.adminPort) return fmt.Errorf("端口 %d 被非 frpc 进程占用, 无法安全停止", pm.adminPort)
} }
if pid <= 0 { if pid <= 0 {
// 尝试从 PID 文件读取
pid = pm.readPIDFile() pid = pm.readPIDFile()
if pid <= 0 { if pid <= 0 {
return fmt.Errorf("无法确定 frpc 进程 PID") return fmt.Errorf("无法确定 frpc 进程 PID")
} }
} }
// 3. 发送 SIGTERM (优雅停止)
proc, err := os.FindProcess(pid) proc, err := os.FindProcess(pid)
if err != nil { if err != nil {
return fmt.Errorf("查找进程失败: %w", err)
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
// 可能进程已退出
pm.deletePIDFile() pm.deletePIDFile()
return nil return nil
} }
if err := proc.Signal(syscall.SIGTERM); err != nil {
// 4. 等待端口释放 pm.deletePIDFile()
return nil
}
start := time.Now() start := time.Now()
for time.Since(start) < StopMaxWaitTime { for time.Since(start) < StopMaxWaitTime {
occupied, _ := pm.CheckPort() occupied, _ := pm.CheckPort()
@@ -736,88 +573,41 @@ func (pm *ProcessManager) Stop(ctx context.Context) error {
} }
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
} }
// 5. 端口未释放, 强制 kill
proc.Kill() proc.Kill()
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
if occupied, _ := pm.CheckPort(); occupied {
// 再次检查
occupied, _ := pm.CheckPort()
if occupied {
return fmt.Errorf("强制停止失败: 端口仍被占用") return fmt.Errorf("强制停止失败: 端口仍被占用")
} }
pm.deletePIDFile() pm.deletePIDFile()
return nil return nil
} }
// Restart 重启 frpc (原子操作)
func (pm *ProcessManager) Restart(ctx context.Context) error { func (pm *ProcessManager) Restart(ctx context.Context) error {
// 1. 获取锁 (整个操作持有锁)
if err := pm.Lock(); err != nil { if err := pm.Lock(); err != nil {
return fmt.Errorf("获取锁失败: %w", err) return fmt.Errorf("获取锁失败: %w", err)
} }
defer pm.Unlock() defer pm.Unlock()
// 2. 停止
if err := pm.stopLocked(ctx); err != nil { if err := pm.stopLocked(ctx); err != nil {
return fmt.Errorf("停止失败: %w", err) return fmt.Errorf("停止失败: %w", err)
} }
// 3. 启动
if err := pm.startLocked(ctx); err != nil { if err := pm.startLocked(ctx); err != nil {
return fmt.Errorf("启动失败: %w", err) 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 return nil
} }
// ================================================================ // ================================================================
// 健康检查 (用于 WebUI 展示) // 健康检查
// ================================================================ // ================================================================
// HealthCheck 返回简要健康状态
func (pm *ProcessManager) HealthCheck() map[string]interface{} { func (pm *ProcessManager) HealthCheck() map[string]interface{} {
result := make(map[string]interface{}) result := map[string]interface{}{"admin_port": pm.adminPort}
result["admin_port"] = pm.adminPort
status, err := pm.Status() status, err := pm.Status()
if err != nil { if err != nil {
result["state"] = "error" result["state"] = "error"
result["error"] = err.Error() result["error"] = err.Error()
return result return result
} }
result["state"] = status.State result["state"] = status.State
result["pid"] = status.PID result["pid"] = status.PID
if status.Version != "" { if status.Version != "" {
@@ -826,6 +616,5 @@ func (pm *ProcessManager) HealthCheck() map[string]interface{} {
if status.Error != "" { if status.Error != "" {
result["error"] = status.Error result["error"] = status.Error
} }
return result return result
} }
+15
View File
@@ -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,
}
}
+65
View File
@@ -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
}
+28
View File
@@ -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
}
+12
View File
@@ -0,0 +1,12 @@
//go:build !linux
package main
import (
"os/exec"
)
// setProcessAttributes 非 Linux 平台 (Windows/macOS) 不做特殊设置
func setProcessAttributes(cmd *exec.Cmd) {
// 非 Linux 平台不需要 Setpgid
}