VideoTools/internal/sysinfo/sysinfo.go
Stu Leak 8f73913817 fix(windows): Hide command windows in hardware detection and fix GPU detection
Issues Fixed:
1. Command Prompts During Benchmark
   - Jake reported 4 command prompt windows appearing when opening benchmark
   - Windows showing: C:\ffmpeg\bin\ffmpeg.exe for each hardware check

2. Wrong GPU Detection
   - Jake's AMD R7 900 XTX not detected
   - System incorrectly showing "13.50.53.699 Virtual Desktop" as GPU
   - Showing "Monitor" instead of actual graphics card

Root Causes:
1. sysinfo package missing ApplyNoWindow() on Windows detection commands
   - detectCPUWindows: wmic cpu query
   - detectGPUWindows: nvidia-smi + wmic VideoController queries
   - detectRAMWindows: wmic computersystem query
   - These 3-4 calls showed command windows

2. GPU Detection Not Filtering Virtual Adapters
   - wmic returns ALL video controllers (physical + virtual)
   - Code was picking first entry which was virtual adapter
   - No filtering for "Virtual Desktop", remote desktop adapters, etc.

Solutions:
1. Applied utils.ApplyNoWindow() to all Windows detection commands
   - Hides wmic, nvidia-smi command windows
   - Consistent with benchmark.go and platform.go patterns
   - No-op on Linux/macOS (cross-platform safe)

2. Enhanced GPU Detection with Virtual Adapter Filtering
   - Iterate through ALL video controllers from wmic
   - Filter out virtual/software adapters:
     * Virtual Desktop adapters
     * Microsoft Basic Display Adapter
     * Remote desktop (VNC, Parsec, TeamViewer)
   - Return first physical GPU (AMD/NVIDIA/Intel)
   - Debug logging shows skipped virtual adapters

Implementation:
- Import internal/utils in sysinfo package
- ApplyNoWindow() on 4 Windows commands:
  * wmic cpu get name,maxclockspeed
  * nvidia-smi --query-gpu=name,driver_version
  * wmic path win32_VideoController get name,driverversion
  * wmic computersystem get totalphysicalmemory
- Enhanced GPU parser with virtual adapter blacklist
- Debug logging for skipped/detected GPUs

Impact:
- No command windows during benchmark initialization (discreet operation)
- Correct physical GPU detection on systems with virtual adapters
- Should properly detect Jake's AMD R7 900 XTX

Files Changed:
- internal/sysinfo/sysinfo.go: ApplyNoWindow + GPU filtering

Reported-by: Jake (4 command prompts, wrong GPU detection)
Tested-on: Linux (build successful, no regressions)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 19:32:15 -05:00

394 lines
9.8 KiB
Go

package sysinfo
import (
"fmt"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
// 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")
utils.ApplyNoWindow(cmd) // Hide command window on Windows
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")
utils.ApplyNoWindow(cmd) // Hide command window on Windows
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")
utils.ApplyNoWindow(cmd) // Hide command window on Windows
output, err = cmd.Output()
if err == nil {
lines := strings.Split(string(output), "\n")
// Iterate through all video controllers, skip virtual/non-physical adapters
for i, line := range lines {
if i == 0 { // Skip header
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Filter out virtual/software adapters
lineLower := strings.ToLower(line)
if strings.Contains(lineLower, "virtual") ||
strings.Contains(lineLower, "microsoft basic") ||
strings.Contains(lineLower, "remote") ||
strings.Contains(lineLower, "vnc") ||
strings.Contains(lineLower, "parsec") ||
strings.Contains(lineLower, "teamviewer") {
logging.Debug(logging.CatSystem, "skipping virtual GPU: %s", line)
continue
}
// Parse: Name DriverVersion
// Use flexible regex to handle varying whitespace
re := regexp.MustCompile(`^(.+?)\s+(\S+)$`)
matches := re.FindStringSubmatch(line)
if len(matches) == 3 {
model = strings.TrimSpace(matches[1])
driver = strings.TrimSpace(matches[2])
logging.Debug(logging.CatSystem, "detected physical GPU: %s (driver: %s)", model, driver)
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")
utils.ApplyNoWindow(cmd) // Hide command window on Windows
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,
)
}