From b470c568cfe162f4909bbe910ee48336938b61a1 Mon Sep 17 00:00:00 2001 From: Phirxian Date: Mon, 20 May 2024 12:04:10 +0200 Subject: [PATCH] adding network !! - rx/tx graph - label rx/tx/error - rewriting some codes - adding packetin/packetout for each pid - adding pcap sniffing port -> count - adding inode mapping to get port <- inode <- pid # Conflicts: # src/main.c # src/process-monitor.c # src/process-monitor.h # src/process-statusbar.c # src/process-window.c # src/process-window.h # src/settings.h # src/task-manager-linux.c # src/task-manager.c # src/task-manager.h starting freebsd get mac address trough getifaddrs function, should be more portable remove mac_get_binary_from_file that use /sys/class/net/%s/address freebsd adding packetin/packetout/active socket trough sockstat - move linux code - adding pseudo inode mapping OpenBSD suppoort & code cleanup NetBSD global network usage & per process hiding of settings and columns on network initialization failure Apply .clang-format file # Conflicts: # src/main.c # src/process-monitor.c # src/process-monitor.h # src/process-statusbar.c # src/process-window.c # src/process-window.h # src/settings-dialog.c # src/settings.h # src/task-manager-bsd.c # src/task-manager.c # src/task-manager.h use AC_CHECK_LIB and add the corresponding ifdef to disable pcap functionality fix compilation openbsd clang format fix compilation netbsd clang format ... disable clang-format on task-manager-bsd, important include order fix compilation freebsd disable clang-format on task-manager-freebsd, important include order typo openbsd include fix segfault on openbsd without permissions fix warning on freebsd : -Wint-to-pointer-cast -Wmissing-declarations -Wshadow fix compilation for "skel" and "solaris" (implementation todo) freebsd iterate over all network interface linux iterate over all network interface and fix some warnings netbsd iterator over all network interface and fix args issue trough kvm_getargv2 fix ptr issue, clang format ide change coding style ? fix close button, hide when "Keep in the notification area" is enable Discard my changes on the README Solaris adding mac adresse, rx/tx/error for the network graph libsocket is in the default package of solaris, it should be linked for getifaddrs adding packet callback for solaris solaris pid to socket mapping (tcp, tcp6, udp, udp6) ! final clang format credit ? Linux adding udp, udp6, icmp, icmp6, raw, raw6. Disable virtual network device on linux. Fix -Wincompatible-pointer-types and -Wdeprecated-declarations use WNCK_CHECK_VERSION and WnckHandle on version >= 43.0.0 asked changes --- configure.ac | 6 + screenshot.png | Bin 0 -> 111047 bytes src/Makefile.am | 4 + src/app-manager.c | 18 +- src/inode-to-sock.c | 45 ++++ src/inode-to-sock.h | 29 +++ src/main.c | 35 ++- src/network-analyzer.c | 168 ++++++++++++++ src/network-analyzer.h | 45 ++++ src/process-monitor.c | 123 +++++----- src/process-monitor.h | 3 +- src/process-statusbar.c | 82 ++++++- src/process-tree-view.c | 59 +++++ src/process-tree-view.h | 3 + src/process-window.c | 41 +++- src/process-window.h | 3 +- src/process-window.ui | 21 ++ src/settings-dialog.c | 22 +- src/settings-dialog.ui | 42 ++++ src/settings.c | 20 ++ src/settings.h | 3 + src/task-manager-bsd.c | 451 ++++++++++++++++++++++++++++++++++++- src/task-manager-freebsd.c | 283 ++++++++++++++++++++++- src/task-manager-linux.c | 364 ++++++++++++++++++++++++++++++ src/task-manager-skel.c | 23 ++ src/task-manager-solaris.c | 435 +++++++++++++++++++++++++++++++++++ src/task-manager.c | 47 ++++ src/task-manager.h | 19 ++ 28 files changed, 2301 insertions(+), 93 deletions(-) create mode 100644 screenshot.png create mode 100644 src/inode-to-sock.c create mode 100644 src/inode-to-sock.h create mode 100644 src/network-analyzer.c create mode 100644 src/network-analyzer.h diff --git a/configure.ac b/configure.ac index a338377..7b77330 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,7 @@ dnl dnl xfce4-taskmanager - A small taskmanager based on the Xfce 4 libraries. dnl +dnl 2024-2024 Jehan-Antoine Vayssade dnl 2014-2021 Simon Steinbess dnl 2018-2019 Rozhuk Ivan dnl 2014 Landry Breuil @@ -81,6 +82,9 @@ dnl *********************************** XDT_CHECK_OPTIONAL_PACKAGE([LIBX11], [x11], [1.6.7], [libx11], [Libx11 support]) XDT_CHECK_OPTIONAL_PACKAGE([WNCK], [libwnck-3.0], [3.2], [wnck3], [building with libwnck3 for window icons/names], [yes]) +AC_CHECK_LIB(pcap, [pcap_open_live]) +AC_CHECK_HEADERS([pcap.h]) + dnl *********************************** dnl ********** Check for skel ********* dnl *********************************** @@ -104,12 +108,14 @@ else ;; dragonfly*|netbsd*|openbsd*|darwin*) ac_os_implementation="bsd" + AC_CHECK_LIB([kvm], [kvm_openfiles]) AC_CHECK_HEADERS([err.h pwd.h stdlib.h string.h sys/param.h sys/sched.h \ sys/swap.h sys/sysctl.h sys/types.h unistd.h]) ;; solaris*) ac_os_implementation="solaris" AC_CHECK_LIB([kstat], [kstat_open]) + AC_CHECK_LIB([socket], [freeifaddrs]) AC_CHECK_HEADERS([fcntl.h kstat.h procfs.h pwd.h stdlib.h string.h \ sys/procfs.h sys/stat.h sys/swap.h sys/types.h]) ;; diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..2af5915589c1da75f9331ade4e10468be7b727d1 GIT binary patch literal 111047 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ΜlYXz5v#|iPrQ&QTOe?A&7V&r%H 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 CXA~s> literal 0 HcmV?d00001 diff --git a/src/Makefile.am b/src/Makefile.am index be34016..f97e672 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -8,6 +8,7 @@ bin_PROGRAMS = \ xfce4_taskmanager_CFLAGS = \ $(CAIRO_CFLAGS) \ + $(LIBPCAP_CFLAGS) \ $(LIBX11_CFLAGS) \ $(LIBXMU_CFLAGS) \ $(GTK3_CFLAGS) \ @@ -18,6 +19,7 @@ xfce4_taskmanager_CFLAGS = \ xfce4_taskmanager_LDADD = \ $(CAIRO_LIBS) \ + $(LIBPCAP_LIBS) \ $(LIBX11_LIBS) \ $(LIBXMU_LIBS) \ $(GTK3_LIBS) \ @@ -30,6 +32,8 @@ xfce4_taskmanager_SOURCES = \ main.c \ process-window_ui.h \ process-window.c process-window.h \ + inode-to-sock.c inode-to-sock.h \ + network-analyzer.c network-analyzer.h \ process-monitor.c process-monitor.h \ process-tree-model.c process-tree-model.h \ process-tree-view.c process-tree-view.h \ diff --git a/src/app-manager.c b/src/app-manager.c index d1b1cc2..193d043 100644 --- a/src/app-manager.c +++ b/src/app-manager.c @@ -44,7 +44,7 @@ static App *apps_lookup_app (GArray *apps, WnckApplication *application); static void application_opened (WnckScreen *screen, WnckApplication *application, XtmAppManager *manager); static void application_closed (WnckScreen *screen, WnckApplication *application, XtmAppManager *manager); static void scale_factor_changed (GdkMonitor *monitor); - +// static WnckHandle *handle = NULL; static void @@ -59,10 +59,18 @@ static void xtm_app_manager_init (XtmAppManager *manager) { WnckApplication *application; - WnckScreen *screen = wnck_screen_get_default (); - GdkMonitor *monitor = gdk_display_get_monitor (gdk_display_get_default (), 0); + WnckScreen *screen; + GdkMonitor *monitor; GList *windows, *l; + // #if WNCK_CHECK_VERSION(43, 0, 0) + // handle = wnck_handle_new (WNCK_CLIENT_TYPE_APPLICATION); + // screen = wnck_handle_get_default_screen (handle); + // #else + screen = wnck_screen_get_default (); + // #endif + monitor = gdk_display_get_monitor (gdk_display_get_default (), 0); + /* Retrieve initial applications */ while (gtk_events_pending ()) gtk_main_iteration (); @@ -210,7 +218,11 @@ application_closed (WnckScreen *screen __unused, WnckApplication *application, X static void scale_factor_changed (GdkMonitor *monitor) { + // #if WNCK_CHECK_VERSION(43, 0, 0) + // wnck_handle_set_default_mini_icon_size (handle, WNCK_DEFAULT_MINI_ICON_SIZE * gdk_monitor_get_scale_factor (monitor)); + // #else wnck_set_default_mini_icon_size (WNCK_DEFAULT_MINI_ICON_SIZE * gdk_monitor_get_scale_factor (monitor)); + // #endif } diff --git a/src/inode-to-sock.c b/src/inode-to-sock.c new file mode 100644 index 0000000..425c7a2 --- /dev/null +++ b/src/inode-to-sock.c @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Jehan-Antoine Vayssade, + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "inode-to-sock.h" +#include "stdio.h" + +XtmInodeToSock * +xtm_create_inode_to_sock (void) +{ + XtmInodeToSock *its = g_new (XtmInodeToSock, 1); + its->hash = g_hash_table_new_full (g_int_hash, g_int_equal, g_free, NULL); + // freebsd only + its->pid = g_hash_table_new_full (g_int_hash, g_int_equal, g_free, NULL); + return its; +} + +void +xtm_destroy_inode_to_sock (XtmInodeToSock *its) +{ + if (its == NULL) + return; + + g_hash_table_destroy (its->hash); + g_hash_table_destroy (its->pid); + + free (its); +} + +XtmInodeToSock * +xtm_inode_to_sock_get_default (void) +{ + static XtmInodeToSock *its = NULL; + if (its == NULL) + its = xtm_create_inode_to_sock (); + return its; +} diff --git a/src/inode-to-sock.h b/src/inode-to-sock.h new file mode 100644 index 0000000..822b4c3 --- /dev/null +++ b/src/inode-to-sock.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Jehan-Antoine Vayssade, + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef INMODE_TO_SOCK_H +#define INMODE_TO_SOCK_H + +#include + +typedef struct _XtmInodeToSock XtmInodeToSock; +struct _XtmInodeToSock +{ + // inode to port + GHashTable *hash; + // inode to pid (freebsd) + GHashTable *pid; +}; + +XtmInodeToSock *xtm_create_inode_to_sock (void); +void xtm_refresh_inode_to_sock (XtmInodeToSock *); +void xtm_destroy_inode_to_sock (XtmInodeToSock *); +XtmInodeToSock *xtm_inode_to_sock_get_default (void); + +#endif diff --git a/src/main.c b/src/main.c index bf967b0..fdedc24 100644 --- a/src/main.c +++ b/src/main.c @@ -12,6 +12,7 @@ #include "config.h" #endif +#include "network-analyzer.h" #include "process-window.h" #include "settings.h" #include "task-manager.h" @@ -28,6 +29,7 @@ static XtmTaskManager *task_manager; static guint timer_id; static gboolean start_hidden = FALSE; static gboolean standalone = FALSE; +static XtmNetworkAnalyzer *analyzer = NULL; static GOptionEntry main_entries[] = { { "start-hidden", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, &start_hidden, "Don't open a task manager window", NULL }, @@ -121,15 +123,14 @@ destroy_window (void) } static gboolean -delete_window (void) +delete_window (GtkWidget *widget, GdkEvent *event, gpointer user_data) { if (!status_icon_get_visible ()) { - if (timer_id > 0) - g_source_remove (timer_id); - gtk_main_quit (); + destroy_window (); return FALSE; } + gtk_widget_hide (window); return TRUE; } @@ -140,8 +141,10 @@ collect_data (void) guint num_processes; gfloat cpu, memory_percent, swap_percent; guint64 swap_used, swap_free, swap_total, memory_used, memory_total; + guint64 tcp_rx, tcp_tx, tcp_error; gchar *used, *total, tooltip[1024], memory_info[64], swap_info[64]; + xtm_task_manager_get_network_info (task_manager, &tcp_rx, &tcp_tx, &tcp_error); xtm_task_manager_get_system_info (task_manager, &num_processes, &cpu, &memory_used, &memory_total, &swap_used, &swap_total); memory_percent = (memory_total != 0) ? ((memory_used * 100.0f) / (float)memory_total) : 0.0f; @@ -159,19 +162,23 @@ collect_data (void) g_free (used); g_free (total); - xtm_process_window_set_system_info (XTM_PROCESS_WINDOW (window), num_processes, cpu, memory_percent, memory_info, swap_percent, swap_info); + xtm_process_window_set_system_info (XTM_PROCESS_WINDOW (window), num_processes, cpu, memory_percent, memory_info, swap_percent, swap_info, tcp_rx, tcp_tx, tcp_error); xtm_task_manager_get_swap_usage (task_manager, &swap_free, &swap_total); xtm_process_window_show_swap_usage (XTM_PROCESS_WINDOW (window), (swap_total > 0)); if (status_icon_get_visible ()) { - g_snprintf (tooltip, sizeof (tooltip), + g_snprintf ( + tooltip, sizeof (tooltip), _("Processes: %u\n" "CPU: %.0f%%\n" "Memory: %s\n" - "Swap: %s"), - num_processes, cpu, memory_info, swap_info); + "Swap: %s\n" + "Network: %.2f / %.2f MB/s"), + num_processes, cpu, memory_info, swap_info, + tcp_rx * 1e-5, tcp_tx * 1e-5); + G_GNUC_BEGIN_IGNORE_DEPRECATIONS gtk_status_icon_set_tooltip_markup (GTK_STATUS_ICON (status_icon_or_null), tooltip); G_GNUC_END_IGNORE_DEPRECATIONS @@ -253,8 +260,7 @@ main (int argc, char *argv[]) if (!xfconf_init (&error)) { xfce_message_dialog (NULL, _("Xfce Notify Daemon"), - "dialog-error", - _("Settings daemon is unavailable"), + "dialog-error", _("Settings daemon is unavailable"), error->message, "application-exit", GTK_RESPONSE_ACCEPT, NULL); @@ -273,6 +279,8 @@ main (int argc, char *argv[]) g_signal_connect_swapped (app, "activate", G_CALLBACK (xtm_process_window_show), window); + //! create net + analyzer = xtm_network_analyzer_get_default (); task_manager = xtm_task_manager_new (xtm_process_window_get_model (XTM_PROCESS_WINDOW (window))); collect_data (); @@ -282,8 +290,11 @@ main (int argc, char *argv[]) g_signal_connect_after (settings, "notify::full-command-line", G_CALLBACK (collect_data), NULL); g_signal_connect (settings, "notify::show-status-icon", G_CALLBACK (show_hide_status_icon), NULL); - g_signal_connect (window, "destroy", G_CALLBACK (destroy_window), NULL); + g_signal_connect (xtm_process_window_get (XTM_PROCESS_WINDOW (window)), "destroy", G_CALLBACK (destroy_window), NULL); + g_signal_connect (xtm_process_window_get (XTM_PROCESS_WINDOW (window)), "delete-event", G_CALLBACK (delete_window), NULL); g_signal_connect (window, "delete-event", G_CALLBACK (delete_window), NULL); + g_signal_connect (window, "destroy", G_CALLBACK (destroy_window), NULL); + if (gtk_widget_get_visible (window) || status_icon_get_visible ()) gtk_main (); @@ -297,5 +308,7 @@ main (int argc, char *argv[]) g_object_unref (status_icon_or_null); xfconf_shutdown (); + xtm_destroy_network_analyzer (analyzer); + return 0; } diff --git a/src/network-analyzer.c b/src/network-analyzer.c new file mode 100644 index 0000000..2238e26 --- /dev/null +++ b/src/network-analyzer.c @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 Jehan-Antoine Vayssade, + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "network-analyzer.h" +#include "task-manager.h" + +#include +#include +#include +#include +#include +#include +#include + +void *network_analyzer_thread (void *ptr); + +void +increament_packet_count (char *mac, char *direction, GHashTable *hash_table, long int port) +{ + gint64 *key = g_new0 (gint64, 1); + *key = port; + gpointer value = g_hash_table_lookup (hash_table, key); + g_hash_table_replace (hash_table, key, (gpointer)(((guint64)value) + 1)); + // printf ("%s -> %s %ld: %ld\n", mac, direction, port, ((guint64)value) + 1); +} + +void * +network_analyzer_thread (void *ptr) +{ +#ifdef HAVE_LIBPCAP + XtmNetworkAnalyzer *analyzer = (XtmNetworkAnalyzer *)ptr; + pcap_loop (analyzer->handle, -1, packet_callback, (void *)analyzer); +#endif + return NULL; +} + +XtmNetworkAnalyzer * +xtm_create_network_analyzer (void) +{ + XtmNetworkAnalyzer *analyzer = NULL; + +#ifdef HAVE_LIBPCAP + XtmNetworkAnalyzer *current = NULL; + char errbuf[PCAP_ERRBUF_SIZE]; + pcap_if_t *it = NULL; + + if (pcap_findalldevs (&it, errbuf) != 0) + { + fprintf (stderr, "Error finding device: %s\n", errbuf); + return NULL; + } + + + while (it) + { + guint8 mac[6]; + char *device = it->name; + + //! todo check pcap lib ? + if (get_mac_address (device, mac) == 0) + { + printf ( + "%s, MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n", + device, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } + else + { + fprintf (stderr, "No mac adresse for %s\n", device); + it = it->next; + continue; + } + + // Open a pcap session + pcap_t *handle = pcap_open_live (device, BUFSIZ, 1, 1000, errbuf); + + if (handle == NULL) + { + fprintf (stderr, "Could not open device %s: %s\n", device, errbuf); + it = it->next; + continue; + } + + if (analyzer == NULL) + { + analyzer = (XtmNetworkAnalyzer *)malloc (sizeof (XtmNetworkAnalyzer)); + analyzer->next = NULL; + current = analyzer; + } + else + { + current->next = (XtmNetworkAnalyzer *)malloc (sizeof (XtmNetworkAnalyzer)); + current = current->next; + current->next = NULL; + } + + current->handle = handle; + current->iface = it; + current->packetin = g_hash_table_new_full (g_int_hash, g_int_equal, g_free, NULL); + current->packetout = g_hash_table_new_full (g_int_hash, g_int_equal, g_free, NULL); + + memcpy (current->mac, mac, sizeof (uint8_t) * 6); + + pthread_mutex_init (¤t->lock, NULL); + pthread_create (¤t->thread, NULL, network_analyzer_thread, (void *)current); + + it = it->next; + } + + if (analyzer == NULL) + { + fprintf (stderr, "Could not open any device %s\n", errbuf); + pcap_freealldevs (it); + } +#endif + + return analyzer; +} + +XtmNetworkAnalyzer * +xtm_network_analyzer_get_default (void) +{ + static int initialized = FALSE; + static XtmNetworkAnalyzer *analyzer = NULL; + + if (initialized == FALSE) + { + initialized = TRUE; + // analyzer may be NULL if no device can be opened + analyzer = xtm_create_network_analyzer (); + } + + return analyzer; +} + +void +xtm_destroy_network_analyzer (XtmNetworkAnalyzer *analyzer) +{ +#ifdef HAVE_LIBPCAP + if (analyzer == NULL) + return; + + XtmNetworkAnalyzer *previous = analyzer; + pcap_if_t *it = analyzer->iface; + + while (analyzer) + { + pthread_cancel (analyzer->thread); + pcap_close (analyzer->handle); + g_hash_table_destroy (analyzer->packetin); + g_hash_table_destroy (analyzer->packetout); + pthread_mutex_destroy (&analyzer->lock); + analyzer = analyzer->next; + free (previous); + previous = analyzer; + } + + pcap_freealldevs (it); +#endif +} diff --git a/src/network-analyzer.h b/src/network-analyzer.h new file mode 100644 index 0000000..1b21347 --- /dev/null +++ b/src/network-analyzer.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Jehan-Antoine Vayssade, + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef NETWORK_ANALYZER_H +#define NETWORK_ANALYZER_H + +#include + +#ifdef HAVE_LIBPCAP +#include +#include +#endif + +typedef struct _XtmNetworkAnalyzer XtmNetworkAnalyzer; +struct _XtmNetworkAnalyzer +{ + // interface mac adress + guint8 mac[6]; + + void *iface; // pcap_if_t + void *handle; // pcap_t + + pthread_t thread; + pthread_mutex_t lock; + + // map port and number of packets + GHashTable *packetin; + GHashTable *packetout; + + // next interface + XtmNetworkAnalyzer *next; +}; + +XtmNetworkAnalyzer *xtm_create_network_analyzer (void); +void increament_packet_count (char *mac, char *direction, GHashTable *hash_table, long int port); +void xtm_destroy_network_analyzer (XtmNetworkAnalyzer *analyzer); +XtmNetworkAnalyzer *xtm_network_analyzer_get_default (void); + +#endif diff --git a/src/process-monitor.c b/src/process-monitor.c index 89d3f1c..3576d46 100644 --- a/src/process-monitor.c +++ b/src/process-monitor.c @@ -16,7 +16,7 @@ #include - +#define MAX_GRAPH 3 enum { @@ -34,8 +34,8 @@ struct _XtmProcessMonitor /**/ gfloat step_size; gint type; - GArray *history; - GArray *history_swap; + GArray *history[MAX_GRAPH]; + gfloat max[MAX_GRAPH]; gboolean dark_mode; }; G_DEFINE_TYPE (XtmProcessMonitor, xtm_process_monitor, GTK_TYPE_DRAWING_AREA) @@ -76,8 +76,12 @@ xtm_process_monitor_init (XtmProcessMonitor *monitor) { GtkSettings *settings = gtk_settings_get_default (); - monitor->history = g_array_new (FALSE, TRUE, sizeof (gfloat)); - monitor->history_swap = g_array_new (FALSE, TRUE, sizeof (gfloat)); + for (int graph = 0; graph < MAX_GRAPH; ++graph) + { + monitor->history[graph] = g_array_new (FALSE, TRUE, sizeof (gfloat)); + monitor->max[graph] = 1.0; + } + monitor->dark_mode = xtm_gtk_widget_is_dark_mode (GTK_WIDGET (monitor)); g_signal_connect_swapped (settings, "notify::gtk-theme-name", G_CALLBACK (xtm_process_monitor_on_notify_theme_name), monitor); @@ -89,8 +93,8 @@ xtm_process_monitor_finalize (GObject *object) { XtmProcessMonitor *monitor = XTM_PROCESS_MONITOR (object); - g_array_free (monitor->history, TRUE); - g_array_free (monitor->history_swap, TRUE); + for (int graph = 0; graph < MAX_GRAPH; ++graph) + g_array_free (monitor->history[graph], TRUE); G_OBJECT_CLASS (xtm_process_monitor_parent_class)->finalize (object); } @@ -142,13 +146,10 @@ xtm_process_monitor_draw (GtkWidget *widget, cairo_t *cr) guint minimum_history_length; minimum_history_length = (guint)(gtk_widget_get_allocated_width (widget) / monitor->step_size); - if (monitor->history->len < minimum_history_length) - { - g_array_set_size (monitor->history, minimum_history_length + 1); - if (monitor->type == 1) - g_array_set_size (monitor->history_swap, minimum_history_length + 1); - } + for (int graph = 0; graph < MAX_GRAPH; ++graph) + if (monitor->history[graph]->len < minimum_history_length) + g_array_set_size (monitor->history[graph], minimum_history_length + 1); xtm_process_monitor_paint (monitor, cr); return FALSE; @@ -172,6 +173,30 @@ xtm_process_monitor_cairo_set_source_rgb (cairo_t *cr, double red, double green, cairo_set_source_rgb (cr, red, green, blue); } +static void +xtm_process_monitor_cairo_set_color (XtmProcessMonitor *monitor, cairo_t *cr, gfloat alpha, guint variant) +{ + if (monitor->type == 0) + xtm_process_monitor_cairo_set_source_rgba (cr, 1.0, 0.43, 0.0, alpha, monitor->dark_mode); + else if (monitor->type == 1) + { + // xtm_process_monitor_cairo_set_source_rgba (cr, 0.67, 0.09, 0.32, alpha, monitor->dark_mode); + if (variant == 0) + xtm_process_monitor_cairo_set_source_rgba (cr, 0.80, 0.22, 0.42, alpha, monitor->dark_mode); + else + xtm_process_monitor_cairo_set_source_rgba (cr, 0.50, 0.50, 0.50, alpha, monitor->dark_mode); + } + else + { + if (variant == 0) + xtm_process_monitor_cairo_set_source_rgba (cr, 0.00, 0.42, 0.64, alpha, monitor->dark_mode); + else if (variant == 1) + xtm_process_monitor_cairo_set_source_rgba (cr, 0.02, 0.71, 0.81, alpha, monitor->dark_mode); + else + xtm_process_monitor_cairo_set_source_rgba (cr, 0.00, 1.00, 1.00, alpha, monitor->dark_mode); + } +} + static cairo_surface_t * xtm_process_monitor_graph_surface_create (XtmProcessMonitor *monitor, gint width, gint height) { @@ -180,69 +205,39 @@ xtm_process_monitor_graph_surface_create (XtmProcessMonitor *monitor, gint width gdouble peak, step_size; gint i; - if (monitor->history->len <= 1) - { - g_warning ("Cannot paint graph with n_peak <= 1"); - return NULL; - } step_size = (gdouble)monitor->step_size; graph_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); cr = cairo_create (graph_surface); /* Draw line and area below the line, distinguish between CPU (0) and Mem (1) color-wise */ - cairo_translate (cr, step_size, 0); cairo_set_line_width (cr, 0.85); cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND); cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND); cairo_set_antialias (cr, CAIRO_ANTIALIAS_DEFAULT); - cairo_move_to (cr, width, height); - /* Create a line before the call to cairo_translate, - * to avoid creating a downward sloping line going off the graph */ - peak = g_array_index (monitor->history, gfloat, 0); - cairo_line_to (cr, width, (1.0 - peak) * height); - - for (i = 0; (step_size * (i - 1)) <= width; i++) + for (int graph = 0; graph < MAX_GRAPH; ++graph) { - peak = g_array_index (monitor->history, gfloat, i); - cairo_translate (cr, -step_size, 0); - cairo_line_to (cr, width, (1.0 - peak) * height); - } + if (monitor->history[graph]->len <= 1) + continue; - if (monitor->type == 0) - xtm_process_monitor_cairo_set_source_rgba (cr, 1.0, 0.43, 0.0, 0.3, monitor->dark_mode); - else - xtm_process_monitor_cairo_set_source_rgba (cr, 0.67, 0.09, 0.32, 0.3, monitor->dark_mode); - cairo_line_to (cr, width, height); - cairo_fill_preserve (cr); - - if (monitor->type == 0) - xtm_process_monitor_cairo_set_source_rgba (cr, 1.0, 0.43, 0.0, 1.0, monitor->dark_mode); - else - xtm_process_monitor_cairo_set_source_rgba (cr, 0.67, 0.09, 0.32, 1.0, monitor->dark_mode); - cairo_stroke (cr); - - /* Draw Swap graph */ - if (monitor->type == 1) - { - cairo_translate (cr, step_size * i, 0); cairo_move_to (cr, width, height); - - peak = g_array_index (monitor->history_swap, gfloat, 0); - cairo_line_to (cr, width, (1.0 - peak) * height); + cairo_translate (cr, step_size, 0); + xtm_process_monitor_cairo_set_color (monitor, cr, 0.3, graph); for (i = 0; (step_size * (i - 1)) <= width; i++) { - peak = g_array_index (monitor->history_swap, gfloat, i); + peak = g_array_index (monitor->history[graph], gfloat, i) / monitor->max[graph]; cairo_translate (cr, -step_size, 0); cairo_line_to (cr, width, (1.0 - peak) * height); } - xtm_process_monitor_cairo_set_source_rgba (cr, 0.33, 0.04, 0.16, 0.3, monitor->dark_mode); + cairo_line_to (cr, width, height); cairo_fill_preserve (cr); - xtm_process_monitor_cairo_set_source_rgba (cr, 0.33, 0.04, 0.16, 1.0, monitor->dark_mode); + xtm_process_monitor_cairo_set_color (monitor, cr, 1.0, graph); cairo_stroke (cr); + + cairo_translate (cr, width, 0); } cairo_destroy (cr); @@ -300,21 +295,17 @@ xtm_process_monitor_new (void) } void -xtm_process_monitor_add_peak (XtmProcessMonitor *monitor, gfloat peak, gfloat peak_swap) +xtm_process_monitor_add_peak (XtmProcessMonitor *monitor, gfloat peak, gint index) { g_return_if_fail (XTM_IS_PROCESS_MONITOR (monitor)); - g_return_if_fail (peak >= 0.0f && peak <= 1.0f); + // g_return_if_fail (peak >= 0.0f && peak <= 1.0f); - g_array_prepend_val (monitor->history, peak); - if (monitor->history->len > 1) - g_array_remove_index (monitor->history, monitor->history->len - 1); + if (peak > monitor->max[index]) + monitor->max[index] = peak; - if (monitor->type == 1) - { - g_array_prepend_val (monitor->history_swap, peak_swap); - if (monitor->history_swap->len > 1) - g_array_remove_index (monitor->history_swap, monitor->history_swap->len - 1); - } + g_array_prepend_val (monitor->history[index], peak); + if (monitor->history[index]->len > 1) + g_array_remove_index (monitor->history[index], monitor->history[index]->len - 1); if (GDK_IS_WINDOW (gtk_widget_get_window (GTK_WIDGET (monitor)))) gdk_window_invalidate_rect (gtk_widget_get_window (GTK_WIDGET (monitor)), NULL, FALSE); @@ -340,8 +331,8 @@ void xtm_process_monitor_clear (XtmProcessMonitor *monitor) { g_return_if_fail (XTM_IS_PROCESS_MONITOR (monitor)); - g_array_set_size (monitor->history, 0); - g_array_set_size (monitor->history_swap, 0); + for (int graph = 0; graph < MAX_GRAPH; ++graph) + g_array_set_size (monitor->history[graph], 0); if (GDK_IS_WINDOW (gtk_widget_get_window (GTK_WIDGET (monitor)))) gdk_window_invalidate_rect (gtk_widget_get_window (GTK_WIDGET (monitor)), NULL, FALSE); } diff --git a/src/process-monitor.h b/src/process-monitor.h index 6755a56..1714899 100644 --- a/src/process-monitor.h +++ b/src/process-monitor.h @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024 Jehan-Antoine Vayssade, * Copyright (c) 2010 Mike Massonnet, * * This program is free software; you can redistribute it and/or modify @@ -24,7 +25,7 @@ typedef struct _XtmProcessMonitor XtmProcessMonitor; GType xtm_process_monitor_get_type (void); GtkWidget *xtm_process_monitor_new (void); -void xtm_process_monitor_add_peak (XtmProcessMonitor *monitor, gfloat peak, gfloat peak_swap); +void xtm_process_monitor_add_peak (XtmProcessMonitor *monitor, gfloat peak, gint index); void xtm_process_monitor_set_step_size (XtmProcessMonitor *monitor, gfloat step_size); void xtm_process_monitor_set_type (XtmProcessMonitor *monitor, gint type); void xtm_process_monitor_clear (XtmProcessMonitor *monitor); diff --git a/src/process-statusbar.c b/src/process-statusbar.c index 484d7d3..d545d92 100644 --- a/src/process-statusbar.c +++ b/src/process-statusbar.c @@ -26,7 +26,11 @@ enum PROP_SWAP, PROP_SHOW_SWAP, PROP_NUM_PROCESSES, + PROP_NETWORK_RX, + PROP_NETWORK_TX, + PROP_NETWORK_ERROR, }; + typedef struct _XtmProcessStatusbarClass XtmProcessStatusbarClass; struct _XtmProcessStatusbarClass { @@ -43,11 +47,19 @@ struct _XtmProcessStatusbar GtkWidget *label_memory; GtkWidget *label_swap; + GtkWidget *label_net_rx; + GtkWidget *label_net_tx; + GtkWidget *label_net_error; + gfloat cpu; gchar memory[64]; gchar swap[64]; guint num_processes; + gfloat tcp_rx; + gfloat tcp_tx; + guint64 tcp_error; + gboolean dark_mode; }; G_DEFINE_TYPE (XtmProcessStatusbar, xtm_process_statusbar, GTK_TYPE_BOX) @@ -74,6 +86,12 @@ xtm_process_statusbar_class_init (XtmProcessStatusbarClass *klass) g_param_spec_boolean ("show-swap", "ShowSwap", "Show or hide swap usage", TRUE, G_PARAM_WRITABLE)); g_object_class_install_property (class, PROP_NUM_PROCESSES, g_param_spec_uint ("num-processes", "NumProcesses", "Number of processes", 0, G_MAXUINT, 0, G_PARAM_CONSTRUCT | G_PARAM_WRITABLE)); + g_object_class_install_property (class, PROP_NETWORK_RX, + g_param_spec_float ("network-rx", "RX", "Net rx", 0, 100, 0, G_PARAM_CONSTRUCT | G_PARAM_WRITABLE)); + g_object_class_install_property (class, PROP_NETWORK_TX, + g_param_spec_float ("network-tx", "TX", "Net tx", 0, 100, 0, G_PARAM_CONSTRUCT | G_PARAM_WRITABLE)); + g_object_class_install_property (class, PROP_NETWORK_ERROR, + g_param_spec_uint64 ("network-error", "NetEror", "Number of error since last update", 0, G_MAXUINT64, 0, G_PARAM_CONSTRUCT | G_PARAM_WRITABLE)); } static void @@ -99,13 +117,16 @@ xtm_process_statusbar_on_notify_theme_name (XtmProcessStatusbar *statusbar) xtm_process_statusbar_set_label_style (statusbar, statusbar->label_num_processes); xtm_process_statusbar_set_label_style (statusbar, statusbar->label_memory); xtm_process_statusbar_set_label_style (statusbar, statusbar->label_swap); + xtm_process_statusbar_set_label_style (statusbar, statusbar->label_net_rx); + xtm_process_statusbar_set_label_style (statusbar, statusbar->label_net_tx); + xtm_process_statusbar_set_label_style (statusbar, statusbar->label_net_error); } static void xtm_process_statusbar_init (XtmProcessStatusbar *statusbar) { GtkSettings *settings = gtk_settings_get_default (); - GtkWidget *hbox, *hbox_cpu, *hbox_mem; + GtkWidget *hbox, *hbox_cpu, *hbox_net, *hbox_mem; GtkStyleContext *context; GtkCssProvider *provider; statusbar->settings = xtm_settings_get_default (); @@ -116,6 +137,7 @@ xtm_process_statusbar_init (XtmProcessStatusbar *statusbar) hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 16); hbox_cpu = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 16); + hbox_net = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 16); hbox_mem = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 16); statusbar->label_cpu = gtk_label_new (NULL); @@ -143,11 +165,39 @@ xtm_process_statusbar_init (XtmProcessStatusbar *statusbar) gtk_box_pack_start (GTK_BOX (hbox_mem), statusbar->label_swap, TRUE, FALSE, 0); context = gtk_widget_get_style_context (statusbar->label_swap); provider = gtk_css_provider_new (); - gtk_css_provider_load_from_data (provider, "* { color: #75324d; } .dark { color: #8acdb2; }", -1, NULL); + gtk_css_provider_load_from_data (provider, "* { color: #808080; } .dark { color: #808080; }", -1, NULL); + gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref (provider); + + statusbar->label_net_rx = gtk_label_new (NULL); + gtk_label_set_ellipsize (GTK_LABEL (statusbar->label_net_rx), PANGO_ELLIPSIZE_END); + gtk_box_pack_start (GTK_BOX (hbox_net), statusbar->label_net_rx, TRUE, FALSE, 0); + context = gtk_widget_get_style_context (statusbar->label_net_rx); + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_data (provider, "* { color: #006ca2; } .dark { color: #ff935d; }", -1, NULL); + gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref (provider); + + statusbar->label_net_tx = gtk_label_new (NULL); + gtk_label_set_ellipsize (GTK_LABEL (statusbar->label_net_tx), PANGO_ELLIPSIZE_END); + gtk_box_pack_start (GTK_BOX (hbox_net), statusbar->label_net_tx, TRUE, FALSE, 0); + context = gtk_widget_get_style_context (statusbar->label_net_tx); + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_data (provider, "* { color: #05b6ce; } .dark { color: #fa4931; }", -1, NULL); + gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref (provider); + + statusbar->label_net_error = gtk_label_new (NULL); + gtk_label_set_ellipsize (GTK_LABEL (statusbar->label_net_error), PANGO_ELLIPSIZE_END); + gtk_box_pack_start (GTK_BOX (hbox_net), statusbar->label_net_error, TRUE, FALSE, 0); + context = gtk_widget_get_style_context (statusbar->label_net_error); + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_data (provider, "* { color: #008080; } .dark { color: #FF0000; }", -1, NULL); gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref (provider); gtk_box_pack_start (GTK_BOX (hbox), hbox_cpu, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (hbox), hbox_net, TRUE, TRUE, 0); gtk_box_pack_start (GTK_BOX (hbox), hbox_mem, TRUE, TRUE, 0); gtk_box_set_homogeneous (GTK_BOX (hbox), TRUE); @@ -157,6 +207,9 @@ xtm_process_statusbar_init (XtmProcessStatusbar *statusbar) xtm_process_statusbar_set_label_style (statusbar, statusbar->label_num_processes); xtm_process_statusbar_set_label_style (statusbar, statusbar->label_memory); xtm_process_statusbar_set_label_style (statusbar, statusbar->label_swap); + xtm_process_statusbar_set_label_style (statusbar, statusbar->label_net_rx); + xtm_process_statusbar_set_label_style (statusbar, statusbar->label_net_tx); + xtm_process_statusbar_set_label_style (statusbar, statusbar->label_net_error); gtk_widget_show_all (hbox); } @@ -224,6 +277,31 @@ xtm_process_statusbar_set_property (GObject *object, guint property_id, const GV g_free (text); break; + case PROP_NETWORK_RX: + statusbar->tcp_rx = g_value_get_float (value); + // statusbar->tcp_rx = interval_to_second(statusbar->tcp_rx, statusbar->settings); + float_value = rounded_float_value (statusbar->tcp_rx, statusbar->settings); + text = g_strdup_printf (_("RX: %s MB/s"), float_value); + gtk_label_set_text (GTK_LABEL (statusbar->label_net_rx), text); + g_free (text); + break; + + case PROP_NETWORK_TX: + statusbar->tcp_tx = g_value_get_float (value); + // statusbar->tcp_tx = interval_to_second(statusbar->tcp_tx, statusbar->settings); + float_value = rounded_float_value (statusbar->tcp_tx, statusbar->settings); + text = g_strdup_printf (_("TX: %s MB/s"), float_value); + gtk_label_set_text (GTK_LABEL (statusbar->label_net_tx), text); + g_free (text); + break; + + case PROP_NETWORK_ERROR: + statusbar->tcp_error = g_value_get_uint64 (value); + text = g_strdup_printf (_("Error: %lu"), statusbar->tcp_error); + gtk_label_set_text (GTK_LABEL (statusbar->label_net_error), text); + g_free (text); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; diff --git a/src/process-tree-view.c b/src/process-tree-view.c index ec86fbb..73c981a 100644 --- a/src/process-tree-view.c +++ b/src/process-tree-view.c @@ -11,6 +11,15 @@ #include "config.h" #endif +#include +#include +#include +#include +#include +#include +#include + +#include "network-analyzer.h" #include "process-tree-model.h" #include "process-tree-view.h" #include "settings.h" @@ -37,6 +46,9 @@ enum COLUMN_UID, COLUMN_CPU, COLUMN_GROUP_CPU, + COLUMN_PACKET_IN, + COLUMN_PACKET_OUT, + COLUMN_ACTIVE_SOCKET, COLUMN_PRIORITY, N_COLUMNS, }; @@ -88,6 +100,7 @@ xtm_process_tree_view_class_init (XtmProcessTreeViewClass *klass) static void xtm_process_tree_view_init (XtmProcessTreeView *treeview) { + XtmNetworkAnalyzer *network; GtkCellRenderer *cell_text, *cell_right_aligned; GtkTreeViewColumn *column; gboolean visible; @@ -122,6 +135,9 @@ xtm_process_tree_view_init (XtmProcessTreeView *treeview) G_TYPE_STRING, /* XTM_PTV_COLUMN_CPU_STR */ G_TYPE_FLOAT, /* XTM_PTV_COLUMN_GROUP_CPU */ G_TYPE_STRING, /* XTM_PTV_COLUMN_GROUP_CPU_STR */ + G_TYPE_UINT64, /* XTM_PTV_COLUMN_PACKET_IN */ + G_TYPE_UINT64, /* XTM_PTV_COLUMN_PACKET_OUT */ + G_TYPE_UINT64, /* XTM_PTV_COLUMN_ACTIVE_SOCKET */ G_TYPE_INT, /* XTM_PTV_COLUMN_PRIORITY */ G_TYPE_STRING, /* XTM_PTV_COLUMN_BACKGROUND */ G_TYPE_STRING, /* XTM_PTV_COLUMN_FOREGROUND */ @@ -245,6 +261,43 @@ xtm_process_tree_view_init (XtmProcessTreeView *treeview) g_signal_connect (column, "clicked", G_CALLBACK (column_clicked), treeview); gtk_tree_view_insert_column (GTK_TREE_VIEW (treeview), column, treeview->columns_positions[COLUMN_GROUP_CPU]); + /*************/ + + g_object_get (treeview->settings, "column-packet-in", &visible, NULL); + column = gtk_tree_view_column_new_with_attributes (_("PacketIn"), cell_right_aligned, "text", XTM_PTV_COLUMN_PACKET_IN, "cell-background", XTM_PTV_COLUMN_BACKGROUND, "foreground", XTM_PTV_COLUMN_FOREGROUND, NULL); + g_object_set (column, COLUMN_PROPERTIES, NULL); + g_object_set_data (G_OBJECT (column), "sort-column-id", GINT_TO_POINTER (XTM_PTV_COLUMN_PACKET_IN)); + g_object_set_data (G_OBJECT (column), "column-id", GINT_TO_POINTER (COLUMN_PACKET_IN)); + g_signal_connect (column, "clicked", G_CALLBACK (column_clicked), treeview); + gtk_tree_view_insert_column (GTK_TREE_VIEW (treeview), column, treeview->columns_positions[COLUMN_PACKET_IN]); + + g_object_get (treeview->settings, "column-packet-out", &visible, NULL); + column = gtk_tree_view_column_new_with_attributes (_("PacketOut"), cell_right_aligned, "text", XTM_PTV_COLUMN_PACKET_OUT, "cell-background", XTM_PTV_COLUMN_BACKGROUND, "foreground", XTM_PTV_COLUMN_FOREGROUND, NULL); + g_object_set (column, COLUMN_PROPERTIES, NULL); + g_object_set_data (G_OBJECT (column), "sort-column-id", GINT_TO_POINTER (XTM_PTV_COLUMN_PACKET_OUT)); + g_object_set_data (G_OBJECT (column), "column-id", GINT_TO_POINTER (COLUMN_PACKET_OUT)); + g_signal_connect (column, "clicked", G_CALLBACK (column_clicked), treeview); + gtk_tree_view_insert_column (GTK_TREE_VIEW (treeview), column, treeview->columns_positions[COLUMN_PACKET_OUT]); + + g_object_get (treeview->settings, "column-active-socket", &visible, NULL); + column = gtk_tree_view_column_new_with_attributes (_("ActiveSocket"), cell_right_aligned, "text", XTM_PTV_COLUMN_ACTIVE_SOCKET, "cell-background", XTM_PTV_COLUMN_BACKGROUND, "foreground", XTM_PTV_COLUMN_FOREGROUND, NULL); + g_object_set (column, COLUMN_PROPERTIES, NULL); + g_object_set_data (G_OBJECT (column), "sort-column-id", GINT_TO_POINTER (XTM_PTV_COLUMN_ACTIVE_SOCKET)); + g_object_set_data (G_OBJECT (column), "column-id", GINT_TO_POINTER (COLUMN_ACTIVE_SOCKET)); + g_signal_connect (column, "clicked", G_CALLBACK (column_clicked), treeview); + gtk_tree_view_insert_column (GTK_TREE_VIEW (treeview), column, treeview->columns_positions[COLUMN_ACTIVE_SOCKET]); + + // insufficient permission + network = xtm_network_analyzer_get_default (); + if (network == NULL) + { + gtk_tree_view_column_set_visible (gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), treeview->columns_positions[COLUMN_PACKET_IN]), FALSE); + gtk_tree_view_column_set_visible (gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), treeview->columns_positions[COLUMN_PACKET_OUT]), FALSE); + gtk_tree_view_column_set_visible (gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), treeview->columns_positions[COLUMN_ACTIVE_SOCKET]), FALSE); + } + + /*******************/ + g_object_get (treeview->settings, "column-priority", &visible, NULL); /* TRANSLATORS: “Prio.” is short for Priority, it appears in the tree view header. */ column = gtk_tree_view_column_new_with_attributes (_("Prio."), cell_right_aligned, "text", XTM_PTV_COLUMN_PRIORITY, "cell-background", XTM_PTV_COLUMN_BACKGROUND, "foreground", XTM_PTV_COLUMN_FOREGROUND, NULL); @@ -842,6 +895,12 @@ settings_changed (GObject *object, GParamSpec *pspec, XtmProcessTreeView *treevi column_id = COLUMN_GROUP_CPU; else if (!g_strcmp0 (pspec->name, "column-priority")) column_id = COLUMN_PRIORITY; + else if (!g_strcmp0 (pspec->name, "column-packet-in")) + column_id = COLUMN_PACKET_IN; + else if (!g_strcmp0 (pspec->name, "column-packet-out")) + column_id = COLUMN_PACKET_OUT; + else if (!g_strcmp0 (pspec->name, "column-active-socket")) + column_id = COLUMN_ACTIVE_SOCKET; g_object_get (object, pspec->name, &visible, NULL); gtk_tree_view_column_set_visible (gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), treeview->columns_positions[column_id]), visible); diff --git a/src/process-tree-view.h b/src/process-tree-view.h index f8950b9..62dcb0d 100644 --- a/src/process-tree-view.h +++ b/src/process-tree-view.h @@ -39,6 +39,9 @@ enum XTM_PTV_COLUMN_CPU_STR, XTM_PTV_COLUMN_GROUP_CPU, XTM_PTV_COLUMN_GROUP_CPU_STR, + XTM_PTV_COLUMN_PACKET_IN, + XTM_PTV_COLUMN_PACKET_OUT, + XTM_PTV_COLUMN_ACTIVE_SOCKET, XTM_PTV_COLUMN_PRIORITY, XTM_PTV_COLUMN_BACKGROUND, XTM_PTV_COLUMN_FOREGROUND, diff --git a/src/process-window.c b/src/process-window.c index eaa020b..2602773 100644 --- a/src/process-window.c +++ b/src/process-window.c @@ -52,6 +52,7 @@ struct _XtmProcessWindow GtkWidget *filter_searchbar; GtkWidget *cpu_monitor; GtkWidget *mem_monitor; + GtkWidget *net_monitor; GtkWidget *vpaned; GtkWidget *treeview; GtkWidget *statusbar; @@ -347,6 +348,13 @@ xtm_process_window_init (XtmProcessWindow *window) gtk_widget_show (window->mem_monitor); gtk_container_add (GTK_CONTAINER (toolitem), window->mem_monitor); + toolitem = GTK_WIDGET (gtk_builder_get_object (window->builder, "graph-net")); + window->net_monitor = xtm_process_monitor_new (); + xtm_process_monitor_set_step_size (XTM_PROCESS_MONITOR (window->net_monitor), refresh_rate / 1000.0f); + xtm_process_monitor_set_type (XTM_PROCESS_MONITOR (window->net_monitor), 2); + gtk_widget_show (window->net_monitor); + gtk_container_add (GTK_CONTAINER (toolitem), window->net_monitor); + g_signal_connect_swapped (window->settings, "notify::refresh-rate", G_CALLBACK (monitor_update_step_size), window); } @@ -489,6 +497,7 @@ monitor_update_step_size (XtmProcessWindow *window) g_object_get (window->settings, "refresh-rate", &refresh_rate, NULL); g_object_set (window->cpu_monitor, "step-size", refresh_rate / 1000.0, NULL); g_object_set (window->mem_monitor, "step-size", refresh_rate / 1000.0, NULL); + g_object_set (window->net_monitor, "step-size", refresh_rate / 1000.0, NULL); } /** @@ -511,6 +520,12 @@ xtm_process_window_show (GtkWidget *widget) GTK_WIDGET_CLASS (xtm_process_window_parent_class)->show (widget); } +GtkWindow * +xtm_process_window_get (XtmProcessWindow *window) +{ + return GTK_WINDOW (window->window); +} + static void xtm_process_window_hide (GtkWidget *widget) { @@ -533,24 +548,40 @@ xtm_process_window_get_model (XtmProcessWindow *window) } void -xtm_process_window_set_system_info (XtmProcessWindow *window, guint num_processes, gfloat cpu, gfloat memory, gchar *memory_str, gfloat swap, gchar *swap_str) +xtm_process_window_set_system_info (XtmProcessWindow *window, guint num_processes, gfloat cpu, gfloat memory, gchar *memory_str, gfloat swap, gchar *swap_str, guint64 tcp_rx, guint64 tcp_tx, guint64 tcp_error) { - gchar text[100]; + gchar text[200]; gchar value[4]; g_return_if_fail (XTM_IS_PROCESS_WINDOW (window)); g_return_if_fail (GTK_IS_BOX (window->statusbar)); - g_object_set (window->statusbar, "num-processes", num_processes, "cpu", cpu, "memory", memory_str, "swap", swap_str, NULL); + g_object_set ( + window->statusbar, + "num-processes", num_processes, + "cpu", cpu, + "memory", memory_str, + "swap", swap_str, + "network-rx", tcp_rx * 1e-5, + "network-tx", tcp_tx * 1e-5, + "network-error", tcp_error, + NULL); - xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->cpu_monitor), cpu / 100.0f, -1.0); + xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->cpu_monitor), cpu / 100.0f, 0); g_snprintf (value, sizeof (value), "%.0f", cpu); g_snprintf (text, sizeof (text), _("CPU: %s%%"), value); gtk_widget_set_tooltip_text (window->cpu_monitor, text); - xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->mem_monitor), memory / 100.0f, swap / 100.0f); + xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->mem_monitor), memory / 100.0f, 0); + xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->mem_monitor), swap / 100.0f, 1); g_snprintf (text, sizeof (text), _("Memory: %s"), memory_str); gtk_widget_set_tooltip_text (window->mem_monitor, text); + + xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->net_monitor), tcp_rx * 1e-5, 0); + xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->net_monitor), tcp_tx * 1e-5, 1); + xtm_process_monitor_add_peak (XTM_PROCESS_MONITOR (window->net_monitor), tcp_error, 2); + g_snprintf (text, sizeof (text), _("Network rx=%0.2f MB/s\nNetwork tx=%0.2f MB/s \nNetwork error=%lu error/s"), tcp_rx * 1e-5, tcp_tx * 1e-5, tcp_error); + gtk_widget_set_tooltip_text (window->net_monitor, text); } void diff --git a/src/process-window.h b/src/process-window.h index 3f552a3..5d30681 100644 --- a/src/process-window.h +++ b/src/process-window.h @@ -34,10 +34,11 @@ typedef struct _XtmProcessWindow XtmProcessWindow; GType xtm_process_window_get_type (void); GtkWidget *xtm_process_window_new (void); +GtkWindow *xtm_process_window_get (XtmProcessWindow *window); void xtm_process_window_settings_init (XtmProcessWindow *window, XfconfChannel *channel); void xtm_process_window_show (GtkWidget *widget); GtkTreeModel *xtm_process_window_get_model (XtmProcessWindow *window); -void xtm_process_window_set_system_info (XtmProcessWindow *window, guint num_processes, gfloat cpu, gfloat memory, gchar *memory_str, gfloat swap, gchar *swap_str); +void xtm_process_window_set_system_info (XtmProcessWindow *window, guint num_processes, gfloat cpu, gfloat memory, gchar *memory_str, gfloat swap, gchar *swap_str, guint64 tcp_rx, guint64 tcp_tx, guint64 tcp_error); void xtm_process_window_show_swap_usage (XtmProcessWindow *window, gboolean show_swap_usage); #endif /* !PROCESS_WINDOW_H */ diff --git a/src/process-window.ui b/src/process-window.ui index 896d832..88d4adb 100644 --- a/src/process-window.ui +++ b/src/process-window.ui @@ -198,6 +198,27 @@ 1 + + + True + False + True + False + 0 + none + + + + + + + + + True + True + 1 + + True diff --git a/src/settings-dialog.c b/src/settings-dialog.c index 18f3ea7..6f7e5ea 100644 --- a/src/settings-dialog.c +++ b/src/settings-dialog.c @@ -11,6 +11,9 @@ #include "config.h" #endif +#include + +#include "network-analyzer.h" #include "settings-dialog.h" #include "settings-dialog_ui.h" #include "settings.h" @@ -19,8 +22,6 @@ #include #endif -#include - static void show_about_dialog (GtkWidget *widget, gpointer user_data); static GtkWidget *xtm_settings_dialog_new (GtkBuilder *builder, GtkWidget *parent_window); @@ -117,18 +118,22 @@ show_about_dialog (GtkWidget *widget, gpointer user_data) "(c) 2005-2008 Johannes Zellner", "", "FreeBSD", + " \342\200\242 Jehan-Antoine Vayssade", " \342\200\242 Rozhuk Ivan", " \342\200\242 Mike Massonnet", " \342\200\242 Oliver Lehmann", "", "OpenBSD", + " \342\200\242 Jehan-Antoine Vayssade", " \342\200\242 Landry Breuil", "", "Linux", + " \342\200\242 Jehan-Antoine Vayssade", " \342\200\242 Johannes Zellner", " \342\200\242 Mike Massonnet", "", "OpenSolaris", + " \342\200\242 Jehan-Antoine Vayssade", " \342\200\242 Mike Massonnet", " \342\200\242 Peter Tribble", NULL @@ -174,6 +179,7 @@ xtm_settings_dialog_new (GtkBuilder *builder, GtkWidget *parent_window) GtkWidget *dialog; GtkWidget *button; XtmSettings *settings; + XtmNetworkAnalyzer *network; settings = xtm_settings_get_default (); dialog = GTK_WIDGET (gtk_builder_get_object (builder, "settings-dialog")); @@ -214,8 +220,20 @@ xtm_settings_dialog_new (GtkBuilder *builder, GtkWidget *parent_window) builder_bind_toggle_button (builder, "uid", settings, "column-uid"); builder_bind_toggle_button (builder, "cpu", settings, "column-cpu"); builder_bind_toggle_button (builder, "group-cpu", settings, "column-group-cpu"); + builder_bind_toggle_button (builder, "packet-in", settings, "column-packet-in"); + builder_bind_toggle_button (builder, "packet-out", settings, "column-packet-out"); + builder_bind_toggle_button (builder, "active-socket", settings, "column-active-socket"); builder_bind_toggle_button (builder, "priority", settings, "column-priority"); + // insufficient permission + network = xtm_network_analyzer_get_default (); + + if (network == NULL) + { + gtk_widget_hide (GTK_WIDGET (gtk_builder_get_object (builder, "packet-in"))); + gtk_widget_hide (GTK_WIDGET (gtk_builder_get_object (builder, "packet-out"))); + gtk_widget_hide (GTK_WIDGET (gtk_builder_get_object (builder, "active-socket"))); + } button = GTK_WIDGET (gtk_builder_get_object (builder, "button-about")); g_signal_connect (button, "clicked", G_CALLBACK (show_about_dialog), dialog); diff --git a/src/settings-dialog.ui b/src/settings-dialog.ui index cd5879f..9fa785c 100644 --- a/src/settings-dialog.ui +++ b/src/settings-dialog.ui @@ -508,6 +508,48 @@ 10 + + + Packet in + True + True + False + True + + + False + True + 10 + + + + + Packet out + True + True + False + True + + + False + True + 10 + + + + + Active Socket + True + True + False + True + + + False + True + 10 + + 1 diff --git a/src/settings.c b/src/settings.c index 2693e22..f226356 100644 --- a/src/settings.c +++ b/src/settings.c @@ -47,6 +47,9 @@ enum PROP_COLUMN_CPU, PROP_COLUMN_GROUP_CPU, PROP_COLUMN_PRIORITY, + PROP_COLUMN_PACKET_IN, + PROP_COLUMN_PACKET_OUT, + PROP_COLUMN_ACTIVE_SOCKET, PROP_SORT_COLUMN_ID, PROP_SORT_TYPE, PROP_HANDLE_POSITION, @@ -116,6 +119,14 @@ xtm_settings_class_init (XtmSettingsClass *klass) g_param_spec_boolean ("column-cpu", "ColumnCPU", "Show column CPU", TRUE, G_PARAM_READWRITE)); g_object_class_install_property (class, PROP_COLUMN_GROUP_CPU, g_param_spec_boolean ("column-group-cpu", "ColumnGroupCPU", "Show column Group CPU", TRUE, G_PARAM_READWRITE)); + + g_object_class_install_property (class, PROP_COLUMN_PACKET_IN, + g_param_spec_boolean ("column-packet-in", "ColumnPacketIn", "Show column packet in", TRUE, G_PARAM_READWRITE)); + g_object_class_install_property (class, PROP_COLUMN_PACKET_OUT, + g_param_spec_boolean ("column-packet-out", "ColumnPacketOut", "Show column packet out", TRUE, G_PARAM_READWRITE)); + g_object_class_install_property (class, PROP_COLUMN_PACKET_OUT, + g_param_spec_boolean ("column-active-socket", "ColumnActiveSocket", "Show number of used socket descriptor", TRUE, G_PARAM_READWRITE)); + g_object_class_install_property (class, PROP_COLUMN_PRIORITY, g_param_spec_boolean ("column-priority", "ColumnPriority", "Show column priority", FALSE, G_PARAM_READWRITE)); g_object_class_install_property (class, PROP_SORT_COLUMN_ID, @@ -219,6 +230,15 @@ xtm_settings_bind_xfconf (XtmSettings *settings, XfconfChannel *channel) G_OBJECT (settings), "column-cpu"); xfconf_g_property_bind (channel, SETTING_COLUMN_GROUP_CPU, G_TYPE_BOOLEAN, G_OBJECT (settings), "column-group-cpu"); + + + xfconf_g_property_bind (channel, SETTING_COLUMN_PACKET_IN, G_TYPE_BOOLEAN, + G_OBJECT (settings), "column-packet-in"); + xfconf_g_property_bind (channel, SETTING_COLUMN_PACKET_OUT, G_TYPE_BOOLEAN, + G_OBJECT (settings), "column-packet-out"); + xfconf_g_property_bind (channel, SETTING_COLUMN_ACTIVE_SOCKET, G_TYPE_BOOLEAN, + G_OBJECT (settings), "column-active-socket"); + xfconf_g_property_bind (channel, SETTING_COLUMN_PRIORITY, G_TYPE_BOOLEAN, G_OBJECT (settings), "column-priority"); diff --git a/src/settings.h b/src/settings.h index 3ed0eea..630e307 100644 --- a/src/settings.h +++ b/src/settings.h @@ -48,6 +48,9 @@ #define SETTING_COLUMN_UID "/columns/column-uid" #define SETTING_COLUMN_CPU "/columns/column-cpu" #define SETTING_COLUMN_GROUP_CPU "/columns/column-group-cpu" +#define SETTING_COLUMN_PACKET_IN "/columns/column-packet-in" +#define SETTING_COLUMN_PACKET_OUT "/columns/column-packet-out" +#define SETTING_COLUMN_ACTIVE_SOCKET "/columns/column-active-socket" #define SETTING_COLUMN_PRIORITY "/columns/column-priority" #define SETTING_COLUMN_SORT_ID "/columns/sort-id" #define SETTING_COLUMN_SORT_TYPE "/columns/sort-type" diff --git a/src/task-manager-bsd.c b/src/task-manager-bsd.c index b0f03b0..cbe6377 100644 --- a/src/task-manager-bsd.c +++ b/src/task-manager-bsd.c @@ -20,6 +20,8 @@ #include "config.h" #endif +#include "inode-to-sock.h" +#include "network-analyzer.h" #include "task-manager.h" #include @@ -40,14 +42,413 @@ /* for struct vmtotal */ #include +// clang-format off +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// clang-format on + /* errno */ #include + +#ifdef __OpenBSD__ extern int errno; +#else +// struct file in NetBSD +// ugly ! where is defined this ? +// plateform dependent +typedef long register_t; +typedef unsigned long paddr_t; +#include +#include +#include +#include +#include +#include +#include +#endif + +#define _KERNEL +#include +#include +#undef _KERNEL char *state_abbrev[] = { "", "start", "run", "sleep", "stop", "zomb", "dead", "onproc" }; +static XtmInodeToSock *inode_to_sock = NULL; +static XtmNetworkAnalyzer *analyzer = NULL; + +gboolean get_if_count (int *data); +gboolean get_network_usage_if (int interface, guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error); +struct kinfo_file *get_process_fds (int *nfiles, int kern, int arg); + + +#ifdef __OpenBSD__ +void list_process_fds (Task *task, struct kinfo_proc *kp); +#else +void list_process_fds (Task *task, struct kinfo_proc2 *kp); +#endif + +#ifdef HAVE_LIBPCAP +void +packet_callback (u_char *args, const struct pcap_pkthdr *header, const u_char *packet) +{ + // Extract source and destination IP addresses and ports from the packet + struct ether_header eth_header; + struct ip ip_header; + struct tcphdr tcp_header; + XtmNetworkAnalyzer *iface; + long int src_port, dst_port; + char local_mac[18]; + char src_mac[18]; + char dst_mac[18]; + + memcpy (ð_header, packet, sizeof (struct ether_header)); + memcpy (&ip_header, packet + sizeof (struct ether_header), sizeof (struct ip)); + memcpy (&tcp_header, packet + sizeof (struct ether_header) + sizeof (struct ip), sizeof (struct ip)); + + // cast -> increases required alignment from 1 to 2 [-Wcast-align] + // const struct ether_header *eth_header = (const struct ether_header *)packet; + // struct ip *ip_header = (struct ip *)(packet + sizeof (struct ether_header)); + // struct tcphdr *tcp_header = (struct tcphdr *)(packet + sizeof (struct ether_header) + sizeof (struct ip)); + // printf("%d, %d \n", eth_heade.ether_type , ip_header.ip_p); + + // Dropped non-ip packet + if (eth_header.ether_type != 8 || ip_header.ip_p != 6) + return; + + iface = (XtmNetworkAnalyzer *)args; + + src_port = ntohs (tcp_header.th_sport); + dst_port = ntohs (tcp_header.th_dport); + + // directly use strcmp on analyzer->mac, eth_header->ether_shost doesnt work + + snprintf (local_mac, sizeof (local_mac), + "%02X:%02X:%02X:%02X:%02X:%02X", + iface->mac[0], iface->mac[1], + iface->mac[2], iface->mac[3], + iface->mac[4], iface->mac[5]); + + snprintf (src_mac, sizeof (local_mac), + "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header.ether_shost[0], eth_header.ether_shost[1], + eth_header.ether_shost[2], eth_header.ether_shost[3], + eth_header.ether_shost[4], eth_header.ether_shost[5]); + + snprintf (dst_mac, sizeof (local_mac), + "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header.ether_dhost[0], eth_header.ether_dhost[1], + eth_header.ether_dhost[2], eth_header.ether_dhost[3], + eth_header.ether_dhost[4], eth_header.ether_dhost[5]); + + // Debug + // pthread_mutex_lock(&iface->lock); + + if (strcmp (local_mac, src_mac) == 0) + increament_packet_count (local_mac, "in ", iface->packetin, src_port); + + if (strcmp (local_mac, dst_mac) == 0) + increament_packet_count (local_mac, "out", iface->packetout, dst_port); + + // pthread_mutex_unlock(&iface->lock); +} +#endif + +gboolean +get_network_usage (guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error) +{ + // can also be get trough tcpstat struct + // int mib[] = { CTL_NET, PF_INET, IPPROTO_TCP, TCPCTL_STATS } + // sysctl(mib, sizeof(mib) / sizeof(mib[0]), %tcpstat, &len, NULL, 0) + // repeat for IPPROTO_UDP or use IPPROTO_ETHERIP + struct ifaddrs *ifaddr, *ifa; + + *tcp_error = 0; + *tcp_rx = 0; + *tcp_tx = 0; + + + if (getifaddrs (&ifaddr) == -1) + return -1; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr == NULL) + continue; + + // Check if the interface is a network interface (AF_PACKET for Linux) + if (ifa->ifa_addr->sa_family == AF_LINK) + { + struct if_data *ifdata = (struct if_data *)ifa->ifa_data; + // Check if the interface has a hardware address (MAC address) + if (ifdata != NULL) + { + *tcp_error += ifdata->ifi_oerrors + ifdata->ifi_ierrors; + ; + *tcp_rx += ifdata->ifi_ibytes; + *tcp_tx += ifdata->ifi_obytes; + } + // printf("%d, %d \n", *tcp_rx, *tcp_tx); + } + } + + freeifaddrs (ifaddr); + + return 0; +} + +int +get_mac_address (const char *device, uint8_t mac[6]) +{ + struct ifaddrs *ifaddr, *ifa; + + if (getifaddrs (&ifaddr) == -1) + return -1; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr == NULL) + continue; + + // Check if the interface is a network interface (AF_PACKET for Linux) + if (ifa->ifa_addr->sa_family == AF_LINK && strcmp (device, ifa->ifa_name) == 0) + { + // Check if the interface has a hardware address (MAC address) + if (ifa->ifa_data != NULL) + { + // warning: cast from 'struct sockaddr *' to 'struct sockaddr_dl *' increases required alignment from 1 to 2 + // struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; + struct sockaddr_dl sdl; + memcpy (&sdl, ifa->ifa_addr, sizeof (struct sockaddr_dl)); + memcpy (mac, sdl.sdl_data + sdl.sdl_nlen, sizeof (uint8_t) * 6); + freeifaddrs (ifaddr); + return 0; + } + } + } + + freeifaddrs (ifaddr); + return -1; +} + +struct kinfo_file * +get_process_fds (int *nfiles, int kern, int arg) +{ + // inspired by NetBSD pstat.c + int mib[6]; + size_t len; + struct kinfo_file *kf; + + // Set the MIB (Management Information Base) for the sysctl call + mib[0] = CTL_KERN; + +#ifdef __OpenBSD__ + mib[1] = KERN_FILE; +#else + mib[1] = KERN_FILE2; +#endif + + mib[2] = kern; + mib[3] = arg; + mib[4] = sizeof (struct kinfo_file); + mib[5] = 0; + + // Get the number of open files + if (sysctl (mib, 6, NULL, &len, NULL, 0) < 0) + { + printf ("sysctl Get the number of opened fd"); + return NULL; + } + + *nfiles = len / sizeof (struct kinfo_file); + + // Allocate memory for the kinfo_file structures + kf = malloc (len); + if (kf == NULL) + { + printf ("malloc"); + return NULL; + } + + mib[5] = len / sizeof (struct kinfo_file); + + // Get the list of open files + if (sysctl (mib, 6, kf, &len, NULL, 0) < 0) + { + printf ("sysctl, enable to get kinfo_file *"); + free (kf); + return NULL; + } + + return kf; +} + +void +xtm_refresh_inode_to_sock (XtmInodeToSock *its) +{ +#ifdef __OpenBSD__ + // unneeded +#else + // unneeded +#endif +} + +void +#ifdef __OpenBSD__ +list_process_fds (Task *task, struct kinfo_proc *kp) +#else +list_process_fds (Task *task, struct kinfo_proc2 *kp) +#endif +{ +#ifdef __OpenBSD__ + // inspired by openbsd netstat/inet.c + // inspired by openbsd fstat/fstat.c + + long int nfiles, port; + XtmNetworkAnalyzer *current; + struct kinfo_file *kf = get_process_fds (&nfiles, KERN_FILE_BYPID, task->pid); + + if (kf == NULL) + return; + + // Parse the kinfo_file structures + for (int i = 0; i < nfiles; i++) + { + if (kf[i].f_type == DTYPE_SOCKET) + { + // interesting properties + // task->packet_in += kf[i].f_rxfer; + // task->packet_in += kf[i].f_rbytes; + // task->packet_out += kf[i].f_rwfer; + // task->packet_out += kf[i].f_wbytes;netstat + + current = analyzer; + while (current != NULL) + { + port = ntohs (kf[i].inp_lport); + task->packet_in += (guint64)g_hash_table_lookup (current->packetin, &port); + task->packet_out += (guint64)g_hash_table_lookup (current->packetout, &port); + current = current->next; + } + + task->active_socket += 1; + } + } + + free (kf); +#else + // inspired by netbsd fstat/fstat.c + XtmNetworkAnalyzer *current; + char *memf, *nlistf; + char buf[_POSIX2_LINE_MAX]; + kvm_t *kd; + struct filedesc filed; + struct fdtab dt; + size_t len; + + fdfile_t **ofiles; + fdfile_t *fp; + struct socket *sock; + struct socket so; + struct file file; + fdfile_t fdfile; + struct protosw proto; + struct domain dom; + struct in4pcb in4pcb; + struct in6pcb in6pcb; + struct inpcb *inp; + long int port; + + nlistf = memf = NULL; + kd = kvm_openfiles (nlistf, memf, NULL, O_RDONLY, buf); + + kvm_read (kd, kp->p_fd, &filed, sizeof (filed)); + kvm_read (kd, (u_long)filed.fd_dt, &dt, sizeof (dt)); + + len = (filed.fd_lastfile + 1) * sizeof (fdfile_t *); + ofiles = malloc (len); + kvm_read (kd, (u_long)&filed.fd_dt->dt_ff, ofiles, (filed.fd_lastfile + 1) * (sizeof (fdfile_t *))); + + for (int i = 0; i <= filed.fd_lastfile; i++) + { + if (ofiles[i] == NULL) + continue; + + fp = ofiles[i]; + + kvm_read (kd, (u_long)fp, &fdfile, sizeof (fdfile)); + + if (fdfile.ff_file == NULL) + continue; + + kvm_read (kd, (u_long)fdfile.ff_file, &file, sizeof (file)); + + if (file.f_type != DTYPE_SOCKET) + continue; + + sock = (struct socket *)file.f_data; + kvm_read (kd, (u_long)sock, &so, sizeof (struct socket)); + kvm_read (kd, (u_long)so.so_proto, &proto, sizeof (struct protosw)); + kvm_read (kd, (u_long)proto.pr_domain, &dom, sizeof (struct domain)); + + port = 0; + + if (dom.dom_family == AF_INET) + { + if (proto.pr_protocol == IPPROTO_TCP) + { + if (so.so_pcb == NULL) + continue; + + kvm_read (kd, (u_long)so.so_pcb, (char *)&in4pcb, sizeof (in4pcb)); + inp = (struct inpcb *)&in4pcb; + port = ntohs (inp->inp_lport); + } + } + + if (dom.dom_family == AF_INET6) + { + if (proto.pr_protocol == IPPROTO_TCP) + { + if (so.so_pcb == NULL) + continue; + + kvm_read (kd, (u_long)so.so_pcb, (char *)&in6pcb, sizeof (in6pcb)); + inp = (struct inpcb *)&in6pcb; + port = ntohs (inp->inp_lport); + } + } + + if (port == 0) + continue; + + task->active_socket += 1; + + current = analyzer; + if (current != NULL) + { + task->packet_in += (guint64)g_hash_table_lookup (current->packetin, &port); + task->packet_out += (guint64)g_hash_table_lookup (current->packetout, &port); + current = current->next; + } + } + + kvm_close (kd); + free (ofiles); + +#endif +} + gboolean get_task_list (GArray *task_list) { @@ -57,11 +458,22 @@ get_task_list (GArray *task_list) struct kinfo_proc *kp; #else struct kinfo_proc2 *kp; + char errbuf[_POSIX2_LINE_MAX]; + kvm_t *kdp; #endif Task t; char **args; - gchar *buf; int nproc, i; + gchar *buf; + +#ifdef __OpenBSD__ +#else + kdp = kvm_open (NULL, NULL, NULL, KVM_NO_FILES, errbuf); +#endif + + analyzer = xtm_network_analyzer_get_default (); + inode_to_sock = xtm_inode_to_sock_get_default (); + xtm_refresh_inode_to_sock (inode_to_sock); mib[0] = CTL_KERN; #ifdef __OpenBSD__ @@ -116,6 +528,8 @@ get_task_list (GArray *task_list) t.rss = p.p_vm_rssize * getpagesize (); g_snprintf (t.state, sizeof t.state, "%s", state_abbrev[p.p_stat]); g_strlcpy (t.name, p.p_comm, strlen (p.p_comm) + 1); + +#ifdef __OpenBSD__ /* shamelessly stolen from top/machine.c */ if (!P_ZOMBIE (&p)) { @@ -132,8 +546,10 @@ get_task_list (GArray *task_list) mib[1] = KERN_PROC_ARGS; mib[2] = t.pid; mib[3] = KERN_PROC_ARGV; + if (sysctl (mib, 4, args, &size, NULL, 0) == 0) break; + if (errno != ENOMEM) { /* ESRCH: process disappeared */ /* printf ("process with pid %d disappeared, errno=%d\n", t.pid, errno); */ @@ -142,15 +558,40 @@ get_task_list (GArray *task_list) break; } } + buf = g_strjoinv (" ", args); g_assert (g_utf8_validate (buf, -1, NULL)); g_strlcpy (t.cmdline, buf, sizeof t.cmdline); g_free (buf); free (args); } +#else + // assuming NetBSD 10.0 + // https://github.com/NetBSD/src/blob/trunk/lib/libkvm/kvm_proc.c#L1116 + // https://github.com/NetBSD/pkgsrc/blob/trunk/sysutils/xfce4-taskmanager/files/task-manager-netbsd.c + // fixing code used in __OpenBSD__ crash at g_strjoinv due to strlen + + if (!(kp[i].p_stat == SDEAD)) + { + args = kvm_getargv2 (kdp, &kp[i], BUFSIZ); + if (args != NULL) + { + buf = g_strjoinv (" ", args); + g_strlcpy (t.cmdline, buf, sizeof (t.cmdline)); + g_free (buf); + // Memory seem stable without that + // otherwise i get a segfault + // free (args); + } + } +#endif t.cpu_user = (100.0f * ((gfloat)p.p_pctcpu / FSCALE)); t.cpu_system = 0.0f; /* TODO ? */ + + if (analyzer != NULL) + list_process_fds (&t, &p); + g_array_append_val (task_list, t); } free (kp); @@ -201,7 +642,15 @@ get_cpu_usage (gushort *cpu_count, gfloat *cpu_user, gfloat *cpu_system) static gulong cur_user = 0, cur_system = 0, cur_total = 0; static gulong old_user = 0, old_system = 0, old_total = 0; +#ifdef KERN_CPTIME int mib[] = { CTL_KERN, KERN_CPTIME }; +#elif defined KERN_CPTIME2 + int mib[] = { CTL_KERN, KERN_CPTIME2 }; +#else + // NetBSD 10.0 + int mib[] = { CTL_KERN, KERN_CP_TIME }; +#endif + glong cp_time[CPUSTATES]; gsize size = sizeof (cp_time); if (sysctl (mib, 2, &cp_time, &size, NULL, 0) < 0) diff --git a/src/task-manager-freebsd.c b/src/task-manager-freebsd.c index 9983f1b..0fd2e58 100644 --- a/src/task-manager-freebsd.c +++ b/src/task-manager-freebsd.c @@ -15,21 +15,40 @@ #include "task-manager.h" +// clang-format off #include #include #include #include + #include #include +#include #include #include #include -#include #if defined(__FreeBSD_version) && __FreeBSD_version >= 900044 #include #endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +// clang-format on + +#include + +#include "inode-to-sock.h" +#include "network-analyzer.h" +#include "task-manager.h" + static const gchar ki_stat2state[] = { ' ', /* - */ 'R', /* SIDL */ @@ -41,6 +60,152 @@ static const gchar ki_stat2state[] = { 'L' /* SLOCK */ }; +static XtmInodeToSock *inode_to_sock = NULL; +static XtmNetworkAnalyzer *analyzer = NULL; + +void list_process_fds (Task *task); +gboolean get_if_count (int *data); +gboolean get_network_usage_if (int interface, guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error); + +#ifdef HAVE_LIBPCAP +void +packet_callback (u_char *args, const struct pcap_pkthdr *header, const u_char *packet) +{ + // Extract source and destination IP addresses and ports from the packet + struct ether_header *eth_header = (struct ether_header *)packet; + struct ip *ip_header = (struct ip *)(packet + sizeof (struct ether_header)); + struct tcphdr *tcp_header = (struct tcphdr *)(packet + sizeof (struct ether_header) + sizeof (struct ip)); + XtmNetworkAnalyzer *iface = (XtmNetworkAnalyzer *)args; + + // Dropped non-ip packet + if (eth_header->ether_type != 8 || ip_header->ip_p != 6) + return; + + long int src_port = ntohs (tcp_header->th_sport); + long int dst_port = ntohs (tcp_header->th_dport); + + // directly use strcmp on analyzer->mac, eth_header->ether_shost doesnt work + + char local_mac[18]; + char src_mac[18]; + char dst_mac[18]; + + sprintf (local_mac, + "%02X:%02X:%02X:%02X:%02X:%02X", + iface->mac[0], iface->mac[1], + iface->mac[2], iface->mac[3], + iface->mac[4], iface->mac[5]); + + sprintf (src_mac, + "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header->ether_shost[0], eth_header->ether_shost[1], + eth_header->ether_shost[2], eth_header->ether_shost[3], + eth_header->ether_shost[4], eth_header->ether_shost[5]); + + sprintf (dst_mac, + "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header->ether_dhost[0], eth_header->ether_dhost[1], + eth_header->ether_dhost[2], eth_header->ether_dhost[3], + eth_header->ether_dhost[4], eth_header->ether_dhost[5]); + + // Debug + // pthread_mutex_lock(&iface->lock); + + if (strcmp (local_mac, src_mac) == 0) + increament_packet_count (local_mac, "in ", iface->packetin, src_port); + + if (strcmp (local_mac, dst_mac) == 0) + increament_packet_count (local_mac, "out", iface->packetout, dst_port); + + // pthread_mutex_unlock(&iface->lock); +} +#endif + + +gboolean +get_if_count (int *data) +{ + size_t len = sizeof (*data); + + static int32_t name[] = { CTL_NET, PF_LINK, NETLINK_GENERIC, IFMIB_SYSTEM, IFMIB_IFCOUNT }; + name[0] = CTL_NET; + name[1] = PF_LINK; + name[2] = NETLINK_GENERIC; + name[3] = IFMIB_SYSTEM; + name[4] = IFMIB_IFCOUNT; + + return sysctl (name, 5, data, &len, 0, 0) < 0; +} + +gboolean +get_network_usage_if (int interface, guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error) +{ + struct ifmibdata data; + size_t len = sizeof (data); + + static int32_t name[] = { CTL_NET, PF_LINK, NETLINK_GENERIC, IFMIB_IFDATA, 0, IFDATA_GENERAL }; + name[4] = interface; + + if (sysctl (name, 6, &data, &len, 0, 0) < 0) + return 1; + + //*tcp_error = data.ifmd_data.ifi_oerrors + data.ifmd_data.ifi_ierrors; + *tcp_rx += data.ifmd_data.ifi_ibytes; + *tcp_tx += data.ifmd_data.ifi_obytes; + + return 0; +} + +gboolean +get_network_usage (guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error) +{ + int ifcount = 0; + + if (get_if_count (&ifcount)) + return 1; + + *tcp_error = 0; + *tcp_rx = 0; + *tcp_tx = 0; + + for (int i = 0; i < ifcount; ++i) + get_network_usage_if (i, tcp_rx, tcp_tx, tcp_error); + + return 0; +} + +int +get_mac_address (const char *device, uint8_t mac[6]) +{ + struct ifaddrs *ifaddr, *ifa; + + if (getifaddrs (&ifaddr) == -1) + return -1; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr == NULL) + continue; + + int family = ifa->ifa_addr->sa_family; + + // Check if the interface is a network interface (AF_PACKET for Linux) + if (family == AF_LINK && strcmp (device, ifa->ifa_name) == 0) + { + // Check if the interface has a hardware address (MAC address) + if (ifa->ifa_data != NULL) + { + struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; + memcpy (mac, sdl->sdl_data + sdl->sdl_nlen, sizeof (uint8_t) * 6); + freeifaddrs (ifaddr); + return 0; + } + } + } + + freeifaddrs (ifaddr); + return -1; +} static guint64 get_mem_by_bytes (const gchar *name) @@ -132,6 +297,114 @@ get_cpu_usage (gushort *cpu_count, gfloat *cpu_user, gfloat *cpu_system) return TRUE; } +// TODO check the OpenBSD version here +// (struct sockaddr_in*)kinfo_file.kf_sa_local + +void +xtm_refresh_inode_to_sock (XtmInodeToSock *its) +{ + FILE *fp; + char line[2048]; + char *token; + char *delim = " "; + + if (analyzer == NULL) + return; + + // Execute the sockstat command and get the output + fp = popen ("sockstat -L -P tcp,udp", "r"); + if (fp == NULL) + return; + + g_hash_table_remove_all (its->hash); + g_hash_table_remove_all (its->pid); + + // Skip the header line + fgets (line, 2048, fp); + + // Parse the output line by line + while (fgets (line, 2048, fp) != NULL) + { + // Remove the newline character + line[strcspn (line, "\n")] = '\0'; + + // Split the line into tokens + token = strtok (line, delim); + + // Parse the columns + char user[50]; + char command[50]; + char pid[10]; + char fd[10]; + char proto[10]; + char local_address[50]; + char foreign_address[50]; + + strcpy (user, token); + token = strtok (NULL, delim); + strcpy (command, token); + token = strtok (NULL, delim); + strcpy (pid, token); + token = strtok (NULL, delim); + strcpy (fd, token); + token = strtok (NULL, delim); + strcpy (proto, token); + token = strtok (NULL, delim); + strcpy (local_address, token); + token = strtok (NULL, delim); + strcpy (foreign_address, token); + + long int local_port, assos; + gint64 *inode1 = g_new0 (gint64, 1); + gint64 *inode2 = g_new0 (gint64, 1); + + *inode1 = atoi (fd); + *inode2 = atoi (fd); + assos = atoi (pid); + + sscanf (local_address, "%*[^:]:%d", &local_port); + // (intptr_t) -> cast to pointer from integer of different size [-Wint-to-pointer-cast] + g_hash_table_replace (its->hash, inode1, (gpointer)(intptr_t)local_port); + g_hash_table_replace (its->pid, inode2, (gpointer)(intptr_t)assos); + + // Print the parsed values + // printf("%d -> %d\n", *inode, local_port); + } + + // Close the pipe + pclose (fp); +} + +void +list_process_fds (Task *task) +{ + XtmNetworkAnalyzer *current; + GHashTableIter iter; + gpointer key, value; + gint64 key_int, value_int; + long int port; + + g_hash_table_iter_init (&iter, inode_to_sock->pid); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + key_int = *(gint64 *)(key); + value_int = GPOINTER_TO_INT (value); + if (task->pid == value_int) + { + port = (long int)g_hash_table_lookup (inode_to_sock->hash, &key_int); + task->active_socket += 1; + + current = analyzer; + while (current) + { + task->packet_in += (guint64)g_hash_table_lookup (current->packetin, &port); + task->packet_out += (guint64)g_hash_table_lookup (current->packetout, &port); + current = current->next; + } + } + } +} + static gboolean get_task_details (struct kinfo_proc *kp, Task *task) { @@ -233,17 +506,25 @@ get_task_details (struct kinfo_proc *kp, Task *task) if (kp->ki_flag & P_JAILED) task->state[i++] = 'J'; + if (analyzer != NULL) + list_process_fds (task); + return TRUE; } gboolean get_task_list (GArray *task_list) { + analyzer = xtm_network_analyzer_get_default (); + kvm_t *kd; struct kinfo_proc *kp; int cnt = 0, i; Task task; + inode_to_sock = xtm_inode_to_sock_get_default (); + xtm_refresh_inode_to_sock (inode_to_sock); + if ((kd = kvm_openfiles (_PATH_DEVNULL, _PATH_DEVNULL, NULL, O_RDONLY, NULL)) == NULL) return FALSE; diff --git a/src/task-manager-linux.c b/src/task-manager-linux.c index aeef8fc..5ef4427 100644 --- a/src/task-manager-linux.c +++ b/src/task-manager-linux.c @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024 Jehan-Antoine Vayssade, * Copyright (c) 2008-2010 Mike Massonnet * Copyright (c) 2006 Johannes Zellner * @@ -12,11 +13,368 @@ #include "config.h" #endif +#include "inode-to-sock.h" +#include "network-analyzer.h" #include "task-manager.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static XtmNetworkAnalyzer *analyzer = NULL; +static XtmInodeToSock *inode_to_sock = NULL; static gushort _cpu_count = 0; static gulong jiffies_total_delta = 0; +void list_process_fds (Task *task); +void xtm_refresh_inode_to_sock_protocol (XtmInodeToSock *its, char *filename); + +void +xtm_refresh_inode_to_sock_protocol (XtmInodeToSock *its, char *filename) +{ + char buffer[8192]; + char rem_addr[128], local_addr[128]; + int local_port, rem_port, inode, count; + gint64 *key; + + FILE *procinfo = fopen (filename, "r"); + + if (procinfo == 0) + { + perror (filename); + return; + } + + // skip header + if (fgets (buffer, sizeof (buffer), procinfo) == 0) + { + printf ("%s no header\n", filename); + fclose (procinfo); + return; + } + + while (fgets (buffer, sizeof (buffer), procinfo)) + { + count = sscanf ( + buffer, + "%*d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %*X %*X:%*X %*X:%*X %*X %*d %*d %d", + local_addr, &local_port, rem_addr, &rem_port, &inode); + + if (count != 5) + continue; + + key = g_new0 (gint64, 1); + *key = inode; + g_hash_table_replace (its->hash, key, (gpointer)(intptr_t)local_port); + } + + fclose (procinfo); +} + +void +xtm_refresh_inode_to_sock (XtmInodeToSock *its) +{ + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/tcp"); + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/tcp6"); + + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/udp"); + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/udp6"); + + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/udplite"); + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/udplite6"); + + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/raw"); + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/raw6"); + + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/icmp"); + xtm_refresh_inode_to_sock_protocol (its, "/proc/net/icmp6"); +} + +#ifdef HAVE_LIBPCAP +void +packet_callback (u_char *args, const struct pcap_pkthdr *header, const u_char *packet) +{ + struct ether_header *eth_header = (struct ether_header *)packet; + XtmNetworkAnalyzer *iface = (XtmNetworkAnalyzer *)args; + + char local_mac[18]; + char src_mac[18]; + char dst_mac[18]; + + int32_t src_port = -1; + int32_t dst_port = -1; + + sprintf (local_mac, "%02X:%02X:%02X:%02X:%02X:%02X", + iface->mac[0], iface->mac[1], iface->mac[2], + iface->mac[3], iface->mac[4], iface->mac[5]); + + sprintf (src_mac, "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header->ether_shost[0], eth_header->ether_shost[1], + eth_header->ether_shost[2], eth_header->ether_shost[3], + eth_header->ether_shost[4], eth_header->ether_shost[5]); + + sprintf (dst_mac, "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header->ether_dhost[0], eth_header->ether_dhost[1], + eth_header->ether_dhost[2], eth_header->ether_dhost[3], + eth_header->ether_dhost[4], eth_header->ether_dhost[5]); + + // IPv4 handling + if (ntohs (eth_header->ether_type) == ETHERTYPE_IP) + { + struct ip *ip_header = (struct ip *)(packet + sizeof (struct ether_header)); + + // TCP handling + if (ip_header->ip_p == IPPROTO_TCP) + { + struct tcphdr *tcp_header = (struct tcphdr *)(packet + sizeof (struct ether_header) + sizeof (struct ip)); + src_port = ntohs (tcp_header->source); + dst_port = ntohs (tcp_header->dest); + } + // UDP handling + else if (ip_header->ip_p == IPPROTO_UDP) + { + struct udphdr *udp_header = (struct udphdr *)(packet + sizeof (struct ether_header) + sizeof (struct ip)); + src_port = ntohs (udp_header->source); + dst_port = ntohs (udp_header->dest); + } + // ICMP handling + else if (ip_header->ip_p == IPPROTO_ICMP) + { + // The Internet Control Message Protocol (ICMP) does not use ports like TCP and UDP + // But the ICMP packet is encapsulated in an IPv4 packet + // 0x1 Reported by /proc/net/raw using ping + src_port = 1; + dst_port = 1; + } + } + // IPv6 handling + else if (ntohs (eth_header->ether_type) == ETHERTYPE_IPV6) + { + struct ip6_hdr *ip6_header = (struct ip6_hdr *)(packet + sizeof (struct ether_header)); + + // TCP handling + if (ip6_header->ip6_nxt == IPPROTO_TCP) + { + struct tcphdr *tcp_header = (struct tcphdr *)(packet + sizeof (struct ether_header) + sizeof (struct ip6_hdr)); + src_port = ntohs (tcp_header->source); + dst_port = ntohs (tcp_header->dest); + } + // UDP handling + else if (ip6_header->ip6_nxt == IPPROTO_UDP) + { + struct udphdr *udp_header = (struct udphdr *)(packet + sizeof (struct ether_header) + sizeof (struct ip6_hdr)); + src_port = ntohs (udp_header->source); + dst_port = ntohs (udp_header->dest); + } + // ICMP handling + else if (ip6_header->ip6_nxt == IPPROTO_ICMPV6) + { + // The Internet Control Message Protocol (ICMP) does not use ports like TCP and UDP + // But the ICMP packet is encapsulated in an IPv6 packet + // 0x3A Reported by /proc/net/raw6 + src_port = 0x3A; + dst_port = 0x3A; + } + } + // Raw packet handling + else + { + src_port = 1; + dst_port = 1; + } + + //! ICMP and RAW packet share the same local port + //! thus the link port -> count is broken in this case + //! since local port become non unique + //! However it still allow to see some unexpected program + + if (src_port != -1 && dst_port != -1) + { + if (strcmp (local_mac, src_mac) == 0) + increament_packet_count (local_mac, "in ", iface->packetin, src_port); + + if (strcmp (local_mac, dst_mac) == 0) + increament_packet_count (local_mac, "out", iface->packetout, dst_port); + } +} +#endif + +int +get_mac_address (const char *device, uint8_t mac[6]) +{ + struct ifaddrs *ifaddr, *ifa; + char device_path[512]; + char link_path[512]; + + snprintf (device_path, 512, "/sys/class/net/%s", device); + ssize_t len = readlink (device_path, link_path, 511); + + // disable localhost, docker, vpn, and other virtual device + // only physical device should remain + if (len == -1 || strstr (link_path, "virtual") != NULL) + return -1; + + if (getifaddrs (&ifaddr) == -1) + return -1; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr == NULL) + continue; + + int family = ifa->ifa_addr->sa_family; + + // Check if the interface is a network interface (AF_PACKET for Linux) + if (family == AF_PACKET && strcmp (device, ifa->ifa_name) == 0) + { + // Check if the interface has a hardware address (MAC address) + if (ifa->ifa_data != NULL) + { + struct sockaddr_ll *sll = (struct sockaddr_ll *)ifa->ifa_addr; + memcpy (mac, sll->sll_addr, sizeof (uint8_t) * 6); + freeifaddrs (ifaddr); + return 0; + } + } + } + + freeifaddrs (ifaddr); + return -1; +} + +gboolean +get_network_usage (guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error) +{ + FILE *file; + gchar buffer[256]; + char *out; + + *tcp_rx = 0; + *tcp_tx = 0; + *tcp_error = 0; + + if ((file = fopen ("/proc/net/dev", "r")) == NULL) + return FALSE; + + out = fgets (buffer, sizeof (buffer), file); + + if (!out) + { + fclose (file); + return FALSE; + } + + out = fgets (buffer, sizeof (buffer), file); + + if (!out) + { + fclose (file); + return FALSE; + } + + while (fgets (buffer, sizeof (buffer), file)) + { + unsigned long int dummy = 0; + unsigned long int r_bytes = 0; + unsigned long int t_bytes = 0; + unsigned long int r_packets = 0; + unsigned long int t_packets = 0; + unsigned long int error = 0; + gchar ifname[256]; + + int count = sscanf ( + buffer, "%[^:]: %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu", + ifname, &r_bytes, &r_packets, &error, + &dummy, &dummy, &dummy, &dummy, &dummy, + &t_bytes, &t_packets); + + if (count != 11) + { + printf ("Something went wrong while reading /proc/net/dev -> expected %d\n", count); + break; + } + + *tcp_rx += r_bytes; + *tcp_tx += t_bytes; + *tcp_error += error; + } + + fclose (file); + + return TRUE; +} + +void +list_process_fds (Task *task) +{ + XtmNetworkAnalyzer *current; + char path[1024]; + char link[2048]; + char target[2048]; + struct dirent *entry; + ssize_t len; + long int port; + DIR *dir; + + task->packet_in = 0; + task->packet_out = 0; + + snprintf (path, sizeof (path), "/proc/%d/fd", (int)task->pid); + + dir = opendir (path); + + if (dir == 0) + return; + + while ((entry = readdir (dir)) != NULL) + { + if (entry->d_type != DT_LNK) + continue; + + snprintf (link, sizeof (link), "%s/%s", path, entry->d_name); + len = readlink (link, target, sizeof (target) - 1); + + if (len == -1) + continue; + + target[len] = '\0'; + if (strncmp (target, "socket:", 7) != 0) + continue; + + int inode; + if (sscanf (target, "socket:[%d]", &inode) == 1) + { + task->active_socket += 1; + port = (long int)g_hash_table_lookup (inode_to_sock->hash, &inode); + + current = analyzer; + while (current) + { + // pthread_mutex_lock(&analyzer->lock); + task->packet_in += (guint64)g_hash_table_lookup (current->packetin, &port); + task->packet_out += (guint64)g_hash_table_lookup (current->packetout, &port); + // pthread_mutex_lock(&analyzer->lock); + current = current->next; + } + } + } + closedir (dir); +} + gboolean get_memory_usage (guint64 *memory_total, guint64 *memory_available, guint64 *memory_free, guint64 *memory_cache, guint64 *memory_buffers, guint64 *swap_total, guint64 *swap_free) { @@ -304,6 +662,8 @@ get_task_details (GPid pid, Task *task) fclose (file); } + list_process_fds (task); + /* Read the full command line */ if (!get_task_cmdline (task)) return FALSE; @@ -319,6 +679,10 @@ get_task_list (GArray *task_list) GPid pid; Task task; + analyzer = xtm_network_analyzer_get_default (); + inode_to_sock = xtm_inode_to_sock_get_default (); + xtm_refresh_inode_to_sock (inode_to_sock); + if ((dir = g_dir_open ("/proc", 0, NULL)) == NULL) return FALSE; diff --git a/src/task-manager-skel.c b/src/task-manager-skel.c index b9b4045..dd404cd 100644 --- a/src/task-manager-skel.c +++ b/src/task-manager-skel.c @@ -26,6 +26,29 @@ static gushort _cpu_count = 0; */ +int +get_mac_address (const char *device, uint8_t mac[6]) +{ + memset (mac, 0, sizeof (uint8_t) * 6); + return FALSE; +} + +gboolean +get_network_usage (guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error) +{ + *tcp_rx = 0; + *tcp_tx = 0; + *tcp_error = 0; + return TRUE; +} + +#ifdef HAVE_LIBPCAP +void +packet_callback (u_char *args, const struct pcap_pkthdr *header, const u_char *packet) +{ +} +#endif + gboolean get_memory_usage (guint64 *memory_total, guint64 *memory_available, guint64 *memory_free, guint64 *memory_cache, guint64 *memory_buffers, guint64 *swap_total, guint64 *swap_free) { diff --git a/src/task-manager-solaris.c b/src/task-manager-solaris.c index 4f4c1df..83c8f99 100644 --- a/src/task-manager-solaris.c +++ b/src/task-manager-solaris.c @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024 Jehan-Antoine Vayssade, * Copyright (c) 2010 Mike Massonnet * Copyright (c) 2009 Peter Tribble * @@ -12,29 +13,456 @@ #include "config.h" #endif +#include "inode-to-sock.h" +#include "network-analyzer.h" #include "task-manager.h" +#include #include +#include /* typedef int (*pfi_t)() for inet/optcom.h */ +#include #include #include #include #include #include +#include #include #include #include #include +// clang-format off +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +// clang-format on + +static XtmInodeToSock *inode_to_sock = NULL; +static XtmNetworkAnalyzer *analyzer = NULL; + static kstat_ctl_t *kc; static gushort _cpu_count = 0; static gulong ticks_total_delta = 0; +void addtoconninode (XtmInodeToSock *its, gint64 pid, char *ip, guint64 port); + static void init_stats (void) { kc = kstat_open (); } +int +get_mac_address (const char *device, uint8_t mac[6]) +{ +#ifdef HAVE_LIBSOCKET + struct ifaddrs *ifaddr, *ifa; + + if (getifaddrs (&ifaddr) == -1) + return -1; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr == NULL) + continue; + + // Check if the interface is a network interface (AF_PACKET for Linux) + if (ifa->ifa_addr->sa_family == AF_LINK && strcmp (device, ifa->ifa_name) == 0) + { + // Check if the interface has a hardware address (MAC address) + if (ifa->ifa_data != NULL) + { + // warning: cast from 'struct sockaddr *' to 'struct sockaddr_dl *' increases required alignment from 1 to 2 + // struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; + struct sockaddr_dl sdl; + memcpy (&sdl, ifa->ifa_addr, sizeof (struct sockaddr_dl)); + memcpy (mac, sdl.sdl_data + sdl.sdl_nlen, sizeof (uint8_t) * 6); + freeifaddrs (ifaddr); + return 0; + } + } + } + + freeifaddrs (ifaddr); + return -1; +#else + memset (mac, 0, sizeof (uint8_t) * 6); + return FALSE; +#endif +} + +gboolean +get_network_usage (guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error) +{ + kstat_named_t *knp; + kstat_t *ksp; + + if (!kc) + init_stats (); + + if (!(ksp = kstat_lookup (kc, "link", -1, NULL))) + { + printf ("kstat_lookup failed\n"); + return FALSE; + } + + kstat_read (kc, ksp, NULL); + *tcp_error = 0; + + if ((knp = kstat_data_lookup (ksp, "rbytes64")) != NULL) + *tcp_rx = knp->value.ui64; + + if ((knp = kstat_data_lookup (ksp, "obytes64")) != NULL) + *tcp_tx = knp->value.ui64; + + if ((knp = kstat_data_lookup (ksp, "ierrors")) != NULL) + *tcp_error += knp->value.ui32; + + if ((knp = kstat_data_lookup (ksp, "oerrors")) != NULL) + *tcp_error += knp->value.ui32; + + return TRUE; +} + +#ifdef HAVE_LIBPCAP +void +packet_callback (u_char *args, const struct pcap_pkthdr *header, const u_char *packet) +{ + // Extract source and destination IP addresses and ports from the packet + struct ether_header eth_header; + struct ip ip_header; + struct tcphdr tcp_header; + XtmNetworkAnalyzer *iface; + long int src_port, dst_port; + char local_mac[18]; + char src_mac[18]; + char dst_mac[18]; + + memcpy (ð_header, packet, sizeof (struct ether_header)); + memcpy (&ip_header, packet + sizeof (struct ether_header), sizeof (struct ip)); + memcpy (&tcp_header, packet + sizeof (struct ether_header) + sizeof (struct ip), sizeof (struct ip)); + + // cast -> increases required alignment from 1 to 2 [-Wcast-align] + // const struct ether_header *eth_header = (const struct ether_header *)packet; + // struct ip *ip_header = (struct ip *)(packet + sizeof (struct ether_header)); + // struct tcphdr *tcp_header = (struct tcphdr *)(packet + sizeof (struct ether_header) + sizeof (struct ip)); + // printf("%d, %d \n", eth_heade.ether_type , ip_header.ip_p); + + // Dropped non-ip packet + if (eth_header.ether_type != 8 || ip_header.ip_p != 6) + return; + + iface = (XtmNetworkAnalyzer *)args; + + src_port = ntohs (tcp_header.th_sport); + dst_port = ntohs (tcp_header.th_dport); + + // directly use strcmp on analyzer->mac, eth_header->ether_shost doesnt work + + snprintf (local_mac, sizeof (local_mac), + "%02X:%02X:%02X:%02X:%02X:%02X", + iface->mac[0], iface->mac[1], + iface->mac[2], iface->mac[3], + iface->mac[4], iface->mac[5]); + + snprintf (src_mac, sizeof (local_mac), + "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header.ether_shost.ether_addr_octet[0], eth_header.ether_shost.ether_addr_octet[1], + eth_header.ether_shost.ether_addr_octet[2], eth_header.ether_shost.ether_addr_octet[3], + eth_header.ether_shost.ether_addr_octet[4], eth_header.ether_shost.ether_addr_octet[5]); + + snprintf (dst_mac, sizeof (local_mac), + "%02X:%02X:%02X:%02X:%02X:%02X", + eth_header.ether_dhost.ether_addr_octet[0], eth_header.ether_dhost.ether_addr_octet[1], + eth_header.ether_dhost.ether_addr_octet[2], eth_header.ether_dhost.ether_addr_octet[3], + eth_header.ether_dhost.ether_addr_octet[4], eth_header.ether_dhost.ether_addr_octet[5]); + + // Debug + // pthread_mutex_lock(&iface->lock); + + if (strcmp (local_mac, src_mac) == 0) + increament_packet_count (local_mac, "in ", iface->packetin, src_port); + + if (strcmp (local_mac, dst_mac) == 0) + increament_packet_count (local_mac, "out", iface->packetout, dst_port); + + // pthread_mutex_unlock(&iface->lock); +} +#endif + +void +set_task_network_data (Task *task, guint64 port) +{ + XtmNetworkAnalyzer *current; + task->active_socket += 1; + + current = analyzer; + while (current) + { + task->packet_in += (guint64)g_hash_table_lookup (current->packetin, &port); + task->packet_out += (guint64)g_hash_table_lookup (current->packetout, &port); + current = current->next; + } +} + +void +list_process_fds (Task *task) +{ + XtmNetworkAnalyzer *current; + GHashTableIter iter; + gpointer key, value; + gint64 key_int, value_int; + + g_hash_table_iter_init (&iter, inode_to_sock->pid); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + key_int = *(gint64 *)(key); + value_int = GPOINTER_TO_INT (value); + + if (task->pid == value_int) + set_task_network_data (task, key_int); + } +} + +void +addtoconninode (XtmInodeToSock *its, gint64 pid, char *ip, guint64 port) +{ + gint64 *inode; + + if (its == NULL) + return; + + // seem like idle socket are given back to pid 0 (kernel ?) + if (pid <= 0) + return; + + if (strcmp (ip, "0.0.0.0") == 0) + return; + + if (strcmp (ip, "::") == 0) + return; + + if (strcmp (ip, "127.0.0.1") == 0) + return; + + inode = g_new0 (gint64, 1); + *inode = port; + + g_hash_table_replace (inode_to_sock->pid, inode, (gpointer)(intptr_t)pid); + + printf ("PID %ld: Local Address: [%s]:%ld\n", pid, ip, port); +} + +void +xtm_refresh_inode_to_sock (XtmInodeToSock *its) +{ + // inspired by : + // nxsensor/src/sysdeps/solaris.c + // net-snmp/agent/mibgroup/mibII/tcpTable.c + // net-snmp/agent/mibgroup/mibgroup/kernel_sunos5.c + // psutil/psutil/_psutil_sunos.c + // illumos-joyent/master/usr/src/uts/common/io/tl.c + // nicstat/nicstat.c + + int sd, ret, flags, getcode, num_ent, i; + char buf[4096]; + char lip[INET6_ADDRSTRLEN]; + + mib2_tcpConnEntry_t tp; + mib2_udpEntry_t ude; + +#if defined(AF_INET6) + mib2_tcp6ConnEntry_t tp6; + mib2_udp6Entry_t ude6; +#endif + + struct strbuf ctlbuf, databuf; + struct T_optmgmt_req tor = { 0 }; + struct T_optmgmt_ack toa = { 0 }; + struct T_error_ack tea = { 0 }; + struct opthdr mibhdr = { 0 }; + + sd = open ("/dev/arp", O_RDWR); + if (sd == -1) + { + perror ("open"); + return; + } + + ret = ioctl (sd, I_PUSH, "tcp"); + if (ret == -1) + { + perror ("ioctl"); + close (sd); + return; + } + + ret = ioctl (sd, I_PUSH, "udp"); + if (ret == -1) + { + perror ("ioctl"); + close (sd); + return; + } + + // g_hash_table_remove_all (its->pid); + // g_hash_table_remove_all (its->hash); + + // Set up the request + tor.PRIM_type = T_SVR4_OPTMGMT_REQ; + tor.OPT_offset = sizeof (struct T_optmgmt_req); + tor.OPT_length = sizeof (struct opthdr); + tor.MGMT_flags = T_CURRENT; + mibhdr.level = MIB2_IP; + mibhdr.name = 0; +#ifdef NEW_MIB_COMPLIANT + mibhdr.len = 1; +#else + mibhdr.len = 0; +#endif + memcpy (buf, &tor, sizeof (tor)); + memcpy (buf + tor.OPT_offset, &mibhdr, sizeof (mibhdr)); + ctlbuf.buf = buf; + ctlbuf.len = tor.OPT_offset + tor.OPT_length; + flags = 0; + + // Send the request + if (putmsg (sd, &ctlbuf, NULL, flags) == -1) + { + perror ("putmsg"); + close (sd); + return; + } + + ctlbuf.maxlen = sizeof (buf); + + for (;;) + { + getcode = getmsg (sd, &ctlbuf, NULL, &flags); + memcpy (&toa, buf, sizeof (toa)); + memcpy (&tea, buf, sizeof (tea)); + + if (getcode != MOREDATA || ctlbuf.len < (int)sizeof (struct T_optmgmt_ack) || + toa.PRIM_type != T_OPTMGMT_ACK || toa.MGMT_flags != T_SUCCESS) + { + break; + } + + if (ctlbuf.len >= (int)sizeof (struct T_error_ack) && tea.PRIM_type == T_ERROR_ACK) + { + fprintf (stderr, "ERROR_ACK\n"); + close (sd); + return; + } + + if (getcode == 0 && ctlbuf.len >= (int)sizeof (struct T_optmgmt_ack) && + toa.PRIM_type == T_OPTMGMT_ACK && toa.MGMT_flags == T_SUCCESS) + { + fprintf (stderr, "ERROR_T_OPTMGMT_ACK\n"); + close (sd); + return; + } + + memset (&mibhdr, 0x0, sizeof (mibhdr)); + memcpy (&mibhdr, buf + toa.OPT_offset, toa.OPT_length); + databuf.maxlen = mibhdr.len; + databuf.len = 0; + databuf.buf = (char *)malloc ((int)mibhdr.len); + if (!databuf.buf) + { + fprintf (stderr, "Out of memory\n"); + close (sd); + return; + } + + flags = 0; + getcode = getmsg (sd, NULL, &databuf, &flags); + + if (getcode < 0) + { + perror ("getmsg"); + free (databuf.buf); + close (sd); + return; + } + + // TCPv4 + if (mibhdr.level == MIB2_TCP && mibhdr.name == MIB2_TCP_13) + { + num_ent = mibhdr.len / sizeof (mib2_tcpConnEntry_t); + for (i = 0; i < num_ent; i++) + { + memcpy (&tp, databuf.buf + i * sizeof (tp), sizeof (tp)); + inet_ntop (AF_INET, &tp.tcpConnLocalAddress, lip, sizeof (lip)); + addtoconninode (inode_to_sock, tp.tcpConnCreationProcess, lip, tp.tcpConnLocalPort); + // interesting properties, allowing to remove pcap + // tp.tcpConnEntryInfo.ce_in_data_inorder_segs + // tp.tcpConnEntryInfo.ce_in_data_unorder_segs + // tp.tcpConnEntryInfo.ce_out_data_segs + // tp.tcpConnEntryInfo.ce_out_retrans_segs + } + } +#if defined(AF_INET6) + // TCPv6 + else if (mibhdr.level == MIB2_TCP6 && mibhdr.name == MIB2_TCP6_CONN) + { + num_ent = mibhdr.len / sizeof (mib2_tcp6ConnEntry_t); + for (i = 0; i < num_ent; i++) + { + memcpy (&tp6, databuf.buf + i * sizeof (tp6), sizeof (tp6)); + inet_ntop (AF_INET6, &tp6.tcp6ConnLocalAddress, lip, sizeof (lip)); + addtoconninode (inode_to_sock, tp6.tcp6ConnCreationProcess, lip, tp6.tcp6ConnLocalPort); + // interesting properties, allowing to remove pcap + // tp.tcpConnEntryInfo.ce_in_data_inorder_segs + // tp.tcpConnEntryInfo.ce_in_data_unorder_segs + // tp.tcpConnEntryInfo.ce_out_data_segs + // tp.tcpConnEntryInfo.ce_out_retrans_segs + } + } +#endif + // UDPv4 + else if (mibhdr.level == MIB2_UDP || mibhdr.level == MIB2_UDP_ENTRY) + { + num_ent = mibhdr.len / sizeof (mib2_udpEntry_t); + for (i = 0; i < num_ent; i++) + { + memcpy (&ude, databuf.buf + i * sizeof (ude), sizeof (ude)); + inet_ntop (AF_INET, &ude.udpLocalAddress, lip, sizeof (lip)); + addtoconninode (inode_to_sock, ude.udpCreationProcess, lip, ude.udpLocalPort); + } + } +#if defined(AF_INET6) + // UDPv6 + else if (mibhdr.level == MIB2_UDP6 || mibhdr.level == MIB2_UDP6_ENTRY) + { + num_ent = mibhdr.len / sizeof (mib2_udp6Entry_t); + for (i = 0; i < num_ent; i++) + { + memcpy (&ude6, databuf.buf + i * sizeof (ude6), sizeof (ude6)); + inet_ntop (AF_INET6, &ude6.udp6LocalAddress, lip, sizeof (lip)); + addtoconninode (inode_to_sock, ude6.udp6CreationProcess, lip, ude6.udp6LocalPort); + } + } +#endif + + free (databuf.buf); + } + + close (sd); +} + gboolean get_memory_usage (guint64 *memory_total, guint64 *memory_available, guint64 *memory_free, guint64 *memory_cache, guint64 *memory_buffers, guint64 *swap_total, guint64 *swap_free) { @@ -201,6 +629,8 @@ get_task_details (GPid pid, Task *task) fclose (file); + list_process_fds (task); + return TRUE; } @@ -212,6 +642,11 @@ get_task_list (GArray *task_list) GPid pid; Task task; + printf ("------------\n"); + analyzer = xtm_network_analyzer_get_default (); + inode_to_sock = xtm_inode_to_sock_get_default (); + xtm_refresh_inode_to_sock (inode_to_sock); + if ((dir = g_dir_open ("/proc", 0, NULL)) == NULL) return FALSE; diff --git a/src/task-manager.c b/src/task-manager.c index 090e37c..38cfdb6 100644 --- a/src/task-manager.c +++ b/src/task-manager.c @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024 Jehan-Antoine Vayssade, * Copyright (c) 2010 Mike Massonnet, * Copyright (c) 2018 Rozhuk Ivan * @@ -12,6 +13,7 @@ #include "config.h" #endif +#include "network-analyzer.h" #include "process-tree-view.h" /* for the columns of the model */ #include "settings.h" #include "task-manager.h" @@ -22,6 +24,10 @@ #include #endif +#ifdef HAVE_LIBPCAP +#include +#endif + #include #include #include @@ -42,6 +48,7 @@ struct _XtmTaskManagerClass { GObjectClass parent_class; }; + struct _XtmTaskManager { GObject parent; @@ -61,6 +68,12 @@ struct _XtmTaskManager guint64 memory_buffers; guint64 swap_total; guint64 swap_free; + guint64 tcp_rx; + guint64 tcp_tx; + guint64 tcp_error; + guint64 old_tcp_rx; + guint64 old_tcp_tx; + guint64 old_tcp_error; }; G_DEFINE_TYPE (XtmTaskManager, xtm_task_manager, G_TYPE_OBJECT) @@ -101,6 +114,9 @@ xtm_task_manager_init (XtmTaskManager *manager) g_object_get (settings, "full-command-line", &full_cmdline, NULL); g_signal_connect (settings, "notify::more-precision", G_CALLBACK (setting_changed), manager); g_signal_connect (settings, "notify::full-command-line", G_CALLBACK (setting_changed), manager); + manager->old_tcp_rx = 0; + manager->old_tcp_tx = 0; + manager->old_tcp_error = 0; } static void @@ -108,6 +124,7 @@ xtm_task_manager_finalize (GObject *object) { XtmTaskManager *manager = XTM_TASK_MANAGER (object); g_array_free (manager->tasks, TRUE); + #ifdef HAVE_WNCK if (manager->app_manager != NULL) { @@ -334,6 +351,9 @@ model_update_tree_iter (XtmTaskManager *manager, GtkTreeIter *iter, glong timest XTM_PTV_COLUMN_CPU_STR, cpu, XTM_PTV_COLUMN_GROUP_CPU, (task->group_cpu_user + task->group_cpu_system), XTM_PTV_COLUMN_GROUP_CPU_STR, group_cpu, + XTM_PTV_COLUMN_PACKET_IN, task->packet_in, + XTM_PTV_COLUMN_PACKET_OUT, task->packet_out, + XTM_PTV_COLUMN_ACTIVE_SOCKET, task->active_socket, XTM_PTV_COLUMN_PRIORITY, task->prio, XTM_PTV_COLUMN_BACKGROUND, background, XTM_PTV_COLUMN_FOREGROUND, foreground, @@ -393,6 +413,33 @@ xtm_task_manager_new (GtkTreeModel *model) return manager; } +void +xtm_task_manager_get_network_info (XtmTaskManager *manager, guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error) +{ + g_return_if_fail (XTM_IS_TASK_MANAGER (manager)); + get_network_usage (&manager->tcp_rx, &manager->tcp_tx, &manager->tcp_error); + + if (manager->old_tcp_rx == 0 && manager->old_tcp_tx == 0 && manager->old_tcp_error == 0) + { + *tcp_rx = 0; + *tcp_tx = 0; + *tcp_error = 0; + } + else + { + gint ms; + g_object_get (settings, "refresh-rate", &ms, NULL); + // ugly approximation in guint64 + *tcp_rx = (manager->tcp_rx - manager->old_tcp_rx) / ms * 1000; + *tcp_tx = (manager->tcp_tx - manager->old_tcp_tx) / ms * 1000; + *tcp_error = (manager->tcp_error - manager->old_tcp_error) / ms * 1000; + } + + manager->old_tcp_rx = manager->tcp_rx; + manager->old_tcp_tx = manager->tcp_tx; + manager->old_tcp_error = manager->tcp_error; +} + void xtm_task_manager_get_system_info (XtmTaskManager *manager, guint *num_processes, gfloat *cpu, guint64 *memory_used, guint64 *memory_total, diff --git a/src/task-manager.h b/src/task-manager.h index 0fc7a45..b20e9eb 100644 --- a/src/task-manager.h +++ b/src/task-manager.h @@ -10,9 +10,17 @@ #ifndef TASK_MANAGER_H #define TASK_MANAGER_H +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + #include #include +#ifdef HAVE_LIBPCAP +#include +#endif + /** * Legend colors */ @@ -45,6 +53,10 @@ struct _Task gfloat group_cpu_system; guint64 group_vsz; guint64 group_rss; + + guint64 packet_in; + guint64 packet_out; + guint64 active_socket; }; /** @@ -53,6 +65,12 @@ struct _Task * memory_available = free + cache + buffers + an-OS-specific-value */ +#ifdef HAVE_LIBPCAP +void packet_callback (u_char *args, const struct pcap_pkthdr *header, const u_char *packet); +#endif + +int get_mac_address (const char *device, uint8_t mac[6]); +gboolean get_network_usage (guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error); gboolean get_memory_usage (guint64 *memory_total, guint64 *memory_available, guint64 *memory_free, guint64 *memory_cache, guint64 *memory_buffers, guint64 *swap_total, guint64 *swap_free); gboolean get_cpu_usage (gushort *cpu_count, gfloat *cpu_user, gfloat *cpu_system); gboolean get_task_list (GArray *task_list); @@ -73,6 +91,7 @@ typedef struct _XtmTaskManager XtmTaskManager; GType xtm_task_manager_get_type (void); XtmTaskManager *xtm_task_manager_new (GtkTreeModel *model); +void xtm_task_manager_get_network_info (XtmTaskManager *manager, guint64 *tcp_rx, guint64 *tcp_tx, guint64 *tcp_error); void xtm_task_manager_get_system_info (XtmTaskManager *manager, guint *num_processes, gfloat *cpu, guint64 *memory_used, guint64 *memory_total, guint64 *swap_used, guint64 *swap_total);