mirror of
https://github.com/Alexander-D-Karpov/netfetch.git
synced 2026-03-16 22:07:03 +03:00
639 lines
15 KiB
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
|
|
}
|