feat: init pc-monitor project

- Client: Go-based Windows hardware monitoring (CPU, GPU, memory, disk, network, power)
- Server: Go + Gin + SQLite backend with REST API
- Frontend: Vue 3 + Element Plus dashboard
- Docker deployment support
- Windows service installation script
This commit is contained in:
672
2026-05-17 01:29:44 +08:00
commit 0e8c9f7bff
49 changed files with 3291 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
package collector
import (
"time"
)
type Metrics struct {
DeviceID string `json:"device_id"`
Timestamp time.Time `json:"timestamp"`
CPU *CPUInfo `json:"cpu"`
Memory *MemoryInfo `json:"memory"`
GPU *GPUInfo `json:"gpu"`
Disks []DiskInfo `json:"disks"`
Net []NetInterface `json:"network"`
Power *PowerInfo `json:"power"`
}
func CollectAll() (*Metrics, error) {
metrics := &Metrics{
Timestamp: time.Now(),
}
// Collect CPU
cpuInfo, err := CollectCPU()
if err == nil {
metrics.CPU = cpuInfo
}
// Collect Memory
memInfo, err := CollectMemory()
if err == nil {
metrics.Memory = memInfo
}
// Collect GPU
gpuInfo, err := CollectGPU()
if err == nil {
metrics.GPU = gpuInfo
}
// Collect Disks
disks, err := CollectDisks()
if err == nil {
metrics.Disks = disks
}
// Collect Network
nets, err := CollectNetwork()
if err == nil {
metrics.Net = nets
}
// Collect Power
powerInfo, err := CollectPower()
if err == nil {
metrics.Power = powerInfo
}
return metrics, nil
}

41
client/collector/cpu.go Normal file
View File

@@ -0,0 +1,41 @@
package collector
import (
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/host"
)
type CPUInfo struct {
Usage float64 `json:"usage"`
Temperature float64 `json:"temperature"`
CoreUsage []float64 `json:"core_usage"`
}
func CollectCPU() (*CPUInfo, error) {
info := &CPUInfo{}
// Get overall CPU usage
percent, err := cpu.Percent(0, false)
if err == nil && len(percent) > 0 {
info.Usage = percent[0]
}
// Get per-core CPU usage
perCPU, err := cpu.Percent(0, true)
if err == nil {
info.CoreUsage = perCPU
}
// Get CPU temperature
temps, err := host.SensorsTemperatures()
if err == nil {
for _, temp := range temps {
if temp.Temperature > 0 {
info.Temperature = temp.Temperature
break
}
}
}
return info, nil
}

38
client/collector/disk.go Normal file
View File

@@ -0,0 +1,38 @@
package collector
import (
"github.com/shirou/gopsutil/v3/disk"
)
type DiskInfo struct {
MountPoint string `json:"mount_point"`
Total uint64 `json:"total"`
Used uint64 `json:"used"`
Usage float64 `json:"usage"`
FileSystem string `json:"file_system"`
}
func CollectDisks() ([]DiskInfo, error) {
partitions, err := disk.Partitions(false)
if err != nil {
return nil, err
}
var disks []DiskInfo
for _, p := range partitions {
usage, err := disk.Usage(p.Mountpoint)
if err != nil {
continue
}
disks = append(disks, DiskInfo{
MountPoint: p.Mountpoint,
Total: usage.Total,
Used: usage.Used,
Usage: usage.UsedPercent,
FileSystem: p.Fstype,
})
}
return disks, nil
}

80
client/collector/gpu.go Normal file
View File

@@ -0,0 +1,80 @@
package collector
import (
"encoding/json"
"os/exec"
"strings"
)
type GPUInfo struct {
Usage float64 `json:"usage"`
Temperature float64 `json:"temperature"`
MemoryTotal uint64 `json:"memory_total"`
MemoryUsed uint64 `json:"memory_used"`
Name string `json:"name"`
}
type nvidiaSMIOutput struct {
GPUs []struct {
Name string `json:"name"`
Utilization struct {
GPU string `json:"gpu"`
} `json:"utilization"`
Temperature struct {
GPU string `json:"gpu_temp"`
} `json:"temperature"`
Memory struct {
Total string `json:"total"`
Used string `json:"used"`
} `json:"fb_memory_usage"`
} `json:"gpus"`
}
func CollectGPU() (*GPUInfo, error) {
// Try NVIDIA GPU first
info, err := collectNvidiaGPU()
if err == nil && info != nil {
return info, nil
}
// Return empty GPU info if no GPU detected
return &GPUInfo{
Name: "No GPU detected",
}, nil
}
func collectNvidiaGPU() (*GPUInfo, error) {
cmd := exec.Command("nvidia-smi", "--query-gpu=name,utilization.gpu,temperature.gpu,memory.total,memory.used", "--format=csv,noheader,nounits")
output, err := cmd.Output()
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) == 0 {
return nil, nil
}
fields := strings.Split(lines[0], ",")
if len(fields) < 5 {
return nil, nil
}
info := &GPUInfo{
Name: strings.TrimSpace(fields[0]),
}
// Parse values (ignore errors, use defaults)
var usage, temp, memTotal, memUsed float64
json.Unmarshal([]byte(strings.TrimSpace(fields[1])), &usage)
json.Unmarshal([]byte(strings.TrimSpace(fields[2])), &temp)
json.Unmarshal([]byte(strings.TrimSpace(fields[3])), &memTotal)
json.Unmarshal([]byte(strings.TrimSpace(fields[4])), &memUsed)
info.Usage = usage
info.Temperature = temp
info.MemoryTotal = uint64(memTotal) * 1024 * 1024 // Convert MB to bytes
info.MemoryUsed = uint64(memUsed) * 1024 * 1024
return info, nil
}

View File

@@ -0,0 +1,24 @@
package collector
import (
"github.com/shirou/gopsutil/v3/mem"
)
type MemoryInfo struct {
Total uint64 `json:"total"`
Used uint64 `json:"used"`
Usage float64 `json:"usage"`
}
func CollectMemory() (*MemoryInfo, error) {
vmStat, err := mem.VirtualMemory()
if err != nil {
return nil, err
}
return &MemoryInfo{
Total: vmStat.Total,
Used: vmStat.Used,
Usage: vmStat.UsedPercent,
}, nil
}

View File

@@ -0,0 +1,68 @@
package collector
import (
"github.com/shirou/gopsutil/v3/net"
)
type NetInterface struct {
Name string `json:"name"`
MACAddress string `json:"mac_address"`
IPAddress string `json:"ip_address"`
BytesSent uint64 `json:"bytes_sent"`
BytesRecv uint64 `json:"bytes_recv"`
Speed uint64 `json:"speed"`
IsUp bool `json:"is_up"`
}
func CollectNetwork() ([]NetInterface, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
counters, err := net.IOCounters(true)
if err != nil {
return nil, err
}
counterMap := make(map[string]net.IOCountersStat)
for _, c := range counters {
counterMap[c.Name] = c
}
var nets []NetInterface
for _, iface := range interfaces {
if iface.Name == "lo" || iface.Name == "Loopback Pseudo-Interface 1" {
continue
}
ni := NetInterface{
Name: iface.Name,
MACAddress: iface.HardwareAddr,
IsUp: false,
}
for _, addr := range iface.Addrs {
if addr.Addr != "" && addr.Addr != "0.0.0.0" {
ni.IPAddress = addr.Addr
break
}
}
for _, flag := range iface.Flags {
if flag == "up" {
ni.IsUp = true
break
}
}
if c, ok := counterMap[iface.Name]; ok {
ni.BytesSent = c.BytesSent
ni.BytesRecv = c.BytesRecv
}
nets = append(nets, ni)
}
return nets, nil
}

47
client/collector/power.go Normal file
View File

@@ -0,0 +1,47 @@
package collector
import (
"github.com/shirou/gopsutil/v3/host"
)
type PowerInfo struct {
Status string `json:"status"`
BatteryLevel int `json:"battery_level"`
PowerSource string `json:"power_source"`
}
func CollectPower() (*PowerInfo, error) {
info := &PowerInfo{
Status: "no_battery",
BatteryLevel: 0,
PowerSource: "ac",
}
// Try to get battery info
battery, err := host.SensorsBattery()
if err != nil {
return info, nil
}
if battery == nil {
return info, nil
}
info.BatteryLevel = int(battery.Percent)
if battery.PowerSupply {
info.PowerSource = "ac"
} else {
info.PowerSource = "battery"
}
if battery.Charging {
info.Status = "charging"
} else if battery.Percent >= 100 {
info.Status = "full"
} else {
info.Status = "discharging"
}
return info, nil
}