diff --git a/extension.js b/extension.js index c528cad6..8d396d90 100644 --- a/extension.js +++ b/extension.js @@ -39,6 +39,7 @@ var VitalsMenuButton = GObject.registerClass({ 'network' : { 'icon': 'network-symbolic.svg', 'icon-rx': 'network-download-symbolic.svg', 'icon-tx': 'network-upload-symbolic.svg', + 'icon-ping': 'network-ping-symbolic.svg', 'icon-ad': '../flags/1x1/ad.svg', 'icon-ae': '../flags/1x1/ae.svg', 'icon-af': '../flags/1x1/af.svg', @@ -337,7 +338,8 @@ var VitalsMenuButton = GObject.registerClass({ 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-public-ip-interval', 'network-public-ip-show-flag', 'network-speed-format', 'storage-measurement', - 'include-static-info', 'include-static-gpu-info' ]; + 'include-static-info', 'include-static-gpu-info', + 'network-ping-host' ]; for (let setting of Object.values(settings)) this._addSettingChangedSignal(setting, this._redrawMenu.bind(this)); diff --git a/icons/gnome/network-ping-symbolic.svg b/icons/gnome/network-ping-symbolic.svg new file mode 100644 index 00000000..b798bef5 --- /dev/null +++ b/icons/gnome/network-ping-symbolic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/original/network-ping-symbolic.svg b/icons/original/network-ping-symbolic.svg new file mode 100644 index 00000000..04834170 --- /dev/null +++ b/icons/original/network-ping-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/prefs.js b/prefs.js index bdeb9a80..eee7a2e0 100644 --- a/prefs.js +++ b/prefs.js @@ -78,6 +78,24 @@ const Settings = new GObject.Class({ this._settings.bind('network-public-ip-interval', this.builder.get_object('network-public-ip-interval'), 'value', Gio.SettingsBindFlags.DEFAULT); + let pingHostEntry = this.builder.get_object('network-ping-host'); + let pingHostSave = this.builder.get_object('network-ping-host-save'); + let getSavedPingHost = () => this._settings.get_string('network-ping-host'); + let savePingHost = () => { + if (pingHostEntry.get_text() === getSavedPingHost()) + return; + + this._settings.set_string('network-ping-host', pingHostEntry.get_text()); + pingHostSave.set_sensitive(false); + }; + + pingHostEntry.set_text(getSavedPingHost()); + pingHostEntry.connect('changed', (widget) => { + pingHostSave.set_sensitive(widget.get_text() !== getSavedPingHost()); + }); + pingHostEntry.connect('activate', () => savePingHost()); + pingHostSave.connect('clicked', () => savePingHost()); + // process individual text entry sensor preferences sensors = [ 'storage-path', 'monitor-cmd' ]; for (let key in sensors) { diff --git a/prefs.ui b/prefs.ui index b3c8c4b2..01a10278 100644 --- a/prefs.ui +++ b/prefs.ui @@ -147,6 +147,47 @@ + + + 100 + 1 + 0 + + + 1 + 6 + 6 + 6 + 6 + 12 + + + 1 + 0 + start + Ping host + + + + + e.g. 1.1.1.1 + 1 + 1 + 18 + + + + + 1 + 0 + Save ping host + object-select-symbolic + + + + + + diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index b84716f2..30155af9 100644 Binary files a/schemas/gschemas.compiled and b/schemas/gschemas.compiled differ diff --git a/schemas/org.gnome.shell.extensions.vitals.gschema.xml b/schemas/org.gnome.shell.extensions.vitals.gschema.xml index fef102db..34cb02b7 100644 --- a/schemas/org.gnome.shell.extensions.vitals.gschema.xml +++ b/schemas/org.gnome.shell.extensions.vitals.gschema.xml @@ -94,6 +94,11 @@ Network speed format Should speed display in bits or bytes? + + "" + Ping target host + Hostname or IP address to ping for latency measurement + "/" Storage path diff --git a/sensors.js b/sensors.js index d61b0909..54a2bc19 100644 --- a/sensors.js +++ b/sensors.js @@ -27,6 +27,7 @@ import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import * as SubProcessModule from './helpers/subprocess.js'; +import Gio from 'gi://Gio'; import * as FileModule from './helpers/file.js'; import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'; import NM from 'gi://NM'; @@ -46,6 +47,8 @@ export const Sensors = GObject.registerClass({ this._settings = settings; this._sensorIcons = sensorIcons; + this._pingCancellable = null; + this.resetHistory(); this._last_processor = { 'core': {}, 'speed': [] }; @@ -54,6 +57,7 @@ export const Sensors = GObject.registerClass({ 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('network-ping-host', () => this._pingCancellable?.cancel()); //this._addSettingChangedSignal('include-static-gpu-info', this._reconfigureNvidiaSmiProcess.bind(this)); this._gpu_drm_vendors = null; @@ -338,6 +342,63 @@ export const Sensors = GObject.registerClass({ this._returnValue(callback, 'WiFi Signal Level', signal, 'network', 'string'); } }).catch(err => { }); + + this._pollPing(value => { + this._returnValue(callback, 'Ping', value, 'network-ping', 'string'); + }); + } + + _pollPing(callback) { + let host = this._settings.get_string('network-ping-host').trim(); + if (!host) return; + + this._pingCancellable?.cancel(); + this._pingCancellable = new Gio.Cancellable(); + let cancellable = this._pingCancellable; + + let subprocess; + try { + subprocess = new Gio.Subprocess({ + argv: ['ping', '-c', '1', '-W', '5', host], + flags: Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_PIPE, + }); + subprocess.init(null); + } catch (e) { + callback('?? ms'); + return; + } + + cancellable.connect(() => subprocess.force_exit()); + + subprocess.communicate_utf8_async(null, cancellable, (proc, result) => { + let value = this._parsePingResult(proc, result, cancellable); + callback(value); + }); + } + + _parsePingResult(proc, result, cancellable) { + let stdout, exitStatus; + try { + [, stdout] = proc.communicate_utf8_finish(result); + exitStatus = proc.get_exit_status(); + } catch (e) { + let cancelled = cancellable.is_cancelled(); + return cancelled ? this._pingTimeoutLabel() : '?? ms'; + } + + if (exitStatus === 0 && stdout) { + let match = /time[=<]([\d.]+)\s*ms/.exec(stdout); + if (match) + return `${parseFloat(match[1]).toFixed(1)} ms`; + } + + return exitStatus === 1 ? this._pingTimeoutLabel() : '?? ms'; + } + + _pingTimeoutLabel() { + let timeoutMs = Math.min(Math.max(this._settings.get_int('update-time'), 1), 5) * 1000; + return `${timeoutMs}+ ms`; } _queryStorage(callback, dwell) { @@ -1066,11 +1127,13 @@ export const Sensors = GObject.registerClass({ this._frameMonitorLastTime = 0; this._frameMonitorFrameCount = 0; this._frameMonitorAccTime = 0; + this._pingCancellable?.cancel(); } destroy() { this._destroyFrameMonitor(); this._terminateNvidiaSmiProcess(); + this._pingCancellable?.cancel(); for (let signal of Object.values(this._settingChangedSignals)) this._settings.disconnect(signal);