import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; import St from 'gi://St'; import Shell from 'gi://Shell'; import GTop from 'gi://GTop'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js'; import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; let debugOutput = false; let pingDepsGtop = true; function print_info(str) { log('[Ping monitor INFO] ' + str); } function print_debug(str) { if (debugOutput) { log('[Ping monitor DEBUG] ' + str); } } function color_from_string(color) { let clutterColor = new Clutter.Color(); clutterColor = Clutter.Color.from_string(color)[1]; return clutterColor; } const PingStyleManager = class PingMonitor_PingStyleManager { constructor() { print_debug('pingStyleManager constructor()'); this._extension = ''; this._iconsize = 1; this._text_scaling = 1; let interfaceSettings = new Gio.Settings({ schema: 'org.gnome.desktop.interface' }); this._text_scaling = interfaceSettings.get_double('text-scaling-factor'); if (!this._text_scaling) { this._text_scaling = 1; } } get(style) { return style + this._extension; } text_scaling() { return this._text_scaling; } }; const PingDialog = GObject.registerClass( class PingDialog extends ModalDialog.ModalDialog { _init() { super._init({styleClass: 'prompt-dialog'}); print_debug('PingDialog construct()'); let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout', vertical: false }); this.contentLayout.add_child(mainContentBox); let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout', vertical: true }); mainContentBox.add_child(messageBox); this._subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline', text: _('Ping Monitor Extension') }); messageBox.add_child(this._subjectLabel); const MESSAGE = _('Dependencies Missing\n\ Please install: \n\ libgtop and gir bindings\n\ \t on Ubuntu: gir1.2-gtop-2.0\n\ \t on Fedora: libgtop2-devel\n\ \t on Arch: libgtop\n\ \t on openSUSE: typelib-1_0-GTop-2_0\n'); this._descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description', text: MESSAGE }); messageBox.add_child(this._descriptionLabel); this.setButtons([{ label: _('Cancel'), action: () => { this.close(); }, key: Clutter.KEY_Escape }]); } }); const StatusSquare = class PingMonitor_StatusSquare { constructor(height, parent) { print_debug('StatusSquare constructor()'); this._width = 12; this._height = 12; this._color = '#ff0000'; this._activityState = 0; this._isPingUpdate = false; this.actor = new St.Widget({ style_class: 'ping-chart', reactive: false, width: this._width, height: this._height, y_align: Clutter.ActorAlign.CENTER, y_expand: false }); this.parentC = parent; this.data = []; this._updateStyle(); } update(color, isPingUpdate) { print_debug('StatusSquare update()'); this._color = color; this._isPingUpdate = isPingUpdate; if (!this.actor.visible) { return; } if (this._isPingUpdate) { this._activityState = (this._activityState + 1) % 4; } this._updateStyle(); } _updateStyle() { print_debug('StatusSquare _updateStyle()'); if (!this.actor.visible) { return; } this.actor.set_style(` background-color: ${this._color}; border-radius: 2px; width: ${this._width}px; height: ${this._height}px; min-width: ${this._width}px; min-height: ${this._height}px; `); } resize(schema, key) { print_debug('StatusSquare resize()'); } }; const PingTipItem = GObject.registerClass( class PingTipItem extends PopupMenu.PopupBaseMenuItem { _init() { super._init(); this.remove_style_class_name('popup-menu-item'); this.add_style_class_name('ping-tooltip-item'); } }); const TipMenu = class PingMonitor_TipMenu extends PopupMenu.PopupMenuBase { constructor(sourceActor) { print_debug('TipMenu constructor()'); super(sourceActor, 'ping-tooltip-box'); this.actor = new Clutter.Actor(); this.actor.add_child(this.box); } _shift() { print_debug('TipMenu _shift()'); let node = this.sourceActor.get_theme_node(); let contentbox = node.get_content_box(this.sourceActor.get_allocation_box()); let allocation = Shell.util_get_transformed_allocation(this.sourceActor); let monitor = Main.layoutManager.findMonitorForActor(this.sourceActor); let [x, y] = [allocation.x1 + contentbox.x1, allocation.y1 + contentbox.y1]; let [cx, cy] = [allocation.x1 + (contentbox.x1 + contentbox.x2) / 2, allocation.y1 + (contentbox.y1 + contentbox.y2) / 2]; let [xm, ym] = [allocation.x1 + contentbox.x2, allocation.y1 + contentbox.y2]; let [width, height] = this.actor.get_size(); let tipx = cx - width / 2; tipx = Math.max(tipx, monitor.x); tipx = Math.min(tipx, monitor.x + monitor.width - width); let tipy = Math.floor(ym); if (allocation.y1 / monitor.height > 0.3) { tipy = allocation.y1 - height; } this.actor.set_position(tipx, tipy); } open(animate) { print_debug('TipMenu open()'); if (this.isOpen) { return; } this.isOpen = true; this.actor.show(); this._shift(); this.actor.get_parent()?.set_child_above_sibling(this.actor, null); this.emit('open-state-changed', true); } close(animate) { print_debug('TipMenu close()'); this.isOpen = false; this.actor.hide(); this.emit('open-state-changed', false); } }; const TipBox = class PingMonitor_TipBox { constructor() { print_debug('TipBox constructor()'); this.show_tooltip = true; this.actor = new St.BoxLayout({ reactive: true, y_align: Clutter.ActorAlign.CENTER }); this.actor._delegate = this; this.set_tip(new TipMenu(this.actor)); this.in_to = this.out_to = 0; this.actor.connect('enter-event', this.on_enter.bind(this)); this.actor.connect('leave-event', this.on_leave.bind(this)); } set_tip(tipmenu) { print_debug('TipBox set_tip()'); if (this.tipmenu) { this.tipmenu.destroy(); } this.tipmenu = tipmenu; if (this.tipmenu) { Main.uiGroup.add_child(this.tipmenu.actor); this.hide_tip(); } } show_tip() { print_debug('TipBox show_tip()'); if (!this.tipmenu) { return; } this.tipmenu.open(); if (this.in_to) { GLib.Source.remove(this.in_to); this.in_to = 0; } } hide_tip() { print_debug('TipBox hide_tip()'); if (!this.tipmenu) { return; } this.tipmenu.close(); if (this.out_to) { GLib.Source.remove(this.out_to); this.out_to = 0; } if (this.in_to) { GLib.Source.remove(this.in_to); this.in_to = 0; } } on_enter() { print_debug('TipBox on_enter()'); let show_tooltip = this.show_tooltip; if (!show_tooltip) { return; } if (this.out_to) { GLib.Source.remove(this.out_to); this.out_to = 0; } if (!this.in_to) { this.in_to = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { this.show_tip(); this.in_to = 0; return GLib.SOURCE_REMOVE; }); } } on_leave() { print_debug('TipBox on_leave()'); if (this.in_to) { GLib.Source.remove(this.in_to); this.in_to = 0; } if (!this.out_to) { this.out_to = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { this.hide_tip(); this.out_to = 0; return GLib.SOURCE_REMOVE; }); } } destroy() { print_debug('TipBox destroy()'); if (this.in_to) { GLib.Source.remove(this.in_to); this.in_to = 0; } if (this.out_to) { GLib.Source.remove(this.out_to); this.out_to = 0; } this.actor.destroy(); } }; const ElementBase = class PingMonitor_ElementBase extends TipBox { constructor(properties, settings, extensionPath) { super(); print_debug('ElementBase constructor()'); this._settings = settings; this._extensionPath = extensionPath; this.elt = ''; this.tag = ''; this.name = ''; this.show_name = false; this.color_name = []; this.text_items = []; this.menu_items = []; this.menu_visible = true; this.color = '#ff0000'; this.refresh_interval = 5000; this.visible = true; this.timeout = undefined; this._pingStdout = null; this._pingDataStdout = null; this._pingStderr = null; this._pingDataStderr = null; this._prepareToDestroy = false; Object.assign(this, properties); this.vals = []; this.tip_labels = []; this.tip_vals = []; this.tip_unit_labels = []; this.chart = new StatusSquare(Math.round(20 * 4 / 5), this); this._settings.connect('changed::background', () => { this.chart._updateStyle(); }); this.actor.visible = this.visible; this.interval = this.refresh_interval; this.add_timeout(); this.label = new St.Label({ text: this.name, style_class: 'ping-status-label', y_align: Clutter.ActorAlign.CENTER, x_expand: false, y_expand: false, natural_height_set: false }); this.label.visible = this.show_name; this.menu_visible = true; this.actor.add_child(this.label); this.text_box = new St.BoxLayout(); this.text_items = this.create_text_items(); this.actor.add_child(this.chart.actor); this.text_box.visible = true; this.chart.actor.visible = this.visible; this.menu_items = this.create_menu_items(); } add_timeout() { this.remove_timeout(); print_debug('Add timeout: ' + this.tag); if (!this._prepareToDestroy) { this.timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this.interval, () => { return this.update(); }); } } remove_timeout() { print_debug('Remove (try) timeout: ' + this.tag); if (this.timeout !== undefined) { print_debug('Remove timeout: ' + this.tag); GLib.Source.remove(this.timeout); this.timeout = undefined; } } tip_format() { print_debug('ElementBase tip_format()'); for (let i = 0; i < this.color_name.length; i++) { let tipline = new PingTipItem(); this.tipmenu.addMenuItem(tipline); this.tip_labels[i] = new St.Label({text: ''}); tipline.add_child(this.tip_labels[i]); this.tip_vals[i] = 0; } } update() { print_debug('ElementBase update()'); this.remove_timeout(); if (!this.menu_visible && !this.actor.visible) { return false; } this.refresh(); return GLib.SOURCE_CONTINUE; } updateDrawing() { this._apply(); this.chart.update(this.color, true); for (let i = 0; i < this.tip_vals.length; i++) { if (this.tip_labels[i]) { this.tip_labels[i].text = this.tip_vals[i].toString(); } } } destroy() { print_debug('ElementBase destroy()'); this.remove_timeout(); TipBox.prototype.destroy.call(this); } stop() { this._prepareToDestroy = true; this.remove_timeout(); } isRunning() { return ( this._pingStdout != null || this._pingDataStdout != null || this._pingStderr != null || this._pingDataStderr != null ); } }; const Ping = class PingMonitor_Ping extends ElementBase { constructor(id, tag, name, address, ping_count, ping_interval, ping_deadline, refresh_interval, active, visible, show_name, show_address, show_tooltip, warning_threshold, settings, extensionPath) { if (show_address) { name = name + '\n' + address; } super({ elt: 'ping', tag: tag, name: name, show_name: show_name, visible: visible, refresh_interval: refresh_interval, color_name: ['used'], }, settings, extensionPath); print_debug('Ping constructor()'); this.ping_message = ''; this.id = id; this.address = address; this.ping_count = ping_count; this.ping_interval = ping_interval; this.ping_deadline = ping_deadline; this.active = active; this.show_address = show_address; this.show_tooltip = show_tooltip; this.warning_threshold = warning_threshold; this.tip_format(); this.update(); } _pingReadStdout() { this._pingDataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, (stream, result) => { if (stream.fill_finish(result) == 0) { try { let buffer = stream.peek_buffer(); if (buffer instanceof Uint8Array) { this._pingOutput = new TextDecoder().decode(buffer); } else { this._pingOutput = buffer.toString(); } if (this._pingOutput) { print_debug('Ping info: ' + this._pingOutput); let firstLine = this._pingOutput.match(/[\w .:()]+\n/m); print_debug('First line: ' + firstLine[0]); let lastLines = this._pingOutput.match(/---[\w\W]+/m); lastLines[0] = lastLines[0].replace(/^\s+|\s+$/g, ''); print_debug('Last lines: ' + lastLines[0]); this.ping_message = firstLine[0] + lastLines[0]; print_debug('Ping info: ' + this.ping_message); let loss = this._pingOutput.match(/received, (\d*)/m); let times = this._pingOutput.match(/mdev = (\d*.\d*)\/(\d*.\d*)\/(\d*.\d*)\/(\d*.\d*)/m); if (times != null && times.length == 5 && loss != null && loss.length == 2) { print_debug('loss: ' + loss[1]); print_debug('min: ' + times[1]); print_debug('avg: ' + times[2]); print_debug('max: ' + times[3]); print_debug('mdev: ' + times[4]); if (loss[1] != 0 && loss[1] != 100) { if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-loss-color'); } } else if (loss[1] == 100) { if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-bad-color'); } } else if (times[3] > this.warning_threshold) { if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-warning-color'); } } else { if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-good-color'); } } } else { if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-bad-color'); } } if (!this._prepareToDestroy) { this.updateDrawing(); } } } catch (e) { print_info(e.toString()); if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-bad-color'); this.updateDrawing(); } } this._pingStdout.close(null); this._pingStdout = null; this._pingDataStdout.close(null); this._pingDataStdout = null; this.add_timeout(); return; } stream.set_buffer_size(2 * stream.get_buffer_size()); this._pingReadStdout(); }); } _pingReadStderr() { this._pingDataStderr.fill_async(-1, GLib.PRIORITY_DEFAULT, null, (stream, result) => { if (stream.fill_finish(result) == 0) { try { let buffer = stream.peek_buffer(); if (buffer instanceof Uint8Array) { this._pingOutputErr = new TextDecoder().decode(buffer); } else { this._pingOutputErr = buffer.toString(); } if (this._pingOutputErr) { this.ping_message = this._pingOutputErr; print_debug('Ping error: ' + this.ping_message); if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-bad-color'); this.updateDrawing(); } } } catch (e) { print_info(e.toString()); if (!this._prepareToDestroy) { this.color = this._settings.get_string('ping-bad-color'); this.updateDrawing(); } } this._pingStderr.close(null); this._pingStderr = null; this._pingDataStderr.close(null); this._pingDataStderr = null; return; } stream.set_buffer_size(2 * stream.get_buffer_size()); this._pingReadStderr(); }); } refresh() { print_debug('Ping refresh()'); try { let script = [ '/bin/bash', this._extensionPath + '/ping.sh', this.address, '' + this.ping_count, '' + this.ping_deadline, '' + this.ping_interval ]; let success; [success, this.child_pid, this.in_fd, this.out_fd, this.err_fd] = GLib.spawn_async_with_pipes( null, script, null, GLib.SpawnFlags.DO_NOT_REAP_CHILD, null ); this._pingStdout = new Gio.UnixInputStream({fd: this.out_fd, close_fd: true}); this._pingDataStdout = new Gio.DataInputStream({base_stream: this._pingStdout}); this._pingStderr = new Gio.UnixInputStream({fd: this.err_fd, close_fd: true}); this._pingDataStderr = new Gio.DataInputStream({base_stream: this._pingStderr}); new Gio.UnixOutputStream({fd: this.in_fd, close_fd: true}).close(null); this._tagWatchChild = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, this.child_pid, (pid, status, data) => { GLib.Source.remove(this._tagWatchChild); GLib.spawn_close_pid(pid); this.child_pid = undefined; } ); this._pingReadStdout(); this._pingReadStderr(); } catch (e) { print_info(e.toString()); } } _apply() { print_debug('Ping _apply()'); this.menu_items[0].text = this.ping_message; this.tip_vals[0] = this.ping_message; } create_text_items() { print_debug('Ping create_text_items()'); return [ new St.Label({ text: '', style_class: 'ping-status-value', y_align: Clutter.ActorAlign.CENTER }), new St.Label({ text: '%', style_class: 'ping-perc-label', y_align: Clutter.ActorAlign.CENTER }) ]; } create_menu_items() { print_debug('Ping create_menu_items()'); return [ new St.Label({ text: '', style_class: 'ping-value-left' }) ]; } }; const Icon = class PingMonitor_Icon { constructor(settings) { print_debug('Icon constructor()'); this._settings = settings; this.actor = new St.Icon({ icon_name: 'system-run-symbolic', style_class: 'system-status-icon' }); this.actor.visible = this._settings.get_boolean('icon-display'); this._settings.connect('changed::icon-display', () => { print_debug('changed icon-display'); this.actor.visible = this._settings.get_boolean('icon-display'); }); } }; export default class PingMonitorExtension extends Extension { enable() { print_info('applet enabling'); this._settings = this.getSettings(); this._style = new PingStyleManager(); this._pingBox = null; this._elts = []; this._tray = null; if (!pingDepsGtop) { this._pingDialog = new PingDialog(); this._dialogTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { this._pingDialog.open(); this._dialogTimeout = null; return GLib.SOURCE_REMOVE; }); } else { this._tray = new PanelMenu.Button(0.5, 'PingMonitor', false); this._icon = new Icon(this._settings); this._elts = []; let isFileOk = false; let path = this._settings.get_string('ping-config-path'); if (path == '') { path = GLib.getenv('HOME') + '/.config/ping-monitor.conf'; this._settings.set_string('ping-config-path', path); } isFileOk = this._read_from_file(path); this._settings.set_boolean('icon-display', !isFileOk); let spacing = '4'; this._pingBox = new St.BoxLayout({style: 'spacing: ' + spacing + 'px;'}); this._tray.add_child(this._pingBox); this._pingBox.add_child(this._icon.actor); for (let elt of this._elts) { this._pingBox.add_child(elt.actor); } let menu_info = new PopupMenu.PopupBaseMenuItem({reactive: false}); let menu_info_box = new St.BoxLayout(); menu_info.add_child(menu_info_box); this._tray.menu.addMenuItem(menu_info, 0); this._build_menu_info(); this._tray.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this._tray.menu.connect('open-state-changed', (menu, isOpen) => { if (isOpen) { this._ping_menu_timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, () => { return GLib.SOURCE_CONTINUE; }); } else { if (this._ping_menu_timeout) { GLib.Source.remove(this._ping_menu_timeout); this._ping_menu_timeout = null; } } }); let item = new PopupMenu.PopupMenuItem(_('Reload config')); item.connect('activate', () => { this._reload_async(); }); this._tray.menu.addMenuItem(item); item = new PopupMenu.PopupMenuItem(_('Preferences...')); item.connect('activate', () => { this.openPreferences(); }); this._tray.menu.addMenuItem(item); Main.panel.addToStatusArea('ping-monitor', this._tray); } print_info('applet enabling done'); } disable() { print_info('disable applet'); if (this._dialogTimeout) { GLib.Source.remove(this._dialogTimeout); this._dialogTimeout = null; } if (this._pingDialog) { this._pingDialog.destroy(); this._pingDialog = null; } if (this._ping_menu_timeout) { GLib.Source.remove(this._ping_menu_timeout); this._ping_menu_timeout = null; } for (let elt of this._elts) { elt.stop(); } if (this._pingBox) { for (let elt of this._elts) { this._pingBox.remove_child(elt.actor); } } for (let elt of this._elts) { elt.destroy(); } if (this._tray) { this._tray.destroy(); this._tray = null; } this._elts = []; this._pingBox = null; this._icon = null; this._settings = null; this._style = null; print_info('applet disabled'); } _read_from_file(path) { print_info('read_from_file()'); try { let [ok, contents] = GLib.file_get_contents(path); if (ok) { if (contents instanceof Uint8Array) { contents = new TextDecoder().decode(contents); } let map = JSON.parse(contents); try { debugOutput = map['debug_output']; } catch (e) { debugOutput = false; } try { for (let i = 0; i < map['ping_config'].length; i++) { let tag = map['ping_config'][i]['tag']; let name = map['ping_config'][i]['name']; let address = map['ping_config'][i]['address']; let ping_count = map['ping_config'][i]['ping_count']; let ping_interval = map['ping_config'][i]['ping_interval']; let ping_deadline = map['ping_config'][i]['ping_deadline']; let refresh_interval = map['ping_config'][i]['refresh_interval']; let active = map['ping_config'][i]['active']; let visible = map['ping_config'][i]['visible']; let show_name = map['ping_config'][i]['show_name']; let show_address = map['ping_config'][i]['show_address']; let show_tooltip = map['ping_config'][i]['show_tooltip']; let warning_threshold = map['ping_config'][i]['warning_threshold']; print_debug('tag: ' + tag); print_debug('name: ' + name); print_debug('address: ' + address); this._elts.push(new Ping( i, tag, name, address, ping_count, ping_interval, ping_deadline, refresh_interval, active, visible, show_name, show_address, show_tooltip, warning_threshold, this._settings, this.path)); } } catch (e) { print_info('could not load config'); print_info('error: ' + e); return false; } } } catch (e) { print_info('Error: ' + e); return false; } return true; } _build_menu_info() { print_debug('build_menu_info()'); if (this._tray.menu._getMenuItems().length && this._tray.menu._getMenuItems()[0].get_last_child) { let lastChild = this._tray.menu._getMenuItems()[0].get_last_child(); if (lastChild) { lastChild.destroy_all_children(); for (let elt of this._elts) { elt.menu_items = elt.create_menu_items(); } } else { return; } } else { return; } let menu_info_box_table = new St.Widget({ style: 'padding: 10px 0px 10px 0px; spacing-rows: 10px; spacing-columns: 15px;', layout_manager: new Clutter.GridLayout({orientation: Clutter.Orientation.VERTICAL}) }); let menu_info_box_table_layout = menu_info_box_table.layout_manager; let row_index = 0; for (let elt of this._elts) { if (!elt.menu_visible) { continue; } menu_info_box_table_layout.attach( new St.Label({ text: elt.name, style_class: 'ping-title', x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER }), 0, row_index, 1, 1); let col_index = 1; for (let item of elt.menu_items) { menu_info_box_table_layout.attach(item, col_index, row_index, 1, 1); col_index++; } row_index++; } let lastChild = this._tray.menu._getMenuItems()[0].get_last_child(); if (lastChild) { lastChild.add_child(menu_info_box_table); } } async _reload_async() { print_info('Reload ping applet async...'); for (let elt of this._elts) { elt.stop(); } for (let elt of this._elts) { while (elt.isRunning()) { print_info('still running'); await new Promise(resolve => GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10, () => { resolve(); return GLib.SOURCE_REMOVE; })); } } for (let elt of this._elts) { this._pingBox.remove_child(elt.actor); } for (let elt of this._elts) { elt.destroy(); } this._elts = []; let isFileOk = false; let path = this._settings.get_string('ping-config-path'); if (path == '') { path = GLib.getenv('HOME') + '/.config/ping-monitor.conf'; this._settings.set_string('ping-config-path', path); } isFileOk = this._read_from_file(path); this._settings.set_boolean('icon-display', !isFileOk); for (let elt of this._elts) { this._pingBox.add_child(elt.actor); } this._build_menu_info(); print_info('Reloaded.'); } }