netfetch/internal/collector/cpu.go

639 lines
15 KiB
Go

package collector
import (
"bufio"
"fmt"
"netfetch/internal/model"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
)
func (c *Collector) collectCPU() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.info.CPU = &model.CPUInfo{}
switch runtime.GOOS {
case "linux":
detectCPULinux(c.info.CPU)
case "darwin":
detectCPUDarwin(c.info.CPU)
case "windows":
detectCPUWindows(c.info.CPU)
case "freebsd", "openbsd", "netbsd":
detectCPUBSD(c.info.CPU)
}
}
func detectCPULinux(cpu *model.CPUInfo) {
cpuinfo := parseCPUInfo("/proc/cpuinfo")
cpu.Name = detectCPUName(cpuinfo)
cpu.Vendor = detectCPUVendor(cpuinfo)
cpu.CoresLogical = uint16(runtime.NumCPU())
cpu.CoresPhysical = getPhysicalCoresLinux()
cpu.CoresOnline = getOnlineCores()
baseFreq, maxFreq := getCPUFrequenciesLinux(cpuinfo)
cpu.FrequencyBase = baseFreq
cpu.FrequencyMax = maxFreq
cpu.Temperature = getCPUTemperatureLinux()
cleanCPUName(cpu)
}
func detectCPUName(cpuinfo map[string]string) string {
if name := cpuinfo["model name"]; name != "" {
return name
}
if name := cpuinfo["Model name"]; name != "" {
return name
}
if name := cpuinfo["Hardware"]; name != "" {
return name
}
if name := cpuinfo["cpu model"]; name != "" {
return name
}
arch := runtime.GOARCH
if arch == "arm64" || arch == "arm" {
return detectARMCPUName(cpuinfo)
}
if arch == "riscv64" {
return detectRISCVCPUName(cpuinfo)
}
return "Unknown"
}
func detectCPUVendor(cpuinfo map[string]string) string {
if vendor := cpuinfo["vendor_id"]; vendor != "" {
return vendor
}
if vendor := cpuinfo["CPU implementer"]; vendor != "" {
return mapARMImplementer(vendor)
}
return ""
}
func detectARMCPUName(cpuinfo map[string]string) string {
implementer := cpuinfo["CPU implementer"]
part := cpuinfo["CPU part"]
variant := cpuinfo["CPU variant"]
if out, err := exec.Command("lscpu").Output(); err == nil {
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "Model name:") {
name := strings.TrimSpace(strings.TrimPrefix(line, "Model name:"))
if name != "" && name != "-" {
return name
}
}
}
}
if implementer != "" && part != "" {
cpuName := lookupARMCPU(implementer, part)
if cpuName != "" {
return cpuName
}
}
if hwModel := cpuinfo["Hardware"]; hwModel != "" {
return hwModel
}
if cpuinfo["CPU architecture"] == "8" {
if variant != "" {
return fmt.Sprintf("ARMv8 Processor rev %s", variant)
}
return "ARMv8 Processor"
}
return "ARM Processor"
}
func mapARMImplementer(implementer string) string {
implementerMap := map[string]string{
"0x41": "ARM",
"0x42": "Broadcom",
"0x43": "Cavium",
"0x44": "DEC",
"0x46": "Fujitsu",
"0x48": "HiSilicon",
"0x49": "Infineon",
"0x4d": "Motorola/Freescale",
"0x4e": "NVIDIA",
"0x50": "APM",
"0x51": "Qualcomm",
"0x53": "Samsung",
"0x56": "Marvell",
"0x61": "Apple",
"0x66": "Faraday",
"0x69": "Intel",
"0xc0": "Ampere",
}
if vendor, ok := implementerMap[implementer]; ok {
return vendor
}
return "ARM"
}
func lookupARMCPU(implementer, part string) string {
armCPUs := map[string]map[string]string{
"0x41": {
"0xd02": "Cortex-A34",
"0xd03": "Cortex-A53",
"0xd04": "Cortex-A35",
"0xd05": "Cortex-A55",
"0xd06": "Cortex-A65",
"0xd07": "Cortex-A57",
"0xd08": "Cortex-A72",
"0xd09": "Cortex-A73",
"0xd0a": "Cortex-A75",
"0xd0b": "Cortex-A76",
"0xd0c": "Neoverse N1",
"0xd0d": "Cortex-A77",
"0xd0e": "Cortex-A76AE",
"0xd40": "Neoverse V1",
"0xd41": "Cortex-A78",
"0xd42": "Cortex-A78AE",
"0xd43": "Cortex-A65AE",
"0xd44": "Cortex-X1",
"0xd46": "Cortex-A510",
"0xd47": "Cortex-A710",
"0xd48": "Cortex-X2",
"0xd49": "Neoverse N2",
"0xd4a": "Neoverse E1",
"0xd4b": "Cortex-A78C",
"0xd4c": "Cortex-X1C",
"0xd4d": "Cortex-A715",
"0xd4e": "Cortex-X3",
"0xd4f": "Neoverse V2",
"0xd80": "Cortex-A520",
"0xd81": "Cortex-A720",
"0xd82": "Cortex-X4",
},
"0x51": {
"0x800": "Kryo 260/280",
"0x801": "Kryo 260/280 Silver",
"0x802": "Kryo 385 Gold",
"0x803": "Kryo 385 Silver",
"0x804": "Kryo 485 Gold",
"0x805": "Kryo 485 Silver",
"0xc00": "Falkor",
"0xc01": "Saphira",
},
"0x61": {
"0x020": "Apple M1 Icestorm",
"0x021": "Apple M1 Firestorm",
"0x022": "Apple M1 Pro Icestorm",
"0x023": "Apple M1 Pro Firestorm",
"0x024": "Apple M1 Max Icestorm",
"0x025": "Apple M1 Max Firestorm",
"0x028": "Apple M1 Ultra Icestorm",
"0x029": "Apple M1 Ultra Firestorm",
"0x030": "Apple M2 Blizzard",
"0x031": "Apple M2 Avalanche",
"0x032": "Apple M2 Pro Blizzard",
"0x033": "Apple M2 Pro Avalanche",
"0x034": "Apple M2 Max Blizzard",
"0x035": "Apple M2 Max Avalanche",
"0x038": "Apple M3 Sawtooth",
"0x039": "Apple M3 Everest",
},
"0x48": {
"0xd01": "Kunpeng-920",
"0xd40": "Cortex-A76 (HiSilicon)",
},
"0xc0": {
"0xac3": "Ampere-1",
"0xac4": "Ampere-1A",
},
}
if parts, ok := armCPUs[implementer]; ok {
if name, ok := parts[part]; ok {
return name
}
}
return ""
}
func detectRISCVCPUName(cpuinfo map[string]string) string {
if model := cpuinfo["uarch"]; model != "" {
return model
}
if model := cpuinfo["model name"]; model != "" {
return model
}
return "RISC-V Processor"
}
func parseCPUInfo(path string) map[string]string {
file, err := os.Open(path)
if err != nil {
return make(map[string]string)
}
defer file.Close()
result := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if idx := strings.Index(line, ":"); idx != -1 {
key := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
if _, exists := result[key]; !exists {
result[key] = value
}
}
}
return result
}
func getPhysicalCoresLinux() uint16 {
coreIDSet := make(map[string]struct{})
physicalIDSet := make(map[string]struct{})
coreIDFiles, err := filepath.Glob("/sys/devices/system/cpu/cpu[0-9]*/topology/core_id")
if err != nil || len(coreIDFiles) == 0 {
return uint16(runtime.NumCPU())
}
for _, coreIDFile := range coreIDFiles {
coreData, err := os.ReadFile(coreIDFile)
if err != nil {
continue
}
physIDFile := strings.Replace(coreIDFile, "core_id", "physical_package_id", 1)
physData, err := os.ReadFile(physIDFile)
if err != nil {
physData = []byte("0")
}
coreID := strings.TrimSpace(string(coreData))
physID := strings.TrimSpace(string(physData))
uniqueKey := physID + ":" + coreID
coreIDSet[uniqueKey] = struct{}{}
physicalIDSet[physID] = struct{}{}
}
if len(coreIDSet) == 0 {
return uint16(runtime.NumCPU())
}
return uint16(len(coreIDSet))
}
func getOnlineCores() uint16 {
onlinePath := "/sys/devices/system/cpu/online"
data, err := os.ReadFile(onlinePath)
if err != nil {
return uint16(runtime.NumCPU())
}
rangeStr := strings.TrimSpace(string(data))
count := 0
for _, part := range strings.Split(rangeStr, ",") {
if strings.Contains(part, "-") {
bounds := strings.Split(part, "-")
if len(bounds) == 2 {
start, _ := strconv.Atoi(bounds[0])
end, _ := strconv.Atoi(bounds[1])
count += end - start + 1
}
} else {
count++
}
}
if count == 0 {
return uint16(runtime.NumCPU())
}
return uint16(count)
}
func getCPUFrequenciesLinux(cpuinfo map[string]string) (uint32, uint32) {
maxFreq := readCPUFreqSysfs()
baseFreq := maxFreq
if maxFreq == 0 {
if freq := extractFrequencyFromName(cpuinfo["model name"]); freq > 0 {
maxFreq = freq
baseFreq = freq
}
}
baseFreqPath := "/sys/devices/system/cpu/cpu0/cpufreq/base_frequency"
if data, err := os.ReadFile(baseFreqPath); err == nil {
if freq, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64); err == nil {
baseFreq = uint32(freq / 1000)
}
}
return baseFreq, maxFreq
}
func readCPUFreqSysfs() uint32 {
freqPaths := []string{
"/sys/devices/system/cpu/cpu0/cpufreq/bios_limit",
"/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq",
"/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq",
}
for _, path := range freqPaths {
data, err := os.ReadFile(path)
if err == nil {
freqStr := strings.TrimSpace(string(data))
if freq, err := strconv.ParseUint(freqStr, 10, 64); err == nil {
return uint32(freq / 1000)
}
}
}
return 0
}
func extractFrequencyFromName(name string) uint32 {
if idx := strings.Index(name, "@"); idx != -1 {
freqStr := strings.TrimSpace(name[idx+1:])
freqStr = strings.TrimSuffix(freqStr, "GHz")
freqStr = strings.TrimSuffix(freqStr, "MHz")
freqStr = strings.TrimSpace(freqStr)
if freq, err := strconv.ParseFloat(freqStr, 64); err == nil {
if freq < 100 {
return uint32(freq * 1000)
}
return uint32(freq)
}
}
return 0
}
func getCPUTemperatureLinux() float64 {
hwmonDirs, _ := filepath.Glob("/sys/class/hwmon/hwmon*")
type hwmonInfo struct {
path string
name string
priority int
}
var sensors []hwmonInfo
priorityMap := map[string]int{
"coretemp": 1,
"k10temp": 1,
"zenpower": 1,
"amdgpu": 2,
"it87": 3,
"nct6775": 3,
"acpitz": 5,
}
for _, dir := range hwmonDirs {
nameData, err := os.ReadFile(filepath.Join(dir, "name"))
if err != nil {
continue
}
name := strings.TrimSpace(string(nameData))
priority := 10
if p, ok := priorityMap[name]; ok {
priority = p
}
sensors = append(sensors, hwmonInfo{
path: dir,
name: name,
priority: priority,
})
}
for p := 1; p <= 10; p++ {
for _, sensor := range sensors {
if sensor.priority != p {
continue
}
tempFiles, _ := filepath.Glob(filepath.Join(sensor.path, "temp*_input"))
for _, tempFile := range tempFiles {
labelFile := strings.Replace(tempFile, "_input", "_label", 1)
if data, err := os.ReadFile(labelFile); err == nil {
label := strings.ToLower(strings.TrimSpace(string(data)))
if !strings.Contains(label, "core") &&
!strings.Contains(label, "tctl") &&
!strings.Contains(label, "tdie") &&
!strings.Contains(label, "package") {
continue
}
}
tempData, err := os.ReadFile(tempFile)
if err != nil {
continue
}
if temp, err := strconv.ParseInt(strings.TrimSpace(string(tempData)), 10, 64); err == nil && temp > 0 {
tempC := float64(temp) / 1000.0
if tempC > 0 && tempC < 150 {
return tempC
}
}
}
}
}
thermalFiles, _ := filepath.Glob("/sys/class/thermal/thermal_zone*/temp")
for _, file := range thermalFiles {
typeFile := filepath.Join(filepath.Dir(file), "type")
if data, err := os.ReadFile(typeFile); err == nil {
zoneType := strings.TrimSpace(string(data))
if !strings.Contains(strings.ToLower(zoneType), "cpu") &&
!strings.Contains(strings.ToLower(zoneType), "x86") &&
!strings.Contains(strings.ToLower(zoneType), "soc") {
continue
}
}
tempData, err := os.ReadFile(file)
if err == nil {
if temp, err := strconv.ParseInt(strings.TrimSpace(string(tempData)), 10, 64); err == nil && temp > 0 {
return float64(temp) / 1000.0
}
}
}
return 0
}
func detectCPUDarwin(cpu *model.CPUInfo) {
cpu.Name = getSysctlString("machdep.cpu.brand_string")
cpu.Vendor = getSysctlString("machdep.cpu.vendor")
if cpu.Name == "" {
if out, err := exec.Command("sysctl", "-n", "hw.model").Output(); err == nil {
cpu.Name = strings.TrimSpace(string(out))
}
}
if ncpu := getSysctlInt("hw.ncpu"); ncpu > 0 {
cpu.CoresLogical = uint16(ncpu)
cpu.CoresOnline = uint16(ncpu)
}
if physCPU := getSysctlInt("hw.physicalcpu"); physCPU > 0 {
cpu.CoresPhysical = uint16(physCPU)
} else {
cpu.CoresPhysical = cpu.CoresLogical
}
if freq := getSysctlInt("hw.cpufrequency"); freq > 0 {
cpu.FrequencyBase = uint32(freq / 1000000)
}
if maxFreq := getSysctlInt("hw.cpufrequency_max"); maxFreq > 0 {
cpu.FrequencyMax = uint32(maxFreq / 1000000)
} else {
cpu.FrequencyMax = cpu.FrequencyBase
}
if cpu.FrequencyMax == 0 {
if pCores := getSysctlInt("hw.perflevel0.cpuspeeds"); pCores > 0 {
cpu.FrequencyMax = uint32(pCores / 1000000)
}
}
cleanCPUName(cpu)
}
func getSysctlString(key string) string {
out, err := exec.Command("sysctl", "-n", key).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func getSysctlInt(key string) int {
out, err := exec.Command("sysctl", "-n", key).Output()
if err != nil {
return 0
}
val, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return 0
}
return val
}
func detectCPUWindows(cpu *model.CPUInfo) {
out, err := exec.Command("wmic", "cpu", "get", "Name,NumberOfCores,NumberOfLogicalProcessors,MaxClockSpeed", "/format:list").Output()
if err != nil {
cpu.Name = "Unknown"
return
}
lines := strings.Split(string(out), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Name=") {
cpu.Name = strings.TrimPrefix(line, "Name=")
} else if strings.HasPrefix(line, "NumberOfCores=") {
if cores, err := strconv.Atoi(strings.TrimPrefix(line, "NumberOfCores=")); err == nil {
cpu.CoresPhysical = uint16(cores)
}
} else if strings.HasPrefix(line, "NumberOfLogicalProcessors=") {
if logical, err := strconv.Atoi(strings.TrimPrefix(line, "NumberOfLogicalProcessors=")); err == nil {
cpu.CoresLogical = uint16(logical)
cpu.CoresOnline = uint16(logical)
}
} else if strings.HasPrefix(line, "MaxClockSpeed=") {
if freq, err := strconv.Atoi(strings.TrimPrefix(line, "MaxClockSpeed=")); err == nil {
cpu.FrequencyMax = uint32(freq)
cpu.FrequencyBase = uint32(freq)
}
}
}
cleanCPUName(cpu)
}
func detectCPUBSD(cpu *model.CPUInfo) {
cpu.Name = getSysctlString("hw.model")
cpu.CoresLogical = uint16(runtime.NumCPU())
cpu.CoresPhysical = cpu.CoresLogical
cpu.CoresOnline = cpu.CoresLogical
if freq := getSysctlInt("hw.clockrate"); freq > 0 {
cpu.FrequencyBase = uint32(freq)
cpu.FrequencyMax = uint32(freq)
}
if freq := getSysctlInt("dev.cpu.0.freq"); freq > 0 {
cpu.FrequencyMax = uint32(freq)
}
cleanCPUName(cpu)
}
func cleanCPUName(cpu *model.CPUInfo) {
name := cpu.Name
removals := []string{
"(R)", "(r)", "(TM)", "(tm)", "(C)", "(c)",
" CPU", " FPU", " APU", " Processor", " processor",
" Dual-Core", " Quad-Core", " Six-Core", " Eight-Core", " Ten-Core",
" Twelve-Core", " Sixteen-Core", " Twenty-Four-Core", " Thirty-Two-Core",
" 2-Core", " 4-Core", " 6-Core", " 8-Core", " 10-Core", " 12-Core",
" 14-Core", " 16-Core", " 24-Core", " 32-Core", " 64-Core",
" with Radeon Graphics", " with Radeon Vega Graphics",
" with Radeon Vega Mobile Gfx",
" RADEON R2, 4 COMPUTE CORES 2C+2G",
" RADEON R4, 5 COMPUTE CORES 2C+3G",
" RADEON R5, 5 COMPUTE CORES 2C+3G",
" RADEON R5, 10 COMPUTE CORES 4C+6G",
" RADEON R7, 10 COMPUTE CORES 4C+6G",
" RADEON R7, 12 COMPUTE CORES 4C+8G",
}
for _, s := range removals {
name = strings.ReplaceAll(name, s, "")
}
if idx := strings.Index(name, "@"); idx != -1 {
name = strings.TrimSpace(name[:idx])
}
name = strings.Join(strings.Fields(name), " ")
cpu.Name = name
}