chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听) - 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP) - 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点) - 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
package frp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
bin string
|
||||
configPath string
|
||||
logPath string
|
||||
logger *slog.Logger
|
||||
|
||||
mu sync.RWMutex
|
||||
cmd *exec.Cmd
|
||||
orphan *os.Process
|
||||
orphanPID int
|
||||
pid int
|
||||
version string
|
||||
started time.Time
|
||||
reloaded time.Time
|
||||
lastErr string
|
||||
}
|
||||
|
||||
func NewManager(bin, configPath, logPath string, logger *slog.Logger) *Manager {
|
||||
return &Manager{bin: bin, configPath: configPath, logPath: logPath, logger: logger.With("module", "FRP")}
|
||||
}
|
||||
|
||||
func (m *Manager) Verify(ctx context.Context, configPath string) error {
|
||||
bin, err := m.BinaryPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, bin, "verify", "-c", configPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("frpc 配置校验失败:%s", safeCommandOutput(output, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) VerifyBinary(ctx context.Context) error {
|
||||
bin, err := m.BinaryPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := exec.CommandContext(ctx, bin, "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("frpc 二进制校验失败:%s", safeCommandOutput(output, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Start() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.runningLocked() {
|
||||
return nil
|
||||
}
|
||||
bin, err := m.binaryPathLocked()
|
||||
if err != nil {
|
||||
m.lastErr = err.Error()
|
||||
return err
|
||||
}
|
||||
logFile, err := os.OpenFile(m.logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(bin, "-c", m.configPath)
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
m.lastErr = err.Error()
|
||||
return fmt.Errorf("启动 frpc 失败:%w", err)
|
||||
}
|
||||
m.cmd = cmd
|
||||
m.pid = cmd.Process.Pid
|
||||
m.started = time.Now().UTC()
|
||||
m.reloaded = m.started
|
||||
m.lastErr = ""
|
||||
go m.wait(cmd, logFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Restart(ctx context.Context) error {
|
||||
if err := m.Stop(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.Start()
|
||||
}
|
||||
|
||||
func (m *Manager) Stop(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
cmd := m.cmd
|
||||
orphan := m.orphan
|
||||
m.mu.Unlock()
|
||||
if cmd == nil && orphan == nil {
|
||||
return nil
|
||||
}
|
||||
process := orphan
|
||||
if cmd != nil {
|
||||
process = cmd.Process
|
||||
}
|
||||
if process == nil {
|
||||
return nil
|
||||
}
|
||||
if err := process.Kill(); err != nil {
|
||||
return fmt.Errorf("停止 frpc 失败:%w", err)
|
||||
}
|
||||
deadline := time.NewTimer(5 * time.Second)
|
||||
defer deadline.Stop()
|
||||
for {
|
||||
if !m.running() {
|
||||
m.mu.Lock()
|
||||
m.lastErr = ""
|
||||
m.orphan = nil
|
||||
m.orphanPID = 0
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-deadline.C:
|
||||
return fmt.Errorf("等待 frpc 停止超时")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Status() RuntimeStatus {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.statusLocked()
|
||||
}
|
||||
|
||||
func (m *Manager) statusLocked() RuntimeStatus {
|
||||
status := RuntimeStatus{
|
||||
Status: "stopped",
|
||||
PID: m.pid,
|
||||
Version: m.version,
|
||||
LastError: m.lastErr,
|
||||
}
|
||||
if m.runningLocked() {
|
||||
status.Status = "running"
|
||||
if status.PID == 0 {
|
||||
status.PID = m.orphanPID
|
||||
}
|
||||
} else if m.orphan != nil {
|
||||
status.PID = 0
|
||||
}
|
||||
if !m.started.IsZero() {
|
||||
status.LastStartedAt = m.started.Format(time.RFC3339)
|
||||
}
|
||||
if !m.reloaded.IsZero() {
|
||||
status.LastReloadedAt = m.reloaded.Format(time.RFC3339)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func (m *Manager) RefreshVersion(ctx context.Context) {
|
||||
bin, err := m.BinaryPath()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output, err := exec.CommandContext(ctx, bin, "--version").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.version = strings.TrimSpace(string(output))
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) wait(cmd *exec.Cmd, logFile *os.File) {
|
||||
err := cmd.Wait()
|
||||
logFile.Close()
|
||||
m.mu.Lock()
|
||||
if m.cmd != cmd {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.cmd = nil
|
||||
m.pid = 0
|
||||
if err != nil {
|
||||
// 退出原因在 frpc 自身日志的尾部(stderr 也写在那里),直接携带进应用日志,避免只看到 exit status 1
|
||||
m.lastErr = fmt.Sprintf("frpc 已退出:%v(详见 %s 末尾)", err, m.logPath)
|
||||
m.logger.Warn("frpc exited", "error", err.Error(), "log_tail", tailFile(m.logPath, 12))
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// tailFile 返回文件最后 maxLines 行文本;读不到时返回空串(不阻断退出处理)。
|
||||
func tailFile(path string, maxLines int) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil || len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
|
||||
if len(lines) > maxLines {
|
||||
lines = lines[len(lines)-maxLines:]
|
||||
}
|
||||
return strings.Join(lines, " | ")
|
||||
}
|
||||
|
||||
func (m *Manager) running() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.runningLocked()
|
||||
}
|
||||
|
||||
func (m *Manager) runningLocked() bool {
|
||||
if m.cmd != nil && m.cmd.Process != nil {
|
||||
return true
|
||||
}
|
||||
return m.orphan != nil && processAlive(m.orphanPID)
|
||||
}
|
||||
|
||||
func (m *Manager) Attach(pid int) bool {
|
||||
bin, err := m.BinaryPath()
|
||||
if err != nil || !processMatches(pid, bin, m.configPath) {
|
||||
return false
|
||||
}
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.orphan = process
|
||||
m.orphanPID = pid
|
||||
m.pid = pid
|
||||
m.lastErr = ""
|
||||
m.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
func processAlive(pid int) bool {
|
||||
alive, _ := probeProcess(pid)
|
||||
return alive
|
||||
}
|
||||
|
||||
func processMatches(pid int, binary, configPath string) bool {
|
||||
alive, commandLine := probeProcess(pid)
|
||||
if !alive {
|
||||
return false
|
||||
}
|
||||
commandLine = strings.ToLower(filepath.ToSlash(commandLine))
|
||||
return strings.Contains(commandLine, strings.ToLower(filepath.ToSlash(binary))) &&
|
||||
strings.Contains(commandLine, strings.ToLower(filepath.ToSlash(configPath)))
|
||||
}
|
||||
|
||||
func (m *Manager) BinaryPath() (string, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.binaryPathLocked()
|
||||
}
|
||||
|
||||
func (m *Manager) binaryPathLocked() (string, error) {
|
||||
if strings.ContainsAny(m.bin, `/\\`) {
|
||||
if _, err := os.Stat(m.bin); err != nil {
|
||||
return "", fmt.Errorf("未找到 frpc 二进制:%s", m.bin)
|
||||
}
|
||||
return m.bin, nil
|
||||
}
|
||||
path, err := exec.LookPath(m.bin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("未找到 frpc 二进制,请设置 FONU_FRPC_BIN")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetBinary(bin string) {
|
||||
m.mu.Lock()
|
||||
m.bin = bin
|
||||
m.version = ""
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) LogPath() string {
|
||||
return filepath.Clean(m.logPath)
|
||||
}
|
||||
|
||||
func safeCommandOutput(output []byte, err error) string {
|
||||
text := strings.TrimSpace(string(output))
|
||||
if text == "" {
|
||||
return err.Error()
|
||||
}
|
||||
if len(text) > 500 {
|
||||
return text[:500]
|
||||
}
|
||||
return text
|
||||
}
|
||||
Reference in New Issue
Block a user