1Panel/agent/app/service/dashboard.go
2025-12-10 14:31:19 +08:00

644 lines
18 KiB
Go

package service
import (
"cmp"
"context"
"encoding/json"
"fmt"
network "net"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
)
type DashboardService struct{}
type IDashboardService interface {
LoadOsInfo() (*dto.OsInfo, error)
LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error)
LoadCurrentInfoForNode() *dto.NodeCurrent
LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent
LoadTopCPU() []dto.Process
LoadTopMem() []dto.Process
LoadQuickOptions() []dto.QuickJump
ChangeQuick(req dto.ChangeQuicks) error
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
ChangeShow(req dto.SettingUpdate) error
ListLauncherOption(filter string) ([]dto.LauncherOption, error)
Restart(operation string) error
}
func NewIDashboardService() IDashboardService {
return &DashboardService{}
}
func (u *DashboardService) Restart(operation string) error {
switch operation {
case "system":
{
go func() {
if err := cmd.RunDefaultBashCf("%s reboot", cmd.SudoHandleCmd()); err != nil {
global.LOG.Errorf("handle reboot failed, %v", err)
}
}()
return nil
}
case "1panel-agent":
controller.RestartPanel(false, true, false)
return nil
case "1panel":
controller.RestartPanel(true, true, false)
return nil
default:
return fmt.Errorf("handle restart operation %s failed, err: nonsupport such operation", operation)
}
}
func (u *DashboardService) LoadOsInfo() (*dto.OsInfo, error) {
var baseInfo dto.OsInfo
hostInfo, err := psutil.HOST.GetHostInfo(false)
if err != nil {
return nil, err
}
baseInfo.OS = hostInfo.OS
baseInfo.Platform = hostInfo.Platform
baseInfo.PlatformFamily = hostInfo.PlatformFamily
baseInfo.KernelArch = hostInfo.KernelArch
baseInfo.KernelVersion = hostInfo.KernelVersion
baseInfo.PrettyDistro = psutil.HOST.GetDistro()
diskInfo, err := psutil.DISK.GetUsage(global.Dir.BaseDir, false)
if err == nil {
baseInfo.DiskSize = int64(diskInfo.Free)
}
if baseInfo.KernelArch == "armv7l" {
baseInfo.KernelArch = "armv7"
}
if baseInfo.KernelArch == "x86_64" {
baseInfo.KernelArch = "amd64"
}
return &baseInfo, nil
}
func (u *DashboardService) LoadCurrentInfoForNode() *dto.NodeCurrent {
var currentInfo dto.NodeCurrent
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
cpuUsedPercent, perCore, cpuDetailedPercent := psutil.CPU.GetCPUUsage()
if len(perCore) == 0 {
currentInfo.CPUTotal = psutil.CPU.NumCPU()
} else {
currentInfo.CPUTotal = len(perCore)
}
currentInfo.CPUUsedPercent = cpuUsedPercent
currentInfo.CPUUsed = cpuUsedPercent * 0.01 * float64(currentInfo.CPUTotal)
currentInfo.CPUDetailedPercent = cpuDetailedPercent
loadInfo, _ := load.Avg()
currentInfo.Load1 = loadInfo.Load1
currentInfo.Load5 = loadInfo.Load5
currentInfo.Load15 = loadInfo.Load15
currentInfo.LoadUsagePercent = loadInfo.Load1 / (float64(currentInfo.CPUTotal*2) * 0.75) * 100
memoryInfo, _ := mem.VirtualMemory()
currentInfo.MemoryTotal = memoryInfo.Total
currentInfo.MemoryAvailable = memoryInfo.Available
currentInfo.MemoryUsed = memoryInfo.Used
currentInfo.MemoryUsedPercent = memoryInfo.UsedPercent
swapInfo, _ := mem.SwapMemory()
currentInfo.SwapMemoryTotal = swapInfo.Total
currentInfo.SwapMemoryAvailable = swapInfo.Free
currentInfo.SwapMemoryUsed = swapInfo.Used
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
return &currentInfo
}
func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error) {
var baseInfo dto.DashboardBase
hostInfo, err := psutil.HOST.GetHostInfo(false)
if err != nil {
return nil, err
}
ss, _ := json.Marshal(hostInfo)
baseInfo = dto.DashboardBase{
Hostname: hostInfo.Hostname,
OS: hostInfo.OS,
Platform: hostInfo.Platform,
PlatformFamily: hostInfo.PlatformFamily,
PlatformVersion: hostInfo.PlatformVersion,
PrettyDistro: psutil.HOST.GetDistro(),
KernelArch: hostInfo.KernelArch,
KernelVersion: hostInfo.KernelVersion,
VirtualizationSystem: string(ss),
IpV4Addr: loadOutboundIP(),
SystemProxy: "noProxy",
}
if proxy := cmp.Or(os.Getenv("http_proxy"), os.Getenv("HTTP_PROXY")); proxy != "" {
baseInfo.SystemProxy = proxy
}
loadQuickJump(&baseInfo)
cpuInfo, err := psutil.CPUInfo.GetCPUInfo(false)
if err == nil && len(cpuInfo) > 0 {
baseInfo.CPUModelName = cpuInfo[0].ModelName
}
baseInfo.CPUCores, _ = psutil.CPUInfo.GetPhysicalCores(false)
baseInfo.CPULogicalCores, _ = psutil.CPUInfo.GetLogicalCores(false)
baseInfo.CPUMhz = cpuInfo[0].Mhz
baseInfo.CurrentInfo = *u.LoadCurrentInfo(ioOption, netOption)
return &baseInfo, nil
}
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
var currentInfo dto.DashboardCurrent
hostInfo, _ := psutil.HOST.GetHostInfo(false)
currentInfo.Uptime = hostInfo.Uptime
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
currentInfo.Procs = hostInfo.Procs
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
cpuUsedPercent, perCore, cpuDetailedPercent := psutil.CPU.GetCPUUsage()
if len(perCore) == 0 {
currentInfo.CPUTotal = psutil.CPU.NumCPU()
} else {
currentInfo.CPUTotal = len(perCore)
}
currentInfo.CPUPercent = perCore
currentInfo.CPUUsedPercent = cpuUsedPercent
currentInfo.CPUUsed = cpuUsedPercent * 0.01 * float64(currentInfo.CPUTotal)
currentInfo.CPUDetailedPercent = cpuDetailedPercent
loadInfo, _ := load.Avg()
currentInfo.Load1 = loadInfo.Load1
currentInfo.Load5 = loadInfo.Load5
currentInfo.Load15 = loadInfo.Load15
currentInfo.LoadUsagePercent = loadInfo.Load1 / (float64(currentInfo.CPUTotal*2) * 0.75) * 100
memoryInfo, _ := mem.VirtualMemory()
currentInfo.MemoryTotal = memoryInfo.Total
currentInfo.MemoryUsed = memoryInfo.Used
currentInfo.MemoryFree = memoryInfo.Free
currentInfo.MemoryCache = memoryInfo.Cached + memoryInfo.Buffers
currentInfo.MemoryShard = memoryInfo.Shared
currentInfo.MemoryAvailable = memoryInfo.Available
currentInfo.MemoryUsedPercent = memoryInfo.UsedPercent
swapInfo, _ := mem.SwapMemory()
currentInfo.SwapMemoryTotal = swapInfo.Total
currentInfo.SwapMemoryAvailable = swapInfo.Free
currentInfo.SwapMemoryUsed = swapInfo.Used
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
currentInfo.DiskData = loadDiskInfo()
currentInfo.GPUData = loadGPUInfo()
currentInfo.XPUData = loadXpuInfo()
if ioOption == "all" {
diskInfo, _ := disk.IOCounters()
for _, state := range diskInfo {
currentInfo.IOReadBytes += state.ReadBytes
currentInfo.IOWriteBytes += state.WriteBytes
currentInfo.IOCount += (state.ReadCount + state.WriteCount)
currentInfo.IOReadTime += state.ReadTime
currentInfo.IOWriteTime += state.WriteTime
}
} else {
diskInfo, _ := disk.IOCounters(ioOption)
for _, state := range diskInfo {
currentInfo.IOReadBytes += state.ReadBytes
currentInfo.IOWriteBytes += state.WriteBytes
currentInfo.IOCount += (state.ReadCount + state.WriteCount)
currentInfo.IOReadTime += state.ReadTime
currentInfo.IOWriteTime += state.WriteTime
}
}
if netOption == "all" {
netInfo, _ := net.IOCounters(false)
if len(netInfo) != 0 {
currentInfo.NetBytesSent = netInfo[0].BytesSent
currentInfo.NetBytesRecv = netInfo[0].BytesRecv
}
} else {
netInfo, _ := net.IOCounters(true)
for _, state := range netInfo {
if state.Name == netOption {
currentInfo.NetBytesSent = state.BytesSent
currentInfo.NetBytesRecv = state.BytesRecv
break
}
}
}
currentInfo.ShotTime = time.Now()
return &currentInfo
}
func (u *DashboardService) LoadTopCPU() []dto.Process {
return loadTopCPU()
}
func (u *DashboardService) LoadTopMem() []dto.Process {
return loadTopMem()
}
func (u *DashboardService) LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error) {
var data []dto.AppLauncher
appInstalls, err := appInstallRepo.ListBy(context.Background())
if err != nil {
return data, err
}
apps, err := appRepo.GetBy()
if err != nil {
return data, err
}
showList, err := launcherRepo.ListName()
defaultList, err := appRepo.GetTopRecommend()
if err != nil {
return data, nil
}
allList := common.RemoveRepeatStr(append(defaultList, showList...))
for _, showItem := range allList {
var itemData dto.AppLauncher
for _, app := range apps {
if showItem == app.Key {
itemData.Key = app.Key
itemData.Type = app.Type
itemData.Name = app.Name
itemData.Icon = app.Icon
itemData.Limit = app.Limit
itemData.Recommend = app.Recommend
itemData.Description = app.GetDescription(ctx)
break
}
}
if len(itemData.Icon) == 0 {
continue
}
for _, install := range appInstalls {
if install.App.Key == showItem {
itemData.IsInstall = true
itemData.Detail = append(itemData.Detail, dto.InstallDetail{
InstallID: install.ID,
DetailID: install.AppDetailId,
Name: install.Name,
Version: install.Version,
Status: install.Status,
Path: install.GetPath(),
WebUI: install.WebUI,
HttpPort: install.HttpPort,
HttpsPort: install.HttpsPort,
})
}
}
if (ArryContains(showList, showItem) && len(itemData.Detail) != 0) ||
(ArryContains(defaultList, showItem) && len(itemData.Detail) == 0) {
data = append(data, itemData)
}
}
sort.Slice(data, func(i, j int) bool {
if data[i].IsInstall != data[j].IsInstall {
return data[i].IsInstall
}
return data[i].Recommend < data[j].Recommend
})
return data, nil
}
func (u *DashboardService) ChangeShow(req dto.SettingUpdate) error {
launcher, _ := launcherRepo.Get(repo.WithByKey(req.Key))
if req.Value == constant.StatusEnable && launcher.ID == 0 {
if err := launcherRepo.Create(&model.AppLauncher{Key: req.Key}); err != nil {
return err
}
}
if req.Value == constant.StatusDisable && launcher.ID != 0 {
if err := launcherRepo.Delete(repo.WithByKey(req.Key)); err != nil {
return err
}
}
return nil
}
func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
quicks := launcherRepo.ListQuickJump(true)
var list []dto.QuickJump
for _, quick := range quicks {
var item dto.QuickJump
_ = copier.Copy(&item, &quick)
list = append(list, item)
}
return list
}
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
showCount := 0
var quicks []model.QuickJump
for _, item := range req.Quicks {
var quick model.QuickJump
if item.IsShow {
showCount++
}
if err := copier.Copy(&quick, &item); err != nil {
return err
}
quicks = append(quicks, quick)
}
if showCount == 0 {
return buserr.New("ErrMinQuickJump")
}
if showCount > 4 {
return buserr.New("ErrMaxQuickJump")
}
return launcherRepo.UpdateQuicks(quicks)
}
func (u *DashboardService) ListLauncherOption(filter string) ([]dto.LauncherOption, error) {
showList, _ := launcherRepo.ListName()
var data []dto.LauncherOption
optionMap := make(map[string]bool)
appInstalls, err := appInstallRepo.ListBy(context.Background())
if err != nil {
return data, err
}
for _, install := range appInstalls {
isShow := false
for _, item := range showList {
if install.App.Key == item {
isShow = true
break
}
}
optionMap[install.App.Key] = isShow
}
for key, val := range optionMap {
if len(filter) != 0 && !strings.Contains(key, filter) {
continue
}
data = append(data, dto.LauncherOption{Key: key, IsShow: val})
}
sort.Slice(data, func(i, j int) bool {
return data[i].Key < data[j].Key
})
return data, nil
}
type diskInfo struct {
Type string
Mount string
Device string
}
func loadDiskInfo() []dto.DiskInfo {
var datas []dto.DiskInfo
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
format := `awk 'NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}'`
stdout, err := cmdMgr.RunWithStdout("bash", "-c", `timeout 2 df -hT -P | `+format)
if err != nil {
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunWithStdout("bash", "-c", `timeout 1 df -lhT -P | `+format)
if err != nil {
return datas
}
}
lines := strings.Split(stdout, "\n")
var mounts []diskInfo
var excludes = []string{"/mnt/cdrom", "/boot", "/boot/efi", "/dev", "/dev/shm", "/run/lock", "/run", "/run/shm", "/run/user"}
for _, line := range lines {
fields := strings.Split(line, "\t")
if len(fields) < 7 {
continue
}
if strings.HasPrefix(fields[6], "/snap") || len(strings.Split(fields[6], "/")) > 10 {
continue
}
if strings.TrimSpace(fields[1]) == "tmpfs" || strings.TrimSpace(fields[1]) == "overlay" {
continue
}
if strings.Contains(fields[2], "K") {
continue
}
if strings.Contains(fields[6], "docker") || strings.Contains(fields[6], "podman") || strings.Contains(fields[6], "containerd") || strings.HasPrefix(fields[6], "/var/lib/containers") {
continue
}
isExclude := false
for _, exclude := range excludes {
if exclude == fields[6] {
isExclude = true
}
}
if isExclude {
continue
}
mounts = append(mounts, diskInfo{Type: fields[1], Device: fields[0], Mount: strings.Join(fields[6:], " ")})
}
var (
wg sync.WaitGroup
mu sync.Mutex
)
wg.Add(len(mounts))
for i := 0; i < len(mounts); i++ {
go func(mount diskInfo) {
defer wg.Done()
var itemData dto.DiskInfo
itemData.Path = mount.Mount
itemData.Type = mount.Type
itemData.Device = mount.Device
type diskResult struct {
state *disk.UsageStat
err error
}
resultCh := make(chan diskResult, 1)
go func() {
state, err := psutil.DISK.GetUsage(mount.Mount, false)
resultCh <- diskResult{state: state, err: err}
}()
select {
case <-time.After(5 * time.Second):
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
case result := <-resultCh:
if result.err != nil {
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
return
}
itemData.Total = result.state.Total
itemData.Free = result.state.Free
itemData.Used = result.state.Used
itemData.UsedPercent = result.state.UsedPercent
itemData.InodesTotal = result.state.InodesTotal
itemData.InodesUsed = result.state.InodesUsed
itemData.InodesFree = result.state.InodesFree
itemData.InodesUsedPercent = result.state.InodesUsedPercent
mu.Lock()
datas = append(datas, itemData)
mu.Unlock()
}
}(mounts[i])
}
wg.Wait()
sort.Slice(datas, func(i, j int) bool {
return datas[i].Path < datas[j].Path
})
return datas
}
func loadGPUInfo() []dto.GPUInfo {
ok, client := gpu.New()
var list []interface{}
if ok {
info, err := client.LoadGpuInfo()
if err != nil || len(info.GPUs) == 0 {
return nil
}
for _, item := range info.GPUs {
list = append(list, item)
}
}
if len(list) == 0 {
return nil
}
var data []dto.GPUInfo
for _, gpu := range list {
var dataItem dto.GPUInfo
if err := copier.Copy(&dataItem, &gpu); err != nil {
continue
}
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
data = append(data, dataItem)
}
return data
}
type AppLauncher struct {
Key string `json:"key"`
}
func ArryContains(arr []string, element string) bool {
for _, v := range arr {
if v == element {
return true
}
}
return false
}
func loadXpuInfo() []dto.XPUInfo {
var list []interface{}
ok, xpuClient := xpu.New()
if ok {
xpus, err := xpuClient.LoadDashData()
if err != nil || len(xpus) == 0 {
return nil
}
for _, item := range xpus {
list = append(list, item)
}
}
if len(list) == 0 {
return nil
}
var data []dto.XPUInfo
for _, gpu := range list {
var dataItem dto.XPUInfo
if err := copier.Copy(&dataItem, &gpu); err != nil {
continue
}
data = append(data, dataItem)
}
return data
}
func loadOutboundIP() string {
conn, err := network.Dial("udp", "8.8.8.8:80")
if err != nil {
return "IPNotFound"
}
defer conn.Close()
localAddr := conn.LocalAddr().(*network.UDPAddr)
return localAddr.IP.String()
}
func loadQuickJump(base *dto.DashboardBase) {
website, _ := websiteRepo.GetBy()
base.WebsiteNumber = len(website)
postgresqlDbs, _ := postgresqlRepo.List()
mysqlDbs, _ := mysqlRepo.List()
base.DatabaseNumber = len(mysqlDbs) + len(postgresqlDbs)
cronjobs, _ := cronjobRepo.List()
base.CronjobNumber = len(cronjobs)
appInstall, _ := appInstallRepo.ListBy(context.Background())
base.AppInstalledNumber = len(appInstall)
quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ {
switch quicks[i].Name {
case "Website":
quicks[i].Detail = fmt.Sprintf("%d", base.WebsiteNumber)
case "Database":
quicks[i].Detail = fmt.Sprintf("%d", base.DatabaseNumber)
case "Cronjob":
quicks[i].Detail = fmt.Sprintf("%d", base.CronjobNumber)
case "AppInstalled":
quicks[i].Detail = fmt.Sprintf("%d", base.AppInstalledNumber)
}
var item dto.QuickJump
_ = copier.Copy(&item, quicks[i])
base.QuickJumps = append(base.QuickJumps, item)
}
sort.Slice(quicks, func(i, j int) bool {
return quicks[i].Recommend < quicks[j].Recommend
})
}