250 lines
6.1 KiB
Go
250 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"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
|
|
)
|
|
|
|
// getFrpcPath 获取当前平台对应的 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"
|
|
default:
|
|
// 降级:尝试从系统 PATH 查找 frpc
|
|
path, err := exec.LookPath("frpc")
|
|
if err == nil {
|
|
cachedFrpcPath = path
|
|
return path, nil
|
|
}
|
|
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
|
|
// 从 embed 读取
|
|
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
|
if err != nil {
|
|
// 降级到 PATH
|
|
path, lookErr := exec.LookPath("frpc")
|
|
if lookErr == nil {
|
|
cachedFrpcPath = path
|
|
return path, nil
|
|
}
|
|
return "", fmt.Errorf("未找到 frpc 文件 (embed 和 PATH 均无): %w", err)
|
|
}
|
|
|
|
// 写入临时目录
|
|
tmpPath := os.TempDir() + "/frpc"
|
|
if runtime.GOOS == "windows" {
|
|
tmpPath += ".exe"
|
|
}
|
|
|
|
// 尝试写入(如果文件已存在且正在使用,忽略错误,直接使用已有文件)
|
|
if err := os.WriteFile(tmpPath, data, 0755); err != nil {
|
|
// 如果写入失败,检查是否已有可用的 frpc
|
|
if _, statErr := os.Stat(tmpPath); statErr == nil {
|
|
cachedFrpcPath = tmpPath
|
|
return tmpPath, nil
|
|
}
|
|
return "", fmt.Errorf("写入临时文件失败: %w", err)
|
|
}
|
|
|
|
cachedFrpcPath = tmpPath
|
|
return tmpPath, nil
|
|
}
|
|
|
|
// GenerateFrpcConfig 生成 frpc.toml 配置文件
|
|
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
|
|
}{
|
|
GlobalConfig: cfg,
|
|
Proxies: activeProxies,
|
|
}
|
|
|
|
// ===== 核心改动 =====
|
|
// 优先使用外部模板文件 frpc.tmpl,如果不存在则使用 embed 中的模板
|
|
var tmplContent string
|
|
if _, err := os.Stat("frpc.tmpl"); err == nil {
|
|
// 外部文件存在,读取它
|
|
content, readErr := os.ReadFile("frpc.tmpl")
|
|
if readErr == nil {
|
|
tmplContent = string(content)
|
|
} else {
|
|
// 读取失败时回退到 embed
|
|
tmplContent = FrpcTemplateContent
|
|
}
|
|
} else {
|
|
// 外部文件不存在,使用 embed 中的模板
|
|
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
|
|
}
|
|
|
|
// StartFrpc 启动 frpc(后台运行)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 检查是否已在运行
|
|
running, _ := GetFrpcStatus()
|
|
if running {
|
|
return nil
|
|
}
|
|
|
|
cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
|
|
if runtime.GOOS == "windows" {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("启动 frpc 失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReloadFrpc 热加载 frpc 配置(仅发信号,不重复写入二进制)
|
|
// ReloadFrpc 热加载 frpc 配置,失败时自动降级为重启
|
|
func ReloadFrpc() error {
|
|
running, err := GetFrpcStatus()
|
|
if err != nil {
|
|
return fmt.Errorf("检查 frpc 状态失败: %w", err)
|
|
}
|
|
if !running {
|
|
return StartFrpc()
|
|
}
|
|
|
|
if cachedFrpcPath == "" {
|
|
if _, err := getFrpcPath(); err != nil {
|
|
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command(cachedFrpcPath, "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
|
|
}
|
|
|
|
// StopFrpc 停止 frpc
|
|
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)
|
|
}
|
|
return 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
|
|
}
|
|
|
|
// GetFrpcStatus 检查 frpc 是否在运行
|
|
func GetFrpcStatus() (bool, error) {
|
|
if runtime.GOOS == "windows" {
|
|
cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq frpc.exe")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
return strings.Contains(string(output), "frpc.exe"), nil
|
|
}
|
|
cmd := exec.Command("pgrep", "-f", "frpc")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
return len(output) > 0, nil
|
|
}
|