正在优化linux端的运行状态于逻辑

This commit is contained in:
2026-07-24 20:13:21 +08:00
parent 2105ed9d8e
commit 699a9e3b92
9 changed files with 106 additions and 54 deletions
+29 -6
View File
@@ -2,7 +2,9 @@ package main
import ( import (
"bytes" "bytes"
"embed"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"strconv" "strconv"
"text/template" "text/template"
@@ -11,45 +13,66 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
//go:embed static/*
var staticFS embed.FS
func SetupRouter() *gin.Engine { func SetupRouter() *gin.Engine {
r := gin.Default() r := gin.Default()
r.Static("/static", "./static") // 从 embed 读取前端静态文件
// 注意:embed.FS 不支持直接作为 http.FileSystem 传参,需要用 http.FS 转换
staticSubFS, _ := fs.Sub(staticFS, "static")
r.StaticFS("/static", http.FS(staticSubFS))
// 根路由:直接返回 index.html
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) { r.GET("/ping", func(c *gin.Context) {
c.String(200, "frpc-console 后端已启动 🎉") c.String(200, "frpc-console 后端已启动 🎉")
}) })
r.GET("/", func(c *gin.Context) {
c.File("./static/index.html")
})
// API 路由
api := r.Group("/api") api := r.Group("/api")
{ {
// 公开接口 // 登录(无需鉴权)
api.GET("/check/users", checkUsersHandler) api.GET("/check/users", checkUsersHandler)
api.POST("/register", registerHandler) api.POST("/register", registerHandler)
api.POST("/login", loginHandler) api.POST("/login", loginHandler)
// 需鉴权 // 需鉴权的路由
auth := api.Group("/") auth := api.Group("/")
auth.Use(AuthMiddleware()) auth.Use(AuthMiddleware())
{ {
// 全局配置
auth.GET("/config", getConfigHandler) auth.GET("/config", getConfigHandler)
auth.PUT("/config", updateConfigHandler) auth.PUT("/config", updateConfigHandler)
// 隧道管理
auth.GET("/proxies", getProxiesHandler) auth.GET("/proxies", getProxiesHandler)
auth.GET("/proxy/:id", getProxyHandler) auth.GET("/proxy/:id", getProxyHandler)
auth.POST("/proxy", createProxyHandler) auth.POST("/proxy", createProxyHandler)
auth.PUT("/proxy/:id", updateProxyHandler) auth.PUT("/proxy/:id", updateProxyHandler)
auth.DELETE("/proxy/:id", deleteProxyHandler) auth.DELETE("/proxy/:id", deleteProxyHandler)
// frpc 控制
auth.POST("/frpc/reload", reloadFrpcHandler) auth.POST("/frpc/reload", reloadFrpcHandler)
auth.POST("/frpc/start", startFrpcHandler) auth.POST("/frpc/start", startFrpcHandler)
auth.POST("/frpc/stop", stopFrpcHandler) auth.POST("/frpc/stop", stopFrpcHandler)
auth.GET("/frpc/status", getFrpcStatusHandler) auth.GET("/frpc/status", getFrpcStatusHandler)
// 导入/导出 TOML
auth.POST("/import/toml", importTomlHandler) auth.POST("/import/toml", importTomlHandler)
auth.GET("/export/toml", ExportTomlHandler) auth.GET("/export/toml", ExportTomlHandler)
// 修改密码
auth.PUT("/user/password", changePasswordHandler) auth.PUT("/user/password", changePasswordHandler)
} }
} }
+55 -40
View File
@@ -4,13 +4,17 @@ import (
"bytes" "bytes"
"embed" "embed"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"text/template" "text/template"
"github.com/creack/pty"
) )
//go:embed bin/* //go:embed bin/*
@@ -24,7 +28,10 @@ var (
frpcPathMutex sync.Mutex frpcPathMutex sync.Mutex
) )
// getFrpcPath 获取当前平台对应的 frpc 路径优先使用缓存) // getFrpcPath 获取 frpc 路径优先级:
// 1. 当前目录 ./bin/<platform> (便于用户手动管理 frpc 版本)
// 2. embed 解压到 /tmp (便于开箱即用)
// 3. 系统 PATH 中的 frpc (降级方案)
func getFrpcPath() (string, error) { func getFrpcPath() (string, error) {
frpcPathMutex.Lock() frpcPathMutex.Lock()
defer frpcPathMutex.Unlock() defer frpcPathMutex.Unlock()
@@ -34,21 +41,24 @@ func getFrpcPath() (string, error) {
if _, err := os.Stat(cachedFrpcPath); err == nil { if _, err := os.Stat(cachedFrpcPath); err == nil {
return cachedFrpcPath, nil return cachedFrpcPath, nil
} }
// 缓存失效,清空
cachedFrpcPath = "" cachedFrpcPath = ""
} }
// 确定文件名 // 确定当前平台的文件名
var fileName string var fileName string
switch { switch {
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
fileName = "frpc_windows_amd64.exe" fileName = "frpc_windows_amd64.exe"
case runtime.GOOS == "windows" && runtime.GOARCH == "386":
fileName = "frpc_windows_386.exe"
case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": case runtime.GOOS == "linux" && runtime.GOARCH == "amd64":
fileName = "frpc_linux_amd64" fileName = "frpc_linux_amd64"
case runtime.GOOS == "linux" && runtime.GOARCH == "arm64": case runtime.GOOS == "linux" && runtime.GOARCH == "arm64":
fileName = "frpc_linux_arm64" fileName = "frpc_linux_arm64"
case runtime.GOOS == "linux" && runtime.GOARCH == "arm":
fileName = "frpc_linux_armv7"
default: default:
// 降级:尝试从系统 PATH 查找 frpc // 不支持的平台,尝试从 PATH
path, err := exec.LookPath("frpc") path, err := exec.LookPath("frpc")
if err == nil { if err == nil {
cachedFrpcPath = path cachedFrpcPath = path
@@ -57,36 +67,39 @@ func getFrpcPath() (string, error) {
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH) return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
} }
// 从 embed 读取 // ===== 优先级 1:从当前目录 ./bin/ 查找 =====
data, err := embeddedFrpc.ReadFile("bin/" + fileName) localPath := filepath.Join(".", "bin", fileName)
if err != nil { if _, err := os.Stat(localPath); err == nil {
// 降级到 PATH cachedFrpcPath = localPath
path, lookErr := exec.LookPath("frpc") return localPath, nil
if lookErr == nil {
cachedFrpcPath = path
return path, nil
}
return "", fmt.Errorf("未找到 frpc 文件 (embed 和 PATH 均无): %w", err)
} }
// 写入临时目录 // ===== 优先级 2:从 embed 读取并解压到临时目录 =====
tmpPath := os.TempDir() + "/frpc" data, err := embeddedFrpc.ReadFile("bin/" + fileName)
if err == nil {
tmpPath := filepath.Join(os.TempDir(), "frpc")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
tmpPath += ".exe" tmpPath += ".exe"
} }
if err := os.WriteFile(tmpPath, data, 0755); err == nil {
// 尝试写入(如果文件已存在且正在使用,忽略错误,直接使用已有文件) cachedFrpcPath = tmpPath
if err := os.WriteFile(tmpPath, data, 0755); err != nil { return tmpPath, nil
// 如果写入失败,检查是否已有可用的 frpc }
// 写入失败时,检查是否已有可用的临时文件
if _, statErr := os.Stat(tmpPath); statErr == nil { if _, statErr := os.Stat(tmpPath); statErr == nil {
cachedFrpcPath = tmpPath cachedFrpcPath = tmpPath
return tmpPath, nil return tmpPath, nil
} }
return "", fmt.Errorf("写入临时文件失败: %w", err)
} }
cachedFrpcPath = tmpPath // ===== 优先级 3:降级到系统 PATH =====
return tmpPath, nil path, err := exec.LookPath("frpc")
if err == nil {
cachedFrpcPath = path
return path, nil
}
return "", fmt.Errorf("未找到 frpc 文件 (本地 bin/、embed、PATH 均无)")
} }
// GenerateFrpcConfig 生成 frpc.toml 配置文件 // GenerateFrpcConfig 生成 frpc.toml 配置文件
@@ -116,23 +129,18 @@ func GenerateFrpcConfig() error {
Proxies: activeProxies, Proxies: activeProxies,
} }
// ===== 核心改动 ===== // 优先使用外部模板文件,不存在则用 embed
// 优先使用外部模板文件 frpc.tmpl,如果不存在则使用 embed 中的模板
var tmplContent string var tmplContent string
if _, err := os.Stat("frpc.tmpl"); err == nil { if _, err := os.Stat("frpc.tmpl"); err == nil {
// 外部文件存在,读取它
content, readErr := os.ReadFile("frpc.tmpl") content, readErr := os.ReadFile("frpc.tmpl")
if readErr == nil { if readErr == nil {
tmplContent = string(content) tmplContent = string(content)
} else { } else {
// 读取失败时回退到 embed
tmplContent = FrpcTemplateContent tmplContent = FrpcTemplateContent
} }
} else { } else {
// 外部文件不存在,使用 embed 中的模板
tmplContent = FrpcTemplateContent tmplContent = FrpcTemplateContent
} }
// ===== 核心改动结束 =====
tmpl, err := template.New("frpc").Parse(tmplContent) tmpl, err := template.New("frpc").Parse(tmplContent)
if err != nil { if err != nil {
@@ -151,38 +159,46 @@ func GenerateFrpcConfig() error {
return nil return nil
} }
// StartFrpc 启动 frpc(后台运行) // StartFrpc 启动 frpc
func StartFrpc() error { func StartFrpc() 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.Stat("./frpc.toml"); os.IsNotExist(err) { if _, err := os.Stat("./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)
} }
} }
// 检查是否已在运行
running, _ := GetFrpcStatus() running, _ := GetFrpcStatus()
if running { if running {
return nil return nil
} }
cmd := exec.Command(frpcPath, "-c", "./frpc.toml") cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
if runtime.GOOS == "windows" {
setWindowHide(cmd) setWindowHide(cmd)
// ✅ Linux 下分配伪终端,让 frpc 认为自己在真实终端中运行
if runtime.GOOS != "windows" {
f, err := pty.Start(cmd)
if err != nil {
return fmt.Errorf("启动伪终端失败: %w", err)
} }
// 可选:丢弃 pty 输出,或写入日志文件
go io.Copy(io.Discard, f)
} else {
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return fmt.Errorf("启动 frpc 失败: %w", err) return fmt.Errorf("启动 frpc 失败: %w", err)
} }
}
return nil return nil
} }
// ReloadFrpc 热加载 frpc 配置(仅发信号,不重复写入二进制) // ReloadFrpc 热加载,失败时自动降级为重启
// ReloadFrpc 热加载 frpc 配置,失败时自动降级为重启
func ReloadFrpc() error { func ReloadFrpc() error {
running, err := GetFrpcStatus() running, err := GetFrpcStatus()
if err != nil { if err != nil {
@@ -192,14 +208,13 @@ func ReloadFrpc() error {
return StartFrpc() return StartFrpc()
} }
if cachedFrpcPath == "" { frpcPath, err := getFrpcPath()
if _, err := getFrpcPath(); err != nil { if err != nil {
return fmt.Errorf("获取 frpc 路径失败: %w", err) return fmt.Errorf("获取 frpc 路径失败: %w", err)
} }
}
cmd := exec.Command(cachedFrpcPath, "reload", "-c", "./frpc.toml") cmd := exec.Command(frpcPath, "reload", "-c", "./frpc.toml")
_, err = cmd.CombinedOutput() // ← 这里改了 _, err = cmd.CombinedOutput()
if err != nil { if err != nil {
log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err) log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err)
if stopErr := StopFrpc(); stopErr != nil { if stopErr := StopFrpc(); stopErr != nil {
+10
View File
@@ -0,0 +1,10 @@
// frp_other.go
//go:build !windows
package main
import "os/exec"
func setWindowHide(cmd *exec.Cmd) {
// 非 Windows 平台什么都不做
}
+1
View File
@@ -14,6 +14,7 @@ require (
github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
+2
View File
@@ -6,6 +6,8 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+1
View File
@@ -7,6 +7,7 @@ const { createApp, ref, reactive, computed, onMounted } = Vue;
// ============================================================ // ============================================================
const API_BASE = "/api"; const API_BASE = "/api";
function getToken() { function getToken() {
return localStorage.getItem("frpc_token") || ""; return localStorage.getItem("frpc_token") || "";
} }