338 lines
7.3 KiB
Go
338 lines
7.3 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 embeddedFrps embed.FS
|
|
|
|
//go:embed frps.tmpl
|
|
var FrpsTemplateContent string
|
|
|
|
var (
|
|
cachedFrpsPath string
|
|
frpsPathMutex sync.Mutex
|
|
)
|
|
|
|
func getFrpsPath() (string, error) {
|
|
frpsPathMutex.Lock()
|
|
defer frpsPathMutex.Unlock()
|
|
|
|
if cachedFrpsPath != "" {
|
|
if _, err := os.Stat(cachedFrpsPath); err == nil {
|
|
return cachedFrpsPath, nil
|
|
}
|
|
cachedFrpsPath = ""
|
|
}
|
|
|
|
var fileName string
|
|
switch {
|
|
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
|
|
fileName = "frps_windows_amd64.exe"
|
|
case runtime.GOOS == "linux" && runtime.GOARCH == "amd64":
|
|
fileName = "frps_linux_amd64"
|
|
case runtime.GOOS == "linux" && runtime.GOARCH == "arm64":
|
|
fileName = "frps_linux_arm64"
|
|
case runtime.GOOS == "linux" && runtime.GOARCH == "arm":
|
|
fileName = "frps_linux_arm_hf"
|
|
default:
|
|
path, err := exec.LookPath("frps")
|
|
if err == nil {
|
|
cachedFrpsPath = 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 {
|
|
cachedFrpsPath = localPath
|
|
return localPath, nil
|
|
}
|
|
|
|
data, err := embeddedFrps.ReadFile("bin/" + fileName)
|
|
if err == nil {
|
|
tmpPath := filepath.Join(os.TempDir(), "frps")
|
|
if runtime.GOOS == "windows" {
|
|
tmpPath += ".exe"
|
|
}
|
|
if err := os.WriteFile(tmpPath, data, 0755); err == nil {
|
|
cachedFrpsPath = tmpPath
|
|
return tmpPath, nil
|
|
}
|
|
if _, statErr := os.Stat(tmpPath); statErr == nil {
|
|
cachedFrpsPath = tmpPath
|
|
return tmpPath, nil
|
|
}
|
|
}
|
|
|
|
path, err := exec.LookPath("frps")
|
|
if err == nil {
|
|
cachedFrpsPath = path
|
|
return path, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("未找到 frps 文件")
|
|
}
|
|
|
|
func GenerateFrpsConfig() error {
|
|
cfg, err := GetGlobalConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("读取全局配置失败: %w", err)
|
|
}
|
|
|
|
// 直接使用 cfg,模板使用 frps.tmpl
|
|
var tmplContent string
|
|
if _, err := os.Stat("frps.tmpl"); err == nil {
|
|
content, readErr := os.ReadFile("frps.tmpl")
|
|
if readErr == nil {
|
|
tmplContent = string(content)
|
|
} else {
|
|
tmplContent = FrpsTemplateContent
|
|
}
|
|
} else {
|
|
tmplContent = FrpsTemplateContent
|
|
}
|
|
|
|
tmpl, err := template.New("frps").Parse(tmplContent)
|
|
if err != nil {
|
|
return fmt.Errorf("解析模板失败: %w", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, cfg); 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/frps.toml", buf.Bytes(), 0644); err != nil {
|
|
return fmt.Errorf("写入配置文件失败: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isFrpsRunning() bool {
|
|
pidData, err := os.ReadFile("./data/frps.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 StartFrps() error {
|
|
frpsPath, err := getFrpsPath()
|
|
if err != nil {
|
|
return fmt.Errorf("获取 frps 路径失败: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll("./data", 0755); err != nil {
|
|
return fmt.Errorf("创建 data 目录失败: %w", err)
|
|
}
|
|
|
|
if _, err := os.Stat("./data/frps.toml"); os.IsNotExist(err) {
|
|
if err := GenerateFrpsConfig(); err != nil {
|
|
return fmt.Errorf("生成配置文件失败: %w", err)
|
|
}
|
|
}
|
|
|
|
if isFrpsRunning() {
|
|
return nil
|
|
}
|
|
|
|
os.Remove("./data/frps.pid")
|
|
|
|
cmd := exec.Command(frpsPath, "-c", "./data/frps.toml")
|
|
setWindowHide(cmd)
|
|
setSysProcAttr(cmd)
|
|
|
|
logFile, err := os.OpenFile("./data/frps.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("启动 frps 失败: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile("./data/frps.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
|
|
return fmt.Errorf("保存 PID 失败: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func StopFrps() error {
|
|
if runtime.GOOS == "windows" {
|
|
cmd := exec.Command("taskkill", "/F", "/IM", "frps.exe")
|
|
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") {
|
|
return fmt.Errorf("停止 frps 失败: %w", err)
|
|
}
|
|
os.Remove("./data/frps.pid")
|
|
return nil
|
|
}
|
|
|
|
pidData, err := os.ReadFile("./data/frps.pid")
|
|
if err != nil {
|
|
cmd := exec.Command("pkill", "-f", "frps")
|
|
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
|
|
return fmt.Errorf("停止 frps 失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData)))
|
|
process, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
os.Remove("./data/frps.pid")
|
|
return nil
|
|
}
|
|
|
|
if err := process.Kill(); err != nil {
|
|
return fmt.Errorf("杀死进程失败: %w", err)
|
|
}
|
|
|
|
os.Remove("./data/frps.pid")
|
|
return nil
|
|
}
|
|
|
|
func GetFrpsStatus() (bool, error) {
|
|
return isFrpsRunning(), nil
|
|
}
|
|
|
|
func ReloadFrps() error {
|
|
running := isFrpsRunning()
|
|
if !running {
|
|
return StartFrps()
|
|
}
|
|
|
|
frpsPath, err := getFrpsPath()
|
|
if err != nil {
|
|
return fmt.Errorf("获取 frps 路径失败: %w", err)
|
|
}
|
|
|
|
cmd := exec.Command(frpsPath, "reload", "-c", "./data/frps.toml")
|
|
_, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frps", err)
|
|
if stopErr := StopFrps(); stopErr != nil {
|
|
return fmt.Errorf("停止 frps 失败: %w", stopErr)
|
|
}
|
|
if startErr := StartFrps(); startErr != nil {
|
|
return fmt.Errorf("启动 frps 失败: %w", startErr)
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readTailLog 通用
|
|
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
|
|
}
|