diff options
Diffstat (limited to 'raveos-gnome-theme/theme-data/extensions/installed/Vitals@CoreCoding.com/sensors.js')
| -rw-r--r-- | raveos-gnome-theme/theme-data/extensions/installed/Vitals@CoreCoding.com/sensors.js | 1072 |
1 files changed, 1072 insertions, 0 deletions
diff --git a/raveos-gnome-theme/theme-data/extensions/installed/Vitals@CoreCoding.com/sensors.js b/raveos-gnome-theme/theme-data/extensions/installed/Vitals@CoreCoding.com/sensors.js new file mode 100644 index 0000000..be8df5d --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/Vitals@CoreCoding.com/sensors.js @@ -0,0 +1,1072 @@ +/* + Copyright (c) 2018, Chris Monahan <chris@corecoding.com> + + 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('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); + this._returnValue(callback, 'Public IP', obj['IPv4'], 'network', '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) { + this._next_public_ip_check = 3600; + + 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); + } +}); |