mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
459 lines
12 KiB
TypeScript
459 lines
12 KiB
TypeScript
/* eslint-disable no-comments/disallowComments */
|
|
import si from 'systeminformation';
|
|
import { logError } from '@/main/modules/logging';
|
|
import { terminateProcess } from '@/utils/process';
|
|
import { getGPUData } from '@/utils/gpu';
|
|
import type {
|
|
CPUCapabilities,
|
|
GPUCapabilities,
|
|
BasicGPUInfo,
|
|
GPUMemoryInfo,
|
|
HardwareDetectionResult,
|
|
} from '@/types/hardware';
|
|
import { spawn } from 'child_process';
|
|
import { formatDeviceName } from '@/utils/format';
|
|
|
|
let cpuCapabilitiesCache: CPUCapabilities | null = null;
|
|
let basicGPUInfoCache: BasicGPUInfo | null = null;
|
|
let gpuCapabilitiesCache: GPUCapabilities | null = null;
|
|
let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
|
|
|
|
export async function detectCPU() {
|
|
if (cpuCapabilitiesCache) {
|
|
return cpuCapabilitiesCache;
|
|
}
|
|
|
|
try {
|
|
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
|
|
|
|
const devices: string[] = [];
|
|
if (cpu.brand) {
|
|
devices.push(formatDeviceName(cpu.brand));
|
|
}
|
|
|
|
const avx = flags.includes('avx') || flags.includes('AVX');
|
|
const avx2 = flags.includes('avx2') || flags.includes('AVX2');
|
|
|
|
cpuCapabilitiesCache = {
|
|
avx,
|
|
avx2,
|
|
devices,
|
|
};
|
|
|
|
return cpuCapabilitiesCache;
|
|
} catch (error) {
|
|
logError('CPU detection failed:', error as Error);
|
|
const fallbackCapabilities = {
|
|
avx: false,
|
|
avx2: false,
|
|
devices: [],
|
|
};
|
|
cpuCapabilitiesCache = fallbackCapabilities;
|
|
return fallbackCapabilities;
|
|
}
|
|
}
|
|
|
|
export async function detectGPU() {
|
|
if (basicGPUInfoCache) {
|
|
return basicGPUInfoCache;
|
|
}
|
|
|
|
try {
|
|
const gpuData = await getGPUData();
|
|
|
|
let hasAMD = false;
|
|
let hasNVIDIA = false;
|
|
const gpuInfo: string[] = [];
|
|
|
|
for (const gpu of gpuData) {
|
|
if (gpu.deviceName) {
|
|
gpuInfo.push(formatDeviceName(gpu.deviceName));
|
|
}
|
|
|
|
const deviceName = gpu.deviceName?.toLowerCase() || '';
|
|
|
|
if (
|
|
deviceName.includes('amd') ||
|
|
deviceName.includes('ati') ||
|
|
deviceName.includes('radeon')
|
|
) {
|
|
hasAMD = true;
|
|
}
|
|
|
|
if (
|
|
deviceName.includes('nvidia') ||
|
|
deviceName.includes('geforce') ||
|
|
deviceName.includes('gtx') ||
|
|
deviceName.includes('rtx')
|
|
) {
|
|
hasNVIDIA = true;
|
|
}
|
|
}
|
|
|
|
basicGPUInfoCache = {
|
|
hasAMD,
|
|
hasNVIDIA,
|
|
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
|
};
|
|
|
|
return basicGPUInfoCache;
|
|
} catch (error) {
|
|
logError('GPU detection failed:', error as Error);
|
|
const fallbackGPUInfo = {
|
|
hasAMD: false,
|
|
hasNVIDIA: false,
|
|
gpuInfo: ['GPU detection failed'],
|
|
};
|
|
basicGPUInfoCache = fallbackGPUInfo;
|
|
return fallbackGPUInfo;
|
|
}
|
|
}
|
|
|
|
export async function detectGPUCapabilities() {
|
|
// WARNING: we're not worrying about the users that update their system
|
|
// during runtime and not restart. Should we be though?
|
|
if (gpuCapabilitiesCache) {
|
|
return gpuCapabilitiesCache;
|
|
}
|
|
|
|
const [cuda, rocm, vulkan, clblast] = await Promise.all([
|
|
detectCUDA(),
|
|
detectROCm(),
|
|
detectVulkan(),
|
|
detectCLBlast(),
|
|
]);
|
|
|
|
gpuCapabilitiesCache = { cuda, rocm, vulkan, clblast };
|
|
|
|
return gpuCapabilitiesCache;
|
|
}
|
|
|
|
async function detectCUDA() {
|
|
try {
|
|
const nvidia = spawn(
|
|
'nvidia-smi',
|
|
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
|
{ timeout: 5000 }
|
|
);
|
|
|
|
let output = '';
|
|
nvidia.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
return new Promise<HardwareDetectionResult>((resolve) => {
|
|
nvidia.on('close', (code) => {
|
|
if (code === 0 && output.trim()) {
|
|
const devices = output
|
|
.trim()
|
|
.split('\n')
|
|
.map((line) => {
|
|
const parts = line.split(',');
|
|
const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU';
|
|
return formatDeviceName(rawName);
|
|
})
|
|
.filter(Boolean);
|
|
|
|
resolve({
|
|
supported: devices.length > 0,
|
|
devices,
|
|
});
|
|
} else {
|
|
resolve({ supported: false, devices: [] });
|
|
}
|
|
});
|
|
|
|
nvidia.on('error', () => {
|
|
resolve({ supported: false, devices: [] });
|
|
});
|
|
|
|
setTimeout(async () => {
|
|
await terminateProcess(nvidia);
|
|
resolve({ supported: false, devices: [] });
|
|
}, 5000);
|
|
});
|
|
} catch {
|
|
return { supported: false, devices: [] };
|
|
}
|
|
}
|
|
|
|
async function findRocminfoCommand() {
|
|
const platform = await import('process').then((p) => p.platform);
|
|
|
|
if (platform === 'win32') {
|
|
return 'hipInfo';
|
|
} else {
|
|
return 'rocminfo';
|
|
}
|
|
}
|
|
|
|
export async function detectROCm() {
|
|
try {
|
|
const rocminfoCommand = await findRocminfoCommand();
|
|
if (!rocminfoCommand) {
|
|
return { supported: false, devices: [] };
|
|
}
|
|
|
|
const isWindows = rocminfoCommand.includes('hipInfo');
|
|
const rocminfo = spawn(rocminfoCommand, [], { timeout: 5000 });
|
|
|
|
let output = '';
|
|
rocminfo.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
return new Promise<HardwareDetectionResult>((resolve) => {
|
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
rocminfo.on('close', (code) => {
|
|
if (code === 0 && output.trim()) {
|
|
const devices: string[] = [];
|
|
|
|
if (isWindows) {
|
|
const lines = output.split('\n');
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith('Name:')) {
|
|
const name = trimmedLine.split('Name:')[1]?.trim();
|
|
if (
|
|
name &&
|
|
!name.toLowerCase().includes('cpu') &&
|
|
!devices.includes(formatDeviceName(name))
|
|
) {
|
|
devices.push(formatDeviceName(name));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const lines = output.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
if (line.includes('Marketing Name:')) {
|
|
const name = line.split('Marketing Name:')[1]?.trim();
|
|
if (name) {
|
|
let deviceType = '';
|
|
|
|
const searchRangeLines = 20;
|
|
const searchStartIndex = Math.max(0, i - searchRangeLines);
|
|
const searchEndIndex = Math.min(
|
|
lines.length,
|
|
i + searchRangeLines
|
|
);
|
|
|
|
for (
|
|
let searchIndex = searchStartIndex;
|
|
searchIndex < searchEndIndex;
|
|
searchIndex++
|
|
) {
|
|
if (lines[searchIndex].includes('Device Type:')) {
|
|
deviceType =
|
|
lines[searchIndex].split('Device Type:')[1]?.trim() ||
|
|
'';
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (deviceType !== 'CPU') {
|
|
devices.push(formatDeviceName(name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resolve({
|
|
supported: devices.length > 0,
|
|
devices,
|
|
});
|
|
} else {
|
|
resolve({ supported: false, devices: [] });
|
|
}
|
|
});
|
|
|
|
rocminfo.on('error', () => {
|
|
resolve({ supported: false, devices: [] });
|
|
});
|
|
|
|
setTimeout(async () => {
|
|
await terminateProcess(rocminfo);
|
|
resolve({ supported: false, devices: [] });
|
|
}, 5000);
|
|
});
|
|
} catch {
|
|
return { supported: false, devices: [] };
|
|
}
|
|
}
|
|
|
|
async function detectVulkan() {
|
|
try {
|
|
const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 });
|
|
|
|
let output = '';
|
|
vulkaninfo.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
return new Promise<HardwareDetectionResult>((resolve) => {
|
|
vulkaninfo.on('close', (code) => {
|
|
if (code === 0 && output.trim()) {
|
|
const devices: string[] = [];
|
|
const lines = output.split('\n');
|
|
|
|
for (const line of lines) {
|
|
if (line.includes('deviceName') && line.includes('=')) {
|
|
const parts = line.split('=');
|
|
if (parts.length >= 2) {
|
|
const name = parts[1]?.trim();
|
|
if (name) {
|
|
devices.push(formatDeviceName(name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resolve({
|
|
supported: devices.length > 0,
|
|
devices,
|
|
});
|
|
} else {
|
|
resolve({ supported: false, devices: [] });
|
|
}
|
|
});
|
|
|
|
vulkaninfo.on('error', () => {
|
|
resolve({ supported: false, devices: [] });
|
|
});
|
|
|
|
setTimeout(async () => {
|
|
await terminateProcess(vulkaninfo);
|
|
resolve({ supported: false, devices: [] });
|
|
}, 5000);
|
|
});
|
|
} catch {
|
|
return { supported: false, devices: [] };
|
|
}
|
|
}
|
|
|
|
function parseClInfoOutput(output: string) {
|
|
const devices: string[] = [];
|
|
const lines = output.split('\n');
|
|
|
|
let currentPlatform = '';
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
|
|
if (line.includes('Platform Name:')) {
|
|
currentPlatform = line.split('Platform Name:')[1]?.trim() || '';
|
|
continue;
|
|
}
|
|
|
|
if (line.includes('Device Type:') && line.includes('GPU')) {
|
|
const deviceName = findDeviceNameInClInfo(lines, i);
|
|
|
|
if (deviceName && currentPlatform) {
|
|
const deviceLabel = `${formatDeviceName(deviceName)} (${currentPlatform})`;
|
|
devices.push(deviceLabel);
|
|
}
|
|
}
|
|
}
|
|
|
|
return devices;
|
|
}
|
|
|
|
function findDeviceNameInClInfo(lines: string[], startIndex: number) {
|
|
for (
|
|
let j = startIndex + 1;
|
|
j < Math.min(startIndex + 50, lines.length);
|
|
j++
|
|
) {
|
|
const nextLine = lines[j].trim();
|
|
if (nextLine.includes('Board name:')) {
|
|
return nextLine.split('Board name:')[1]?.trim() || '';
|
|
}
|
|
}
|
|
|
|
for (
|
|
let j = startIndex + 1;
|
|
j < Math.min(startIndex + 100, lines.length);
|
|
j++
|
|
) {
|
|
const nextLine = lines[j].trim();
|
|
if (nextLine.startsWith('Name:')) {
|
|
return nextLine.split('Name:')[1]?.trim() || '';
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
async function detectCLBlast() {
|
|
try {
|
|
const clinfo = spawn('clinfo', [], { timeout: 3000 });
|
|
|
|
let output = '';
|
|
clinfo.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
return new Promise<HardwareDetectionResult>((resolve) => {
|
|
clinfo.on('close', (code) => {
|
|
if (code === 0 && output.trim()) {
|
|
const devices = parseClInfoOutput(output);
|
|
resolve({
|
|
supported: devices.length > 0,
|
|
devices,
|
|
});
|
|
} else {
|
|
resolve({ supported: false, devices: [] });
|
|
}
|
|
});
|
|
|
|
clinfo.on('error', () => {
|
|
resolve({ supported: false, devices: [] });
|
|
});
|
|
|
|
setTimeout(async () => {
|
|
await terminateProcess(clinfo);
|
|
resolve({ supported: false, devices: [] });
|
|
}, 3000);
|
|
});
|
|
} catch {
|
|
return { supported: false, devices: [] };
|
|
}
|
|
}
|
|
|
|
export async function detectGPUMemory() {
|
|
if (gpuMemoryInfoCache) {
|
|
return gpuMemoryInfoCache;
|
|
}
|
|
|
|
const memoryInfo: GPUMemoryInfo[] = [];
|
|
|
|
try {
|
|
const gpuData = await getGPUData();
|
|
|
|
for (const gpu of gpuData) {
|
|
if (gpu.deviceName) {
|
|
let vram: number | null = gpu.memoryTotal;
|
|
|
|
if (!vram || vram <= 1) {
|
|
vram = null;
|
|
}
|
|
|
|
memoryInfo.push({
|
|
deviceName: formatDeviceName(gpu.deviceName),
|
|
totalMemoryMB: vram,
|
|
});
|
|
}
|
|
}
|
|
|
|
gpuMemoryInfoCache = memoryInfo;
|
|
} catch (error) {
|
|
logError('GPU memory detection failed:', error as Error);
|
|
gpuMemoryInfoCache = [];
|
|
}
|
|
|
|
return gpuMemoryInfoCache;
|
|
}
|