Files
frpc-console/frp.go
T
2026-08-07 08:30:17 +08:00

505 lines
12 KiB
Go

package main
import (
"bytes"
"embed"
"fmt"
"io"
"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)
}
localPath := filepath.Join(".", "bin", fileName)
if _, err := os.Stat(localPath); err == nil {
cachedFrpcPath = localPath
return localPath, nil
}
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
}
}
path, err := exec.LookPath("frpc")
if err == nil {
cachedFrpcPath = path
return path, nil
}
return "", fmt.Errorf("未找到 frpc 文件")
}
// ================================================================
// 配置生成 (保持不变)
// ================================================================
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
}{
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 fmt.Errorf("解析模板失败: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return fmt.Errorf("渲染模板失败: %w", err)
}
if err := os.MkdirAll("./data", 0755); err != nil {
return fmt.Errorf("创建 data 目录失败: %w", err)
}
if err := os.WriteFile("./data/frpc.toml", buf.Bytes(), 0644); err != nil {
return fmt.Errorf("写入配置文件失败: %w", err)
}
return nil
}
// ================================================================
// 进程管理 (兼容层,实际调用 ProcessManager)
// 2.6-preview: 所有进程管理逻辑迁移到 process_manager.go
// ================================================================
// isFrpcRunning 检查 frpc 是否在运行
// 优先使用 ProcessManager,降级到 PID 文件
func isFrpcRunning() bool {
// 如果 ProcessManager 已初始化,使用它
if processManager != nil {
status, err := processManager.Status()
if err != nil {
log.Printf("[WARN] ProcessManager.Status() 失败: %v,降级到 PID 文件", err)
return isFrpcRunningLegacy()
}
return status.State == "running"
}
return isFrpcRunningLegacy()
}
// isFrpcRunningLegacy 旧版 PID 文件检测 (降级方案)
func isFrpcRunningLegacy() bool {
pidData, err := os.ReadFile("./data/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
}
// StartFrpc 启动 frpc (幂等)
// 优先使用 ProcessManager,降级到旧逻辑
func StartFrpc() error {
if processManager != nil {
log.Println("[INFO] 使用 ProcessManager 启动 frpc")
return processManager.Start(nil)
}
log.Println("[WARN] ProcessManager 未初始化,使用兼容模式启动 frpc")
return startFrpcLegacy()
}
// startFrpcLegacy 旧版启动逻辑 (降级方案)
func startFrpcLegacy() error {
frpcPath, err := getFrpcPath()
if err != nil {
return fmt.Errorf("获取 frpc 路径失败: %w", err)
}
if err := os.MkdirAll("./data", 0755); err != nil {
return fmt.Errorf("创建 data 目录失败: %w", err)
}
if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) {
if err := GenerateFrpcConfig(); err != nil {
return fmt.Errorf("生成配置文件失败: %w", err)
}
}
if isFrpcRunningLegacy() {
return nil
}
os.Remove("./data/frpc.pid")
cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml")
setWindowHide(cmd)
setSysProcAttr(cmd)
logFile, err := os.OpenFile("./data/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)
}
go func() {
if err := cmd.Wait(); err != nil {
log.Printf("frpc 子进程退出: %v", err)
}
os.Remove("./data/frpc.pid")
}()
if err := os.WriteFile("./data/frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
return fmt.Errorf("保存 PID 失败: %w", err)
}
return nil
}
// StopFrpc 停止 frpc (幂等)
// 优先使用 ProcessManager,降级到旧逻辑
func StopFrpc() error {
if processManager != nil {
log.Println("[INFO] 使用 ProcessManager 停止 frpc")
return processManager.Stop(nil)
}
log.Println("[WARN] ProcessManager 未初始化,使用兼容模式停止 frpc")
return stopFrpcLegacy()
}
// stopFrpcLegacy 旧版停止逻辑 (降级方案)
func stopFrpcLegacy() 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("./data/frpc.pid")
return nil
}
pidData, err := os.ReadFile("./data/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("./data/frpc.pid")
return nil
}
if err := process.Kill(); err != nil {
return fmt.Errorf("杀死进程失败: %w", err)
}
os.Remove("./data/frpc.pid")
return nil
}
// RestartFrpc 重启 frpc (原子操作)
// 优先使用 ProcessManager,降级到旧逻辑
func RestartFrpc() error {
if processManager != nil {
log.Println("[INFO] 使用 ProcessManager 重启 frpc")
return processManager.Restart(nil)
}
log.Println("[WARN] ProcessManager 未初始化,使用兼容模式重启 frpc")
if err := stopFrpcLegacy(); err != nil {
return err
}
return startFrpcLegacy()
}
// GetFrpcStatus 获取 frpc 详细状态 (供 API 调用)
// 优先使用 ProcessManager,降级到旧逻辑
func GetFrpcStatus() (map[string]interface{}, error) {
if processManager != nil {
status, err := processManager.Status()
if err != nil {
return nil, err
}
return map[string]interface{}{
"state": status.State,
"pid": status.PID,
"port": status.Port,
}, nil
}
// 降级: 返回简单布尔值
running := isFrpcRunningLegacy()
return map[string]interface{}{
"state": map[bool]string{true: "running", false: "stopped"}[running],
"pid": 0,
"port": 0,
"legacy": true,
}, nil
}
// ReloadFrpc 热加载 frpc 配置
// 优先尝试 reload,失败则降级到重启
func ReloadFrpc() error {
// 如果 ProcessManager 未初始化,降级到旧逻辑
if processManager == nil {
return reloadFrpcLegacy()
}
// 检查 frpc 是否在运行
status, err := processManager.Status()
if err != nil {
return fmt.Errorf("获取 frpc 状态失败: %w", err)
}
if status.State != "running" {
// 未运行,直接启动
return processManager.Start(nil)
}
// 尝试热加载 (使用 frpc reload 命令)
frpcPath, err := getFrpcPath()
if err != nil {
return fmt.Errorf("获取 frpc 路径失败: %w", err)
}
cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml")
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err)
log.Printf(" reload 输出: %s", string(output))
// 降级: 重启
if err := processManager.Restart(nil); err != nil {
return fmt.Errorf("重启 frpc 失败: %w", err)
}
return nil
}
log.Printf("✅ frpc 热加载成功")
return nil
}
// reloadFrpcLegacy 旧版热加载 (降级方案)
func reloadFrpcLegacy() error {
if !isFrpcRunningLegacy() {
return startFrpcLegacy()
}
frpcPath, err := getFrpcPath()
if err != nil {
return fmt.Errorf("获取 frpc 路径失败: %w", err)
}
cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml")
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err)
if stopErr := stopFrpcLegacy(); stopErr != nil {
return fmt.Errorf("停止 frpc 失败: %w", stopErr)
}
if startErr := startFrpcLegacy(); startErr != nil {
return fmt.Errorf("启动 frpc 失败: %w", startErr)
}
return nil
}
log.Printf("✅ frpc 热加载成功 (兼容模式)")
return nil
}
// ================================================================
// 日志读取 (保持不变)
// ================================================================
// 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
}