'use strict'; // @ts-check // ================================================================================== // graphics.js // ---------------------------------------------------------------------------------- // Description: System Information - library // for Node.js // Copyright: (c) 2014 - 2022 // Author: Sebastian Hildebrandt // ---------------------------------------------------------------------------------- // License: MIT // ================================================================================== // 7. Graphics (controller, display) // ---------------------------------------------------------------------------------- const fs = require('fs'); const exec = require('child_process').exec; const execSync = require('child_process').execSync; const util = require('./util'); let _platform = process.platform; let _nvidiaSmiPath = ''; const _linux = (_platform === 'linux' || _platform === 'android'); const _darwin = (_platform === 'darwin'); const _windows = (_platform === 'win32'); const _freebsd = (_platform === 'freebsd'); const _openbsd = (_platform === 'openbsd'); const _netbsd = (_platform === 'netbsd'); const _sunos = (_platform === 'sunos'); let _resolutionX = 0; let _resolutionY = 0; let _pixelDepth = 0; let _refreshRate = 0; const videoTypes = { '-2': 'UNINITIALIZED', '-1': 'OTHER', '0': 'HD15', '1': 'SVIDEO', '2': 'Composite video', '3': 'Component video', '4': 'DVI', '5': 'HDMI', '6': 'LVDS', '8': 'D_JPN', '9': 'SDI', '10': 'DP', '11': 'DP embedded', '12': 'UDI', '13': 'UDI embedded', '14': 'SDTVDONGLE', '15': 'MIRACAST', '2147483648': 'INTERNAL' }; function getVendorFromModel(model) { const manufacturers = [ { pattern: '^LG.+', manufacturer: 'LG' }, { pattern: '^BENQ.+', manufacturer: 'BenQ' }, { pattern: '^ASUS.+', manufacturer: 'Asus' }, { pattern: '^DELL.+', manufacturer: 'Dell' }, { pattern: '^SAMSUNG.+', manufacturer: 'Samsung' }, { pattern: '^VIEWSON.+', manufacturer: 'ViewSonic' }, { pattern: '^SONY.+', manufacturer: 'Sony' }, { pattern: '^ACER.+', manufacturer: 'Acer' }, { pattern: '^AOC.+', manufacturer: 'AOC Monitors' }, { pattern: '^HP.+', manufacturer: 'HP' }, { pattern: '^EIZO.?', manufacturer: 'Eizo' }, { pattern: '^PHILIPS.?', manufacturer: 'Philips' }, { pattern: '^IIYAMA.?', manufacturer: 'Iiyama' }, { pattern: '^SHARP.?', manufacturer: 'Sharp' }, { pattern: '^NEC.?', manufacturer: 'NEC' }, { pattern: '^LENOVO.?', manufacturer: 'Lenovo' }, { pattern: 'COMPAQ.?', manufacturer: 'Compaq' }, { pattern: 'APPLE.?', manufacturer: 'Apple' }, { pattern: 'INTEL.?', manufacturer: 'Intel' }, { pattern: 'AMD.?', manufacturer: 'AMD' }, { pattern: 'NVIDIA.?', manufacturer: 'NVDIA' }, ]; let result = ''; if (model) { model = model.toUpperCase(); manufacturers.forEach((manufacturer) => { const re = RegExp(manufacturer.pattern); if (re.test(model)) { result = manufacturer.manufacturer; } }); } return result; } function getVendorFromId(id) { const vendors = { '610': 'Apple', '1e6d': 'LG', '10ac': 'DELL', '4dd9': 'Sony', '38a3': 'NEC', }; return vendors[id] || ''; } function vendorToId(str) { let result = ''; str = (str || '').toLowerCase(); if (str.indexOf('apple') >= 0) { result = '0x05ac'; } else if (str.indexOf('nvidia') >= 0) { result = '0x10de'; } else if (str.indexOf('intel') >= 0) { result = '0x8086'; } else if (str.indexOf('ati') >= 0 || str.indexOf('amd') >= 0) { result = '0x1002'; } return result; } function getMetalVersion(id) { const families = { 'spdisplays_mtlgpufamilymac1': 'mac1', 'spdisplays_mtlgpufamilymac2': 'mac2', 'spdisplays_mtlgpufamilyapple1': 'apple1', 'spdisplays_mtlgpufamilyapple2': 'apple2', 'spdisplays_mtlgpufamilyapple3': 'apple3', 'spdisplays_mtlgpufamilyapple4': 'apple4', 'spdisplays_mtlgpufamilyapple5': 'apple5', 'spdisplays_mtlgpufamilyapple6': 'apple6', 'spdisplays_mtlgpufamilyapple7': 'apple7', 'spdisplays_metalfeaturesetfamily11': 'family1_v1', 'spdisplays_metalfeaturesetfamily12': 'family1_v2', 'spdisplays_metalfeaturesetfamily13': 'family1_v3', 'spdisplays_metalfeaturesetfamily14': 'family1_v4', 'spdisplays_metalfeaturesetfamily21': 'family2_v1' }; return families[id] || ''; } function graphics(callback) { function parseLinesDarwin(graphicsArr) { const res = { controllers: [], displays: [] }; try { graphicsArr.forEach(function (item) { // controllers const bus = ((item.sppci_bus || '').indexOf('builtin') > -1 ? 'Built-In' : ((item.sppci_bus || '').indexOf('pcie') > -1 ? 'PCIe' : '')); const vram = (parseInt((item.spdisplays_vram || ''), 10) || 0) * (((item.spdisplays_vram || '').indexOf('GB') > -1) ? 1024 : 1); const vramDyn = (parseInt((item.spdisplays_vram_shared || ''), 10) || 0) * (((item.spdisplays_vram_shared || '').indexOf('GB') > -1) ? 1024 : 1); let metalVersion = getMetalVersion(item.spdisplays_metal || item.spdisplays_metalfamily || ''); res.controllers.push({ vendor: getVendorFromModel(item.spdisplays_vendor || '') || item.spdisplays_vendor || '', model: item.sppci_model || '', bus, vramDynamic: bus === 'Built-In', vram: vram || vramDyn || null, deviceId: item['spdisplays_device-id'] || '', vendorId: item['spdisplays_vendor-id'] || vendorToId((item['spdisplays_vendor'] || '') + (item.sppci_model || '')), external: (item.sppci_device_type === 'spdisplays_egpu'), cores: item['sppci_cores'] || null, metalVersion }); // displays if (item.spdisplays_ndrvs && item.spdisplays_ndrvs.length) { item.spdisplays_ndrvs.forEach(function (displayItem) { const connectionType = displayItem['spdisplays_connection_type'] || ''; const currentResolutionParts = (displayItem['_spdisplays_resolution'] || '').split('@'); const currentResolution = currentResolutionParts[0].split('x'); const pixelParts = (displayItem['_spdisplays_pixels'] || '').split('x'); const pixelDepthString = displayItem['spdisplays_depth'] || ''; const serial = displayItem['_spdisplays_display-serial-number'] || displayItem['_spdisplays_display-serial-number2'] || null; res.displays.push({ vendor: getVendorFromId(displayItem['_spdisplays_display-vendor-id'] || '') || getVendorFromModel(displayItem['_name'] || ''), vendorId: displayItem['_spdisplays_display-vendor-id'] || '', model: displayItem['_name'] || '', productionYear: displayItem['_spdisplays_display-year'] || null, serial: serial !== '0' ? serial : null, displayId: displayItem['_spdisplays_displayID'] || null, main: displayItem['spdisplays_main'] ? displayItem['spdisplays_main'] === 'spdisplays_yes' : false, builtin: (displayItem['spdisplays_display_type'] || '').indexOf('built-in') > -1, connection: ((connectionType.indexOf('_internal') > -1) ? 'Internal' : ((connectionType.indexOf('_displayport') > -1) ? 'Display Port' : ((connectionType.indexOf('_hdmi') > -1) ? 'HDMI' : null))), sizeX: null, sizeY: null, pixelDepth: (pixelDepthString === 'CGSThirtyBitColor' ? 30 : (pixelDepthString === 'CGSThirtytwoBitColor' ? 32 : (pixelDepthString === 'CGSTwentyfourBitColor' ? 24 : null))), resolutionX: pixelParts.length > 1 ? parseInt(pixelParts[0], 10) : null, resolutionY: pixelParts.length > 1 ? parseInt(pixelParts[1], 10) : null, currentResX: currentResolution.length > 1 ? parseInt(currentResolution[0], 10) : null, currentResY: currentResolution.length > 1 ? parseInt(currentResolution[1], 10) : null, positionX: 0, positionY: 0, currentRefreshRate: currentResolutionParts.length > 1 ? parseInt(currentResolutionParts[1], 10) : null, }); }); } }); return res; } catch (e) { return res; } } function parseLinesLinuxControllers(lines) { let controllers = []; let currentController = { vendor: '', model: '', bus: '', busAddress: '', vram: null, vramDynamic: false, pciID: '' }; let isGraphicsController = false; // PCI bus IDs let pciIDs = []; try { pciIDs = execSync('export LC_ALL=C; dmidecode -t 9 2>/dev/null; unset LC_ALL | grep "Bus Address: "').toString().split('\n'); for (let i = 0; i < pciIDs.length; i++) { pciIDs[i] = pciIDs[i].replace('Bus Address:', '').replace('0000:', '').trim(); } pciIDs = pciIDs.filter(function (el) { return el != null && el; }); } catch (e) { util.noop(); } for (let i = 0; i < lines.length; i++) { if ('' !== lines[i].trim()) { if (' ' !== lines[i][0] && '\t' !== lines[i][0]) { // first line of new entry let isExternal = (pciIDs.indexOf(lines[i].split(' ')[0]) >= 0); let vgapos = lines[i].toLowerCase().indexOf(' vga '); let _3dcontrollerpos = lines[i].toLowerCase().indexOf('3d controller'); if (vgapos !== -1 || _3dcontrollerpos !== -1) { // VGA if (_3dcontrollerpos !== -1 && vgapos === -1) { vgapos = _3dcontrollerpos; } if (currentController.vendor || currentController.model || currentController.bus || currentController.vram !== null || currentController.vramDynamic) { // already a controller found controllers.push(currentController); currentController = { vendor: '', model: '', bus: '', busAddress: '', vram: null, vramDynamic: false, }; } const pciIDCandidate = lines[i].split(' ')[0]; if (/[\da-fA-F]{2}:[\da-fA-F]{2}\.[\da-fA-F]/.test(pciIDCandidate)) { currentController.busAddress = pciIDCandidate; } isGraphicsController = true; let endpos = lines[i].search(/\[[0-9a-f]{4}:[0-9a-f]{4}]|$/); let parts = lines[i].substr(vgapos, endpos - vgapos).split(':'); currentController.busAddress = lines[i].substr(0, vgapos).trim(); if (parts.length > 1) { parts[1] = parts[1].trim(); if (parts[1].toLowerCase().indexOf('corporation') >= 0) { currentController.vendor = parts[1].substr(0, parts[1].toLowerCase().indexOf('corporation') + 11).trim(); currentController.model = parts[1].substr(parts[1].toLowerCase().indexOf('corporation') + 11, 200).trim().split('(')[0]; currentController.bus = (pciIDs.length > 0 && isExternal) ? 'PCIe' : 'Onboard'; currentController.vram = null; currentController.vramDynamic = false; } else if (parts[1].toLowerCase().indexOf(' inc.') >= 0) { if ((parts[1].match(new RegExp(']', 'g')) || []).length > 1) { currentController.vendor = parts[1].substr(0, parts[1].toLowerCase().indexOf(']') + 1).trim(); currentController.model = parts[1].substr(parts[1].toLowerCase().indexOf(']') + 1, 200).trim().split('(')[0].trim(); } else { currentController.vendor = parts[1].substr(0, parts[1].toLowerCase().indexOf(' inc.') + 5).trim(); currentController.model = parts[1].substr(parts[1].toLowerCase().indexOf(' inc.') + 5, 200).trim().split('(')[0].trim(); } currentController.bus = (pciIDs.length > 0 && isExternal) ? 'PCIe' : 'Onboard'; currentController.vram = null; currentController.vramDynamic = false; } else if (parts[1].toLowerCase().indexOf(' ltd.') >= 0) { if ((parts[1].match(new RegExp(']', 'g')) || []).length > 1) { currentController.vendor = parts[1].substr(0, parts[1].toLowerCase().indexOf(']') + 1).trim(); currentController.model = parts[1].substr(parts[1].toLowerCase().indexOf(']') + 1, 200).trim().split('(')[0].trim(); } else { currentController.vendor = parts[1].substr(0, parts[1].toLowerCase().indexOf(' ltd.') + 5).trim(); currentController.model = parts[1].substr(parts[1].toLowerCase().indexOf(' ltd.') + 5, 200).trim().split('(')[0].trim(); } } } } else { isGraphicsController = false; } } if (isGraphicsController) { // within VGA details let parts = lines[i].split(':'); if (parts.length > 1 && parts[0].replace(/ +/g, '').toLowerCase().indexOf('devicename') !== -1 && parts[1].toLowerCase().indexOf('onboard') !== -1) { currentController.bus = 'Onboard'; } if (parts.length > 1 && parts[0].replace(/ +/g, '').toLowerCase().indexOf('region') !== -1 && parts[1].toLowerCase().indexOf('memory') !== -1) { let memparts = parts[1].split('='); if (memparts.length > 1) { currentController.vram = parseInt(memparts[1]); } } } } } if (currentController.vendor || currentController.model || currentController.bus || currentController.busAddress || currentController.vram !== null || currentController.vramDynamic) { // already a controller found controllers.push(currentController); } return (controllers); } function parseLinesLinuxClinfo(controllers, lines) { const fieldPattern = /\[([^\]]+)\]\s+(\w+)\s+(.*)/; const devices = lines.reduce((devices, line) => { const field = fieldPattern.exec(line.trim()); if (field) { if (!devices[field[1]]) { devices[field[1]] = {}; } devices[field[1]][field[2]] = field[3]; } return devices; }, {}); for (let deviceId in devices) { const device = devices[deviceId]; if (device['CL_DEVICE_TYPE'] === 'CL_DEVICE_TYPE_GPU') { let busAddress; if (device['CL_DEVICE_TOPOLOGY_AMD']) { const bdf = device['CL_DEVICE_TOPOLOGY_AMD'].match(/[a-zA-Z0-9]+:\d+\.\d+/); if (bdf) { busAddress = bdf[0]; } } else if (device['CL_DEVICE_PCI_BUS_ID_NV'] && device['CL_DEVICE_PCI_SLOT_ID_NV']) { const bus = parseInt(device['CL_DEVICE_PCI_BUS_ID_NV']); const slot = parseInt(device['CL_DEVICE_PCI_SLOT_ID_NV']); if (!isNaN(bus) && !isNaN(slot)) { const b = bus & 0xff; const d = (slot >> 3) & 0xff; const f = slot & 0x07; busAddress = `${b.toString().padStart(2, '0')}:${d.toString().padStart(2, '0')}.${f}`; } } if (busAddress) { let controller = controllers.find(controller => controller.busAddress === busAddress); if (!controller) { controller = { vendor: '', model: '', bus: '', busAddress, vram: null, vramDynamic: false }; controllers.push(controller); } controller.vendor = device['CL_DEVICE_VENDOR']; if (device['CL_DEVICE_BOARD_NAME_AMD']) { controller.model = device['CL_DEVICE_BOARD_NAME_AMD']; } else { controller.model = device['CL_DEVICE_NAME']; } const memory = parseInt(device['CL_DEVICE_GLOBAL_MEM_SIZE']); if (!isNaN(memory)) { controller.vram = Math.round(memory / 1024 / 1024); } } } } return controllers; } function getNvidiaSmi() { if (_nvidiaSmiPath) { return _nvidiaSmiPath; } if (_windows) { try { const basePath = util.WINDIR + '\\System32\\DriverStore\\FileRepository'; // find all directories that have an nvidia-smi.exe file const candidateDirs = fs.readdirSync(basePath).filter(dir => { return fs.readdirSync([basePath, dir].join('/')).includes('nvidia-smi.exe'); }); // use the directory with the most recently created nvidia-smi.exe file const targetDir = candidateDirs.reduce((prevDir, currentDir) => { const previousNvidiaSmi = fs.statSync([basePath, prevDir, 'nvidia-smi.exe'].join('/')); const currentNvidiaSmi = fs.statSync([basePath, currentDir, 'nvidia-smi.exe'].join('/')); return (previousNvidiaSmi.ctimeMs > currentNvidiaSmi.ctimeMs) ? prevDir : currentDir; }); if (targetDir) { _nvidiaSmiPath = [basePath, targetDir, 'nvidia-smi.exe'].join('/'); } } catch (e) { util.noop(); } } else if (_linux) { _nvidiaSmiPath = 'nvidia-smi'; } return _nvidiaSmiPath; } function nvidiaSmi(options) { const nvidiaSmiExe = getNvidiaSmi(); options = options || util.execOptsWin; if (nvidiaSmiExe) { const nvidiaSmiOpts = '--query-gpu=driver_version,pci.sub_device_id,name,pci.bus_id,fan.speed,memory.total,memory.used,memory.free,utilization.gpu,utilization.memory,temperature.gpu,temperature.memory,power.draw,power.limit,clocks.gr,clocks.mem --format=csv,noheader,nounits'; const cmd = nvidiaSmiExe + ' ' + nvidiaSmiOpts + (_linux ? ' 2>/dev/null' : ''); try { const res = execSync(cmd, options).toString(); return res; } catch (e) { util.noop(); } } return ''; } function nvidiaDevices() { function safeParseNumber(value) { if ([null, undefined].includes(value)) { return value; } return parseFloat(value); } const stdout = nvidiaSmi(); if (!stdout) { return []; } const gpus = stdout.split('\n').filter(Boolean); const results = gpus.map(gpu => { const splittedData = gpu.split(', ').map(value => value.includes('N/A') ? undefined : value); if (splittedData.length === 16) { return { driverVersion: splittedData[0], subDeviceId: splittedData[1], name: splittedData[2], pciBus: splittedData[3], fanSpeed: safeParseNumber(splittedData[4]), memoryTotal: safeParseNumber(splittedData[5]), memoryUsed: safeParseNumber(splittedData[6]), memoryFree: safeParseNumber(splittedData[7]), utilizationGpu: safeParseNumber(splittedData[8]), utilizationMemory: safeParseNumber(splittedData[9]), temperatureGpu: safeParseNumber(splittedData[10]), temperatureMemory: safeParseNumber(splittedData[11]), powerDraw: safeParseNumber(splittedData[12]), powerLimit: safeParseNumber(splittedData[13]), clockCore: safeParseNumber(splittedData[14]), clockMemory: safeParseNumber(splittedData[15]), }; } }); return results; } function mergeControllerNvidia(controller, nvidia) { if (nvidia.driverVersion) { controller.driverVersion = nvidia.driverVersion; } if (nvidia.subDeviceId) { controller.subDeviceId = nvidia.subDeviceId; } if (nvidia.name) { controller.name = nvidia.name; } if (nvidia.pciBus) { controller.pciBus = nvidia.pciBus; } if (nvidia.fanSpeed) { controller.fanSpeed = nvidia.fanSpeed; } if (nvidia.memoryTotal) { controller.memoryTotal = nvidia.memoryTotal; controller.vram = nvidia.memoryTotal; controller.vramDynamic = false; } if (nvidia.memoryUsed) { controller.memoryUsed = nvidia.memoryUsed; } if (nvidia.memoryFree) { controller.memoryFree = nvidia.memoryFree; } if (nvidia.utilizationGpu) { controller.utilizationGpu = nvidia.utilizationGpu; } if (nvidia.utilizationMemory) { controller.utilizationMemory = nvidia.utilizationMemory; } if (nvidia.temperatureGpu) { controller.temperatureGpu = nvidia.temperatureGpu; } if (nvidia.temperatureMemory) { controller.temperatureMemory = nvidia.temperatureMemory; } if (nvidia.powerDraw) { controller.powerDraw = nvidia.powerDraw; } if (nvidia.powerLimit) { controller.powerLimit = nvidia.powerLimit; } if (nvidia.clockCore) { controller.clockCore = nvidia.clockCore; } if (nvidia.clockMemory) { controller.clockMemory = nvidia.clockMemory; } return controller; } function parseLinesLinuxEdid(edid) { // parsen EDID // --> model // --> resolutionx // --> resolutiony // --> builtin = false // --> pixeldepth (?) // --> sizex // --> sizey let result = { vendor: '', model: '', deviceName: '', main: false, builtin: false, connection: '', sizeX: null, sizeY: null, pixelDepth: null, resolutionX: null, resolutionY: null, currentResX: null, currentResY: null, positionX: 0, positionY: 0, currentRefreshRate: null }; // find first "Detailed Timing Description" let start = 108; if (edid.substr(start, 6) === '000000') { start += 36; } if (edid.substr(start, 6) === '000000') { start += 36; } if (edid.substr(start, 6) === '000000') { start += 36; } if (edid.substr(start, 6) === '000000') { start += 36; } result.resolutionX = parseInt('0x0' + edid.substr(start + 8, 1) + edid.substr(start + 4, 2)); result.resolutionY = parseInt('0x0' + edid.substr(start + 14, 1) + edid.substr(start + 10, 2)); result.sizeX = parseInt('0x0' + edid.substr(start + 28, 1) + edid.substr(start + 24, 2)); result.sizeY = parseInt('0x0' + edid.substr(start + 29, 1) + edid.substr(start + 26, 2)); // monitor name start = edid.indexOf('000000fc00'); // find first "Monitor Description Data" if (start >= 0) { let model_raw = edid.substr(start + 10, 26); if (model_raw.indexOf('0a') !== -1) { model_raw = model_raw.substr(0, model_raw.indexOf('0a')); } try { if (model_raw.length > 2) { result.model = model_raw.match(/.{1,2}/g).map(function (v) { return String.fromCharCode(parseInt(v, 16)); }).join(''); } } catch (e) { util.noop(); } } else { result.model = ''; } return result; } function parseLinesLinuxDisplays(lines, depth) { let displays = []; let currentDisplay = { vendor: '', model: '', deviceName: '', main: false, builtin: false, connection: '', sizeX: null, sizeY: null, pixelDepth: null, resolutionX: null, resolutionY: null, currentResX: null, currentResY: null, positionX: 0, positionY: 0, currentRefreshRate: null }; let is_edid = false; let is_current = false; let edid_raw = ''; let start = 0; for (let i = 1; i < lines.length; i++) { // start with second line if ('' !== lines[i].trim()) { if (' ' !== lines[i][0] && '\t' !== lines[i][0] && lines[i].toLowerCase().indexOf(' connected ') !== -1) { // first line of new entry if (currentDisplay.model || currentDisplay.main || currentDisplay.builtin || currentDisplay.connection || currentDisplay.sizeX !== null || currentDisplay.pixelDepth !== null || currentDisplay.resolutionX !== null) { // push last display to array displays.push(currentDisplay); currentDisplay = { vendor: '', model: '', main: false, builtin: false, connection: '', sizeX: null, sizeY: null, pixelDepth: null, resolutionX: null, resolutionY: null, currentResX: null, currentResY: null, positionX: 0, positionY: 0, currentRefreshRate: null }; } let parts = lines[i].split(' '); currentDisplay.connection = parts[0]; currentDisplay.main = lines[i].toLowerCase().indexOf(' primary ') >= 0; currentDisplay.builtin = (parts[0].toLowerCase().indexOf('edp') >= 0); } // try to read EDID information if (is_edid) { if (lines[i].search(/\S|$/) > start) { edid_raw += lines[i].toLowerCase().trim(); } else { // parsen EDID let edid_decoded = parseLinesLinuxEdid(edid_raw); currentDisplay.vendor = edid_decoded.vendor; currentDisplay.model = edid_decoded.model; currentDisplay.resolutionX = edid_decoded.resolutionX; currentDisplay.resolutionY = edid_decoded.resolutionY; currentDisplay.sizeX = edid_decoded.sizeX; currentDisplay.sizeY = edid_decoded.sizeY; currentDisplay.pixelDepth = depth; is_edid = false; } } if (lines[i].toLowerCase().indexOf('edid:') >= 0) { is_edid = true; start = lines[i].search(/\S|$/); } if (lines[i].toLowerCase().indexOf('*current') >= 0) { const parts1 = lines[i].split('('); if (parts1 && parts1.length > 1 && parts1[0].indexOf('x') >= 0) { const resParts = parts1[0].trim().split('x'); currentDisplay.currentResX = util.toInt(resParts[0]); currentDisplay.currentResY = util.toInt(resParts[1]); } is_current = true; } if (is_current && lines[i].toLowerCase().indexOf('clock') >= 0 && lines[i].toLowerCase().indexOf('hz') >= 0 && lines[i].toLowerCase().indexOf('v: height') >= 0) { const parts1 = lines[i].split('clock'); if (parts1 && parts1.length > 1 && parts1[1].toLowerCase().indexOf('hz') >= 0) { currentDisplay.currentRefreshRate = util.toInt(parts1[1]); } is_current = false; } } } // pushen displays if (currentDisplay.model || currentDisplay.main || currentDisplay.builtin || currentDisplay.connection || currentDisplay.sizeX !== null || currentDisplay.pixelDepth !== null || currentDisplay.resolutionX !== null) { // still information there displays.push(currentDisplay); } return displays; } // function starts here return new Promise((resolve) => { process.nextTick(() => { let result = { controllers: [], displays: [] }; if (_darwin) { let cmd = 'system_profiler -xml -detailLevel full SPDisplaysDataType'; exec(cmd, function (error, stdout) { if (!error) { try { let output = stdout.toString(); result = parseLinesDarwin(util.plistParser(output)[0]._items); } catch (e) { util.noop(); } } if (callback) { callback(result); } resolve(result); }); } if (_linux) { // Raspberry: https://elinux.org/RPI_vcgencmd_usage if (util.isRaspberry() && util.isRaspbian()) { let cmd = 'fbset -s | grep \'mode "\'; vcgencmd get_mem gpu; tvservice -s; tvservice -n;'; exec(cmd, function (error, stdout) { let lines = stdout.toString().split('\n'); if (lines.length > 3 && lines[0].indexOf('mode "') >= -1 && lines[2].indexOf('0x12000a') > -1) { const parts = lines[0].replace('mode', '').replace(/"/g, '').trim().split('x'); if (parts.length === 2) { result.displays.push({ vendor: '', model: util.getValue(lines, 'device_name', '='), main: true, builtin: false, connection: 'HDMI', sizeX: null, sizeY: null, pixelDepth: null, resolutionX: parseInt(parts[0], 10), resolutionY: parseInt(parts[1], 10), currentResX: null, currentResY: null, positionX: 0, positionY: 0, currentRefreshRate: null }); } } if (lines.length > 1 && stdout.toString().indexOf('gpu=') >= -1) { result.controllers.push({ vendor: 'Broadcom', model: 'VideoCore IV', bus: '', vram: util.getValue(lines, 'gpu', '=').replace('M', ''), vramDynamic: true }); } if (callback) { callback(result); } resolve(result); }); } else { let cmd = 'lspci -vvv 2>/dev/null'; exec(cmd, function (error, stdout) { if (!error) { let lines = stdout.toString().split('\n'); result.controllers = parseLinesLinuxControllers(lines); const nvidiaData = nvidiaDevices(); // needs to be rewritten ... using no spread operators result.controllers = result.controllers.map((controller) => { // match by busAddress return mergeControllerNvidia(controller, nvidiaData.find((contr) => contr.pciBus.toLowerCase().endsWith(controller.busAddress.toLowerCase())) || {}); }); } let cmd = 'clinfo --raw'; exec(cmd, function (error, stdout) { if (!error) { let lines = stdout.toString().split('\n'); result.controllers = parseLinesLinuxClinfo(result.controllers, lines); } let cmd = 'xdpyinfo 2>/dev/null | grep \'depth of root window\' | awk \'{ print $5 }\''; exec(cmd, function (error, stdout) { let depth = 0; if (!error) { let lines = stdout.toString().split('\n'); depth = parseInt(lines[0]) || 0; } let cmd = 'xrandr --verbose 2>/dev/null'; exec(cmd, function (error, stdout) { if (!error) { let lines = stdout.toString().split('\n'); result.displays = parseLinesLinuxDisplays(lines, depth); } if (callback) { callback(result); } resolve(result); }); }); }); }); } } if (_freebsd || _openbsd || _netbsd) { if (callback) { callback(null); } resolve(null); } if (_sunos) { if (callback) { callback(null); } resolve(null); } if (_windows) { // https://blogs.technet.microsoft.com/heyscriptingguy/2013/10/03/use-powershell-to-discover-multi-monitor-information/ // https://devblogs.microsoft.com/scripting/use-powershell-to-discover-multi-monitor-information/ try { const workload = []; workload.push(util.powerShell('Get-WmiObject win32_VideoController | fl *')); workload.push(util.powerShell('gp "HKLM:\\SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\*" -ErrorAction SilentlyContinue | where MatchingDeviceId $null -NE | select MatchingDeviceId,HardwareInformation.qwMemorySize | fl')); workload.push(util.powerShell('Get-WmiObject win32_desktopmonitor | fl *')); workload.push(util.powerShell('Get-CimInstance -Namespace root\\wmi -ClassName WmiMonitorBasicDisplayParams | fl')); workload.push(util.powerShell('Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::AllScreens')); workload.push(util.powerShell('Get-CimInstance -Namespace root\\wmi -ClassName WmiMonitorConnectionParams | fl')); workload.push(util.powerShell('gwmi WmiMonitorID -Namespace root\\wmi | ForEach-Object {(($_.ManufacturerName -notmatch 0 | foreach {[char]$_}) -join "") + "|" + (($_.ProductCodeID -notmatch 0 | foreach {[char]$_}) -join "") + "|" + (($_.UserFriendlyName -notmatch 0 | foreach {[char]$_}) -join "") + "|" + (($_.SerialNumberID -notmatch 0 | foreach {[char]$_}) -join "") + "|" + $_.InstanceName}')); const nvidiaData = nvidiaDevices(); Promise.all( workload ).then(data => { // controller + vram let csections = data[0].replace(/\r/g, '').split(/\n\s*\n/); let vsections = data[1].replace(/\r/g, '').split(/\n\s*\n/); result.controllers = parseLinesWindowsControllers(csections, vsections); result.controllers = result.controllers.map((controller) => { // match by subDeviceId if (controller.vendor.toLowerCase() === 'nvidia') { return mergeControllerNvidia(controller, nvidiaData.find(device => { let windowsSubDeviceId = (controller.subDeviceId || '').toLowerCase(); const nvidiaSubDeviceIdParts = device.subDeviceId.split('x'); let nvidiaSubDeviceId = nvidiaSubDeviceIdParts.length > 1 ? nvidiaSubDeviceIdParts[1].toLowerCase() : nvidiaSubDeviceIdParts[0].toLowerCase(); const lengthDifference = Math.abs(windowsSubDeviceId.length - nvidiaSubDeviceId.length); if (windowsSubDeviceId.length > nvidiaSubDeviceId.length) { for (let i = 0; i < lengthDifference; i++) { nvidiaSubDeviceId = '0' + nvidiaSubDeviceId; } } else if (windowsSubDeviceId.length < nvidiaSubDeviceId.length) { for (let i = 0; i < lengthDifference; i++) { windowsSubDeviceId = '0' + windowsSubDeviceId; } } return windowsSubDeviceId === nvidiaSubDeviceId; }) || {}); } else { return controller; } }); // displays let dsections = data[2].replace(/\r/g, '').split(/\n\s*\n/); // result.displays = parseLinesWindowsDisplays(dsections); if (dsections[0].trim() === '') { dsections.shift(); } if (dsections.length && dsections[dsections.length - 1].trim() === '') { dsections.pop(); } // monitor (powershell) let msections = data[3].replace(/\r/g, '').split('Active '); msections.shift(); // forms.screens (powershell) let ssections = data[4].replace(/\r/g, '').split('BitsPerPixel '); ssections.shift(); // connection params (powershell) - video type let tsections = data[5].replace(/\r/g, '').split(/\n\s*\n/); tsections.shift(); // monitor ID (powershell) - model / vendor const res = data[6].replace(/\r/g, '').split(/\n/); let isections = []; res.forEach(element => { const parts = element.split('|'); if (parts.length === 5) { isections.push({ vendor: parts[0], code: parts[1], model: parts[2], serial: parts[3], instanceId: parts[4] }); } }); result.displays = parseLinesWindowsDisplaysPowershell(ssections, msections, dsections, tsections, isections); if (result.displays.length === 1) { if (_resolutionX) { result.displays[0].resolutionX = _resolutionX; if (!result.displays[0].currentResX) { result.displays[0].currentResX = _resolutionX; } } if (_resolutionY) { result.displays[0].resolutionY = _resolutionY; if (result.displays[0].currentResY === 0) { result.displays[0].currentResY = _resolutionY; } } if (_pixelDepth) { result.displays[0].pixelDepth = _pixelDepth; } if (_refreshRate && !result.displays[0].currentRefreshRate) { result.displays[0].currentRefreshRate = _refreshRate; } } if (callback) { callback(result); } resolve(result); }) .catch(() => { if (callback) { callback(result); } resolve(result); }); } catch (e) { if (callback) { callback(result); } resolve(result); } } }); }); function parseLinesWindowsControllers(sections, vections) { const memorySizes = {}; for (const i in vections) { if ({}.hasOwnProperty.call(vections, i)) { if (vections[i].trim() !== '') { const lines = vections[i].trim().split('\n'); const matchingDeviceId = util.getValue(lines, 'MatchingDeviceId').match(/PCI\\(VEN_[0-9A-F]{4})&(DEV_[0-9A-F]{4})(?:&(SUBSYS_[0-9A-F]{8}))?(?:&(REV_[0-9A-F]{2}))?/i); if (matchingDeviceId) { const quadWordmemorySize = parseInt(util.getValue(lines, 'HardwareInformation.qwMemorySize')); if (!isNaN(quadWordmemorySize)) { let deviceId = matchingDeviceId[1].toUpperCase() + '&' + matchingDeviceId[2].toUpperCase(); if (matchingDeviceId[3]) { deviceId += '&' + matchingDeviceId[3].toUpperCase(); } if (matchingDeviceId[4]) { deviceId += '&' + matchingDeviceId[4].toUpperCase(); } memorySizes[deviceId] = quadWordmemorySize; } } } } } let controllers = []; for (let i in sections) { if ({}.hasOwnProperty.call(sections, i)) { if (sections[i].trim() !== '') { let lines = sections[i].trim().split('\n'); let pnpDeviceId = util.getValue(lines, 'PNPDeviceID', ':').match(/PCI\\(VEN_[0-9A-F]{4})&(DEV_[0-9A-F]{4})(?:&(SUBSYS_[0-9A-F]{8}))?(?:&(REV_[0-9A-F]{2}))?/i); let subDeviceId = null; let memorySize = null; if (pnpDeviceId) { subDeviceId = pnpDeviceId[3] || ''; if (subDeviceId) { subDeviceId = subDeviceId.split('_')[1]; } // Match PCI device identifier (there's an order of increasing generality): // https://docs.microsoft.com/en-us/windows-hardware/drivers/install/identifiers-for-pci-devices // PCI\VEN_v(4)&DEV_d(4)&SUBSYS_s(4)n(4)&REV_r(2) if (memorySize == null && pnpDeviceId[3] && pnpDeviceId[4]) { const deviceId = pnpDeviceId[1].toUpperCase() + '&' + pnpDeviceId[2].toUpperCase() + '&' + pnpDeviceId[3].toUpperCase() + '&' + pnpDeviceId[4].toUpperCase(); if ({}.hasOwnProperty.call(memorySizes, deviceId)) { memorySize = memorySizes[deviceId]; } } // PCI\VEN_v(4)&DEV_d(4)&SUBSYS_s(4)n(4) if (memorySize == null && pnpDeviceId[3]) { const deviceId = pnpDeviceId[1].toUpperCase() + '&' + pnpDeviceId[2].toUpperCase() + '&' + pnpDeviceId[3].toUpperCase(); if ({}.hasOwnProperty.call(memorySizes, deviceId)) { memorySize = memorySizes[deviceId]; } } // PCI\VEN_v(4)&DEV_d(4)&REV_r(2) if (memorySize == null && pnpDeviceId[4]) { const deviceId = pnpDeviceId[1].toUpperCase() + '&' + pnpDeviceId[2].toUpperCase() + '&' + pnpDeviceId[4].toUpperCase(); if ({}.hasOwnProperty.call(memorySizes, deviceId)) { memorySize = memorySizes[deviceId]; } } // PCI\VEN_v(4)&DEV_d(4) if (memorySize == null) { const deviceId = pnpDeviceId[1].toUpperCase() + '&' + pnpDeviceId[2].toUpperCase(); if ({}.hasOwnProperty.call(memorySizes, deviceId)) { memorySize = memorySizes[deviceId]; } } } controllers.push({ vendor: util.getValue(lines, 'AdapterCompatibility', ':'), model: util.getValue(lines, 'name', ':'), bus: util.getValue(lines, 'PNPDeviceID', ':').startsWith('PCI') ? 'PCI' : '', vram: (memorySize == null ? util.toInt(util.getValue(lines, 'AdapterRAM', ':')) : memorySize) / 1024 / 1024, vramDynamic: (util.getValue(lines, 'VideoMemoryType', ':') === '2'), subDeviceId }); _resolutionX = util.toInt(util.getValue(lines, 'CurrentHorizontalResolution', ':')) || _resolutionX; _resolutionY = util.toInt(util.getValue(lines, 'CurrentVerticalResolution', ':')) || _resolutionY; _refreshRate = util.toInt(util.getValue(lines, 'CurrentRefreshRate', ':')) || _refreshRate; _pixelDepth = util.toInt(util.getValue(lines, 'CurrentBitsPerPixel', ':')) || _pixelDepth; } } } return controllers; } function parseLinesWindowsDisplaysPowershell(ssections, msections, dsections, tsections, isections) { let displays = []; let vendor = ''; let model = ''; let deviceID = ''; let resolutionX = 0; let resolutionY = 0; if (dsections && dsections.length) { let linesDisplay = dsections[0].split('\n'); vendor = util.getValue(linesDisplay, 'MonitorManufacturer', ':'); model = util.getValue(linesDisplay, 'Name', ':'); deviceID = util.getValue(linesDisplay, 'PNPDeviceID', ':').replace(/&/g, '&').toLowerCase(); resolutionX = util.toInt(util.getValue(linesDisplay, 'ScreenWidth', ':')); resolutionY = util.toInt(util.getValue(linesDisplay, 'ScreenHeight', ':')); } for (let i = 0; i < ssections.length; i++) { if (ssections[i].trim() !== '') { ssections[i] = 'BitsPerPixel ' + ssections[i]; msections[i] = 'Active ' + msections[i]; // tsections can be empty OR undefined on earlier versions of powershell (<=2.0) // Tag connection type as UNKNOWN by default if this information is missing if (tsections.length === 0 || tsections[i] === undefined) { tsections[i] = 'Unknown'; } let linesScreen = ssections[i].split('\n'); let linesMonitor = msections[i].split('\n'); let linesConnection = tsections[i].split('\n'); const bitsPerPixel = util.getValue(linesScreen, 'BitsPerPixel'); const bounds = util.getValue(linesScreen, 'Bounds').replace('{', '').replace('}', '').replace(/=/g, ':').split(','); const primary = util.getValue(linesScreen, 'Primary'); const sizeX = util.getValue(linesMonitor, 'MaxHorizontalImageSize'); const sizeY = util.getValue(linesMonitor, 'MaxVerticalImageSize'); const instanceName = util.getValue(linesMonitor, 'InstanceName').toLowerCase(); const videoOutputTechnology = util.getValue(linesConnection, 'VideoOutputTechnology'); const deviceName = util.getValue(linesScreen, 'DeviceName'); let displayVendor = ''; let displayModel = ''; isections.forEach(element => { if (element.instanceId.toLowerCase().startsWith(instanceName) && vendor.startsWith('(') && model.startsWith('PnP')) { displayVendor = element.vendor; displayModel = element.model; } }); displays.push({ vendor: instanceName.startsWith(deviceID) && displayVendor === '' ? vendor : displayVendor, model: instanceName.startsWith(deviceID) && displayModel === '' ? model : displayModel, deviceName, main: primary.toLowerCase() === 'true', builtin: videoOutputTechnology === '2147483648', connection: videoOutputTechnology && videoTypes[videoOutputTechnology] ? videoTypes[videoOutputTechnology] : '', resolutionX: util.toInt(util.getValue(bounds, 'Width', ':')), resolutionY: util.toInt(util.getValue(bounds, 'Height', ':')), sizeX: sizeX ? parseInt(sizeX, 10) : null, sizeY: sizeY ? parseInt(sizeY, 10) : null, pixelDepth: bitsPerPixel, currentResX: util.toInt(util.getValue(bounds, 'Width', ':')), currentResY: util.toInt(util.getValue(bounds, 'Height', ':')), positionX: util.toInt(util.getValue(bounds, 'X', ':')), positionY: util.toInt(util.getValue(bounds, 'Y', ':')), }); } } if (ssections.length === 0) { displays.push({ vendor, model, main: true, sizeX: null, sizeY: null, resolutionX, resolutionY, pixelDepth: null, currentResX: resolutionX, currentResY: resolutionY, positionX: 0, positionY: 0 }); } return displays; } } exports.graphics = graphics;