目录重构完成,但是Preview通道尚未可用

This commit is contained in:
2026-08-07 19:42:49 +08:00
parent 3a381c85ed
commit 86df4bf3c5
16 changed files with 1620 additions and 24 deletions
-3
View File
@@ -50,14 +50,12 @@ func GetFrpcPath() (string, error) {
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
}
// 尝试从本地 bin 目录加载
localPath := filepath.Join(".", "bin", fileName)
if _, err := os.Stat(localPath); err == nil {
cachedFrpcPath = localPath
return localPath, nil
}
// 尝试从 embed 提取到临时目录
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
if err == nil {
tmpPath := filepath.Join(os.TempDir(), "frpc")
@@ -74,7 +72,6 @@ func GetFrpcPath() (string, error) {
}
}
// 最后尝试从系统 PATH 查找
path, err := exec.LookPath("frpc")
if err == nil {
cachedFrpcPath = path
+78
View File
@@ -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
View File
@@ -15,7 +15,7 @@ import (
// ================================================================
// 兼容层:保持对外接口不变
// 这些函数供 api 和外部调用,实际委托给 process.Manager
// 这些函数供 api 调用,实际委托给 process.Manager
// ================================================================
// IsRunning 检查 frpc 是否在运行
@@ -263,23 +263,3 @@ func reloadLegacy() error {
log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output))
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,
}
}
}
+81
View File
@@ -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
}
+13
View File
@@ -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) {}
+21
View File
@@ -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 不需要隐藏窗口
}
+20
View File
@@ -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
}
+175
View File
@@ -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
}