package backend import ( "context" "os/exec" "strconv" "strings" "time" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/mem" ps "github.com/shirou/gopsutil/v3/process" "github.com/wailsapp/wails/v2/pkg/runtime" ) type SysInfo struct { ctx context.Context cancel context.CancelFunc } func NewSysInfo(ctx context.Context) *SysInfo { cctx, cancel := context.WithCancel(ctx) s := &SysInfo{ctx: cctx, cancel: cancel} go s.startEmitter() return s } func (s *SysInfo) startEmitter() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-s.ctx.Done(): return case <-ticker.C: info, _ := s.CollectOnce() // try to attach GPU info if available name, tot, used, util := queryGPU(s.ctx) if name != "" { info.GPUName = name info.GPUTotal = tot info.GPUUsed = used info.GPUUtil = util } // emit via Wails runtime runtime.EventsEmit(s.ctx, "metrics", info) } } } type ProcessInfo struct { PID int32 `json:"pid"` Name string `json:"name"` CPU float64 `json:"cpu"` Mem uint64 `json:"mem"` } type Metrics struct { CPUPercent float64 `json:"cpu_percent"` TotalMem uint64 `json:"total_mem"` FreeMem uint64 `json:"free_mem"` Processes []ProcessInfo `json:"processes"` Timestamp int64 `json:"timestamp"` GPUName string `json:"gpu_name,omitempty"` GPUTotal uint64 `json:"gpu_total_mem,omitempty"` GPUUsed uint64 `json:"gpu_used_mem,omitempty"` GPUUtil float64 `json:"gpu_util_percent,omitempty"` } // CollectOnce gathers a snapshot of system metrics. func (s *SysInfo) CollectOnce() (*Metrics, error) { cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false) vm, _ := mem.VirtualMemory() // collect top processes by CPU (sample few) procs, _ := ps.Processes() var list []ProcessInfo for i, p := range procs { if i >= 30 { break } name, _ := p.Name() cpuPct, _ := p.CPUPercent() memInfo, _ := p.MemoryInfo() var memBytes uint64 if memInfo != nil { memBytes = memInfo.RSS } list = append(list, ProcessInfo{PID: p.Pid, Name: name, CPU: cpuPct, Mem: memBytes}) } var cpuVal float64 if len(cpuPercents) > 0 { cpuVal = cpuPercents[0] } return &Metrics{ CPUPercent: cpuVal, TotalMem: vm.Total, FreeMem: vm.Available, Processes: list, Timestamp: time.Now().UnixMilli(), GPUName: "", GPUTotal: 0, GPUUsed: 0, GPUUtil: 0, }, nil } // try to query GPU info via nvidia-smi; returns name, totalMB, usedMB, utilPercent func queryGPU(ctx context.Context) (string, uint64, uint64, float64) { // Use nvidia-smi if available cmd := exec.CommandContext(ctx, "nvidia-smi", "--query-gpu=name,memory.total,memory.used,utilization.gpu", "--format=csv,noheader,nounits") out, err := cmd.Output() if err != nil { return "", 0, 0, 0 } line := strings.TrimSpace(string(out)) if line == "" { return "", 0, 0, 0 } // fields: name, totalMB, usedMB, util parts := strings.Split(line, ",") if len(parts) < 4 { return "", 0, 0, 0 } name := strings.TrimSpace(parts[0]) totStr := strings.TrimSpace(parts[1]) usedStr := strings.TrimSpace(parts[2]) utilStr := strings.TrimSpace(parts[3]) tot, _ := strconv.ParseUint(strings.Fields(totStr)[0], 10, 64) used, _ := strconv.ParseUint(strings.Fields(usedStr)[0], 10, 64) utilVal, _ := strconv.ParseFloat(strings.Fields(utilStr)[0], 64) return name, tot * 1024 * 1024, used * 1024 * 1024, utilVal } // GetMetrics is an exported method callable from frontend. func (s *SysInfo) GetMetrics() (*Metrics, error) { return s.CollectOnce() }