/* Copyright (c) 2018, Chris Monahan Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the GNOME nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import * as SubProcessModule from './helpers/subprocess.js'; import * as FileModule from './helpers/file.js'; import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'; import NM from 'gi://NM'; let GTop, hasGTop = true; try { ({default: GTop} = await import('gi://GTop')); } catch (err) { log(err); hasGTop = false; }; export const Sensors = GObject.registerClass({ GTypeName: 'Sensors', }, class Sensors extends GObject.Object { _init(settings, sensorIcons) { this._settings = settings; this._sensorIcons = sensorIcons; this.resetHistory(); this._last_processor = { 'core': {}, 'speed': [] }; this._settingChangedSignals = []; this._addSettingChangedSignal('show-gpu', this._reconfigureNvidiaSmiProcess.bind(this)); this._addSettingChangedSignal('update-time', this._reconfigureNvidiaSmiProcess.bind(this)); this._addSettingChangedSignal('network-public-ip-interval', () => {this._lastPublicIPCheck = 0;}); //this._addSettingChangedSignal('include-static-gpu-info', this._reconfigureNvidiaSmiProcess.bind(this)); this._gpu_drm_vendors = null; this._gpu_drm_indices = null; this._nvidia_smi_process = null; this._nvidia_labels = []; this._bad_split_count = 0; this._frameMonitorSignalId = 0; this._frameMonitorLastTime = 0; this._frameMonitorFrameCount = 0; this._frameMonitorAccTime = 0; this._frameMonitorCurrentHz = 0; if (hasGTop) { this.storage = new GTop.glibtop_fsusage(); this._storageDevice = ''; this._findStorageDevice(); this._lastRead = 0; this._lastWrite = 0; } } _addSettingChangedSignal(key, callback) { this._settingChangedSignals.push(this._settings.connect('changed::' + key, callback)); } _refreshIPAddress(callback) { // check IP address new FileModule.File('https://ipv4.corecoding.com').read().then(contents => { let obj = JSON.parse(contents); let cc = (obj && typeof obj['countryCode'] === 'string') ? obj['countryCode'].trim().toLowerCase() : ''; let ip = (obj && typeof obj['IPv4'] === 'string') ? obj['IPv4'].trim() : ''; const showFlag = this._settings.get_boolean('network-public-ip-show-flag'); let typeOut = (showFlag && /^[a-z]{2}$/.test(cc)) ? ('network-' + cc) : 'network'; this._returnValue(callback, 'Public IP', ip, typeOut, 'string'); }).catch(err => { }); } _findStorageDevice() { new FileModule.File('/proc/mounts').read("\n").then(lines => { for (let line of lines) { let loadArray = line.trim().split(/\s+/); if (loadArray[1] == this._settings.get_string('storage-path')) { this._storageDevice = loadArray[0]; break; } } }).catch(err => { }); } query(callback, dwell) { if (!this._hardware_detected) { // we could set _hardware_detected in discoverHardwareMonitors, but by // doing it here, we guarantee avoidance of race conditions this._hardware_detected = true; this._discoverHardwareMonitors(callback); } for (let sensor in this._sensorIcons) { if (this._settings.get_boolean('show-' + sensor)) { if (sensor == 'temperature' || sensor == 'voltage' || sensor == 'fan') { // for temp, volt, fan, we have a shared handler this._queryTempVoltFan(callback, sensor); } else { // directly call queryFunction below let method = '_query' + sensor[0].toUpperCase() + sensor.slice(1); this[method](callback, dwell); } } } } _queryTempVoltFan(callback, type) { for (let label in this._tempVoltFanSensors[type]) { let sensor = this._tempVoltFanSensors[type][label]; new FileModule.File(sensor['path']).read().then(value => { this._returnValue(callback, label, value, type, sensor['format']); }).catch(err => { this._returnValue(callback, label, 'disabled', type, sensor['format']); }); } } _queryMemory(callback) { // check memory info new FileModule.File('/proc/meminfo').read().then(lines => { let values = '', total = 0, avail = 0, swapTotal = 0, swapFree = 0, cached = 0, memFree = 0; if (values = lines.match(/MemTotal:(\s+)(\d+) kB/)) total = values[2]; if (values = lines.match(/MemAvailable:(\s+)(\d+) kB/)) avail = values[2]; if (values = lines.match(/SwapTotal:(\s+)(\d+) kB/)) swapTotal = values[2]; if (values = lines.match(/SwapFree:(\s+)(\d+) kB/)) swapFree = values[2]; if (values = lines.match(/Cached:(\s+)(\d+) kB/)) cached = values[2]; if (values = lines.match(/MemFree:(\s+)(\d+) kB/)) memFree = values[2]; let used = total - avail let utilized = used / total; let swapUsed = swapTotal - swapFree let swapUtilized = swapUsed / swapTotal; this._returnValue(callback, 'Usage', utilized, 'memory', 'percent'); this._returnValue(callback, 'memory', utilized, 'memory-group', 'percent'); this._returnValue(callback, 'Physical', total, 'memory', 'memory'); this._returnValue(callback, 'Available', avail, 'memory', 'memory'); this._returnValue(callback, 'Allocated', used, 'memory', 'memory'); this._returnValue(callback, 'Cached', cached, 'memory', 'memory'); this._returnValue(callback, 'Free', memFree, 'memory', 'memory'); this._returnValue(callback, 'Swap Total', swapTotal, 'memory', 'memory'); this._returnValue(callback, 'Swap Free', swapFree, 'memory', 'memory'); this._returnValue(callback, 'Swap Used', swapUsed, 'memory', 'memory'); this._returnValue(callback, 'Swap Usage', swapUtilized, 'memory', 'percent'); }).catch(err => { }); } _queryProcessor(callback, dwell) { let columns = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal', 'guest', 'guest_nice']; // check processor usage new FileModule.File('/proc/stat').read("\n").then(lines => { let statistics = {}; for (let line of lines) { let reverse_data = line.match(/^(cpu\d*\s)(.+)/); if (reverse_data) { let cpu = reverse_data[1].trim(); if (!(cpu in statistics)) statistics[cpu] = {}; if (!(cpu in this._last_processor['core'])) this._last_processor['core'][cpu] = 0; let stats = reverse_data[2].trim().split(' ').reverse(); for (let column of columns) statistics[cpu][column] = parseInt(stats.pop()); } } let cores = Object.keys(statistics).length - 1; for (let cpu in statistics) { let total = statistics[cpu]['user'] + statistics[cpu]['nice'] + statistics[cpu]['system']; // make sure we have data to report if (this._last_processor['core'][cpu] > 0) { let delta = (total - this._last_processor['core'][cpu]) / dwell; // /proc/stat provides overall usage for us under the 'cpu' heading if (cpu == 'cpu') { delta = delta / cores; this._returnValue(callback, 'processor', delta / 100, 'processor-group', 'percent'); this._returnValue(callback, 'Usage', delta / 100, 'processor', 'percent'); } else { this._returnValue(callback, _('Core %d').format(cpu.substr(3)), delta / 100, 'processor', 'percent'); } } this._last_processor['core'][cpu] = total; } // if frequency scaling is enabled, gather cpu-freq values if (!this._processor_uses_cpu_info) { for (let core = 0; core <= cores; core++) { new FileModule.File('/sys/devices/system/cpu/cpu' + core + '/cpufreq/scaling_cur_freq').read().then(value => { this._last_processor['speed'][core] = parseInt(value); }).catch(err => { }); } } }).catch(err => { }); // if frequency scaling is disabled, use cpuinfo for speed if (this._processor_uses_cpu_info) { // grab CPU frequency new FileModule.File('/proc/cpuinfo').read("\n").then(lines => { let freqs = []; for (let line of lines) { // grab megahertz let value = line.match(/^cpu MHz(\s+): ([+-]?\d+(\.\d+)?)/); if (value) freqs.push(parseFloat(value[2])); } let sum = freqs.reduce((a, b) => a + b); let hertz = (sum / freqs.length) * 1000 * 1000; this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz'); let max_hertz = freqs.reduce((a, b) => Math.max(a, b)) * 1000 * 1000; this._returnValue(callback, 'Max frequency', max_hertz, 'processor', 'hertz'); let min_hertz = freqs.reduce((a, b) => Math.min(a, b)) * 1000 * 1000; this._returnValue(callback, 'Min frequency', min_hertz, 'processor', 'hertz'); }).catch(err => { }); // if frequency scaling is enabled, cpu-freq reports } else if (Object.values(this._last_processor['speed']).length > 0) { let sum = this._last_processor['speed'].reduce((a, b) => a + b); let hertz = (sum / this._last_processor['speed'].length) * 1000; this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz'); let max_hertz = this._last_processor['speed'].reduce((a, b) => Math.max(a, b)) * 1000; this._returnValue(callback, 'Max frequency', max_hertz, 'processor', 'hertz'); let min_hertz = this._last_processor['speed'].reduce((a, b) => Math.min(a, b)) * 1000; this._returnValue(callback, 'Min frequency', min_hertz, 'processor', 'hertz'); } } _querySystem(callback) { // check load average new FileModule.File('/proc/sys/fs/file-nr').read("\t").then(loadArray => { this._returnValue(callback, 'Open Files', loadArray[0], 'system', 'string'); }).catch(err => { }); // check load average new FileModule.File('/proc/loadavg').read(' ').then(loadArray => { let proc = loadArray[3].split('/'); this._returnValue(callback, 'Load 1m', loadArray[0], 'system', 'load'); this._returnValue(callback, 'system', loadArray[0], 'system-group', 'load'); this._returnValue(callback, 'Load 5m', loadArray[1], 'system', 'load'); this._returnValue(callback, 'Load 15m', loadArray[2], 'system', 'load'); this._returnValue(callback, 'Threads Active', proc[0], 'system', 'string'); this._returnValue(callback, 'Threads Total', proc[1], 'system', 'string'); }).catch(err => { }); // check uptime new FileModule.File('/proc/uptime').read(' ').then(upArray => { this._returnValue(callback, 'Uptime', upArray[0], 'system', 'uptime'); let cores = Object.keys(this._last_processor['core']).length - 1; if (cores > 0) this._returnValue(callback, 'Process Time', upArray[0] - upArray[1] / cores, 'processor', 'uptime'); }).catch(err => { }); } _queryNetwork(callback, dwell) { // check network speed let directions = ['tx', 'rx']; let netbase = '/sys/class/net/'; new FileModule.File(netbase).list().then(interfaces => { for (let iface of interfaces) { for (let direction of directions) { // lo tx and rx are the same if (iface == 'lo' && direction == 'rx') continue; new FileModule.File(netbase + iface + '/statistics/' + direction + '_bytes').read().then(value => { // issue #217 - don't include 'lo' traffic in Maximum calculations in values.js // by not using network-rx or network-tx let name = iface + ((iface == 'lo')?'':' ' + direction); let type = 'network' + ((iface=='lo')?'':'-' + direction); this._returnValue(callback, name, value, type, 'storage'); }).catch(err => { }); } } }).catch(err => { }); // some may not want public ip checking if (this._settings.get_boolean('include-public-ip')) { // check the public ip every hour or when waking from sleep if (this._next_public_ip_check <= 0) { let intervalMinutes = this._settings.get_int('network-public-ip-interval'); this._next_public_ip_check = intervalMinutes * 60; this._refreshIPAddress(callback); } this._next_public_ip_check -= dwell; } // wireless interface statistics new FileModule.File('/proc/net/wireless').read("\n", true).then(lines => { // wireless has two headers - first is stripped in helper function lines.shift(); // if multiple wireless device, we use the last one for (let line of lines) { let netArray = line.trim().split(/\s+/); let quality_pct = netArray[2].substr(0, netArray[2].length-1) / 70; let signal = netArray[3].substr(0, netArray[3].length-1); this._returnValue(callback, 'WiFi Link Quality', quality_pct, 'network', 'percent'); this._returnValue(callback, 'WiFi Signal Level', signal, 'network', 'string'); } }).catch(err => { }); } _queryStorage(callback, dwell) { // display zfs arc status, if available new FileModule.File('/proc/spl/kstat/zfs/arcstats').read().then(lines => { let values = '', target = 0, maximum = 0, current = 0; if (values = lines.match(/c(\s+)(\d+)(\s+)(\d+)/)) target = values[4]; if (values = lines.match(/c_max(\s+)(\d+)(\s+)(\d+)/)) maximum = values[4]; if (values = lines.match(/size(\s+)(\d+)(\s+)(\d+)/)) current = values[4]; // ZFS statistics this._returnValue(callback, 'ARC Target', target, 'storage', 'storage'); this._returnValue(callback, 'ARC Maximum', maximum, 'storage', 'storage'); this._returnValue(callback, 'ARC Current', current, 'storage', 'storage'); }).catch(err => { }); // check disk performance stats new FileModule.File('/proc/diskstats').read("\n").then(lines => { for (let line of lines) { let loadArray = line.trim().split(/\s+/); if ('/dev/' + loadArray[2] == this._storageDevice) { var read = (loadArray[5] * 512); var write = (loadArray[9] * 512); this._returnValue(callback, 'Read total', read, 'storage', 'storage'); this._returnValue(callback, 'Write total', write, 'storage', 'storage'); this._returnValue(callback, 'Read rate', (read - this._lastRead) / dwell, 'storage', 'storage'); this._returnValue(callback, 'Write rate', (write - this._lastWrite) / dwell, 'storage', 'storage'); this._lastRead = read; this._lastWrite = write; break; } } }).catch(err => { }); // skip rest of stats if gtop not available if (!hasGTop) return; GTop.glibtop_get_fsusage(this.storage, this._settings.get_string('storage-path')); let total = this.storage.blocks * this.storage.block_size; let avail = this.storage.bavail * this.storage.block_size; let free = this.storage.bfree * this.storage.block_size; let used = total - free; let reserved = (total - avail) - used; let freePercent = 0; let usedPercent = 0; if (total > 0) { freePercent = Math.round((free / total) * 100); usedPercent = Math.round((used / total) * 100); } this._returnValue(callback, 'Total', total, 'storage', 'storage'); this._returnValue(callback, 'Used', used, 'storage', 'storage'); this._returnValue(callback, 'Reserved', reserved, 'storage', 'storage'); this._returnValue(callback, 'Free', avail, 'storage', 'storage'); this._returnValue(callback, 'Used %', usedPercent + '%', 'storage', 'string'); this._returnValue(callback, 'Free %', freePercent + '%', 'storage', 'string'); this._returnValue(callback, 'storage', avail, 'storage-group', 'storage'); } _queryBattery(callback) { let battery_slot = this._settings.get_int('battery-slot'); // create a mapping of indices to battery paths (from prefs.ui) const BATTERY_PATHS = { 0: 'BAT0', 1: 'BAT1', 2: 'BAT2', 3: 'BATT', 4: 'CMB0', 5: 'CMB1', 6: 'CMB2', 7: 'macsmc-battery' }; // uevent has all necessary fields, no need to read individual files let battery_path = '/sys/class/power_supply/' + BATTERY_PATHS[battery_slot] + '/uevent'; new FileModule.File(battery_path).read("\n").then(lines => { let output = {}; for (let line of lines) { let split = line.split('='); output[split[0].replace('POWER_SUPPLY_', '')] = split[1]; } if ('STATUS' in output) { this._returnValue(callback, 'State', output['STATUS'], 'battery', ''); } if ('CYCLE_COUNT' in output) { this._returnValue(callback, 'Cycles', output['CYCLE_COUNT'], 'battery', ''); } if ('VOLTAGE_NOW' in output) { this._returnValue(callback, 'Voltage', output['VOLTAGE_NOW'] / 1000, 'battery', 'in'); } if ('CAPACITY_LEVEL' in output) { this._returnValue(callback, 'Level', output['CAPACITY_LEVEL'], 'battery', ''); } if ('CAPACITY' in output) { this._returnValue(callback, 'Percentage', output['CAPACITY'] / 100, 'battery', 'percent'); } if ('VOLTAGE_NOW' in output && 'CURRENT_NOW' in output && (!('POWER_NOW' in output))) { output['POWER_NOW'] = (output['VOLTAGE_NOW'] * output['CURRENT_NOW']) / 1000000; } if ('POWER_NOW' in output) { const powerValue = ( parseFloat(output['POWER_NOW']) * (output['STATUS'] === 'Discharging' ? -1 : 1) ); this._returnValue(callback, 'Power Rate', powerValue, 'battery', 'watt'); this._returnValue(callback, 'battery', powerValue, 'battery-group', 'watt'); } if ('CHARGE_FULL' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL' in output))) { output['ENERGY_FULL'] = (output['CHARGE_FULL'] * output['VOLTAGE_MIN_DESIGN']) / 1000000; } if ('ENERGY_FULL' in output) { this._returnValue(callback, 'Energy (full)', output['ENERGY_FULL'], 'battery', 'watt-hour'); } if ('CHARGE_FULL_DESIGN' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL_DESIGN' in output))) { output['ENERGY_FULL_DESIGN'] = (output['CHARGE_FULL_DESIGN'] * output['VOLTAGE_MIN_DESIGN']) / 1000000; } if ('ENERGY_FULL_DESIGN' in output) { this._returnValue(callback, 'Energy (design)', output['ENERGY_FULL_DESIGN'], 'battery', 'watt-hour'); if ('ENERGY_FULL' in output) { this._returnValue(callback, 'Capacity', (output['ENERGY_FULL'] / output['ENERGY_FULL_DESIGN']), 'battery', 'percent'); } } if ('VOLTAGE_MIN_DESIGN' in output && 'CHARGE_NOW' in output && (!('ENERGY_NOW' in output))) { output['ENERGY_NOW'] = (output['VOLTAGE_MIN_DESIGN'] * output['CHARGE_NOW']) / 1000000; } if ('ENERGY_NOW' in output) { this._returnValue(callback, 'Energy (now)', output['ENERGY_NOW'], 'battery', 'watt-hour'); } if ('ENERGY_FULL' in output && 'ENERGY_NOW' in output && 'POWER_NOW' in output && output['POWER_NOW'] !== 0 && 'STATUS' in output && (output['STATUS'] == 'Charging' || output['STATUS'] == 'Discharging')) { let timeLeft = 0; // two different formulas depending on if we are charging or discharging if (output['STATUS'] == 'Charging') { timeLeft = ((output['ENERGY_FULL'] - output['ENERGY_NOW']) / output['POWER_NOW']); } else { timeLeft = (output['ENERGY_NOW'] / Math.abs(output['POWER_NOW'])); } // don't process Infinity values if (timeLeft !== Infinity) { if (this._battery_charge_status != output['STATUS']) { // clears history due to state change this._battery_time_left_history = []; // clear time left history when laptop goes in and out of charging this._battery_charge_status = output['STATUS']; } // add latest time left estimate to our history this._battery_time_left_history.push(parseInt(timeLeft * 3600)); // keep track of last 15 time left estimates by erasing the first if (this._battery_time_left_history.length > 10) this._battery_time_left_history.shift(); // sum up and create average of our time left history let sum = this._battery_time_left_history.reduce((a, b) => a + b); let avg = sum / this._battery_time_left_history.length; // use time left history to update screen this._returnValue(callback, 'Time left', parseInt(avg), 'battery', 'runtime'); } } else { this._returnValue(callback, 'Time left', output['STATUS'], 'battery', ''); } }).catch(err => { }); } _initFrameMonitor() { if (this._frameMonitorSignalId) return; this._frameMonitorLastTime = 0; this._frameMonitorFrameCount = 0; this._frameMonitorAccTime = 0; this._frameMonitorCurrentHz = 0; this._frameMonitorSignalId = global.stage.connect('after-paint', () => { this._onAfterPaint(); }); } _destroyFrameMonitor() { if (this._frameMonitorSignalId) { global.stage.disconnect(this._frameMonitorSignalId); this._frameMonitorSignalId = 0; } this._frameMonitorLastTime = 0; this._frameMonitorCurrentHz = 0; } _onAfterPaint() { const now = GLib.get_monotonic_time(); if (this._frameMonitorLastTime === 0) { this._frameMonitorLastTime = now; return; } const delta = now - this._frameMonitorLastTime; this._frameMonitorLastTime = now; this._frameMonitorFrameCount++; this._frameMonitorAccTime += delta; if (this._frameMonitorAccTime >= 500000) { this._frameMonitorCurrentHz = this._frameMonitorFrameCount / (this._frameMonitorAccTime / 1000000); this._frameMonitorFrameCount = 0; this._frameMonitorAccTime = 0; } } _queryGpu(callback) { if (this._frameMonitorCurrentHz > 0) this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1', 'hertz'); if (!this._nvidia_smi_process) { // no nvidia-smi, so we use sysfs DRM if any cards was discovered if (!this._gpu_drm_indices){ if (this._frameMonitorCurrentHz > 0) this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1-group', 'hertz'); this._disableGpuLabels(callback); return; } else { this._readGpuDrm(callback); return; } } this._nvidia_smi_process.read('\n').then(lines => { /// for debugging multi-gpu on systems with only one gpu /// duplicates the first gpu's data 3 times, for 4 total gpus ///if(lines.length == 0) return; ///for(let _gpuNum = 1; _gpuNum <= 3; _gpuNum++) /// lines.push(lines[0]); for (let i = 0; i < lines.length; i++) { this._parseNvidiaSmiLine(callback, lines[i], i + 1, lines.length > 1); } // if we've already updated the static info during the last parse, then stop doing so. // this is so the _parseNvidiaSmiLine function won't return static info anymore // and the nvidia-smi commmand won't be queried for static info either if(!this._nvidia_static_returned) { this._nvidia_static_returned = true; //reconfigure the process to stop querying static info this._reconfigureNvidiaSmiProcess(); } }).catch(err => { this._disableGpuLabels(callback); this._terminateNvidiaSmiProcess(); }); } _parseNvidiaSmiLine(callback, csv, gpuNum, multiGpu) { const expectedSplitLength = 19; let csv_split = csv.split(','); // occasionally the nvidia-smi command can get cut off before it can be fully read, thus the parse function only gets part of a line // hence we count the number of bad splits and only terminate the process after a few bad splits in a row // this prevents anomalous readings from terminating the process if (csv_split.length < expectedSplitLength) { this._bad_split_count++; //if we've had 2 bad splits/reads in a row, try to restart the process if (this._bad_split_count == 2) this._reconfigureNvidiaSmiProcess(); //if we still get a bad read after that, then it's not an anomaly; terminate the process else if (this._bad_split_count >= 3) this._terminateNvidiaSmiProcess(); return; } this._bad_split_count = 0; let [ label, fan_speed_pct, temp_gpu, temp_mem, mem_total, mem_used, mem_reserved, mem_free, util_gpu, util_mem, util_encoder, util_decoder, clock_gpu, clock_mem, clock_encode_decode, power, power_avg, link_gen_current, link_width_current ] = csv_split; const staticNames = [ 'temp_limit', 'power_limit', 'link_gen_max', 'link_width_max', 'addressing_mode', 'driver_version', 'vbios', 'serial', 'domain_num', 'bus_num', 'device_num', 'device_id', 'sub_device_id' ]; let staticInfo = {}; // if we have queried static info this time around, populate our static info object if(csv_split.length == (expectedSplitLength + staticNames.length)){ for(let i = 0; i < staticNames.length; i++) { //set the static info to a default (0) if it's undefined const value = csv_split[expectedSplitLength + i]; staticInfo[staticNames[i]] = (typeof value !== "undefined") ? value : 0; } } const typeName = 'gpu#' + gpuNum; const globalLabel = 'GPU' + (multiGpu ? ' ' + gpuNum : ''); const memTempValid = !isNaN(parseInt(temp_mem)); this._returnGpuValue(callback, 'Graphics', parseInt(util_gpu) * 0.01, typeName + '-group', 'percent'); this._returnGpuValue(callback, 'Name', label, typeName, ''); this._returnGpuValue(callback, globalLabel, parseInt(fan_speed_pct) * 0.01, 'fan', 'percent'); this._returnGpuValue(callback, 'Fan', parseInt(fan_speed_pct) * 0.01, typeName, 'percent'); this._returnGpuValue(callback, globalLabel, parseInt(temp_gpu) * 1000, 'temperature', 'temp'); this._returnGpuValue(callback, 'Temperature', parseInt(temp_gpu) * 1000, typeName, 'temp'); this._returnGpuValue(callback, 'Memory Temperature', parseInt(temp_mem) * 1000, typeName, 'temp', memTempValid); this._returnStaticGpuValue(callback, 'Temperature Limit', parseInt(staticInfo['temp_limit']) * 1000, typeName, 'temp'); this._returnGpuValue(callback, 'Memory Usage', parseInt(mem_used) / parseInt(mem_total), typeName, 'percent'); this._returnGpuValue(callback, 'Memory Total', parseInt(mem_total) * 1000, typeName, 'memory'); this._returnGpuValue(callback, 'Memory Used', parseInt(mem_used) * 1000, typeName, 'memory'); this._returnGpuValue(callback, 'Memory Reserved', parseInt(mem_reserved) * 1000, typeName, 'memory'); this._returnGpuValue(callback, 'Memory Free', parseInt(mem_free) * 1000, typeName, 'memory'); this._returnGpuValue(callback, 'Memory Utilization', parseInt(util_mem) * 0.01, typeName, 'percent'); this._returnGpuValue(callback, 'Utilization', parseInt(util_gpu) * 0.01, typeName, 'percent'); this._returnGpuValue(callback, 'Encoder Utilization', parseInt(util_encoder) * 0.01, typeName, 'percent'); this._returnGpuValue(callback, 'Decoder Utilization', parseInt(util_decoder) * 0.01, typeName, 'percent'); this._returnGpuValue(callback, 'Frequency', parseInt(clock_gpu) * 1000 * 1000, typeName, 'hertz'); this._returnGpuValue(callback, 'Memory Frequency', parseInt(clock_mem) * 1000 * 1000, typeName, 'hertz'); this._returnGpuValue(callback, 'Encoder/Decoder Frequency', parseInt(clock_encode_decode) * 1000 * 1000, typeName, 'hertz'); //this._returnGpuValue(callback, 'Encoder Sessions', parseInt(encoder_sessions), typeName, 'string'); this._returnGpuValue(callback, 'Power', power, typeName, 'watt-gpu'); this._returnGpuValue(callback, 'Average Power', power_avg, typeName, 'watt-gpu'); this._returnStaticGpuValue(callback, 'Power Limit', parseInt(staticInfo['power_limit']), typeName, 'watt-gpu'); this._returnGpuValue(callback, 'Link Speed', link_gen_current + 'x' + link_width_current, typeName, 'pcie'); this._returnStaticGpuValue(callback, 'Maximum Link Speed', staticInfo['link_gen_max'] + 'x' + staticInfo['link_width_max'], typeName, 'pcie'); this._returnStaticGpuValue(callback, 'Addressing Mode', staticInfo['addressing_mode'], typeName, 'string'); this._returnStaticGpuValue(callback, 'Driver Version', staticInfo['driver_version'], typeName, 'string'); this._returnStaticGpuValue(callback, 'vBIOS Version', staticInfo['vbios'], typeName, 'string'); this._returnStaticGpuValue(callback, 'Serial Number', staticInfo['serial'], typeName, 'string'); this._returnStaticGpuValue(callback, 'Domain Number', staticInfo['domain_num'], typeName, 'string'); this._returnStaticGpuValue(callback, 'Bus Number', staticInfo['bus_num'], typeName, 'string'); this._returnStaticGpuValue(callback, 'Device Number', staticInfo['device_num'], typeName, 'string'); this._returnStaticGpuValue(callback, 'Device ID', staticInfo['device_id'], typeName, 'string'); this._returnStaticGpuValue(callback, 'Sub Device ID', staticInfo['sub_device_id'], typeName, 'string'); } _readGpuDrm(callback){ const multiGpu = this._gpu_drm_indices.length > 1; const unit = this._settings.get_int('memory-measurement') ? 1000 : 1024; for (let z = 0; z < this._gpu_drm_indices.length; z++ ) { let i = this._gpu_drm_indices[z]; const typeName = 'gpu#' + i; const vendor = this._gpu_drm_vendors[z]; // AMD if(vendor === "0x1002") { // read GPU usage and create group lebel for card new FileModule.File('/sys/class/drm/card'+i+'/device/gpu_busy_percent').read().then(value => { // create group this._returnGpuValue(callback, 'Graphics', parseInt(value) * 0.01, typeName + '-group', 'percent'); this._returnGpuValue(callback, 'Vendor', "AMD", typeName, 'string'); this._returnGpuValue(callback, 'Usage', parseInt(value) * 0.01, typeName, 'percent'); }).catch(err => { // nothing to do, keep old value displayed }); new FileModule.File('/sys/class/drm/card'+i+'/device/mem_info_vram_used').read().then(value => { this._returnGpuValue(callback, 'Memory Used', parseInt(value) / unit, typeName, 'memory'); }).catch(err => { // nothing to do, keep old value displayed }); new FileModule.File('/sys/class/drm/card'+i+'/device/mem_info_vram_total').read().then(value => { this._returnGpuValue(callback, 'Memory Total', parseInt(value) / unit, typeName, 'memory'); }).catch(err => { // nothing to do, keep old value displayed }); } else { // for other vendors only show basic card info let vendorName = null; switch (vendor){ case '0x10DE': vendorName = 'NVIDIA'; break; // should be never used as nvidia-smi should be preferred case '0x13B5': vendorName = 'ARM'; break; case '0x5143': vendorName = 'Qualcomm'; break; case '0x8086': vendorName = 'Intel'; break; default: vendorName = "Unknown " + vendor; } this._returnGpuValue(callback, 'Graphics', vendorName, typeName + '-group', 'string'); } } } _disableGpuLabels(callback) { for (let labelObj of this._nvidia_labels) this._returnValue(callback, labelObj.label, 'disabled', labelObj.type, labelObj.format); } _returnStaticGpuValue(callback, label, value, type, format) { //if we've already tried to return existing static info before or if the option isn't enabled, then do nothing. if (this._nvidia_static_returned || !this._settings.get_boolean('include-static-gpu-info')) return; //we don't need to disable static info labels, so just use ordinary returnValue function this._returnValue(callback, label, value, type, format); } _returnGpuValue(callback, label, value, type, format, display = true) { if(!display) return; if(format !== "string" && (value === 'N/A' || value === '[N/A]' || isNaN(value))) return; let nvidiaLabel = {'label': label, 'type': type, 'format': format}; if (!this._nvidia_labels.includes(nvidiaLabel)) this._nvidia_labels.push(nvidiaLabel); this._returnValue(callback, label, value, type, format); } _returnValue(callback, label, value, type, format) { // don't return if value is not a number - will revisit later //if (isNaN(value)) return; callback(label, value, type, format); } _discoverHardwareMonitors(callback) { this._tempVoltFanSensors = { 'temperature': {}, 'voltage': {}, 'fan': {} }; let hwbase = '/sys/class/hwmon/'; // process sensor_types now so it is not called multiple times below let sensor_types = {}; if (this._settings.get_boolean('show-temperature')) sensor_types['temp'] = 'temperature'; if (this._settings.get_boolean('show-voltage')) sensor_types['in'] = 'voltage'; if (this._settings.get_boolean('show-fan')) sensor_types['fan'] = 'fan'; // a little informal, but this code has zero I/O block new FileModule.File(hwbase).list().then(files => { for (let file of files) { // grab name of sensor new FileModule.File(hwbase + file + '/name').read().then(name => { // are we dealing with a CPU? if (name == 'coretemp') { // determine which processor (socket) we are dealing with new FileModule.File(hwbase + file + '/temp1_label').read().then(prefix => { this._processTempVoltFan(callback, sensor_types, prefix, hwbase + file, file); }).catch(err => { // this shouldn't be necessary, but just in case temp1_label doesn't exist // attempt to fix #266 this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file); }); } else { // not a CPU, process all other sensors this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file); } }).catch(err => { new FileModule.File(hwbase + file + '/device/name').read().then(name => { this._processTempVoltFan(callback, sensor_types, name, hwbase + file + '/device', file); }).catch(err => { }); }); } }).catch(err => { }); // does this system support cpu scaling? if so we will use it to grab Frequency and Boost below new FileModule.File('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq').read().then(value => { this._processor_uses_cpu_info = false; }).catch(err => { }); // is static CPU information enabled? if (this._settings.get_boolean('include-static-info')) { // grab static CPU information new FileModule.File('/proc/cpuinfo').read("\n").then(lines => { let vendor_id = ''; let bogomips = ''; let sockets = {}; let cache = ''; for (let line of lines) { let value = ''; // grab cpu vendor if (value = line.match(/^vendor_id(\s+): (\w+.*)/)) vendor_id = value[2]; // grab bogomips if (value = line.match(/^bogomips(\s+): (\d*\.?\d*)$/)) bogomips = value[2]; // grab processor count if (value = line.match(/^physical id(\s+): (\d+)$/)) sockets[value[2]] = 1; // grab cache if (value = line.match(/^cache size(\s+): (\d+) KB$/)) cache = value[2]; } this._returnValue(callback, 'Vendor', vendor_id, 'processor', 'string'); this._returnValue(callback, 'Bogomips', bogomips, 'processor', 'string'); this._returnValue(callback, 'Sockets', Object.keys(sockets).length, 'processor', 'string'); this._returnValue(callback, 'Cache', cache, 'processor', 'memory'); }).catch(err => { }); // grab static CPU information new FileModule.File('/proc/version').read(' ').then(kernelArray => { this._returnValue(callback, 'Kernel', kernelArray[2], 'system', 'string'); }).catch(err => { }); } // Launch nvidia-smi subprocess if nvidia querying is enabled this._reconfigureNvidiaSmiProcess(); this._discoverGpuDrm(); this._initFrameMonitor(); } _discoverGpuDrm() { // use DRM only if nvidia-smi is not used if (this._settings.get_boolean('show-gpu') && this._nvidia_smi_process == null) { // try to discover up to 10 cards starting from index 0 for(let i = 0; i < 10 ; i++){ new FileModule.File('/sys/class/drm/card'+i+'/device/vendor').read().then(value => { if(!this._gpu_drm_indices){ this._gpu_drm_indices = []; this._gpu_drm_vendors = []; } this._gpu_drm_indices.push(i); this._gpu_drm_vendors.push(value); }).catch(err => { }); } } else { this._gpu_drm_vendors = null; this._gpu_drm_indices = null; } } // The nvidia-smi subprocess will keep running and print new sensor data to stdout every // `update_time` seconds. _queryNvidiaSmi() will be called at roughly the same interval and // read from the subprocess's stdout to get new sensor data. // Regarding "keeping main process & sub process in sync", there are two possible scenarios: // - For some reason, nvidia-smi prints at a somewhat higher frequency than we call // _queryNvidiaSmi() to read data. This is okay, eventually one call to _queryNvidiaSmi() // will read two sensor data updates in a single call. // - For some reason, _queryNvidiaSmi() is called at a somewhat higher frequency than // nvidia-smi prints data. This is the more likely scenario with user actions triggering // additional reads. This eventually triggers an "IO PENDING" error while attempting to // read, because the previous async read is still waiting. To solve this, the subprocess // module simply ignores PENDING errors. After ignoring the error, the earlier read will // eventually return and sensor data will be updated, so this scenario is handled correctly. // Generally speaking, the call to _queryNvidiaSmi() and nvidia-smi's printing to stdout do // not happen at the same time. So the async call in _queryNvidiaSmi() will usually have to // wait up to `update_time` seconds before getting any results and reporting them through the // callback. _reconfigureNvidiaSmiProcess() { if (this._settings.get_boolean('show-gpu')) { this._terminateNvidiaSmiProcess(); try { let update_time = this._settings.get_int('update-time'); let query_interval = Math.max(update_time, 1); let command = [ 'nvidia-smi', '--query-gpu=name,' + 'fan.speed,' + 'temperature.gpu,temperature.memory,' + 'memory.total,memory.used,memory.reserved,memory.free,' + 'utilization.gpu,utilization.memory,utilization.encoder,utilization.decoder,' + 'clocks.gr,clocks.mem,clocks.video,' + 'power.draw.instant,power.draw.average,' + 'pcie.link.gen.gpucurrent,pcie.link.width.current,' + (!this._nvidia_static_returned && this._settings.get_boolean('include-static-gpu-info') ? 'temperature.gpu.tlimit,' + 'power.limit,' + 'pcie.link.gen.max,pcie.link.width.max,' + 'addressing_mode,'+ 'driver_version,vbios_version,serial,' + 'pci.domain,pci.bus,pci.device,pci.device_id,pci.sub_device_id,' : ''), '--format=csv,noheader,nounits', '-l', query_interval.toString() ]; this._nvidia_smi_process = new SubProcessModule.SubProcess(command); } catch(e) { // proprietary nvidia driver not installed this._terminateNvidiaSmiProcess(); } } else { this._terminateNvidiaSmiProcess(); } } _terminateNvidiaSmiProcess() { if (this._nvidia_smi_process) { this._nvidia_smi_process.terminate(); this._nvidia_smi_process = null; } } _processTempVoltFan(callback, sensor_types, name, path, file) { let sensor_files = [ 'input', 'label' ]; // grab files from directory new FileModule.File(path).list().then(files2 => { let trisensors = {}; // loop over files from directory for (let file2 of Object.values(files2)) { // simple way of processing input and label (from above) for (let key of Object.values(sensor_files)) { // process toggled on sensors from extension preferences for (let sensor_type in sensor_types) { if (file2.substr(0, sensor_type.length) == sensor_type && file2.substr(-(key.length+1)) == '_' + key) { let key2 = file + file2.substr(0, file2.indexOf('_')); if (!(key2 in trisensors)) { trisensors[key2] = { 'type': sensor_types[sensor_type], 'format': sensor_type, 'label': path + '/name' }; } trisensors[key2][key] = path + '/' + file2; } } } } for (let obj of Object.values(trisensors)) { if (!('input' in obj)) continue; new FileModule.File(obj['input']).read().then(value => { let extra = (obj['label'].indexOf('_label')==-1) ? ' ' + obj['input'].substr(obj['input'].lastIndexOf('/')+1).split('_')[0] : ''; if (value > 0 || !this._settings.get_boolean('hide-zeros') || obj['type'] == 'fan') { new FileModule.File(obj['label']).read().then(label => { this._addTempVoltFan(callback, obj, name, label, extra, value); }).catch(err => { let tmpFile = obj['label'].substr(0, obj['label'].lastIndexOf('/')) + '/name'; new FileModule.File(tmpFile).read().then(label => { this._addTempVoltFan(callback, obj, name, label, extra, value); }).catch(err => { }); }); } }).catch(err => { }); } }).catch(err => { }); } _addTempVoltFan(callback, obj, name, label, extra, value) { // prepend module that provided sensor data if (name != label) label = name + ' ' + label; //if (label == 'nvme Composite') label = 'NVMe'; //if (label == 'nouveau') label = 'Nvidia'; label = label + extra; // in the future we will read /etc/sensors3.conf if (label == 'acpitz temp1') label = 'ACPI Thermal Zone'; if (label == 'pch_cannonlake temp1') label = 'Platform Controller Hub'; if (label == 'iwlwifi_1 temp1') label = 'Wireless Adapter'; if (label == 'Package id 0') label = 'Processor 0'; if (label == 'Package id 1') label = 'Processor 1'; label = label.replace('Package id', 'CPU'); let types = [ 'temperature', 'voltage', 'fan' ]; for (let type of types) { // check if this label already exists if (label in this._tempVoltFanSensors[type]) { for (let i = 2; i <= 9; i++) { // append an incremented number to end let new_label = label + ' ' + i; // if new label is available, use it if (!(new_label in this._tempVoltFanSensors[type])) { label = new_label; break; } } } } // update screen on initial build to prevent delay on update this._returnValue(callback, label, value, obj['type'], obj['format']); this._tempVoltFanSensors[obj['type']][label] = { 'format': obj['format'], 'path': obj['input'] }; } resetHistory() { this._next_public_ip_check = 0; this._hardware_detected = false; this._nvidia_static_returned = false; this._processor_uses_cpu_info = true; this._battery_time_left_history = []; this._battery_charge_status = ''; this._nvidia_labels = []; this._bad_split_count = 0; this._frameMonitorLastTime = 0; this._frameMonitorFrameCount = 0; this._frameMonitorAccTime = 0; } destroy() { this._destroyFrameMonitor(); this._terminateNvidiaSmiProcess(); for (let signal of Object.values(this._settingChangedSignals)) this._settings.disconnect(signal); } });