michaelknap / gnome-system-monitor-indicator

System Monitor Extension For Gnome Shell SysTray
MIT License
5 stars 2 forks source link

Support network monitoring. #1

Open fxzxmicah opened 1 month ago

fxzxmicah commented 1 month ago

Support network monitoring.

michaelknap commented 4 weeks ago

Thank you for your suggestion!

I have considered this and think it could be a nice addition. However, to keep this extension lean, as I use it in various VMs for monitoring intensive workloads, I might develop the network stats feature in a separate repository. Managing two repositories is not ideal, but it would allow me to keep this one as basic as it is.

For my desktop use, this would indeed be a handy feature. I'd love to get your feedback on whether this should be enabled by default or via settings.

I will keep this issue open to see if there is any more interest.

fxzxmicah commented 4 weeks ago

I have a little programming skills, so I made some modifications and merged his network monitoring related code into it.

'use strict';

import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import St from 'gi://St';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';

import {
    Button
} from 'resource:///org/gnome/shell/ui/panelMenu.js';
import {
    panel, sessionMode
} from 'resource:///org/gnome/shell/ui/main.js';

// Define the main class for the system monitor indicator
export class SystemMonitorIndicator extends Button {

    // Initialize the indicator
    _init() {
        super._init(0, 'System Monitor Indicator', false);

        // Create a layout box to contain labels
        this.box = new St.BoxLayout();

        // Initialize CPU usage label
        this.cpu_usage_label = new St.Label({
            text: 'CPU: 0%',
            y_align: Clutter.ActorAlign.CENTER,
            style: 'margin-left: 4px; margin-right: 4px;'
        });
        this.box.add_child(this.cpu_usage_label);

        // Initialize Memory usage label
        this.mem_usage_label = new St.Label({
            text: 'Mem: 0%',
            y_align: Clutter.ActorAlign.CENTER,
            style: 'margin-left: 4px; margin-right: 4px;'
        });
        this.box.add_child(this.mem_usage_label);

        // Initialize Swap usage label
        this.swap_usage_label = new St.Label({
            text: 'Swap: 0%',
            y_align: Clutter.ActorAlign.CENTER,
            style: 'margin-left: 4px; margin-right: 4px;'
        });
        this.box.add_child(this.swap_usage_label);

        // Initialize Swap usage label
        this.net_usage_label = new St.Label({
            text: 'Net: ↓ ↑',
            y_align: Clutter.ActorAlign.CENTER,
            style: 'margin-left: 4px; margin-right: 4px;'
        });
        this.box.add_child(this.net_usage_label);

        // Add the layout box to this actor
        this.add_child(this.box);

        // Initialize previous CPU values
        this.prev_idle = 0;
        this.prev_total = 0;

        // Start updating metrics
        this._update_metrics();
    }

    _file_open(file_path) {
        try {
            // Get file name and return lines of the file as array
            const file = Gio.File.new_for_path(file_path);
            const [, content] = file.load_contents(null);
            const text_decoder = new TextDecoder("utf-8");
            const content_str = text_decoder.decode(content);
            return content_str.split('\n');
        } catch (e) {
            logError(e, `PROCESSING ERROR IN FILE: ${file_path}`);
        }
  }

    // Function to update all metrics (CPU, Memory)
    _update_metrics() {
        const priority = GLib.PRIORITY_DEFAULT_IDLE;
        const refresh_time = 1; // Time in seconds

        // Update individual metrics
        this._update_cpu_usage();
        this._update_memory_usage();
        this._update_net_usage();

        // Remove existing timeout if any
        if (this._timeout) {
            GLib.source_remove(this._timeout);
        }

        // Set a timeout to refresh metrics
        this._timeout = GLib.timeout_add_seconds(priority, refresh_time, () => {
            this._update_metrics();
            return true;
        });
    }

    // Function to update CPU usage
    _update_cpu_usage() {
        try {
            const content_lines = this._file_open('/proc/stat');

            let current_cpu_used = 0;
            let current_cpu_total = 0;
            let current_cpu_usage = 0;

            for (let i = 0; i < content_lines.length; i++) {
                const fields = content_lines[i].trim().split(/\s+/);

                if (fields[0] === 'cpu') {
                    const nums = fields.slice(1).map(Number);
                    const user = nums[0];
                    const nice = nums[1];
                    const system = nums[2];
                    const idle = nums[3];
                    const iowait = nums[4] || 0; // Include iowait, defaulting to 0 if not present

                    current_cpu_total = nums.slice(0, 4).reduce((a, b) => a + b, 0) +
                        iowait;
                    current_cpu_used = current_cpu_total - idle - iowait;

                    // Ensure previous values are set on the first run
                    this.prev_used = this.prev_used || current_cpu_used;
                    this.prev_total = this.prev_total || current_cpu_total;

                    // Calculate CPU usage as the difference from the previous measurement
                    const total_diff = current_cpu_total - this.prev_total;
                    const used_diff = current_cpu_used - this.prev_used;

                    if (total_diff > 0) { // Check to avoid division by zero
                        current_cpu_usage = (used_diff / total_diff) * 100;
                        this.cpu_usage_label.set_text(
                            `CPU: ${current_cpu_usage.toFixed(2)}%`);
                    }

                    // Store current values for the next calculation
                    this.prev_used = current_cpu_used;
                    this.prev_total = current_cpu_total;

                    break; // Break after processing the first 'cpu' line
                }
            }
        } catch (e) {
            logError(e, `Failed to update CPU usage.`);
        }
    }

    // Function to update Memory usage
    _update_memory_usage() {
        try {
            const content_lines = this._file_open('/proc/meminfo');

            let mem_total = null;
            let mem_available = null;
            let mem_used = null;
            let mem_usage = null;
            let swap_total = null;
            let swap_free = null;
            let swap_used = null;
            let swap_usage = null;

            content_lines.forEach((line) => {
                let [key, value] = line.split(':');
                if (value) {
                    value = parseInt(value.trim(), 10);
                }

                switch (key) {
                    case 'MemTotal':
                        mem_total = value;
                        break;
                    case 'MemAvailable':
                        mem_available = value;
                        break;
                    case 'SwapTotal':
                        swap_total = value;
                        break;
                    case 'SwapFree':
                        swap_free = value;
                        break;
                }
            });

            // Update RAM usage label
            if (mem_total !== null && mem_available !== null) {
                mem_used = mem_total - mem_available;
                mem_usage = (mem_used / mem_total) * 100;
                this.mem_usage_label.set_text(`Mem: ${mem_usage.toFixed(2)}%`);
            }

            // Update Swap usage label only if swap is available
            if (swap_total !== null && swap_total > 0 && swap_free !== null) {
                swap_used = swap_total - swap_free;
                swap_usage = (swap_used / swap_total) * 100;
                this.swap_usage_label.set_text(`Swap: ${swap_usage.toFixed(2)}%`);
                this.swap_usage_label.show();
            } else {
                this.swap_usage_label.hide(); // Hide the label because there's no swap
            }
        } catch (e) {
            logError(e, `Failed to update memory usage.`);
        }
    }

    _update_net_usage() {
        try {
            // Get Active Interface name for parsing
            let activeInterfaceName = '';
            let [result, output,] = GLib.spawn_command_line_sync('bash -c "ip route get 1 | awk \'{print $5; exit}\'"');
            if (result) {
                const textDecoder = new TextDecoder("utf-8");
                activeInterfaceName = textDecoder.decode(new Uint8Array(output)).trim();
            }

            const content_lines = this._file_open('/proc/net/dev');
            const interface_name = activeInterfaceName;
            let tx_bytes = 0;
            let rx_bytes = 0;

            for (let i = 2; i < content_lines.length; i++) {
                const line = content_lines[i].trim();
                if (line.startsWith(interface_name)) {
                    const values = line.split(/\s+/);
                    tx_bytes = parseInt(values[9]);
                    rx_bytes = parseInt(values[1]);
                    break;
                }
            }

            // Calculate network traffic speed in bytes per second
            const current_time = Date.now() / 1000; // Convert milliseconds to seconds
            const time_difference = current_time - this.prev_time;

            let tx_speed = ((tx_bytes - this.prev_tx_bytes) / time_difference);
            let rx_speed = ((rx_bytes - this.prev_rx_bytes) / time_difference);

            const units = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s'];
            let tx_unit_index = 0;
            let rx_unit_index = 0;

            while (tx_speed > 1024 || rx_speed > 1024) {
                if(tx_speed > 1024){
                    tx_speed /= 1024;
                    tx_unit_index++;
                }
                if(rx_speed > 1024){
                    rx_speed /= 1024;
                    rx_unit_index++;
                }
            }

            if (activeInterfaceName !== '') {
                // Update labels with network traffic speed
                let rx_label;
                let tx_label;

                rx_label = rx_unit_index == 0 ? '<1KB/s' : `${rx_speed.toFixed(0)}${units[rx_unit_index]}`;
                tx_label = tx_unit_index == 0 ? '<1KB/s' : `${tx_speed.toFixed(0)}${units[tx_unit_index]}`;

                this.net_usage_label.set_text(`Net: ↓ ${rx_label} ↑ ${tx_label}`);
                this.net_usage_label.show();
            } else {
                this.net_usage_label.hide(); // Hide the label because there's no net interface
            }

            // Store current values for the next calculation
            this.prev_time = current_time;
            this.prev_tx_bytes = tx_bytes;
            this.prev_rx_bytes = rx_bytes;
        } catch (e) {
            logError(e, `Failed to update network traffic speed.`);
        }
    }

    // Stop updates
    stop() {
        if (this._timeout) {
            GLib.source_remove(this._timeout);
        }
        this._timeout = undefined;
    }
}

// Register the SystemMonitorIndicator class
GObject.registerClass({
    GTypeName: 'SystemMonitorIndicator'
}, SystemMonitorIndicator);

// Export the main extension class
export default class SystemMonitorExtension {
    _indicator;

    // Enable the extension
    enable() {
        this._indicator = new SystemMonitorIndicator();
        let pos = sessionMode.panel.left.length;
        panel.addToStatusArea('system-indicator', this._indicator, pos, 'left');
    }

    // Disable the extension
    disable() {
        this._indicator.stop();
        this._indicator.destroy();
        this._indicator = undefined;
    }
}

Another thing I don't understand is that the official example uses GTop. Is there any consideration in not using it here?