diff --git a/internal/sysinfo/sysinfo.go b/internal/sysinfo/sysinfo.go new file mode 100644 index 0000000..c9f1b2f --- /dev/null +++ b/internal/sysinfo/sysinfo.go @@ -0,0 +1,369 @@ +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, + ) +}