300 lines
6.8 KiB
Go
300 lines
6.8 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"embed"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"syscall"
|
||
"text/template"
|
||
)
|
||
|
||
//go:embed bin/*
|
||
var embeddedFrpc embed.FS
|
||
|
||
//go:embed frpc.tmpl
|
||
var FrpcTemplateContent string
|
||
|
||
var (
|
||
cachedFrpcPath string
|
||
frpcPathMutex sync.Mutex
|
||
)
|
||
|
||
// ============================================================
|
||
// 获取 frpc 路径
|
||
// ============================================================
|
||
|
||
func getFrpcPath() (string, error) {
|
||
frpcPathMutex.Lock()
|
||
defer frpcPathMutex.Unlock()
|
||
|
||
if cachedFrpcPath != "" {
|
||
if _, err := os.Stat(cachedFrpcPath); err == nil {
|
||
return cachedFrpcPath, nil
|
||
}
|
||
cachedFrpcPath = ""
|
||
}
|
||
|
||
var fileName string
|
||
switch {
|
||
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
|
||
fileName = "frpc_windows_amd64.exe"
|
||
case runtime.GOOS == "linux" && runtime.GOARCH == "amd64":
|
||
fileName = "frpc_linux_amd64"
|
||
case runtime.GOOS == "linux" && runtime.GOARCH == "arm64":
|
||
fileName = "frpc_linux_arm64"
|
||
case runtime.GOOS == "linux" && runtime.GOARCH == "arm":
|
||
fileName = "frpc_linux_arm_hf"
|
||
default:
|
||
path, err := exec.LookPath("frpc")
|
||
if err == nil {
|
||
cachedFrpcPath = path
|
||
return path, nil
|
||
}
|
||
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||
}
|
||
|
||
// 优先级1:本地 ./bin/
|
||
localPath := filepath.Join(".", "bin", fileName)
|
||
if _, err := os.Stat(localPath); err == nil {
|
||
cachedFrpcPath = localPath
|
||
return localPath, nil
|
||
}
|
||
|
||
// 优先级2:embed 解压
|
||
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
||
if err == nil {
|
||
tmpPath := filepath.Join(os.TempDir(), "frpc")
|
||
if runtime.GOOS == "windows" {
|
||
tmpPath += ".exe"
|
||
}
|
||
if err := os.WriteFile(tmpPath, data, 0755); err == nil {
|
||
cachedFrpcPath = tmpPath
|
||
return tmpPath, nil
|
||
}
|
||
if _, statErr := os.Stat(tmpPath); statErr == nil {
|
||
cachedFrpcPath = tmpPath
|
||
return tmpPath, nil
|
||
}
|
||
}
|
||
|
||
// 优先级3:系统 PATH
|
||
path, err := exec.LookPath("frpc")
|
||
if err == nil {
|
||
cachedFrpcPath = path
|
||
return path, nil
|
||
}
|
||
|
||
return "", fmt.Errorf("未找到 frpc 文件")
|
||
}
|
||
|
||
// ============================================================
|
||
// 生成 frpc.toml(v2.0 支持 wireProtocol)
|
||
// ============================================================
|
||
|
||
func GenerateFrpcConfig() error {
|
||
cfg, err := GetGlobalConfig()
|
||
if err != nil {
|
||
return fmt.Errorf("读取全局配置失败: %w", err)
|
||
}
|
||
|
||
proxies, err := GetProxies()
|
||
if err != nil {
|
||
return fmt.Errorf("读取隧道列表失败: %w", err)
|
||
}
|
||
|
||
var activeProxies []Proxy
|
||
for _, p := range proxies {
|
||
if p.Enabled {
|
||
activeProxies = append(activeProxies, p)
|
||
}
|
||
}
|
||
|
||
// 构建模板数据
|
||
data := struct {
|
||
*GlobalConfig
|
||
Proxies []Proxy
|
||
WireProtocolLine string // v2 协议配置行(为空则不输出)
|
||
}{
|
||
GlobalConfig: cfg,
|
||
Proxies: activeProxies,
|
||
}
|
||
|
||
// v2.0: 如果启用 v2 协议,生成配置行
|
||
if cfg.WireProtocolV2 {
|
||
data.WireProtocolLine = `wireProtocol = "v2"`
|
||
} else {
|
||
data.WireProtocolLine = "" // 不输出,使用默认 v1
|
||
}
|
||
|
||
// 读取模板
|
||
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 fmt.Errorf("解析模板失败: %w", err)
|
||
}
|
||
|
||
var buf bytes.Buffer
|
||
if err := tmpl.Execute(&buf, data); err != nil {
|
||
return fmt.Errorf("渲染模板失败: %w", err)
|
||
}
|
||
|
||
if err := os.WriteFile("./frpc.toml", buf.Bytes(), 0644); err != nil {
|
||
return fmt.Errorf("写入配置文件失败: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ============================================================
|
||
// frpc 进程管理
|
||
// ============================================================
|
||
|
||
func isFrpcRunning() bool {
|
||
pidData, err := os.ReadFile("./frpc.pid")
|
||
if err != nil {
|
||
return false
|
||
}
|
||
pid, err := strconv.Atoi(strings.TrimSpace(string(pidData)))
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
if runtime.GOOS == "windows" {
|
||
cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid))
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return strings.Contains(string(output), strconv.Itoa(pid))
|
||
}
|
||
|
||
process, err := os.FindProcess(pid)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return process.Signal(syscall.Signal(0)) == nil
|
||
}
|
||
|
||
func StartFrpc() error {
|
||
frpcPath, err := getFrpcPath()
|
||
if err != nil {
|
||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||
}
|
||
|
||
if _, err := os.Stat("./frpc.toml"); os.IsNotExist(err) {
|
||
if err := GenerateFrpcConfig(); err != nil {
|
||
return fmt.Errorf("生成配置文件失败: %w", err)
|
||
}
|
||
}
|
||
|
||
if isFrpcRunning() {
|
||
return nil
|
||
}
|
||
|
||
os.Remove("./frpc.pid")
|
||
|
||
cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
|
||
setWindowHide(cmd)
|
||
setSysProcAttr(cmd)
|
||
|
||
logFile, err := os.OpenFile("./frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||
if err != nil {
|
||
return fmt.Errorf("打开日志文件失败: %w", err)
|
||
}
|
||
cmd.Stdout = logFile
|
||
cmd.Stderr = logFile
|
||
|
||
if err := cmd.Start(); err != nil {
|
||
return fmt.Errorf("启动 frpc 失败: %w", err)
|
||
}
|
||
|
||
if err := os.WriteFile("./frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
|
||
return fmt.Errorf("保存 PID 失败: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func StopFrpc() error {
|
||
if runtime.GOOS == "windows" {
|
||
cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe")
|
||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") {
|
||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||
}
|
||
os.Remove("./frpc.pid")
|
||
return nil
|
||
}
|
||
|
||
pidData, err := os.ReadFile("./frpc.pid")
|
||
if err != nil {
|
||
cmd := exec.Command("pkill", "-f", "frpc")
|
||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
|
||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData)))
|
||
process, err := os.FindProcess(pid)
|
||
if err != nil {
|
||
os.Remove("./frpc.pid")
|
||
return nil
|
||
}
|
||
|
||
if err := process.Kill(); err != nil {
|
||
return fmt.Errorf("杀死进程失败: %w", err)
|
||
}
|
||
|
||
os.Remove("./frpc.pid")
|
||
return nil
|
||
}
|
||
|
||
func GetFrpcStatus() (bool, error) {
|
||
return isFrpcRunning(), nil
|
||
}
|
||
|
||
func ReloadFrpc() error {
|
||
running := isFrpcRunning()
|
||
if !running {
|
||
return StartFrpc()
|
||
}
|
||
|
||
frpcPath, err := getFrpcPath()
|
||
if err != nil {
|
||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||
}
|
||
|
||
cmd := exec.Command(frpcPath, "reload", "-c", "./frpc.toml")
|
||
_, err = cmd.CombinedOutput()
|
||
if err != nil {
|
||
log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err)
|
||
if stopErr := StopFrpc(); stopErr != nil {
|
||
return fmt.Errorf("停止 frpc 失败: %w", stopErr)
|
||
}
|
||
if startErr := StartFrpc(); startErr != nil {
|
||
return fmt.Errorf("启动 frpc 失败: %w", startErr)
|
||
}
|
||
return nil
|
||
}
|
||
return nil
|
||
}
|