目录重构完成,但是Preview通道尚未可用
This commit is contained in:
@@ -0,0 +1,417 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"frpc-console/internal/auth"
|
||||||
|
"frpc-console/internal/db"
|
||||||
|
"frpc-console/internal/frp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 认证 Handler
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func CheckUsersHandler(c *gin.Context) {
|
||||||
|
count, err := db.CountUsers()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"data": gin.H{"hasUsers": count > 0, "count": count},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterHandler(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := db.CountUsers()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"code": 1, "msg": "已存在管理员账户,请登录"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Username) < 5 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "用户名至少 5 位"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.ValidatePassword(req.Password) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.CreateUser(req.Username, string(hash)); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建用户失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := auth.GenerateJWT(req.Username)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "注册成功",
|
||||||
|
"data": gin.H{"token": token},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginHandler(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := db.GetUserByUsername(req.Username)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := auth.GenerateJWT(user.Username)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "登录成功",
|
||||||
|
"data": gin.H{"token": token},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangePasswordHandler(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
OldPassword string `json:"oldPassword"`
|
||||||
|
NewPassword string `json:"newPassword"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := auth.GetUsernameFromContext(c)
|
||||||
|
if username == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未登录"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := db.GetUserByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "用户不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "当前密码错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.ValidatePassword(req.NewPassword) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.UpdateUserPassword(username, string(hash)); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新密码失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 配置 Handler
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func GetConfigHandler(c *gin.Context) {
|
||||||
|
cfg, err := db.GetGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": cfg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateConfigHandler(c *gin.Context) {
|
||||||
|
var cfg db.GlobalConfig
|
||||||
|
if err := c.ShouldBindJSON(&cfg); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.TcpMux = true
|
||||||
|
|
||||||
|
if err := db.UpdateGlobalConfig(&cfg); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := frp.GenerateConfig(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := frp.Reload(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 隧道 Handler
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func GetProxiesHandler(c *gin.Context) {
|
||||||
|
proxies, err := db.GetProxies()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道列表失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": proxies})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetProxyHandler(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, err := db.GetProxy(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "隧道不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": p})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateProxyHandler(c *gin.Context) {
|
||||||
|
var p db.Proxy
|
||||||
|
if err := c.ShouldBindJSON(&p); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.Enabled = true
|
||||||
|
|
||||||
|
if err := db.CreateProxy(&p); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generateAndReload(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道创建成功", "data": gin.H{"id": p.ID}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateProxyHandler(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var p db.Proxy
|
||||||
|
if err := c.ShouldBindJSON(&p); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.ID = id
|
||||||
|
|
||||||
|
if err := db.UpdateProxy(&p); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generateAndReload(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道更新成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteProxyHandler(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DeleteProxy(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generateAndReload(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// frpc 进程管理 Handler
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func ReloadFrpcHandler(c *gin.Context) {
|
||||||
|
if err := frp.GenerateConfig(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := frp.Reload(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "热加载成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartFrpcHandler(c *gin.Context) {
|
||||||
|
if err := frp.Start(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "启动失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 启动成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopFrpcHandler(c *gin.Context) {
|
||||||
|
if err := frp.Stop(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "停止失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 已停止"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFrpcStatusHandler(c *gin.Context) {
|
||||||
|
status, err := frp.GetStatus()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询状态失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": status})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Ping Handler
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func PingHandler(c *gin.Context) {
|
||||||
|
target := c.Query("target")
|
||||||
|
if target == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 target 参数"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := db.GetGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port := cfg.ServerPort
|
||||||
|
address := net.JoinHostPort(target, strconv.Itoa(port))
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 1, "msg": "ping 失败", "latency": -1})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
latency := time.Since(start).Milliseconds()
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "latency": latency})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 日志 Handler
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func GetFrpcLogHandler(c *gin.Context) {
|
||||||
|
lines, err := frp.ReadTailLog("./data/frpc.log", 200)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"data": gin.H{
|
||||||
|
"lines": []string{},
|
||||||
|
"total": 0,
|
||||||
|
"error": err.Error(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"data": gin.H{
|
||||||
|
"lines": lines,
|
||||||
|
"total": len(lines),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 辅助函数
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func generateAndReload() error {
|
||||||
|
if err := frp.GenerateConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return frp.Reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// readTailLog 读取文件末尾 n 行 (临时放在这里,后续移到独立包)
|
||||||
|
func readTailLog(filePath string, n int) ([]string, error) {
|
||||||
|
// 这个函数在 frp 模块中也有,但为了避免循环依赖,在这里实现一份简单的
|
||||||
|
// 或者直接调用 frp.ReadTailLog 如果导出的话
|
||||||
|
// 目前保持和原来一致,后续可以统一到 pkg/utils
|
||||||
|
// 为了编译通过,先简单返回空
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"frpc-console/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed static/*
|
||||||
|
var staticFS embed.FS
|
||||||
|
|
||||||
|
// SetupRouter 设置路由
|
||||||
|
func SetupRouter() *gin.Engine {
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// 从 embed 读取前端静态文件
|
||||||
|
staticSubFS, _ := fs.Sub(staticFS, "static")
|
||||||
|
r.StaticFS("/static", http.FS(staticSubFS))
|
||||||
|
|
||||||
|
// 根路由
|
||||||
|
r.GET("/", func(c *gin.Context) {
|
||||||
|
content, err := staticFS.ReadFile("static/index.html")
|
||||||
|
if err != nil {
|
||||||
|
c.String(500, "加载前端页面失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "text/html; charset=utf-8", content)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
r.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.String(200, "frpc-console 后端已启动 🎉")
|
||||||
|
})
|
||||||
|
|
||||||
|
api := r.Group("/api")
|
||||||
|
{
|
||||||
|
// ---- 公开路由(不需要认证) ----
|
||||||
|
api.GET("/check/users", CheckUsersHandler)
|
||||||
|
api.POST("/register", RegisterHandler)
|
||||||
|
api.POST("/login", LoginHandler)
|
||||||
|
api.GET("/ping", PingHandler)
|
||||||
|
|
||||||
|
// ---- 需要认证的路由 ----
|
||||||
|
authGroup := api.Group("/")
|
||||||
|
authGroup.Use(auth.AuthMiddleware())
|
||||||
|
{
|
||||||
|
authGroup.GET("/config", GetConfigHandler)
|
||||||
|
authGroup.PUT("/config", UpdateConfigHandler)
|
||||||
|
|
||||||
|
authGroup.GET("/proxies", GetProxiesHandler)
|
||||||
|
authGroup.GET("/proxy/:id", GetProxyHandler)
|
||||||
|
authGroup.POST("/proxy", CreateProxyHandler)
|
||||||
|
authGroup.PUT("/proxy/:id", UpdateProxyHandler)
|
||||||
|
authGroup.DELETE("/proxy/:id", DeleteProxyHandler)
|
||||||
|
|
||||||
|
authGroup.POST("/frpc/reload", ReloadFrpcHandler)
|
||||||
|
authGroup.POST("/frpc/start", StartFrpcHandler)
|
||||||
|
authGroup.POST("/frpc/stop", StopFrpcHandler)
|
||||||
|
authGroup.GET("/frpc/status", GetFrpcStatusHandler)
|
||||||
|
authGroup.GET("/frpc/log", GetFrpcLogHandler)
|
||||||
|
|
||||||
|
authGroup.POST("/import/toml", ImportTomlHandler)
|
||||||
|
authGroup.GET("/export/toml", ExportTomlHandler)
|
||||||
|
|
||||||
|
authGroup.PUT("/user/password", ChangePasswordHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -50,14 +50,12 @@ func GetFrpcPath() (string, error) {
|
|||||||
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试从本地 bin 目录加载
|
|
||||||
localPath := filepath.Join(".", "bin", fileName)
|
localPath := filepath.Join(".", "bin", fileName)
|
||||||
if _, err := os.Stat(localPath); err == nil {
|
if _, err := os.Stat(localPath); err == nil {
|
||||||
cachedFrpcPath = localPath
|
cachedFrpcPath = localPath
|
||||||
return localPath, nil
|
return localPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试从 embed 提取到临时目录
|
|
||||||
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tmpPath := filepath.Join(os.TempDir(), "frpc")
|
tmpPath := filepath.Join(os.TempDir(), "frpc")
|
||||||
@@ -74,7 +72,6 @@ func GetFrpcPath() (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最后尝试从系统 PATH 查找
|
|
||||||
path, err := exec.LookPath("frpc")
|
path, err := exec.LookPath("frpc")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cachedFrpcPath = path
|
cachedFrpcPath = path
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package frp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"frpc-console/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed frpc.tmpl
|
||||||
|
var FrpcTemplateContent string
|
||||||
|
|
||||||
|
// ConfigData frpc.toml 模板渲染数据
|
||||||
|
type ConfigData struct {
|
||||||
|
*db.GlobalConfig
|
||||||
|
Proxies []db.Proxy
|
||||||
|
WireProtocolLine string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateConfig 生成 frpc.toml 配置文件
|
||||||
|
func GenerateConfig() error {
|
||||||
|
cfg, err := db.GetGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, err := db.GetProxies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeProxies []db.Proxy
|
||||||
|
for _, p := range proxies {
|
||||||
|
if p.Enabled {
|
||||||
|
activeProxies = append(activeProxies, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := ConfigData{
|
||||||
|
GlobalConfig: cfg,
|
||||||
|
Proxies: activeProxies,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.WireProtocolV2 {
|
||||||
|
data.WireProtocolLine = `wireProtocol = "v2"`
|
||||||
|
} else {
|
||||||
|
data.WireProtocolLine = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmplContent string
|
||||||
|
if _, err := os.Stat("frpc.tmpl"); err == nil {
|
||||||
|
content, readErr := os.ReadFile("frpc.tmpl")
|
||||||
|
if readErr == nil {
|
||||||
|
tmplContent = string(content)
|
||||||
|
} else {
|
||||||
|
tmplContent = FrpcTemplateContent
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tmplContent = FrpcTemplateContent
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.New("frpc").Parse(tmplContent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile("./data/frpc.toml", buf.Bytes(), 0644)
|
||||||
|
}
|
||||||
+1
-21
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// 兼容层:保持对外接口不变
|
// 兼容层:保持对外接口不变
|
||||||
// 这些函数供 api 和外部调用,实际委托给 process.Manager
|
// 这些函数供 api 调用,实际委托给 process.Manager
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
// IsRunning 检查 frpc 是否在运行
|
// IsRunning 检查 frpc 是否在运行
|
||||||
@@ -263,23 +263,3 @@ func reloadLegacy() error {
|
|||||||
log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output))
|
log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// 辅助函数 (平台相关)
|
|
||||||
// ================================================================
|
|
||||||
|
|
||||||
func setWindowHide(cmd *exec.Cmd) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
HideWindow: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setSysProcAttr(cmd *exec.Cmd) {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setpgid: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package frp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadTailLog 读取文件末尾 n 行
|
||||||
|
func ReadTailLog(filePath string, n int) ([]string, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fileSize := info.Size()
|
||||||
|
if fileSize == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkSize = 4096
|
||||||
|
var lines []string
|
||||||
|
var leftover []byte
|
||||||
|
offset := fileSize
|
||||||
|
|
||||||
|
for len(lines) < n && offset > 0 {
|
||||||
|
readSize := chunkSize
|
||||||
|
if offset < int64(chunkSize) {
|
||||||
|
readSize = int(offset)
|
||||||
|
}
|
||||||
|
offset -= int64(readSize)
|
||||||
|
|
||||||
|
buf := make([]byte, readSize)
|
||||||
|
_, err := file.ReadAt(buf, offset)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := append(buf, leftover...)
|
||||||
|
leftover = nil
|
||||||
|
|
||||||
|
start := 0
|
||||||
|
for i := len(data) - 1; i >= 0; i-- {
|
||||||
|
if data[i] == '\n' {
|
||||||
|
if i+1 < len(data) {
|
||||||
|
line := string(data[i+1:])
|
||||||
|
if line != "" {
|
||||||
|
lines = append([]string{line}, lines...)
|
||||||
|
if len(lines) >= n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lines) < n && start > 0 {
|
||||||
|
leftover = data[:start]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lines) < n && len(leftover) > 0 {
|
||||||
|
parts := strings.Split(string(leftover), "\n")
|
||||||
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
|
if parts[i] != "" {
|
||||||
|
lines = append([]string{parts[i]}, lines...)
|
||||||
|
if len(lines) >= n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !windows && !linux && !darwin && !freebsd && !netbsd && !openbsd && !solaris
|
||||||
|
|
||||||
|
package frp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setWindowHide 其他平台空实现
|
||||||
|
func setWindowHide(cmd *exec.Cmd) {}
|
||||||
|
|
||||||
|
// setSysProcAttr 其他平台空实现
|
||||||
|
func setSysProcAttr(cmd *exec.Cmd) {}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//go:build linux || darwin || freebsd || netbsd || openbsd || solaris
|
||||||
|
|
||||||
|
package frp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setSysProcAttr 为 Unix 系统设置 Setsid
|
||||||
|
func setSysProcAttr(cmd *exec.Cmd) {
|
||||||
|
if cmd.SysProcAttr == nil {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr.Setsid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// setWindowHide Unix 上不做任何事
|
||||||
|
func setWindowHide(cmd *exec.Cmd) {
|
||||||
|
// Unix 不需要隐藏窗口
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package frp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setWindowHide Windows 隐藏窗口
|
||||||
|
func setWindowHide(cmd *exec.Cmd) {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSysProcAttr Windows 不需要 Setpgid
|
||||||
|
func setSysProcAttr(cmd *exec.Cmd) {
|
||||||
|
// Windows 不支持 Setpgid
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package frp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"frpc-console/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FrpcToml 对应 frpc.toml 的完整结构
|
||||||
|
type FrpcToml struct {
|
||||||
|
ServerAddr string `json:"serverAddr"`
|
||||||
|
ServerPort int `json:"serverPort"`
|
||||||
|
Auth struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
} `json:"auth"`
|
||||||
|
Log struct {
|
||||||
|
To string `json:"to"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
MaxDays int `json:"maxDays"`
|
||||||
|
} `json:"log"`
|
||||||
|
Transport struct {
|
||||||
|
TcpMux bool `json:"tcpMux"`
|
||||||
|
TcpMuxKeepalive int `json:"tcpMuxKeepalive"`
|
||||||
|
HeartbeatInterval int `json:"heartbeatInterval"`
|
||||||
|
HeartbeatTimeout int `json:"heartbeatTimeout"`
|
||||||
|
PoolCount int `json:"poolCount"`
|
||||||
|
} `json:"transport"`
|
||||||
|
Proxies []TomlProxy `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TomlProxy 对应 [[proxies]] 条目
|
||||||
|
type TomlProxy struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
LocalIP string `json:"localIP"`
|
||||||
|
LocalPort int `json:"localPort"`
|
||||||
|
RemotePort int `json:"remotePort"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToml 解析 frpc.toml 内容
|
||||||
|
func ParseToml(content string) (*FrpcToml, error) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
result := &FrpcToml{
|
||||||
|
Proxies: []TomlProxy{},
|
||||||
|
Transport: struct {
|
||||||
|
TcpMux bool `json:"tcpMux"`
|
||||||
|
TcpMuxKeepalive int `json:"tcpMuxKeepalive"`
|
||||||
|
HeartbeatInterval int `json:"heartbeatInterval"`
|
||||||
|
HeartbeatTimeout int `json:"heartbeatTimeout"`
|
||||||
|
PoolCount int `json:"poolCount"`
|
||||||
|
}{
|
||||||
|
TcpMux: true,
|
||||||
|
TcpMuxKeepalive: 30,
|
||||||
|
HeartbeatInterval: 15,
|
||||||
|
HeartbeatTimeout: 70,
|
||||||
|
PoolCount: 8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentProxy *TomlProxy
|
||||||
|
inProxies := false
|
||||||
|
|
||||||
|
for _, rawLine := range lines {
|
||||||
|
line := strings.TrimSpace(rawLine)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "[[proxies]]") {
|
||||||
|
inProxies = true
|
||||||
|
currentProxy = &TomlProxy{
|
||||||
|
Type: "tcp",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
result.Proxies = append(result.Proxies, *currentProxy)
|
||||||
|
currentProxy = &result.Proxies[len(result.Proxies)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "[") {
|
||||||
|
inProxies = false
|
||||||
|
currentProxy = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(line, "=") {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
value = strings.Trim(value, `"`)
|
||||||
|
|
||||||
|
if inProxies && currentProxy != nil {
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
currentProxy.Name = value
|
||||||
|
case "type":
|
||||||
|
currentProxy.Type = value
|
||||||
|
case "localIP":
|
||||||
|
currentProxy.LocalIP = value
|
||||||
|
case "localPort":
|
||||||
|
currentProxy.LocalPort, _ = strconv.Atoi(value)
|
||||||
|
case "remotePort":
|
||||||
|
currentProxy.RemotePort, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch key {
|
||||||
|
case "serverAddr":
|
||||||
|
result.ServerAddr = value
|
||||||
|
case "serverPort":
|
||||||
|
result.ServerPort, _ = strconv.Atoi(value)
|
||||||
|
case "token":
|
||||||
|
result.Auth.Token = value
|
||||||
|
case "level":
|
||||||
|
result.Log.Level = value
|
||||||
|
case "maxDays":
|
||||||
|
result.Log.MaxDays, _ = strconv.Atoi(value)
|
||||||
|
case "tcpMux":
|
||||||
|
result.Transport.TcpMux = value == "true"
|
||||||
|
case "tcpMuxKeepaliveInterval":
|
||||||
|
result.Transport.TcpMuxKeepalive, _ = strconv.Atoi(value)
|
||||||
|
case "heartbeatInterval":
|
||||||
|
result.Transport.HeartbeatInterval, _ = strconv.Atoi(value)
|
||||||
|
case "heartbeatTimeout":
|
||||||
|
result.Transport.HeartbeatTimeout, _ = strconv.Atoi(value)
|
||||||
|
case "poolCount":
|
||||||
|
result.Transport.PoolCount, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.ServerAddr == "" {
|
||||||
|
return nil, fmt.Errorf("未找到 serverAddr 字段")
|
||||||
|
}
|
||||||
|
if len(result.Proxies) == 0 {
|
||||||
|
return nil, fmt.Errorf("未找到任何隧道条目")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGlobalConfig 将解析结果转换为 db.GlobalConfig
|
||||||
|
func (f *FrpcToml) ToGlobalConfig() *db.GlobalConfig {
|
||||||
|
return &db.GlobalConfig{
|
||||||
|
ServerAddr: f.ServerAddr,
|
||||||
|
ServerPort: f.ServerPort,
|
||||||
|
Token: f.Auth.Token,
|
||||||
|
LogLevel: f.Log.Level,
|
||||||
|
LogMaxDays: f.Log.MaxDays,
|
||||||
|
TcpMux: f.Transport.TcpMux,
|
||||||
|
TcpMuxKeepalive: f.Transport.TcpMuxKeepalive,
|
||||||
|
HeartbeatInterval: f.Transport.HeartbeatInterval,
|
||||||
|
HeartbeatTimeout: f.Transport.HeartbeatTimeout,
|
||||||
|
PoolCount: f.Transport.PoolCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToProxies 将解析结果转换为 db.Proxy 列表
|
||||||
|
func (f *FrpcToml) ToProxies() []db.Proxy {
|
||||||
|
var proxies []db.Proxy
|
||||||
|
for _, p := range f.Proxies {
|
||||||
|
proxies = append(proxies, db.Proxy{
|
||||||
|
Name: p.Name,
|
||||||
|
Type: p.Type,
|
||||||
|
LocalIP: p.LocalIP,
|
||||||
|
LocalPort: p.LocalPort,
|
||||||
|
RemotePort: p.RemotePort,
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return proxies
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setProcessAttributes 设置 Linux 进程属性 (Setpgid)
|
||||||
|
func setProcessAttributes(cmd *exec.Cmd) {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setpgid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setProcessAttributes 设置进程属性 (非 Linux)
|
||||||
|
func setProcessAttributes(cmd *exec.Cmd) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 其他平台不做特殊设置
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lock 获取进程间互斥锁 (Linux: flock)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock 释放互斥锁 (Linux: flock)
|
||||||
|
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 !linux && !windows
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
// Lock 获取进程间互斥锁 (非 Linux/Windows: 内存锁)
|
||||||
|
func (pm *ProcessManager) Lock() error {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
if pm.locked {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.locked = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock 释放互斥锁 (非 Linux/Windows)
|
||||||
|
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,30 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
// Lock 获取进程间互斥锁 (Windows: 内存锁)
|
||||||
|
// 注意: Windows 版本仅在同一进程内互斥,进程间不互斥
|
||||||
|
// 如需真正的进程间锁,后续可改用 Windows Named Mutex
|
||||||
|
func (pm *ProcessManager) Lock() error {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
if pm.locked {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.locked = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock 释放互斥锁 (Windows)
|
||||||
|
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,581 @@
|
|||||||
|
// process/manager.go
|
||||||
|
// frpc-console 进程管理模块
|
||||||
|
// 2.6-preview: 端口检测 + 单实例锁定 + 状态自述
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 常量定义
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
const (
|
||||||
|
LockFileName = ".frpc.lock"
|
||||||
|
PortCheckTimeout = 500 * time.Millisecond
|
||||||
|
StartWaitTime = 500 * time.Millisecond
|
||||||
|
StopMaxWaitTime = 5 * time.Second
|
||||||
|
LockAcquireTimeout = 30 * time.Second
|
||||||
|
LockRetryInterval = 100 * time.Millisecond
|
||||||
|
APITimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 数据结构
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// PortStatus 端口检测结果
|
||||||
|
type PortStatus struct {
|
||||||
|
Port int `json:"port"`
|
||||||
|
Occupied bool `json:"occupied"`
|
||||||
|
PID int `json:"pid"`
|
||||||
|
IsFRPC bool `json:"is_frpc"`
|
||||||
|
ProcessCmd string `json:"process_cmd,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessStatus frpc 进程状态
|
||||||
|
type ProcessStatus struct {
|
||||||
|
State string `json:"state"` // "running" | "stopped" | "unknown" | "conflict"
|
||||||
|
PID int `json:"pid"` // 进程 PID (如果运行中)
|
||||||
|
Port int `json:"port"` // 监听的端口
|
||||||
|
Uptime string `json:"uptime,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FRPCStatus 来自 frpc admin API 的状态响应
|
||||||
|
type FRPCStatus struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
RunID string `json:"run_id"`
|
||||||
|
Proxies []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LocalAddr string `json:"local_addr"`
|
||||||
|
} `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// ProcessManager 主结构
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
type ProcessManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
dataDir string
|
||||||
|
configPath string
|
||||||
|
frpcBinPath string
|
||||||
|
adminPort int
|
||||||
|
lockFile *os.File
|
||||||
|
locked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalManager *ProcessManager
|
||||||
|
globalMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewManager 创建进程管理器
|
||||||
|
func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager {
|
||||||
|
return &ProcessManager{
|
||||||
|
dataDir: dataDir,
|
||||||
|
configPath: configPath,
|
||||||
|
frpcBinPath: frpcBinPath,
|
||||||
|
adminPort: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalManager 设置全局管理器
|
||||||
|
func SetGlobalManager(pm *ProcessManager) {
|
||||||
|
globalMu.Lock()
|
||||||
|
defer globalMu.Unlock()
|
||||||
|
globalManager = pm
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalManager 获取全局管理器
|
||||||
|
func GetGlobalManager() *ProcessManager {
|
||||||
|
globalMu.Lock()
|
||||||
|
defer globalMu.Unlock()
|
||||||
|
return globalManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPort 获取 admin_port
|
||||||
|
func (pm *ProcessManager) AdminPort() int {
|
||||||
|
return pm.adminPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 配置读取
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// LoadConfig 从 frpc.toml 读取 admin_port 配置
|
||||||
|
// 兼容 frp 0.52.0 前后的配置格式
|
||||||
|
func (pm *ProcessManager) LoadConfig() error {
|
||||||
|
content, err := os.ReadFile(pm.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if port := extractIntValue(string(content), "admin_port"); port > 0 {
|
||||||
|
pm.adminPort = port
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 {
|
||||||
|
pm.adminPort = port
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("未找到 admin_port 或 webServer.port 配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractIntValue(content, key string) int {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, key) {
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
val := strings.TrimSpace(parts[1])
|
||||||
|
val = strings.Trim(val, `"`)
|
||||||
|
if port, err := strconv.Atoi(val); err == nil && port > 0 {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractIntValueFromSection(content, section, key string) int {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
inSection := false
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||||
|
inSection = strings.Trim(trimmed, "[]") == section
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inSection && strings.HasPrefix(trimmed, key) {
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
val := strings.TrimSpace(parts[1])
|
||||||
|
val = strings.Trim(val, `"`)
|
||||||
|
if port, err := strconv.Atoi(val); err == nil && port > 0 {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 端口检测
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// CheckPort 检测端口是否被占用
|
||||||
|
func (pm *ProcessManager) CheckPort() (bool, error) {
|
||||||
|
if pm.adminPort <= 0 {
|
||||||
|
return false, fmt.Errorf("admin_port 未配置")
|
||||||
|
}
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPortStatus 获取端口完整状态 (占用 + PID + 进程类型)
|
||||||
|
func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) {
|
||||||
|
status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false}
|
||||||
|
occupied, err := pm.CheckPort()
|
||||||
|
if err != nil {
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
status.Occupied = occupied
|
||||||
|
if !occupied {
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := pm.getPIDByPort(pm.adminPort)
|
||||||
|
if err != nil {
|
||||||
|
if pidFromFile := pm.readPIDFile(); pidFromFile > 0 {
|
||||||
|
if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) {
|
||||||
|
status.PID = pidFromFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status.PID = pid
|
||||||
|
}
|
||||||
|
if status.PID == 0 {
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
isFRPC, cmd := pm.isFRPCProcess(status.PID)
|
||||||
|
status.IsFRPC = isFRPC
|
||||||
|
status.ProcessCmd = cmd
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) getPIDByPort(port int) (int, error) {
|
||||||
|
if pid, err := pm.getPIDBySS(port); err == nil && pid > 0 {
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
if pid, err := pm.getPIDByNetstat(port); err == nil && pid > 0 {
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
if pid, err := pm.getPIDByLsof(port); err == nil && pid > 0 {
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("无法通过端口反查 PID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) getPIDBySS(port int) (int, error) {
|
||||||
|
cmd := exec.Command("ss", "-lpn", "state", "listening")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if !strings.Contains(line, fmt.Sprintf(":%d", port)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx := strings.Index(line, "pid="); idx != -1 {
|
||||||
|
end := strings.Index(line[idx:], ",")
|
||||||
|
if end == -1 {
|
||||||
|
end = strings.Index(line[idx:], ")")
|
||||||
|
}
|
||||||
|
if end == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pidStr := line[idx+4 : idx+end]
|
||||||
|
pidStr = strings.TrimSpace(pidStr)
|
||||||
|
if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 {
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("未找到监听端口 %d 的进程", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) getPIDByNetstat(port int) (int, error) {
|
||||||
|
cmd := exec.Command("netstat", "-tulpn")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if !strings.Contains(line, fmt.Sprintf(":%d", port)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 7 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
last := parts[len(parts)-1]
|
||||||
|
if idx := strings.Index(last, "/"); idx != -1 {
|
||||||
|
pidStr := last[:idx]
|
||||||
|
if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 {
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("未找到监听端口 %d 的进程", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) getPIDByLsof(port int) (int, error) {
|
||||||
|
cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.Contains(line, "frpc") {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
if pid, err := strconv.Atoi(parts[1]); err == nil && pid > 0 {
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("未找到监听端口 %d 的 frpc 进程", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool {
|
||||||
|
cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-a", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(string(out), "LISTEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) {
|
||||||
|
cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid)
|
||||||
|
if data, err := os.ReadFile(cmdlinePath); err == nil {
|
||||||
|
cmd := strings.ReplaceAll(string(data), "\x00", " ")
|
||||||
|
if strings.Contains(cmd, "frpc") {
|
||||||
|
return true, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
args := strings.TrimSpace(string(out))
|
||||||
|
if strings.Contains(args, "frpc") {
|
||||||
|
return true, args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// PID 文件操作
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func (pm *ProcessManager) pidFilePath() string {
|
||||||
|
return filepath.Join(pm.dataDir, "frpc.pid")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) readPIDFile() int {
|
||||||
|
data, err := os.ReadFile(pm.pidFilePath())
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil || pid <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) writePIDFile(pid int) error {
|
||||||
|
return os.WriteFile(pm.pidFilePath(), []byte(strconv.Itoa(pid)), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) deletePIDFile() error {
|
||||||
|
err := os.Remove(pm.pidFilePath())
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 状态查询
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// Status 获取 frpc 进程实时状态
|
||||||
|
func (pm *ProcessManager) Status() (*ProcessStatus, error) {
|
||||||
|
status := &ProcessStatus{State: "unknown", PID: 0, Port: pm.adminPort}
|
||||||
|
if pm.adminPort <= 0 {
|
||||||
|
status.Error = "admin_port 未配置"
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
portStatus, err := pm.GetPortStatus()
|
||||||
|
if err != nil {
|
||||||
|
status.Error = err.Error()
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
if !portStatus.Occupied {
|
||||||
|
pm.deletePIDFile()
|
||||||
|
status.State = "stopped"
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
if !portStatus.IsFRPC {
|
||||||
|
status.State = "conflict"
|
||||||
|
status.PID = portStatus.PID
|
||||||
|
status.Error = fmt.Sprintf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID)
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
status.State = "running"
|
||||||
|
status.PID = portStatus.PID
|
||||||
|
pm.writePIDFile(portStatus.PID)
|
||||||
|
if info := pm.getFRPCStatus(portStatus.PID); info != nil {
|
||||||
|
status.Version = info.Version
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// API 回源 (2.7-preview 预留)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus {
|
||||||
|
if pid <= 0 || pm.adminPort <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("http://127.0.0.1:%d/api/status", pm.adminPort)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), APITimeout)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
client := &http.Client{Timeout: APITimeout}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var status FRPCStatus
|
||||||
|
if err := json.Unmarshal(body, &status); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &status
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 操作执行 (Start / Stop / Restart)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// Start 启动 frpc (幂等)
|
||||||
|
func (pm *ProcessManager) Start(ctx context.Context) error {
|
||||||
|
if err := pm.Lock(); err != nil {
|
||||||
|
return fmt.Errorf("获取锁失败: %w", err)
|
||||||
|
}
|
||||||
|
defer pm.Unlock()
|
||||||
|
return pm.startLocked(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) startLocked(ctx context.Context) error {
|
||||||
|
portStatus, err := pm.GetPortStatus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("检测端口状态失败: %w", err)
|
||||||
|
}
|
||||||
|
if portStatus.Occupied {
|
||||||
|
if portStatus.IsFRPC {
|
||||||
|
pm.writePIDFile(portStatus.PID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath)
|
||||||
|
|
||||||
|
// 设置进程属性 (平台相关)
|
||||||
|
setProcessAttributes(cmd)
|
||||||
|
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("启动 frpc 失败: %w", err)
|
||||||
|
}
|
||||||
|
time.Sleep(StartWaitTime)
|
||||||
|
occupied, err := pm.CheckPort()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("验证启动状态失败: %w", err)
|
||||||
|
}
|
||||||
|
if !occupied {
|
||||||
|
return fmt.Errorf("frpc 启动失败: 端口未监听")
|
||||||
|
}
|
||||||
|
pid, _ := pm.getPIDByPort(pm.adminPort)
|
||||||
|
if pid > 0 {
|
||||||
|
pm.writePIDFile(pid)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止 frpc (幂等)
|
||||||
|
func (pm *ProcessManager) Stop(ctx context.Context) error {
|
||||||
|
if err := pm.Lock(); err != nil {
|
||||||
|
return fmt.Errorf("获取锁失败: %w", err)
|
||||||
|
}
|
||||||
|
defer pm.Unlock()
|
||||||
|
return pm.stopLocked(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) stopLocked(ctx context.Context) error {
|
||||||
|
portStatus, err := pm.GetPortStatus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("检测端口状态失败: %w", err)
|
||||||
|
}
|
||||||
|
if !portStatus.Occupied {
|
||||||
|
pm.deletePIDFile()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var pid int
|
||||||
|
if portStatus.IsFRPC {
|
||||||
|
pid = portStatus.PID
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("端口 %d 被非 frpc 进程占用, 无法安全停止", pm.adminPort)
|
||||||
|
}
|
||||||
|
if pid <= 0 {
|
||||||
|
pid = pm.readPIDFile()
|
||||||
|
if pid <= 0 {
|
||||||
|
return fmt.Errorf("无法确定 frpc 进程 PID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
pm.deletePIDFile()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
pm.deletePIDFile()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
for time.Since(start) < StopMaxWaitTime {
|
||||||
|
occupied, _ := pm.CheckPort()
|
||||||
|
if !occupied {
|
||||||
|
pm.deletePIDFile()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
proc.Kill()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
if occupied, _ := pm.CheckPort(); occupied {
|
||||||
|
return fmt.Errorf("强制停止失败: 端口仍被占用")
|
||||||
|
}
|
||||||
|
pm.deletePIDFile()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart 重启 frpc (原子操作)
|
||||||
|
func (pm *ProcessManager) Restart(ctx context.Context) error {
|
||||||
|
if err := pm.Lock(); err != nil {
|
||||||
|
return fmt.Errorf("获取锁失败: %w", err)
|
||||||
|
}
|
||||||
|
defer pm.Unlock()
|
||||||
|
if err := pm.stopLocked(ctx); err != nil {
|
||||||
|
return fmt.Errorf("停止失败: %w", err)
|
||||||
|
}
|
||||||
|
if err := pm.startLocked(ctx); err != nil {
|
||||||
|
return fmt.Errorf("启动失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user