Files

159 lines
4.7 KiB
Go

package proxy
import (
"ant-chrome/backend/internal/config"
"ant-chrome/backend/internal/logger"
"fmt"
"os"
"os/exec"
"strings"
"sync"
)
// ClashManager Clash 进程管理器
type ClashManager struct {
Config *config.Config
AppRoot string // 应用根目录,所有相对路径基于此解析
Processes map[string]*exec.Cmd
NodeBridges map[string]*MihomoNodeBridge
mu sync.Mutex
launchLocks map[string]*bridgeLaunchLock
}
// NewClashManager 创建 Clash 管理器
func NewClashManager(cfg *config.Config, appRoot string) *ClashManager {
return &ClashManager{
Config: cfg,
AppRoot: appRoot,
Processes: make(map[string]*exec.Cmd),
NodeBridges: make(map[string]*MihomoNodeBridge),
launchLocks: make(map[string]*bridgeLaunchLock),
}
}
// ClashProfile Clash 配置接口
type ClashProfile interface {
GetProfileId() string
GetClashEnabled() bool
GetClashRunning() bool
GetClashConfigPath() string
GetClashProxyPort() int
SetClashRunning(bool)
SetClashPid(int)
SetClashProxyPort(int)
SetClashLastError(string)
}
// StartForProfile 为配置启动 Clash 进程
func (m *ClashManager) StartForProfile(profile ClashProfile, userDataDir string) error {
log := logger.New("Clash")
if !profile.GetClashEnabled() {
return nil
}
if profile.GetClashRunning() {
return nil
}
clashBinaryPath := strings.TrimSpace(m.Config.Browser.ClashBinaryPath)
if clashBinaryPath == "" {
err := fmt.Errorf("clash binary path not configured")
profile.SetClashLastError(err.Error())
log.Error("Clash 启动失败", logger.F("profile_id", profile.GetProfileId()), logger.F("error", err))
return err
}
if _, err := os.Stat(clashBinaryPath); err != nil {
profile.SetClashLastError(err.Error())
log.Error("Clash 启动失败", logger.F("profile_id", profile.GetProfileId()), logger.F("error", err))
return err
}
templatePath := strings.TrimSpace(profile.GetClashConfigPath())
if templatePath == "" {
err := fmt.Errorf("clash config path not configured")
profile.SetClashLastError(err.Error())
log.Error("Clash 启动失败", logger.F("profile_id", profile.GetProfileId()), logger.F("error", err))
return err
}
if _, err := os.Stat(templatePath); err != nil {
profile.SetClashLastError(err.Error())
log.Error("Clash 启动失败", logger.F("profile_id", profile.GetProfileId()), logger.F("error", err))
return err
}
port := profile.GetClashProxyPort()
if port == 0 {
p, err := nextAvailablePort()
if err != nil {
profile.SetClashLastError(err.Error())
log.Error("Clash 端口分配失败", logger.F("profile_id", profile.GetProfileId()), logger.F("error", err))
return err
}
port = p
profile.SetClashProxyPort(port)
}
args := []string{
"-f", templatePath,
"-d", userDataDir,
}
cmd := exec.Command(clashBinaryPath, args...)
hideWindow(cmd)
if err := cmd.Start(); err != nil {
profile.SetClashLastError(err.Error())
log.Error("Clash 启动失败", logger.F("profile_id", profile.GetProfileId()), logger.F("error", err))
return err
}
m.mu.Lock()
m.Processes[profile.GetProfileId()] = cmd
m.mu.Unlock()
profile.SetClashRunning(true)
profile.SetClashPid(cmd.Process.Pid)
profile.SetClashLastError("")
log.Info("Clash 内核进程已启动", logger.F("engine", "clash"), logger.F("profile_id", profile.GetProfileId()), logger.F("pid", cmd.Process.Pid), logger.F("port", port))
return nil
}
// StopForProfile 停止配置的 Clash 进程
func (m *ClashManager) StopForProfile(profile ClashProfile) {
log := logger.New("Clash")
m.mu.Lock()
cmd := m.Processes[profile.GetProfileId()]
delete(m.Processes, profile.GetProfileId())
m.mu.Unlock()
if cmd != nil && cmd.Process != nil {
if err := cmd.Process.Kill(); err != nil {
log.Error("Clash 停止失败", logger.F("profile_id", profile.GetProfileId()), logger.F("error", err))
}
}
profile.SetClashRunning(false)
profile.SetClashPid(0)
log.Info("Clash 已停止", logger.F("profile_id", profile.GetProfileId()))
}
// StopAll 停止所有 Clash 进程
func (m *ClashManager) StopAll() {
m.mu.Lock()
processes := make([]*exec.Cmd, 0, len(m.Processes))
for profileID, cmd := range m.Processes {
if cmd != nil {
processes = append(processes, cmd)
}
delete(m.Processes, profileID)
}
bridges := make([]*MihomoNodeBridge, 0, len(m.NodeBridges))
for key, bridge := range m.NodeBridges {
if bridge != nil {
bridge.Running = false
bridges = append(bridges, bridge)
}
delete(m.NodeBridges, key)
}
m.mu.Unlock()
for _, cmd := range processes {
if cmd != nil && cmd.Process != nil {
_ = cmd.Process.Kill()
}
}
for _, bridge := range bridges {
if bridge != nil && bridge.Cmd != nil && bridge.Cmd.Process != nil {
_ = bridge.Cmd.Process.Kill()
}
}
}