feat: add openconnect
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"github.com/engigu/baihu-panel/internal/database"
|
||||
"github.com/engigu/baihu-panel/internal/executor"
|
||||
"github.com/engigu/baihu-panel/internal/logger"
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
|
||||
@@ -78,7 +79,11 @@ func GetAgentWSManager() *AgentWSManager {
|
||||
ipFailCount: make(map[string]int),
|
||||
remoteWaiters: make(map[string]chan *models.AgentTaskResult),
|
||||
}
|
||||
go agentWSManager.cleanupLoop()
|
||||
// 启动时,先将所有 "online" 状态的 Agent 重置为 "offline"
|
||||
NewAgentService().ResetAllAgentsToOffline()
|
||||
|
||||
// 将清理任务注册到系统内部 Cron,每 30 秒执行一次
|
||||
executor.GetSysCron().AddJob("@every 30s", agentWSManager.cleanupLoop)
|
||||
})
|
||||
return agentWSManager
|
||||
}
|
||||
@@ -282,66 +287,51 @@ func (m *AgentWSManager) OnlineCount() int {
|
||||
return len(m.connections)
|
||||
}
|
||||
|
||||
// cleanupLoop 清理超时连接
|
||||
// cleanupLoop 清理超时连接 (由 SysCron 每 30 秒调用一次)
|
||||
func (m *AgentWSManager) cleanupLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Errorf("[AgentWS] cleanupLoop panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动时,先将所有 "online" 状态的 Agent 重置为 "offline"
|
||||
// 因为 WebSocket 连接在应用启动时是空的,所有 Agent 客观上都是离线状态
|
||||
// 等它们重新连接上来后,会变为 "online"
|
||||
NewAgentService().ResetAllAgentsToOffline()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
now := time.Now()
|
||||
|
||||
for range ticker.C {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Errorf("[AgentWS] cleanupLoop panic: %v", r)
|
||||
}
|
||||
}()
|
||||
m.mu.Lock()
|
||||
now := time.Now()
|
||||
|
||||
// 清理超时连接
|
||||
for agentID, conn := range m.connections {
|
||||
if now.Sub(conn.LastPing) > 2*time.Minute {
|
||||
// 减少 IP 连接计数
|
||||
if conn.IP != "" {
|
||||
if count, ok := m.ipConnections[conn.IP]; ok && count > 0 {
|
||||
m.ipConnections[conn.IP] = count - 1
|
||||
}
|
||||
}
|
||||
conn.Close()
|
||||
delete(m.connections, agentID)
|
||||
// 更新数据库状态
|
||||
database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", constant.AgentStatusOffline)
|
||||
logger.Infof("[AgentWS] Agent #%s 心跳超时,已断开", agentID)
|
||||
// 清理超时连接
|
||||
for agentID, conn := range m.connections {
|
||||
if now.Sub(conn.LastPing) > 2*time.Minute {
|
||||
// 减少 IP 连接计数
|
||||
if conn.IP != "" {
|
||||
if count, ok := m.ipConnections[conn.IP]; ok && count > 0 {
|
||||
m.ipConnections[conn.IP] = count - 1
|
||||
}
|
||||
}
|
||||
conn.Close()
|
||||
delete(m.connections, agentID)
|
||||
// 更新数据库状态
|
||||
database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", constant.AgentStatusOffline)
|
||||
logger.Infof("[AgentWS] Agent #%s 心跳超时,已断开", agentID)
|
||||
}
|
||||
}
|
||||
|
||||
// 定期清理数据库中的过期状态(处理服务重启或异常终止的情况)
|
||||
// 有些 Agent 虽然没有连接,但数据库状态可能是 "online"
|
||||
cutoff := now.Add(-2 * time.Minute)
|
||||
database.DB.Model(&models.Agent{}).
|
||||
Where("status = ? AND last_seen < ?", constant.AgentStatusOnline, cutoff).
|
||||
Update("status", constant.AgentStatusOffline)
|
||||
// 定期清理数据库中的过期状态(处理服务重启或异常终止的情况)
|
||||
cutoff := now.Add(-2 * time.Minute)
|
||||
database.DB.Model(&models.Agent{}).
|
||||
Where("status = ? AND last_seen < ?", constant.AgentStatusOnline, cutoff).
|
||||
Update("status", constant.AgentStatusOffline)
|
||||
|
||||
// 清理过期的限流记录(超过 10 分钟未活动)
|
||||
|
||||
// 清理过期的限流记录(超过 10 分钟未活动)
|
||||
for ip, lastAttempt := range m.ipLastAttempt {
|
||||
if now.Sub(lastAttempt) > 10*time.Minute {
|
||||
delete(m.ipLastAttempt, ip)
|
||||
delete(m.ipFailCount, ip)
|
||||
// 只清理没有活跃连接的 IP 计数
|
||||
if m.ipConnections[ip] == 0 {
|
||||
delete(m.ipConnections, ip)
|
||||
}
|
||||
}
|
||||
// 清理过期的限流记录(超过 10 分钟未活动)
|
||||
for ip, lastAttempt := range m.ipLastAttempt {
|
||||
if now.Sub(lastAttempt) > 10*time.Minute {
|
||||
delete(m.ipLastAttempt, ip)
|
||||
delete(m.ipFailCount, ip)
|
||||
// 只清理没有活跃连接的 IP 计数
|
||||
if m.ipConnections[ip] == 0 {
|
||||
delete(m.ipConnections, ip)
|
||||
}
|
||||
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/database"
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
"github.com/engigu/baihu-panel/internal/utils"
|
||||
)
|
||||
|
||||
type InterconnectService struct{}
|
||||
|
||||
func NewInterconnectService() *InterconnectService {
|
||||
return &InterconnectService{}
|
||||
}
|
||||
|
||||
func (s *InterconnectService) GetNodes() ([]*models.InterconnectNode, error) {
|
||||
var nodes []*models.InterconnectNode
|
||||
err := database.DB.Find(&nodes).Error
|
||||
return nodes, err
|
||||
}
|
||||
|
||||
func (s *InterconnectService) GetNodeByID(id string) (*models.InterconnectNode, error) {
|
||||
var node models.InterconnectNode
|
||||
err := database.DB.Where("id = ?", id).First(&node).Error
|
||||
return &node, err
|
||||
}
|
||||
|
||||
func (s *InterconnectService) CreateNode(name, url, token, remark string) (*models.InterconnectNode, error) {
|
||||
nodeID := utils.GenerateID()
|
||||
if url == "" {
|
||||
url = "tunnel://" + nodeID
|
||||
}
|
||||
node := &models.InterconnectNode{
|
||||
ID: nodeID,
|
||||
Name: name,
|
||||
URL: url,
|
||||
Token: strings.ToLower(token),
|
||||
Remark: remark,
|
||||
CreatedAt: models.Now(),
|
||||
UpdatedAt: models.Now(),
|
||||
}
|
||||
err := database.DB.Create(node).Error
|
||||
return node, err
|
||||
}
|
||||
|
||||
func (s *InterconnectService) UpdateNode(id, name, url, token, remark string) (*models.InterconnectNode, error) {
|
||||
node, err := s.GetNodeByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Name = name
|
||||
if url != "" {
|
||||
node.URL = url
|
||||
}
|
||||
node.Token = token
|
||||
node.Remark = remark
|
||||
node.UpdatedAt = models.Now()
|
||||
|
||||
err = database.DB.Save(node).Error
|
||||
return node, err
|
||||
}
|
||||
|
||||
func (s *InterconnectService) DeleteNode(id string) error {
|
||||
return database.DB.Where("id = ?", id).Delete(&models.InterconnectNode{}).Error
|
||||
}
|
||||
|
||||
func (s *InterconnectService) GetNodeByToken(token string) (*models.InterconnectNode, error) {
|
||||
var node models.InterconnectNode
|
||||
err := database.DB.Where("token = ?", token).First(&node).Error
|
||||
return &node, err
|
||||
}
|
||||
|
||||
func (s *InterconnectService) UpdateNodeMonitorData(id string, metrics models.NodeMetrics) error {
|
||||
now := models.Now()
|
||||
return database.DB.Model(&models.InterconnectNode{}).
|
||||
Where("id = ?", id).
|
||||
Select("status", "metrics", "last_heartbeat_at", "updated_at").
|
||||
Updates(models.InterconnectNode{
|
||||
Status: "online",
|
||||
Metrics: metrics,
|
||||
LastHeartbeatAt: &now,
|
||||
UpdatedAt: now,
|
||||
}).Error
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
type HostMetrics struct {
|
||||
CPUPercent float64
|
||||
VMem *mem.VirtualMemoryStat
|
||||
DiskUsage *disk.UsageStat
|
||||
HostInfo *host.InfoStat
|
||||
}
|
||||
|
||||
type MonitorService struct {
|
||||
hostMu sync.RWMutex
|
||||
lastUpdate time.Time
|
||||
metrics HostMetrics
|
||||
}
|
||||
|
||||
var (
|
||||
monitorServiceInstance *MonitorService
|
||||
monitorServiceOnce sync.Once
|
||||
)
|
||||
|
||||
// GetMonitorService 获取系统监控服务单例
|
||||
func GetMonitorService() *MonitorService {
|
||||
monitorServiceOnce.Do(func() {
|
||||
monitorServiceInstance = &MonitorService{}
|
||||
})
|
||||
return monitorServiceInstance
|
||||
}
|
||||
|
||||
// GetHostMetrics 获取并返回物理机状态(带有缓存和演示模式伪装)
|
||||
func (ms *MonitorService) GetHostMetrics() HostMetrics {
|
||||
ms.hostMu.Lock()
|
||||
defer ms.hostMu.Unlock()
|
||||
|
||||
// 缓存 2 秒
|
||||
if time.Since(ms.lastUpdate) < 2*time.Second && ms.metrics.VMem != nil {
|
||||
return ms.metrics
|
||||
}
|
||||
|
||||
if constant.DemoMode {
|
||||
ms.updateDemoMetrics()
|
||||
return ms.metrics
|
||||
}
|
||||
|
||||
cpuPercents, _ := cpu.Percent(0, false)
|
||||
if len(cpuPercents) > 0 {
|
||||
ms.metrics.CPUPercent = cpuPercents[0]
|
||||
}
|
||||
ms.metrics.VMem, _ = mem.VirtualMemory()
|
||||
ms.metrics.DiskUsage, _ = disk.Usage("/")
|
||||
ms.metrics.HostInfo, _ = host.Info()
|
||||
ms.lastUpdate = time.Now()
|
||||
|
||||
// 提供默认值防空指针
|
||||
if ms.metrics.VMem == nil {
|
||||
ms.metrics.VMem = &mem.VirtualMemoryStat{}
|
||||
}
|
||||
if ms.metrics.DiskUsage == nil {
|
||||
ms.metrics.DiskUsage = &disk.UsageStat{}
|
||||
}
|
||||
if ms.metrics.HostInfo == nil {
|
||||
ms.metrics.HostInfo = &host.InfoStat{}
|
||||
}
|
||||
|
||||
return ms.metrics
|
||||
}
|
||||
|
||||
type RuntimeMetrics struct {
|
||||
NumGoroutine int
|
||||
MemStats runtime.MemStats
|
||||
}
|
||||
|
||||
var (
|
||||
runtimeMu sync.RWMutex
|
||||
lastRuntime time.Time
|
||||
cachedRuntime RuntimeMetrics
|
||||
)
|
||||
|
||||
// GetRuntimeMetrics 获取 Go 运行时指标(缓存 2 秒,防止高并发下频繁触发 STW)
|
||||
func (ms *MonitorService) GetRuntimeMetrics() RuntimeMetrics {
|
||||
runtimeMu.Lock()
|
||||
defer runtimeMu.Unlock()
|
||||
|
||||
if time.Since(lastRuntime) < 2*time.Second && cachedRuntime.NumGoroutine > 0 {
|
||||
return cachedRuntime
|
||||
}
|
||||
|
||||
cachedRuntime.NumGoroutine = runtime.NumGoroutine()
|
||||
runtime.ReadMemStats(&cachedRuntime.MemStats)
|
||||
lastRuntime = time.Now()
|
||||
|
||||
return cachedRuntime
|
||||
}
|
||||
|
||||
func (ms *MonitorService) updateDemoMetrics() {
|
||||
ms.metrics.CPUPercent = 10 + rand.Float64()*40 // 10% - 50% 的随机 CPU 波动
|
||||
|
||||
totalMem := uint64(8 * 1024 * 1024 * 1024) // 8GB 内存
|
||||
usedMem := uint64(float64(totalMem) * (0.3 + rand.Float64()*0.3)) // 30% - 60% 随机使用率
|
||||
ms.metrics.VMem = &mem.VirtualMemoryStat{
|
||||
Total: totalMem,
|
||||
Used: usedMem,
|
||||
UsedPercent: float64(usedMem) / float64(totalMem) * 100,
|
||||
}
|
||||
|
||||
totalDisk := uint64(500 * 1024 * 1024 * 1024) // 500GB 硬盘
|
||||
usedDisk := uint64(float64(totalDisk) * 0.45) // 固定 45% 使用率
|
||||
ms.metrics.DiskUsage = &disk.UsageStat{
|
||||
Total: totalDisk,
|
||||
Used: usedDisk,
|
||||
UsedPercent: float64(usedDisk) / float64(totalDisk) * 100,
|
||||
}
|
||||
|
||||
ms.metrics.HostInfo = &host.InfoStat{
|
||||
Platform: "Demo Environment",
|
||||
OS: "linux",
|
||||
Uptime: uint64(time.Now().Unix() - 1700000000), // 生成一个较长且持续增加的运行时间
|
||||
}
|
||||
ms.lastUpdate = time.Now()
|
||||
}
|
||||
Reference in New Issue
Block a user