- Apply go formatting across internal packages - Clean up imports and code style
370 lines
8.8 KiB
Go
370 lines
8.8 KiB
Go
package sysinfo
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
|
|
)
|
|
|
|
// HardwareInfo contains system hardware information
|
|
type HardwareInfo struct {
|
|
CPU string `json:"cpu"`
|
|
CPUCores int `json:"cpu_cores"`
|
|
CPUMHz string `json:"cpu_mhz"`
|
|
GPU string `json:"gpu"`
|
|
GPUDriver string `json:"gpu_driver"`
|
|
RAM string `json:"ram"`
|
|
RAMMBytes uint64 `json:"ram_mb"`
|
|
OS string `json:"os"`
|
|
Arch string `json:"arch"`
|
|
}
|
|
|
|
// Detect gathers system hardware information
|
|
func Detect() HardwareInfo {
|
|
info := HardwareInfo{
|
|
OS: runtime.GOOS,
|
|
Arch: runtime.GOARCH,
|
|
CPUCores: runtime.NumCPU(),
|
|
}
|
|
|
|
// Detect CPU
|
|
info.CPU, info.CPUMHz = detectCPU()
|
|
|
|
// Detect GPU
|
|
info.GPU, info.GPUDriver = detectGPU()
|
|
|
|
// Detect RAM
|
|
info.RAM, info.RAMMBytes = detectRAM()
|
|
|
|
return info
|
|
}
|
|
|
|
// detectCPU returns CPU model and clock speed
|
|
func detectCPU() (model, mhz string) {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
return detectCPULinux()
|
|
case "windows":
|
|
return detectCPUWindows()
|
|
case "darwin":
|
|
return detectCPUDarwin()
|
|
default:
|
|
return "Unknown CPU", "Unknown"
|
|
}
|
|
}
|
|
|
|
func detectCPULinux() (model, mhz string) {
|
|
// Read /proc/cpuinfo
|
|
cmd := exec.Command("cat", "/proc/cpuinfo")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
logging.Debug(logging.CatSystem, "failed to read /proc/cpuinfo: %v", err)
|
|
return "Unknown CPU", "Unknown"
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "model name") {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) == 2 {
|
|
model = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
if strings.HasPrefix(line, "cpu MHz") {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) == 2 {
|
|
mhzStr := strings.TrimSpace(parts[1])
|
|
if mhzFloat, err := strconv.ParseFloat(mhzStr, 64); err == nil {
|
|
mhz = fmt.Sprintf("%.0f MHz", mhzFloat)
|
|
}
|
|
}
|
|
}
|
|
// Exit early once we have both
|
|
if model != "" && mhz != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
if model == "" {
|
|
model = "Unknown CPU"
|
|
}
|
|
if mhz == "" {
|
|
mhz = "Unknown"
|
|
}
|
|
|
|
return model, mhz
|
|
}
|
|
|
|
func detectCPUWindows() (model, mhz string) {
|
|
// Use wmic to get CPU info
|
|
cmd := exec.Command("wmic", "cpu", "get", "name,maxclockspeed")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
logging.Debug(logging.CatSystem, "failed to run wmic cpu: %v", err)
|
|
return "Unknown CPU", "Unknown"
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
if len(lines) >= 2 {
|
|
// Parse the second line (first is header)
|
|
fields := strings.Fields(lines[1])
|
|
if len(fields) >= 2 {
|
|
mhzStr := fields[len(fields)-1] // Last field is clock speed
|
|
model = strings.Join(fields[:len(fields)-1], " ")
|
|
if mhzInt, err := strconv.Atoi(mhzStr); err == nil {
|
|
mhz = fmt.Sprintf("%d MHz", mhzInt)
|
|
}
|
|
}
|
|
}
|
|
|
|
if model == "" {
|
|
model = "Unknown CPU"
|
|
}
|
|
if mhz == "" {
|
|
mhz = "Unknown"
|
|
}
|
|
|
|
return model, mhz
|
|
}
|
|
|
|
func detectCPUDarwin() (model, mhz string) {
|
|
// Use sysctl to get CPU info
|
|
cmdModel := exec.Command("sysctl", "-n", "machdep.cpu.brand_string")
|
|
if output, err := cmdModel.Output(); err == nil {
|
|
model = strings.TrimSpace(string(output))
|
|
}
|
|
|
|
cmdMHz := exec.Command("sysctl", "-n", "hw.cpufrequency")
|
|
if output, err := cmdMHz.Output(); err == nil {
|
|
if hz, err := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 64); err == nil {
|
|
mhz = fmt.Sprintf("%.0f MHz", float64(hz)/1000000)
|
|
}
|
|
}
|
|
|
|
if model == "" {
|
|
model = "Unknown CPU"
|
|
}
|
|
if mhz == "" {
|
|
mhz = "Unknown"
|
|
}
|
|
|
|
return model, mhz
|
|
}
|
|
|
|
// detectGPU returns GPU model and driver version
|
|
func detectGPU() (model, driver string) {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
return detectGPULinux()
|
|
case "windows":
|
|
return detectGPUWindows()
|
|
case "darwin":
|
|
return detectGPUDarwin()
|
|
default:
|
|
return "Unknown GPU", "Unknown"
|
|
}
|
|
}
|
|
|
|
func detectGPULinux() (model, driver string) {
|
|
// Try nvidia-smi first (most common for encoding)
|
|
cmd := exec.Command("nvidia-smi", "--query-gpu=name,driver_version", "--format=csv,noheader")
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
parts := strings.Split(strings.TrimSpace(string(output)), ",")
|
|
if len(parts) >= 2 {
|
|
model = strings.TrimSpace(parts[0])
|
|
driver = "NVIDIA " + strings.TrimSpace(parts[1])
|
|
return model, driver
|
|
}
|
|
}
|
|
|
|
// Try lspci for any GPU
|
|
cmd = exec.Command("lspci")
|
|
output, err = cmd.Output()
|
|
if err == nil {
|
|
lines := strings.Split(string(output), "\n")
|
|
for _, line := range lines {
|
|
if strings.Contains(strings.ToLower(line), "vga compatible") ||
|
|
strings.Contains(strings.ToLower(line), "3d controller") {
|
|
// Extract GPU name from lspci output
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) == 2 {
|
|
model = strings.TrimSpace(parts[1])
|
|
driver = "Unknown"
|
|
return model, driver
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "No GPU detected", "N/A"
|
|
}
|
|
|
|
func detectGPUWindows() (model, driver string) {
|
|
// Use nvidia-smi if available (NVIDIA GPUs)
|
|
cmd := exec.Command("nvidia-smi", "--query-gpu=name,driver_version", "--format=csv,noheader")
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
parts := strings.Split(strings.TrimSpace(string(output)), ",")
|
|
if len(parts) >= 2 {
|
|
model = strings.TrimSpace(parts[0])
|
|
driver = "NVIDIA " + strings.TrimSpace(parts[1])
|
|
return model, driver
|
|
}
|
|
}
|
|
|
|
// Try wmic for generic GPU info
|
|
cmd = exec.Command("wmic", "path", "win32_VideoController", "get", "name,driverversion")
|
|
output, err = cmd.Output()
|
|
if err == nil {
|
|
lines := strings.Split(string(output), "\n")
|
|
if len(lines) >= 2 {
|
|
// Skip header, get first GPU
|
|
line := strings.TrimSpace(lines[1])
|
|
if line != "" {
|
|
// Parse: Name DriverVersion
|
|
re := regexp.MustCompile(`^(.+?)\s+(\S+)$`)
|
|
matches := re.FindStringSubmatch(line)
|
|
if len(matches) == 3 {
|
|
model = strings.TrimSpace(matches[1])
|
|
driver = strings.TrimSpace(matches[2])
|
|
return model, driver
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "No GPU detected", "N/A"
|
|
}
|
|
|
|
func detectGPUDarwin() (model, driver string) {
|
|
// macOS uses system_profiler for GPU info
|
|
cmd := exec.Command("system_profiler", "SPDisplaysDataType")
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
lines := strings.Split(string(output), "\n")
|
|
for _, line := range lines {
|
|
if strings.Contains(line, "Chipset Model:") {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) == 2 {
|
|
model = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
if strings.Contains(line, "Metal:") {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) == 2 {
|
|
driver = "Metal " + strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if model == "" {
|
|
model = "Unknown GPU"
|
|
}
|
|
if driver == "" {
|
|
driver = "Unknown"
|
|
}
|
|
|
|
return model, driver
|
|
}
|
|
|
|
// detectRAM returns total system RAM
|
|
func detectRAM() (readable string, mb uint64) {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
return detectRAMLinux()
|
|
case "windows":
|
|
return detectRAMWindows()
|
|
case "darwin":
|
|
return detectRAMDarwin()
|
|
default:
|
|
return "Unknown", 0
|
|
}
|
|
}
|
|
|
|
func detectRAMLinux() (readable string, mb uint64) {
|
|
cmd := exec.Command("cat", "/proc/meminfo")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
logging.Debug(logging.CatSystem, "failed to read /proc/meminfo: %v", err)
|
|
return "Unknown", 0
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "MemTotal:") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 {
|
|
if kb, err := strconv.ParseUint(fields[1], 10, 64); err == nil {
|
|
mb = kb / 1024
|
|
gb := float64(mb) / 1024.0
|
|
readable = fmt.Sprintf("%.1f GB", gb)
|
|
return readable, mb
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "Unknown", 0
|
|
}
|
|
|
|
func detectRAMWindows() (readable string, mb uint64) {
|
|
cmd := exec.Command("wmic", "computersystem", "get", "totalphysicalmemory")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
logging.Debug(logging.CatSystem, "failed to run wmic computersystem: %v", err)
|
|
return "Unknown", 0
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
if len(lines) >= 2 {
|
|
bytesStr := strings.TrimSpace(lines[1])
|
|
if bytes, err := strconv.ParseUint(bytesStr, 10, 64); err == nil {
|
|
mb = bytes / (1024 * 1024)
|
|
gb := float64(mb) / 1024.0
|
|
readable = fmt.Sprintf("%.1f GB", gb)
|
|
return readable, mb
|
|
}
|
|
}
|
|
|
|
return "Unknown", 0
|
|
}
|
|
|
|
func detectRAMDarwin() (readable string, mb uint64) {
|
|
cmd := exec.Command("sysctl", "-n", "hw.memsize")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
logging.Debug(logging.CatSystem, "failed to run sysctl hw.memsize: %v", err)
|
|
return "Unknown", 0
|
|
}
|
|
|
|
bytesStr := strings.TrimSpace(string(output))
|
|
if bytes, err := strconv.ParseUint(bytesStr, 10, 64); err == nil {
|
|
mb = bytes / (1024 * 1024)
|
|
gb := float64(mb) / 1024.0
|
|
readable = fmt.Sprintf("%.1f GB", gb)
|
|
return readable, mb
|
|
}
|
|
|
|
return "Unknown", 0
|
|
}
|
|
|
|
// Summary returns a human-readable summary of hardware info
|
|
func (h HardwareInfo) Summary() string {
|
|
return fmt.Sprintf("%s\n%s (%d cores @ %s)\nGPU: %s\nDriver: %s\nRAM: %s",
|
|
h.OS+"/"+h.Arch,
|
|
h.CPU,
|
|
h.CPUCores,
|
|
h.CPUMHz,
|
|
h.GPU,
|
|
h.GPUDriver,
|
|
h.RAM,
|
|
)
|
|
}
|