重构前最后一份代码(bug较多,请勿使用)
This commit is contained in:
@@ -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,73 +316,57 @@ 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],
|
||||||
"pid": 0,
|
"pid": 0,
|
||||||
"port": 0,
|
"port": 0,
|
||||||
"legacy": true,
|
"legacy": true,
|
||||||
}, 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 {
|
||||||
@@ -502,4 +454,4 @@ func readTailLog(filePath string, n int) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -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,不需要额外设置
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func main() {
|
|||||||
// 2.6-preview: 使用 ProcessManager 管理 frpc 进程
|
// 2.6-preview: 使用 ProcessManager 管理 frpc 进程
|
||||||
// ============================================================
|
// ============================================================
|
||||||
processManager = NewProcessManager(
|
processManager = NewProcessManager(
|
||||||
"./data", // 数据目录
|
"./data", // 数据目录
|
||||||
"./data/frpc.toml", // 配置文件路径
|
"./data/frpc.toml", // 配置文件路径
|
||||||
"./data/frpc", // frpc 二进制路径
|
"./data/frpc", // frpc 二进制路径
|
||||||
)
|
)
|
||||||
@@ -106,4 +106,4 @@ func startWatchdog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-271
@@ -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,57 +30,40 @@ import (
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// 锁文件路径 (相对于 DataDir)
|
LockFileName = ".frpc.lock"
|
||||||
LockFileName = ".frpc.lock"
|
PortCheckTimeout = 500 * time.Millisecond
|
||||||
|
StartWaitTime = 500 * time.Millisecond
|
||||||
// 端口检测超时
|
StopMaxWaitTime = 5 * time.Second
|
||||||
PortCheckTimeout = 500 * time.Millisecond
|
|
||||||
|
|
||||||
// 启动后等待端口就绪的时间
|
|
||||||
StartWaitTime = 500 * time.Millisecond
|
|
||||||
|
|
||||||
// 停止时等待端口释放的最大时间
|
|
||||||
StopMaxWaitTime = 5 * time.Second
|
|
||||||
|
|
||||||
// 锁获取超时
|
|
||||||
LockAcquireTimeout = 30 * time.Second
|
LockAcquireTimeout = 30 * time.Second
|
||||||
|
LockRetryInterval = 100 * time.Millisecond
|
||||||
// 锁重试间隔
|
APITimeout = 2 * time.Second
|
||||||
LockRetryInterval = 100 * time.Millisecond
|
|
||||||
LockMaxRetries = 5
|
|
||||||
|
|
||||||
// API 请求超时
|
|
||||||
APITimeout = 2 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 数据结构
|
// 数据结构
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
// PortStatus 端口检测结果
|
|
||||||
type PortStatus struct {
|
type PortStatus struct {
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Occupied bool `json:"occupied"`
|
Occupied bool `json:"occupied"`
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
IsFRPC bool `json:"is_frpc"`
|
IsFRPC bool `json:"is_frpc"`
|
||||||
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"`
|
||||||
Proxies []struct {
|
Proxies []struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -96,24 +76,21 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setProcessAttributes 非 Linux 平台 (Windows/macOS) 不做特殊设置
|
||||||
|
func setProcessAttributes(cmd *exec.Cmd) {
|
||||||
|
// 非 Linux 平台不需要 Setpgid
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user