diff --git a/meson.build b/meson.build index 2b432eb3097ee17260a80509b2dbd7755e4f6d9e..c4610b3c4bc78719dd71bc42aec8252a9eba7e1e 100644 --- a/meson.build +++ b/meson.build @@ -11,6 +11,7 @@ valac = meson.get_compiler('vala') glib_dep = dependency('glib-2.0', version : '>=2.38') gobject_dep = dependency('gobject-2.0') gio_dep = dependency('gio-2.0') +gio_unix_dep = dependency('gio-unix-2.0') gtk_dep = dependency('gtk+-3.0', version : '>=3.20.10') libdazzle_dep = dependency('libdazzle-1.0', version : '>=3.30') libgtop_dep = dependency('libgtop-2.0', version : '>= 2.34.0') diff --git a/src/app-item.vala b/src/app-item.vala index d25fb8c58e63d16e1b1bee46d229a0a8f0c3351a..810a5d173b15b4294fbf44389b0bb8082c4ae2a4 100644 --- a/src/app-item.vala +++ b/src/app-item.vala @@ -12,27 +12,53 @@ namespace Usage public bool gamemode {get; private set; } private static HashTable? apps_info; + private static HashTable? appid_map; private AppInfo? app_info = null; public static void init() { - apps_info = new HashTable(str_hash, str_equal); - var _apps_info = AppInfo.get_all(); + apps_info = new HashTable (str_hash, str_equal); + appid_map = new HashTable (str_hash, str_equal); + + var _apps_info = AppInfo.get_all (); foreach (AppInfo info in _apps_info) { - string cmd = info.get_commandline(); - sanity_cmd(ref cmd); + GLib.DesktopAppInfo? dai = info as GLib.DesktopAppInfo; + + if (dai != null) { + string id = dai.get_string ("X-Flatpak"); + if (id != null) + appid_map.insert (id, info); + } + + string cmd = info.get_commandline (); - if(cmd != null) - apps_info.insert(cmd, info); + if (cmd == null) + continue; + + sanitize_cmd (ref cmd); + apps_info.insert (cmd, info); } } - public static bool have_app_info(string cmdline) { - return apps_info.get(cmdline) != null ? true : false; + public static AppInfo? app_info_for_process (Process p) { + AppInfo? info = null; + + if (p.cmdline != null) + info = apps_info[p.cmdline]; + + if (info == null && p.app_id != null) + info = appid_map[p.app_id]; + + return info; + } + + public static bool have_app_info (Process p) { + AppInfo? info = app_info_for_process (p); + return info != null; } public AppItem(Process process) { - app_info = apps_info.get(process.cmdline); + app_info = app_info_for_process (process); representative_cmdline = process.cmdline; representative_uid = process.uid; display_name = find_display_name(); @@ -104,6 +130,10 @@ namespace Usage cpu_load = double.min(100, cpu_load); } + public void remove_process (Process process) { + processes.remove (process.pid); + } + public void replace_process(Process process) { processes.replace(process.pid, process); } @@ -129,26 +159,24 @@ namespace Usage } } - private static void sanity_cmd(ref string? commandline) { - if(commandline != null) { - //flatpak - if(commandline.contains("flatpak run")) { - var index = commandline.index_of("--command=") + 10; - commandline = commandline.substring(index); - } + private static void sanitize_cmd(ref string? commandline) { + if (commandline == null) + return; - try { - var rgx = new Regex("[^a-zA-Z0-9._-]"); + // flatpak: parse the command line of the containerized program + if (commandline.contains("flatpak run")) { + var index = commandline.index_of ("--command=") + 10; + commandline = commandline.substring (index); + } - commandline = Path.get_basename(commandline.split(" ")[0]); - commandline = rgx.replace(commandline, commandline.length, 0, ""); - } catch (RegexError e) { - warning ("Unable to obtain process command: %s", e.message); - } + // TODO: unify this with the logic in get_full_process_cmd + commandline = Process.first_component (commandline); + commandline = Path.get_basename (commandline); + commandline = Process.sanitize_name (commandline); - if(commandline.contains("google-chrome-stable")) //Workaround for google-chrome - commandline = "chrome"; - } + // Workaround for google-chrome + if (commandline.contains ("google-chrome-stable")) + commandline = "chrome"; } } } \ No newline at end of file diff --git a/src/cpu-monitor.vala b/src/cpu-monitor.vala index 3a0b563c36a32bb3804ce5c0c4eb239bdc98019b..ec3fdcfa64c8ecf1025962844ca0009cbad6c2d0 100644 --- a/src/cpu-monitor.vala +++ b/src/cpu-monitor.vala @@ -82,6 +82,7 @@ namespace Usage process.cpu_load = cpu_load; process.cpu_last_used = proc_time.rtime; process.x_cpu_last_used = proc_time.xcpu_utime[process.last_processor] + proc_time.xcpu_stime[process.last_processor]; + process.start_time = proc_time.start_time; } } } diff --git a/src/meson.build b/src/meson.build index 932fdfd7532020dadcbcb541a6d15caaac6f7320..c3a7ddc263cb8e0433c06bae54c15fceb669d93f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -50,6 +50,7 @@ vala_sources = [ deps = [ gio_dep, + gio_unix_dep, glib_dep, gobject_dep, gtk_dep, @@ -58,6 +59,7 @@ deps = [ libdazzle_dep, cc.find_library('m'), valac.find_library('config', dirs: vapi_dir), + valac.find_library('stopgap', dirs: vapi_dir), valac.find_library('posix') ] diff --git a/src/process.vala b/src/process.vala index 890ac5a71422a89e8874ec5036d1c00a0a3969d2..729c1928dc5271f5c16cc20025b1097af517abf8 100644 --- a/src/process.vala +++ b/src/process.vala @@ -25,6 +25,7 @@ namespace Usage public Pid pid { get; private set; } public string cmdline { get; private set; } public uint uid { get; private set; } + public uint64 start_time { get; set; default = 0; } public double cpu_load { get; set; default = 0; } public double x_cpu_load { get; set; default = 0; } @@ -39,10 +40,13 @@ namespace Usage public bool mark_as_updated { get; set; default = true; } public ProcessStatus status { get; private set; default = ProcessStatus.SLEEPING; } - public Process(Pid pid, string cmdline) + private string? _app_id = null; + private bool _app_id_checked = false; + + public Process(Pid pid) { this.pid = pid; - this.cmdline = cmdline; + this.cmdline = get_full_process_cmd (pid); this.uid = _get_uid(); } @@ -81,6 +85,127 @@ namespace Usage GTop.get_proc_uid(out procUid, pid); return procUid.uid; } + + public string? app_id { + get { + if (!_app_id_checked) + _app_id = read_app_id (pid); + + return _app_id; + } + } + + /* static pid related methods */ + public static string get_full_process_cmd (Pid pid) { + GTop.ProcArgs proc_args; + GTop.ProcState proc_state; + string[] args = GTop.get_proc_argv (out proc_args, pid, 0); + GTop.get_proc_state (out proc_state, pid); + string cmd = (string) proc_state.cmd; + + /* cmd is most likely a truncated version, therefore + * we check the first two arguments of the full argv + * vector if they match cmd and if so, use that */ + for (int i = 0; i < 2; i++) { + if (args[i] == null) + continue; + + /* TODO: this will fail if args[i] is a commandline, + * i.e. composed of multiple segments and one of the + * later ones is a unix path */ + var name = Path.get_basename (args[i]); + if (!name.has_prefix (cmd)) + continue; + + name = Process.first_component (name); + return Process.sanitize_name (name); + } + + return Process.sanitize_name (cmd); + } + + public static string? read_app_id (Pid pid) { + KeyFile? kf = read_flatpak_info (pid); + + if (kf == null) + return null; + + string? name = null; + try { + if (kf.has_key ("Application", "name")) + name = kf.get_string ("Application", "name"); + } catch (Error e) { + warning (@"Failed to parse faltpak info for: $pid"); + } + + return name; + } + + public static KeyFile? read_flatpak_info (Pid pid) { + string path = "/proc/%u/root".printf ((uint) pid); + int flags = Posix.O_RDONLY | StopGap.O_CLOEXEC | Posix.O_NOCTTY; + + int root = StopGap.openat (StopGap.AT_FDCWD, path, flags | Posix.O_NONBLOCK | StopGap.O_DIRECTORY); + + if (root == -1) + return null; + + int fd = StopGap.openat (root, ".flatpak-info", flags); + + Posix.close (root); + + if (fd == -1) + return null; + + KeyFile kf = new KeyFile (); + + try { + string? data = null; + size_t len; + + // TODO use MappedFile.from_fd, requires vala 0.46 + IOChannel ch = new IOChannel.unix_new (fd); + ch.set_close_on_unref (true); + + var status = ch.read_to_end (out data, out len); + if (status != IOStatus.NORMAL) + return null; + + kf.load_from_data (data, len, 0); + } catch (Error e) { + return null; + } + + return kf; + } + + /* static utility methods */ + public static string? sanitize_name (string name) { + string? result = null; + + if (name == null) + return null; + + try { + var rgx = new Regex ("[^a-zA-Z0-9._-]"); + result = rgx.replace (name, name.length, 0, ""); + } catch (RegexError e) { + warning ("Unable to sanitize name: %s", e.message); + } + + return result; + } + + public static string first_component (string str) { + + for (int i = 0; i < str.length; i++) { + if (str[i] == ' ') { + return str.substring(0, i); + } + } + + return str; + } } public enum ProcessStatus diff --git a/src/system-monitor.vala b/src/system-monitor.vala index d92752b2284bebb22c4a776e4fdd5b68e072da27..aa8cd884c134de9c4447e5e8cb7ac7f5c92d76e6 100644 --- a/src/system-monitor.vala +++ b/src/system-monitor.vala @@ -36,6 +36,7 @@ namespace Usage private GameMode.PidList gamemode_pids; private HashTable app_table; + private HashTable process_table; private int process_mode = GTop.KERN_PROC_ALL; private static SystemMonitor system_monitor; @@ -67,6 +68,7 @@ namespace Usage gamemode_pids = new GameMode.PidList(); app_table = new HashTable(str_hash, str_equal); + process_table = new HashTable(direct_hash, direct_equal); var settings = Settings.get_default(); init(); @@ -88,6 +90,10 @@ namespace Usage app_table.insert("system" , system); } + foreach (var p in process_table.get_values ()) { + process_added (p); + } + update_data(); Timeout.add(settings.data_update_interval, () => { @@ -108,129 +114,142 @@ namespace Usage swap_usage = memory_monitor.get_swap_usage(); swap_total = memory_monitor.get_swap_total(); + foreach (var app in app_table.get_values ()) + app.mark_as_not_updated(); + + /* Try to find the difference between the old list of pids, + * and the new ones, i.e. the one that got added and removed */ GTop.Proclist proclist; var pids = GTop.get_proclist (out proclist, process_mode); + intptr[] old = (intptr[]) process_table.get_keys_as_array (); + + size_t new_len = (size_t) proclist.number; + size_t old_len = process_table.length; + + sort_pids (pids, sizeof (GLib.Pid), new_len); + sort_pids (old, sizeof (intptr), old_len); + + debug ("new_len: %lu, old_len: %lu\n", new_len, old_len); + uint removed = 0; + uint added = 0; + for (size_t i = 0, j = 0; i < new_len || j < old_len; ) { + uint32 n = i < new_len ? pids[i] : uint32.MAX; + uint32 o = j < old_len ? (uint32) old[j] : uint32.MAX; + + /* pids: [ 1, 3, 4 ] + * old: [ 1, 2, 4, 5 ] → 2,5 removed, 3 added + * i [for pids]: 0 | 1 | 1 | 2 | 3 + * j [for old]: 0 | 1 | 2 | 2 | 3 + * n = pids[i]: 1 | 3 | 3 | 4 | MAX [oob] + * o = old[j]: 1 | 2 | 4 | 4 | 5 + * = | n > o | n < o | = | n > o + * increment: i,j | j | i | i,j | j + * Process op: chk | del | add | chk | del + */ + + if (n > o) { + /* delete to process not in the new array */ + Process p = process_table[(GLib.Pid) o]; + debug ("process removed: %u\n", o); + + process_removed (p); + removed++; + + j++; /* let o := old[j] catch up */ + } else if (n < o) { + /* new process */ + var p = new Process ((GLib.Pid) n); + update_process (ref p); // state, time + + debug ("process added: %u\n", n); + + process_added (p); + added++; + + i++; /* let n := pids[i] catch up */ + } else { + /* equal pids, might have rolled over though + * better check, match start time */ + Process p = process_table[(GLib.Pid) n]; + + GTop.ProcTime ptime; + GTop.get_proc_time (out ptime, p.pid); + + /* no match: -> old removed, new added */ + if (ptime.start_time != p.start_time) { + debug ("start time mismtach: %u\n", n); + process_removed (p); + + p = new Process ((GLib.Pid) n); + process_added (p); + } - foreach(var app in app_table.get_values()) - app.mark_as_not_updated(); + update_process (ref p); - for(uint i = 0; i < proclist.number; i++) - { - string cmd = get_full_process_cmd(pids[i]); - string app_id = cmd; - - if(group_system_apps && is_system_app(cmd)) - app_id = "system"; - - if (!(app_id in app_table)) - { - var process = new Process(pids[i], cmd); - update_process(ref process); - var app = new AppItem(process); - app_table.insert (app_id, (owned) app); - } - else - { - AppItem app = app_table[app_id]; - - if (!app.contains_process(pids[i])) - { - var process = new Process(pids[i], cmd); - update_process(ref process); - app.insert_process(process); - } - else - { - var process = app.get_process_by_pid(pids[i]); - update_process(ref process); - app.replace_process(process); - } + i++; j++; /* both indices move */ } } - foreach(var app in app_table.get_values()) - app.remove_processes(); + foreach (var app in app_table.get_values ()) + app.remove_processes (); + + debug ("removed: %u, added: %u\n", removed, added); + debug ("app table size: %u\n", app_table.length); + debug ("process table size: %u\n", process_table.length); return true; } - private void update_process(ref Process process) - { - cpu_monitor.update_process(ref process); - memory_monitor.update_process(ref process); - process.update_status(); - process.gamemode = gamemode_pids.contains((int) process.pid); - } + private void process_added (Process p) { + string app_id = get_app_id_for_process (p); - private string? sanity_cmd(string commandline) - { - string? cmd = null; + AppItem? item = app_table[app_id]; - if(commandline != null) - { - try { - var rgx = new Regex("[^a-zA-Z0-9._-]"); - cmd = Path.get_basename(commandline.split(" ")[0]); - cmd = rgx.replace(commandline, commandline.length, 0, ""); - } catch (RegexError e) { - warning ("Unable to obtain process command: %s", e.message); - } + if (item == null) { + item = new AppItem (p); + app_table.insert (app_id, item); + } else if (! item.contains_process (p.pid)) { + item.insert_process (p); } - return cmd; + + process_table.insert (p.pid, p); } - private string get_full_process_cmd (Pid pid) - { - GTop.ProcArgs proc_args; - GTop.ProcState proc_state; - string[] args = GTop.get_proc_argv (out proc_args, pid, 0); - GTop.get_proc_state (out proc_state, pid); - string cmd = (string) proc_state.cmd; - string cmd_parameter = ""; + private void process_removed (Process p) { + string app_id = get_app_id_for_process (p); - var secure_arguments = new string[2]; + AppItem? item = app_table[app_id]; - for(int i = 0; i < 2; i++) - { - if(args[i] != null) - { - secure_arguments[i] = args[i]; - } - else - { - secure_arguments[i] = ""; - if (i == 0) - secure_arguments[1] = ""; - break; - } - } + if (item != null) + item.remove_process (p); - for (int i = 0; i < secure_arguments.length; i++) - { - var name = Path.get_basename(secure_arguments[i]); - - if (name.has_prefix(cmd)) - { - for (int j = 0; j < name.length; j++) - { - if(name[j] == ' ') - name = name.substring(0, j); - } - if(i == 0) - cmd_parameter = secure_arguments[1]; - else - cmd_parameter = secure_arguments[0]; + process_table.remove (p.pid); + } - return sanity_cmd(name); - } - } + private string get_app_id_for_process (Process p) { + AppInfo? info = AppItem.app_info_for_process (p); + + if (info != null) + return info.get_id (); + else if (group_system_apps) + return "system"; - return sanity_cmd(cmd); + return p.cmdline; + } + + private void update_process(ref Process process) + { + cpu_monitor.update_process(ref process); + memory_monitor.update_process(ref process); + process.update_status(); + process.gamemode = gamemode_pids.contains((int) process.pid); } - private bool is_system_app(string cmdline) + public static void sort_pids (void *pids, size_t elm, size_t length) { - return !AppItem.have_app_info(cmdline); + Posix.qsort (pids, length, elm, (a, b) => { + return (*(GLib.Pid *) a) - (* (GLib.Pid *) b); + }); } } } diff --git a/vapi/stopgap.vapi b/vapi/stopgap.vapi new file mode 100644 index 0000000000000000000000000000000000000000..43eddd986315a07c2232632718706eb26a349f9a --- /dev/null +++ b/vapi/stopgap.vapi @@ -0,0 +1,13 @@ +[CCode (cprefix = "", lower_case_cprefix = "")] +namespace StopGap { + + [CCode (cheader_filename = "fcntl.h")] + public const int O_DIRECTORY; + [CCode (cheader_filename = "fcntl.h")] + public const int O_CLOEXEC; + [CCode (cheader_filename = "fcntl.h")] + public const int AT_FDCWD; + [CCode (cheader_filename = "fcntl.h", feature_test_macro = "_GNU_SOURCE")] + public int openat (int dirfd, string path, int oflag, Posix.mode_t mode=0); + +} \ No newline at end of file