From 83cc7a94a41266d315908a1afdb7f3dac4dd063c Mon Sep 17 00:00:00 2001 From: Carter Li Date: Mon, 24 Aug 2026 16:05:36 +0800 Subject: [PATCH] Top: adds new module --- CMakeLists.txt | 13 ++ doc/json_schema.json | 77 ++++++++++- src/common/impl/commandoption.c | 8 ++ src/common/impl/jsonconfig.c | 10 ++ src/detection/top/top.c | 117 ++++++++++++++++ src/detection/top/top.h | 27 ++++ src/detection/top/top_apple.c | 57 ++++++++ src/detection/top/top_bsd.c | 67 ++++++++++ src/detection/top/top_dbsd.c | 67 ++++++++++ src/detection/top/top_gnu.c | 78 +++++++++++ src/detection/top/top_haiku.c | 47 +++++++ src/detection/top/top_linux.c | 126 ++++++++++++++++++ src/detection/top/top_nbsd.c | 65 +++++++++ src/detection/top/top_obsd.c | 44 ++++++ src/detection/top/top_sunos.c | 66 +++++++++ src/detection/top/top_windows.c | 48 +++++++ src/modules/modules.c | 3 + src/modules/modules.h | 1 + src/modules/options.h | 1 + src/modules/top/option.h | 23 ++++ src/modules/top/top.c | 229 ++++++++++++++++++++++++++++++++ src/modules/top/top.h | 11 ++ 22 files changed, 1183 insertions(+), 2 deletions(-) create mode 100644 src/detection/top/top.c create mode 100644 src/detection/top/top.h create mode 100644 src/detection/top/top_apple.c create mode 100644 src/detection/top/top_bsd.c create mode 100644 src/detection/top/top_dbsd.c create mode 100644 src/detection/top/top_gnu.c create mode 100644 src/detection/top/top_haiku.c create mode 100644 src/detection/top/top_linux.c create mode 100644 src/detection/top/top_nbsd.c create mode 100644 src/detection/top/top_obsd.c create mode 100644 src/detection/top/top_sunos.c create mode 100644 src/detection/top/top_windows.c create mode 100644 src/modules/top/option.h create mode 100644 src/modules/top/top.c create mode 100644 src/modules/top/top.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d4070af9..d8313c345 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -484,6 +484,7 @@ set(LIBFASTFETCH_SRC src/detection/physicalmemory/physicalmemory.c src/detection/publicip/publicip.c src/detection/terminaltheme/terminaltheme.c + src/detection/top/top.c src/detection/terminalfont/terminalfont.c src/detection/terminalshell/terminalshell.c src/detection/version/version.c @@ -559,6 +560,7 @@ set(LIBFASTFETCH_SRC src/modules/terminalsize/terminalsize.c src/modules/theme/theme.c src/modules/title/title.c + src/modules/top/top.c src/modules/tpm/tpm.c src/modules/uptime/uptime.c src/modules/users/users.c @@ -651,6 +653,7 @@ if(LINUX) src/detection/terminalshell/terminalshell_linux.c src/detection/terminalsize/terminalsize_linux.c src/detection/theme/theme_linux.c + src/detection/top/top_linux.c src/detection/tpm/tpm_linux.c src/detection/uptime/uptime_linux.c src/detection/users/users_linux.c @@ -822,11 +825,13 @@ elseif(FreeBSD) if(DragonFly) list(APPEND LIBFASTFETCH_SRC src/detection/bluetooth/bluetooth_nosupport.c + src/detection/top/top_dbsd.c src/detection/wifi/wifi_nosupport.c ) else() list(APPEND LIBFASTFETCH_SRC src/detection/bluetooth/bluetooth_bsd.c + src/detection/top/top_bsd.c src/detection/wifi/wifi_bsd.c ) endif() @@ -904,6 +909,7 @@ elseif(NetBSD) src/detection/terminalsize/terminalsize_linux.c src/detection/theme/theme_linux.c src/detection/tpm/tpm_nosupport.c + src/detection/top/top_nbsd.c src/detection/uptime/uptime_bsd.c src/detection/users/users_linux.c src/detection/wallpaper/wallpaper_linux.c @@ -990,6 +996,7 @@ elseif(OpenBSD) src/detection/terminalsize/terminalsize_linux.c src/detection/theme/theme_linux.c src/detection/tpm/tpm_nosupport.c + src/detection/top/top_obsd.c src/detection/uptime/uptime_bsd.c src/detection/users/users_obsd.c src/detection/wallpaper/wallpaper_linux.c @@ -1057,6 +1064,7 @@ elseif(APPLE) src/detection/terminalshell/terminalshell_linux.c src/detection/terminalsize/terminalsize_linux.c src/detection/theme/theme_apple.c + src/detection/top/top_apple.c src/detection/tpm/tpm_apple.c src/detection/uptime/uptime_bsd.c src/detection/users/users_linux.c @@ -1149,6 +1157,7 @@ elseif(WIN32) src/detection/terminalshell/terminalshell_windows.c src/detection/terminalsize/terminalsize_windows.c src/detection/theme/theme_windows.c + src/detection/top/top_windows.c src/detection/tpm/tpm_windows.c src/detection/uptime/uptime_windows.c src/detection/users/users_windows.c @@ -1231,6 +1240,7 @@ elseif(SunOS) src/detection/terminalshell/terminalshell_linux.c src/detection/terminalsize/terminalsize_linux.c src/detection/theme/theme_linux.c + src/detection/top/top_sunos.c src/detection/tpm/tpm_nosupport.c src/detection/uptime/uptime_sunos.c src/detection/users/users_linux.c @@ -1301,6 +1311,7 @@ elseif(Haiku) src/detection/terminalshell/terminalshell_linux.c src/detection/terminalsize/terminalsize_linux.c src/detection/theme/theme_nosupport.c + src/detection/top/top_haiku.c src/detection/tpm/tpm_nosupport.c src/detection/uptime/uptime_haiku.c src/detection/users/users_linux.c @@ -1385,6 +1396,7 @@ elseif(GNU) src/detection/terminalshell/terminalshell_linux.c src/detection/terminalsize/terminalsize_linux.c src/detection/theme/theme_linux.c + src/detection/top/top_gnu.c src/detection/tpm/tpm_nosupport.c src/detection/uptime/uptime_linux.c src/detection/users/users_linux.c @@ -1995,6 +2007,7 @@ elseif(SunOS) elseif(GNU) target_link_libraries(libfastfetch PRIVATE "m" + PRIVATE "ps" ) elseif(ANDROID) target_link_libraries(libfastfetch diff --git a/doc/json_schema.json b/doc/json_schema.json index 9e0aabc34..411cb9ce0 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -322,7 +322,7 @@ "type": "string" }, "cpuFormat": { - "description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name\n 14. {technology}: CPU technology", + "description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count", "type": "string" }, "cpucacheFormat": { @@ -505,6 +505,10 @@ "description": "Output format for the `Title` module. See Wiki for formatting syntax\n 1. {user-name}: User name\n 2. {host-name}: Host name\n 3. {home-dir}: Home directory\n 4. {exe-path}: Executable path of current process\n 5. {user-shell}: User's default shell\n 6. {user-name-colored}: User name (colored)\n 7. {at-symbol-colored}: @ symbol (colored)\n 8. {host-name-colored}: Host name (colored)\n 9. {full-user-name}: Full user name\n 10. {user-id}: UID (*nix) / SID (Windows)\n 11. {pid}: PID of current process\n 12. {cwd}: CWD with home dir replaced by `~`", "type": "string" }, + "topFormat": { + "description": "Output format for the `Top` module. See Wiki for formatting syntax\n 1. {name}: Process name\n 2. {path}: Executable path\n 3. {pid}: Process ID\n 4. {cpu}: CPU usage\n 5. {mem}: Memory usage (RSS) in bytes\n 6. {disk-read}: Disk read bytes per second\n 7. {disk-write}: Disk write bytes per second\n 8. {cpu-percentage}: CPU usage percentage\n 9. {mem-formatted}: Memory usage (RSS) formatted\n 10. {disk-read-formatted}: Disk read formatted\n 11. {disk-write-formatted}: Disk write formatted", + "type": "string" + }, "themeFormat": { "description": "Output format for the `Theme` module. See Wiki for formatting syntax\n 1. {theme1}: Theme part 1\n 2. {theme2}: Theme part 2", "type": "string" @@ -542,7 +546,7 @@ "type": "string" }, "wifiFormat": { - "description": "Output format for the `Wifi` module. See Wiki for formatting syntax\n 1. {inf-desc}: Interface description\n 2. {inf-status}: Interface status\n 3. {status}: Connection status\n 4. {ssid}: Connection SSID\n 5. {bssid}: Connection BSSID\n 6. {protocol}: Connection protocol\n 7. {signal-quality}: Connection signal quality (percentage num)\n 8. {rx-rate}: Connection RX rate\n 9. {tx-rate}: Connection TX rate\n 10. {security}: Connection Security algorithm\n 11. {signal-quality-bar}: Connection signal quality (percentage bar)\n 12. {channel}: Connection channel number\n 13. {band}: Connection channel band in GHz", + "description": "Output format for the `Wifi` module. See Wiki for formatting syntax\n 1. {inf-desc}: Interface description\n 2. {inf-status}: Interface status\n 3. {status}: Connection status\n 4. {ssid}: Connection SSID\n 5. {bssid}: Connection BSSID\n 6. {protocol}: Connection protocol\n 7. {signal-quality}: Connection signal quality (percentage num)\n 8. {rx-rate}: Connection RX rate\n 9. {tx-rate}: Connection TX rate\n 10. {security}: Connection Security algorithm\n 11. {signal-quality-bar}: Connection signal quality (percentage bar)\n 12. {channel}: Connection channel number\n 13. {channel-width}: Connection channel width in MHz\n 14. {band}: Connection channel band in GHz", "type": "string" }, "wmthemeFormat": { @@ -1592,6 +1596,7 @@ "poweradapter", "processes", "publicip", + "top", "separator", "shell", "sound", @@ -4424,6 +4429,74 @@ } } }, + { + "title": "Top", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "top", + "description": "Print processes with the highest CPU or memory usage" + }, + "sort": { + "type": "string", + "description": "Sort processes by CPU, memory or disk IO usage", + "enum": [ + "cpu", + "memory", + "disk-read", + "disk-write" + ], + "default": "cpu" + }, + "processes": { + "type": "integer", + "description": "Number of top processes to print", + "minimum": 1, + "default": 5 + }, + "waitTime": { + "type": "integer", + "description": "Wait time between process samples, in milliseconds", + "minimum": 1, + "default": 500 + }, + "compact": { + "type": "boolean", + "description": "Print all process on a single line", + "default": false + }, + "kernelTask": { + "type": "boolean", + "description": "Whether to include kernel tasks (kworker, System, kernel_task) in the process list", + "default": false + }, + "percent": { + "$ref": "#/$defs/percent" + }, + "key": { + "$ref": "#/$defs/key" + }, + "keyColor": { + "$ref": "#/$defs/keyColor" + }, + "keyIcon": { + "$ref": "#/$defs/keyIcon" + }, + "keyWidth": { + "$ref": "#/$defs/keyWidth" + }, + "outputColor": { + "$ref": "#/$defs/outputColor" + }, + "format": { + "$ref": "#/$defs/topFormat" + }, + "condition": { + "$ref": "#/$defs/conditions" + } + } + }, { "title": "TPM", "type": "object", diff --git a/src/common/impl/commandoption.c b/src/common/impl/commandoption.c index a8764d610..d8922a48e 100644 --- a/src/common/impl/commandoption.c +++ b/src/common/impl/commandoption.c @@ -111,6 +111,14 @@ void ffPrepareCommandOption(FFdata* data) { break; #endif + #if !FF_MODULE_DISABLE_TOP + case 'T': + case 't': + FF_IF_MODULE_MATCH(ffTopModuleInfo.name) + ffPrepareTopProcesses(); + break; + #endif + #if !FF_MODULE_DISABLE_WEATHER case 'W': case 'w': diff --git a/src/common/impl/jsonconfig.c b/src/common/impl/jsonconfig.c index 7c9d44a88..3d26e922e 100644 --- a/src/common/impl/jsonconfig.c +++ b/src/common/impl/jsonconfig.c @@ -195,6 +195,16 @@ static void prepareModuleJsonObject(const char* type, yyjson_val* module) { } #endif + #if !FF_MODULE_DISABLE_TOP + case 't': + case 'T': { + if (ffStrEqualsIgnCase(type, ffTopModuleInfo.name)) { + ffPrepareTopProcesses(); + } + break; + } + #endif + #if !FF_MODULE_DISABLE_WEATHER case 'w': case 'W': { diff --git a/src/detection/top/top.c b/src/detection/top/top.c new file mode 100644 index 000000000..659b47f0e --- /dev/null +++ b/src/detection/top/top.c @@ -0,0 +1,117 @@ +#include "top.h" + +#include "common/time.h" + +static FFlist first; +static double startTick; + +void ffPrepareTopProcesses(void) { + if (startTick != 0) { + return; // Already prepared + } + + ffListInit(&first); + startTick = ffTimeGetTick(); + ffTopGetProcessSnapshot(&first); +} + +// clang-format off +static int compareCpuResults(const FFTopProcessResult* a, const FFTopProcessResult* b) { + if (a->cpuPercent < b->cpuPercent) return 1; + if (a->cpuPercent > b->cpuPercent) return -1; + return (int) (a->pid - b->pid); +} + +static int compareMemoryResults(const FFTopProcessResult* a, const FFTopProcessResult* b) { + if (a->memBytes < b->memBytes) return 1; + if (a->memBytes > b->memBytes) return -1; + return (int) (a->pid - b->pid); +} + +static int compareDiskReadResults(const FFTopProcessResult* a, const FFTopProcessResult* b) { + if (a->bytesRead < b->bytesRead) return 1; + if (a->bytesRead > b->bytesRead) return -1; + return (int) (a->pid - b->pid); +} + +static int compareDiskWriteResults(const FFTopProcessResult* a, const FFTopProcessResult* b) { + if (a->bytesWritten < b->bytesWritten) return 1; + if (a->bytesWritten > b->bytesWritten) return -1; + return (int) (a->pid - b->pid); +} +// clang-format on + +const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) { + ffListClear(result); + if (options->nProcesses == 0) { + return nullptr; + } + + if (startTick == 0) { + ffPrepareTopProcesses(); + } + + double elapsedTime = ffTimeGetTick() - startTick; + if (elapsedTime < (double) options->waitTime) { + ffTimeSleep(options->waitTime - (uint32_t) elapsedTime); + } + + if (first.length == 0) { + return "No processes found"; + } + + FF_LIST_AUTO_DESTROY second = ffListCreate(); + const char* error = ffTopGetProcessSnapshot(&second); + const double elapsed = ffTimeGetTick() - startTick; + + if (error || elapsed <= 0) { + FF_LIST_FOR_EACH (FFTopProcessSnapshot, item, second) { + ffStrbufDestroy(&item->name); + } + return error ?: "Invalid process sampling interval"; + } + + ffListInitA(result, sizeof(FFTopProcessResult), first.length < options->nProcesses ? first.length : options->nProcesses); + FF_LIST_FOR_EACH (FFTopProcessSnapshot, oldItem, first) { + FFTopProcessSnapshot* newItem = nullptr; + FF_LIST_FOR_EACH (FFTopProcessSnapshot, item, second) { + if (item->pid == oldItem->pid) { + newItem = item; + break; + } + } + if (!newItem || newItem->startTime != oldItem->startTime || newItem->cpuTime < oldItem->cpuTime || newItem->bytesRead < oldItem->bytesRead || newItem->bytesWritten < oldItem->bytesWritten) { + ffStrbufDestroy(&oldItem->name); + continue; + } + + FFTopProcessResult* item = FF_LIST_ADD(FFTopProcessResult, *result); + item->pid = newItem->pid; + item->memBytes = newItem->memBytes; + item->bytesRead = (newItem->bytesRead - oldItem->bytesRead) * 1000u / (uint64_t) elapsed; + item->bytesWritten = (newItem->bytesWritten - oldItem->bytesWritten) * 1000u / (uint64_t) elapsed; + item->cpuPercent = (double) (newItem->cpuTime - oldItem->cpuTime) / elapsed * 100.0; + item->startTime = newItem->startTime; + ffStrbufInitMove(&item->name, &oldItem->name); + } + + const void* compare = options->sort == FF_TOP_SORT_DISK_WRITE ? (void*) compareDiskWriteResults + : options->sort == FF_TOP_SORT_DISK_READ ? (void*) compareDiskReadResults + : options->sort == FF_TOP_SORT_MEMORY ? (void*) compareMemoryResults + : (void*) compareCpuResults; + ffListSort(result, sizeof(FFTopProcessResult), (void*) compare); + if (result->length > options->nProcesses) { + FFTopProcessResult* items = (FFTopProcessResult*) result->data; + for (uint32_t i = options->nProcesses; i < result->length; ++i) { + ffStrbufDestroy(&items[i].name); + } + result->length = options->nProcesses; + } + + // Reuse `second` as the baseline of the next call + ffListDestroy(&first); + ffListInitMove(&first, &second); + startTick = ffTimeGetTick(); + + return nullptr; +} diff --git a/src/detection/top/top.h b/src/detection/top/top.h new file mode 100644 index 000000000..b732a1fd6 --- /dev/null +++ b/src/detection/top/top.h @@ -0,0 +1,27 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/top/option.h" + +typedef struct FFTopProcessResult { + FFstrbuf name; + uint64_t startTime; + double cpuPercent; + uint64_t memBytes; + uint64_t bytesRead; + uint64_t bytesWritten; + uint32_t pid; +} FFTopProcessResult; + +typedef struct FFTopProcessSnapshot { + FFstrbuf name; // comm + uint64_t cpuTime; // user_time + system_time + uint64_t memBytes; // RSS + uint64_t bytesRead; // Physical storage read + uint64_t bytesWritten; // Physical storage written + uint64_t startTime; // Differs between platforms + uint32_t pid; +} FFTopProcessSnapshot; + +const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result); +const char* ffTopGetProcessSnapshot(FFlist* snapshots); diff --git a/src/detection/top/top_apple.c b/src/detection/top/top_apple.c new file mode 100644 index 000000000..0fdcb00be --- /dev/null +++ b/src/detection/top/top_apple.c @@ -0,0 +1,57 @@ +#include "top.h" +#include "common/mallocHelper.h" + +#include +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; + size_t length; + + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}) failed"; + } + + // The process table may change between the two sysctl calls; retry with a larger buffer. + FF_AUTO_FREE struct kinfo_proc* processes = nullptr; + for (int attempts = 0;; ++attempts) { + length += length / 8 + sizeof(struct kinfo_proc); + struct kinfo_proc* newProcesses = (struct kinfo_proc*) realloc(processes, length); + if (!newProcesses) { + return "realloc(struct kinfo_proc[]) failed"; + } + processes = newProcesses; + if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) == 0) { + break; + } + if ((errno != ENOMEM && errno != EINVAL) || attempts >= 4) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}) failed"; + } + } + uint32_t count = (uint32_t) (length / sizeof(struct kinfo_proc)); + + for (uint32_t i = 0; i < count; ++i) { + const struct kinfo_proc* proc = &processes[i]; + pid_t pid = proc->kp_proc.p_pid; + if (pid <= 0) { + continue; + } + + struct rusage_info_v2 rusage; + if (proc_pid_rusage(pid, RUSAGE_INFO_V2, (rusage_info_t*) &rusage) != 0) { + continue; // The process may have exited + } + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + ffStrbufInitS(&item->name, proc->kp_proc.p_comm); + item->pid = (uint32_t) pid; + item->cpuTime = (rusage.ri_user_time + rusage.ri_system_time) / 1000000u; // ns -> ms + item->memBytes = rusage.ri_resident_size; + item->bytesRead = rusage.ri_diskio_bytesread; + item->bytesWritten = rusage.ri_diskio_byteswritten; + item->startTime = rusage.ri_proc_start_abstime; + } + + return nullptr; +} diff --git a/src/detection/top/top_bsd.c b/src/detection/top/top_bsd.c new file mode 100644 index 000000000..87a67bba2 --- /dev/null +++ b/src/detection/top/top_bsd.c @@ -0,0 +1,67 @@ +#include "top.h" +#include "common/mallocHelper.h" + +#include +#include +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC }; + size_t length; + + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed"; + } + + // The process table may change between the two sysctl calls; retry with a larger buffer. + FF_AUTO_FREE struct kinfo_proc* processes = nullptr; + for (int attempts = 0;; ++attempts) { + length += length / 8 + sizeof(struct kinfo_proc); + struct kinfo_proc* newProcesses = (struct kinfo_proc*) realloc(processes, length); + if (!newProcesses) { + return "realloc(struct kinfo_proc[]) failed"; + } + processes = newProcesses; + if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) == 0) { + break; + } + if ((errno != ENOMEM && errno != EINVAL) || attempts >= 4) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed"; + } + } + uint32_t count = (uint32_t) (length / sizeof(struct kinfo_proc)); + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + + for (uint32_t i = 0; i < count; ++i) { + const struct kinfo_proc* proc = &processes[i]; + + if (proc->ki_flag & P_KPROC) { + continue; + } + + uint32_t pid = (uint32_t) proc->ki_pid; + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + + ffStrbufInitS(&item->name, proc->ki_comm); + item->pid = pid; + + uint64_t userMs = (uint64_t) proc->ki_rusage.ru_utime.tv_sec * 1000 + + (uint64_t) proc->ki_rusage.ru_utime.tv_usec / 1000; + uint64_t sysMs = (uint64_t) proc->ki_rusage.ru_stime.tv_sec * 1000 + + (uint64_t) proc->ki_rusage.ru_stime.tv_usec / 1000; + item->cpuTime = userMs + sysMs; + + item->memBytes = (uint64_t) proc->ki_rssize * (uint64_t) pageSize; + + item->startTime = (uint64_t) proc->ki_start.tv_sec * 1000 + + (uint64_t) proc->ki_start.tv_usec / 1000; + + item->bytesRead = (uint64_t) proc->ki_rusage.ru_inblock * DEV_BSIZE; + item->bytesWritten = (uint64_t) proc->ki_rusage.ru_oublock * DEV_BSIZE; + } + + return NULL; +} diff --git a/src/detection/top/top_dbsd.c b/src/detection/top/top_dbsd.c new file mode 100644 index 000000000..872ba97f8 --- /dev/null +++ b/src/detection/top/top_dbsd.c @@ -0,0 +1,67 @@ +#include "top.h" +#include "common/mallocHelper.h" + +#include +#include +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; + size_t length; + + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}) failed"; + } + + // The process table may change between the two sysctl calls; retry with a larger buffer. + FF_AUTO_FREE struct kinfo_proc* processes = nullptr; + for (int attempts = 0;; ++attempts) { + length += length / 8 + sizeof(struct kinfo_proc); + struct kinfo_proc* newProcesses = (struct kinfo_proc*) realloc(processes, length); + if (!newProcesses) { + return "realloc(struct kinfo_proc[]) failed"; + } + processes = newProcesses; + if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) == 0) { + break; + } + if ((errno != ENOMEM && errno != EINVAL) || attempts >= 4) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_ALL}) failed"; + } + } + uint32_t count = (uint32_t) (length / sizeof(struct kinfo_proc)); + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + + for (uint32_t i = 0; i < count; ++i) { + const struct kinfo_proc* proc = &processes[i]; + + // Kernel threads are reported with P_SYSTEM flags and pid -1. + if ((proc->kp_flags & P_SYSTEM) || proc->kp_stat == SZOMB) { + continue; + } + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + + ffStrbufInitS(&item->name, proc->kp_comm); + item->pid = (uint32_t) proc->kp_pid; + + // kp_ru only accounts for exited lwps; the cpu times of live lwps are + // kept in microseconds in kp_lwp.kl_uticks/kl_sticks (see calcru(9)). + uint64_t exitedMs = (uint64_t) (proc->kp_ru.ru_utime.tv_sec + proc->kp_ru.ru_stime.tv_sec) * 1000 + + (uint64_t) (proc->kp_ru.ru_utime.tv_usec + proc->kp_ru.ru_stime.tv_usec) / 1000; + uint64_t liveMs = (proc->kp_lwp.kl_uticks + proc->kp_lwp.kl_sticks) / 1000; + item->cpuTime = exitedMs + liveMs; + + item->memBytes = (uint64_t) proc->kp_vm_rssize * pageSize; + + item->startTime = (uint64_t) proc->kp_start.tv_sec * 1000 + + (uint64_t) proc->kp_start.tv_usec / 1000; + + item->bytesRead = (uint64_t) (proc->kp_ru.ru_inblock + proc->kp_lwp.kl_ru.ru_inblock) * DEV_BSIZE; + item->bytesWritten = (uint64_t) (proc->kp_ru.ru_oublock + proc->kp_lwp.kl_ru.ru_oublock) * DEV_BSIZE; + } + + return nullptr; +} diff --git a/src/detection/top/top_gnu.c b/src/detection/top/top_gnu.c new file mode 100644 index 000000000..3545ddac0 --- /dev/null +++ b/src/detection/top/top_gnu.c @@ -0,0 +1,78 @@ +#include "top.h" + +#include +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + struct ps_context* context = nullptr; + if (ps_context_create(getproc(), &context) != 0) { + return "ps_context_create(getproc()) failed"; + } + + const char* error = nullptr; + struct proc_stat_list* list = nullptr; + if (proc_stat_list_create(context, &list) != 0) { + error = "proc_stat_list_create() failed"; + goto done; + } + + if (proc_stat_list_add_all(list, nullptr, nullptr) != 0) { + error = "proc_stat_list_add_all() failed"; + goto done; + } + + // Flags that cannot be fetched for some process are simply left unset; + // each field must be checked before use. + if (proc_stat_list_set_flags(list, PSTAT_ARGS | PSTAT_STATE | PSTAT_TASK_BASIC) != 0) { + error = "proc_stat_list_set_flags() failed"; + goto done; + } + + for (unsigned i = 0; i < list->num_procs; ++i) { + struct proc_stat* stat = list->proc_stats[i]; + + if (!proc_stat_has(stat, PSTAT_PID | PSTAT_STATE)) { + continue; + } + if (proc_stat_state(stat) & PSTAT_STATE_P_ZOMBIE) { + continue; + } + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + + // There is no kernel-side comm on the Hurd; use the first argument. + ffStrbufInit(&item->name); + if (proc_stat_has(stat, PSTAT_ARGS) && proc_stat_args(stat)) { + size_t len = strnlen(proc_stat_args(stat), proc_stat_args_len(stat)); + ffStrbufAppendNS(&item->name, (uint32_t) len, proc_stat_args(stat)); + } + if (item->name.length == 0) { + ffStrbufAppendS(&item->name, "(unknown)"); + } + + item->pid = (uint32_t) proc_stat_pid(stat); + + if (proc_stat_has(stat, PSTAT_TASK_BASIC)) { + const task_basic_info_t info = proc_stat_task_basic_info(stat); + item->cpuTime = (uint64_t) info->user_time.seconds * 1000 + + (uint64_t) info->user_time.microseconds / 1000 + + (uint64_t) info->system_time.seconds * 1000 + + (uint64_t) info->system_time.microseconds / 1000; + item->memBytes = (uint64_t) info->resident_size; + } + + // Neither process start times nor storage io counters are exposed by + // the Hurd; leave them zeroed. + item->startTime = 0; + item->bytesRead = 0; + item->bytesWritten = 0; + } + +done: + if (list) { + proc_stat_list_free(list); + } + ps_context_free(context); + return error; +} diff --git a/src/detection/top/top_haiku.c b/src/detection/top/top_haiku.c new file mode 100644 index 000000000..39efdb2b0 --- /dev/null +++ b/src/detection/top/top_haiku.c @@ -0,0 +1,47 @@ +#include "top.h" + +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + int32 cookie = 0; + team_info team; + while (get_next_team_info(&cookie, &team) == B_OK) { + if (team.team == B_SYSTEM_TEAM) { + continue; // The kernel team + } + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + + ffStrbufInitS(&item->name, team.name); + + item->pid = (uint32_t) team.team; + + // User and kernel time of all threads of the team combined in microseconds. + item->cpuTime = 0; + team_usage_info usage; + if (get_team_usage_info(team.team, B_TEAM_USAGE_SELF, &usage) == B_OK) { + item->cpuTime = ((uint64_t) usage.user_time + (uint64_t) usage.kernel_time) / 1000; + } + + // Sum up the physically resident memory of all areas of the team; + // ram_size is the amount of physical pages committed to an area, + // whereas size is just its virtual reservation. + uint64_t memBytes = 0; + ssize_t areaCookie = 0; + area_info area; + while (get_next_area_info(team.team, &areaCookie, &area) == B_OK) { + memBytes += area.ram_size; + } + item->memBytes = memBytes; + + // start_time is microseconds since boot. + item->startTime = (uint64_t) team.start_time; + + // Storage io counters are not exposed by Haiku; leave them zeroed. + item->bytesRead = 0; + item->bytesWritten = 0; + } + + return nullptr; +} diff --git a/src/detection/top/top_linux.c b/src/detection/top/top_linux.c new file mode 100644 index 000000000..81b53c957 --- /dev/null +++ b/src/detection/top/top_linux.c @@ -0,0 +1,126 @@ +#include "top.h" + +#include "common/io.h" +#include "common/memrchr.h" +#include "common/strutil.h" + +#include + +#define PF_KTHREAD 0x00200000 // defined in kernel include/linux/sched.h + +static bool parseStat(const char* buffer, size_t length, FFTopProcessSnapshot* result) { + const char* open = strchr(buffer, '('); + const char* close = (const char*) memrchr(buffer, ')', length); + if (!open || !close || close <= open || close + 2 >= buffer + length) { + return false; + } + + ffStrbufSetNS(&result->name, (uint32_t) (close - open - 1), open + 1); + const char* cursor = close + 2; // state, then fields 4.. + if (*cursor == '\0') { + return false; + } + ++cursor; + + uint64_t values[19]; + for (uint32_t field = 4; field <= 22; ++field) { + while (*cursor == ' ') { + ++cursor; + } + if (*cursor == '\0') { + return false; + } + char* end; + errno = 0; + unsigned long long value = strtoull(cursor, &end, 10); + if (end == cursor || errno == ERANGE) { + return false; + } + values[field - 4] = (uint64_t) value; + cursor = end; + } + + if (values[5] & PF_KTHREAD) { // values[5] is field 9 (flags) + return false; + } + + result->cpuTime = values[11] + values[12]; // user + system + result->startTime = values[18]; + return true; +} + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + const long ticks = sysconf(_SC_CLK_TCK); + const long pageSize = instance.state.platform.sysinfo.pageSize; + if (ticks <= 0 || pageSize <= 0) { + return "sysconf(_SC_CLK_TCK or _SC_PAGESIZE) failed"; + } + + FF_AUTO_CLOSE_DIR DIR* dir = opendir("/proc"); + if (!dir) { + return "opendir(\"/proc\") failed"; + } + + int procfd = dirfd(dir); + + struct dirent* entry; + char statBuffer[4096]; + char statmBuffer[256]; + while ((entry = readdir(dir))) { + if (!entry->d_name[0] || !ffCharIsDigit(entry->d_name[0])) { + continue; + } + + unsigned long pid = strtoul(entry->d_name, nullptr, 10); + if (pid == 0 || pid > UINT32_MAX) { + continue; + } + + FF_AUTO_CLOSE_FD int subfd = openat(procfd, entry->d_name, O_RDONLY | O_DIRECTORY); + if (subfd < 0) { + continue; + } + + ssize_t statLength; + if ((statLength = ffReadFileDataRelative(subfd, "stat", sizeof(statBuffer) - 1, statBuffer)) < 0) { + continue; + } + statBuffer[statLength] = '\0'; + + ssize_t statmLength; + if ((statmLength = ffReadFileDataRelative(subfd, "statm", sizeof(statmBuffer) - 1, statmBuffer)) < 0) { + continue; + } + statmBuffer[statmLength] = '\0'; + + char ioBuffer[512]; + ssize_t ioLength; + if ((ioLength = ffReadFileDataRelative(subfd, "io", sizeof(ioBuffer) - 1, ioBuffer)) < 0) { + continue; + } + ioBuffer[ioLength] = '\0'; + + auto snapshot = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + ffStrbufInit(&snapshot->name); + if (!parseStat(statBuffer, (size_t) statLength, snapshot)) { + ffStrbufDestroy(&snapshot->name); + --snapshots->length; + continue; + } + + unsigned long long rssPages; + if (__builtin_expect(sscanf(statmBuffer, "%*llu %llu", &rssPages) != 1, false)) { + ffStrbufDestroy(&snapshot->name); + --snapshots->length; + continue; + } + + snapshot->pid = (uint32_t) pid; + snapshot->memBytes = rssPages * (uint64_t) pageSize; + snapshot->bytesRead = 0; + snapshot->bytesWritten = 0; + sscanf(ioBuffer, "rchar: %*" SCNu64 "\nwchar: %*" SCNu64 "\nsyscr: %*" SCNu64 "\nsyscw: %*" SCNu64 "\nread_bytes: %" SCNu64 "\nwrite_bytes: %" SCNu64, &snapshot->bytesRead, &snapshot->bytesWritten); + snapshot->cpuTime = snapshot->cpuTime * 1000u / (uint64_t) ticks; + } + return nullptr; +} diff --git a/src/detection/top/top_nbsd.c b/src/detection/top/top_nbsd.c new file mode 100644 index 000000000..3fee77049 --- /dev/null +++ b/src/detection/top/top_nbsd.c @@ -0,0 +1,65 @@ +#include "top.h" +#include "common/mallocHelper.h" + +#include +#include +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + int request[] = { CTL_KERN, KERN_PROC2, KERN_PROC_ALL, -1, sizeof(struct kinfo_proc2), INT_MAX }; + size_t length; + + if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL}) failed"; + } + + // The process table may change between the two sysctl calls; retry with a larger buffer. + FF_AUTO_FREE struct kinfo_proc2* processes = nullptr; + for (int attempts = 0;; ++attempts) { + length += length / 8 + sizeof(struct kinfo_proc2); + struct kinfo_proc2* newProcesses = (struct kinfo_proc2*) realloc(processes, length); + if (!newProcesses) { + return "realloc(struct kinfo_proc2[]) failed"; + } + processes = newProcesses; + if (sysctl(request, ARRAY_SIZE(request), processes, &length, nullptr, 0) == 0) { + break; + } + if ((errno != ENOMEM && errno != EINVAL) || attempts >= 4) { + return "sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL}) failed"; + } + } + uint32_t count = (uint32_t) (length / sizeof(struct kinfo_proc2)); + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + + for (uint32_t i = 0; i < count; ++i) { + const struct kinfo_proc2* proc = &processes[i]; + + if ((proc->p_flag & P_SYSTEM) || !proc->p_uvalid) { // Kernel process or zombie + continue; + } + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + + ffStrbufInitS(&item->name, proc->p_comm); + item->pid = (uint32_t) proc->p_pid; + + uint64_t userMs = (uint64_t) proc->p_uutime_sec * 1000 + + (uint64_t) proc->p_uutime_usec / 1000; + uint64_t sysMs = (uint64_t) proc->p_ustime_sec * 1000 + + (uint64_t) proc->p_ustime_usec / 1000; + item->cpuTime = userMs + sysMs; + + item->memBytes = (uint64_t) proc->p_vm_rssize * (uint64_t) pageSize; + + item->startTime = (uint64_t) proc->p_ustart_sec * 1000 + + (uint64_t) proc->p_ustart_usec / 1000; + + item->bytesRead = (uint64_t) proc->p_uru_inblock * DEV_BSIZE; + item->bytesWritten = (uint64_t) proc->p_uru_oublock * DEV_BSIZE; + } + + return NULL; +} diff --git a/src/detection/top/top_obsd.c b/src/detection/top/top_obsd.c new file mode 100644 index 000000000..99d7b0a8f --- /dev/null +++ b/src/detection/top/top_obsd.c @@ -0,0 +1,44 @@ +#include "top.h" + +#include +#include // DEV_BSIZE +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); + if (!kd) { + return "kvm_open() failed"; + } + + int count = 0; + // KERN_PROC_ALL returns all user-level processes, excluding kernel processes and threads + const struct kinfo_proc* processes = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &count); + if (!processes) { + kvm_close(kd); + return "kvm_getprocs() failed"; + } + + const uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + + for (int i = 0; i < count; ++i) { + const struct kinfo_proc* proc = &processes[i]; + + if (!proc->p_uvalid) { // Zombie process; p_u* members are invalid + continue; + } + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + ffStrbufInitS(&item->name, proc->p_comm); + item->pid = (uint32_t) proc->p_pid; + item->cpuTime = ((uint64_t) proc->p_uutime_sec * 1000 + (uint64_t) proc->p_uutime_usec / 1000) + + ((uint64_t) proc->p_ustime_sec * 1000 + (uint64_t) proc->p_ustime_usec / 1000); + item->memBytes = (uint64_t) proc->p_vm_rssize * pageSize; + item->startTime = (uint64_t) proc->p_ustart_sec * 1000 + (uint64_t) proc->p_ustart_usec / 1000; + item->bytesRead = (uint64_t) proc->p_uru_inblock * DEV_BSIZE; + item->bytesWritten = (uint64_t) proc->p_uru_oublock * DEV_BSIZE; + } + + kvm_close(kd); + return nullptr; +} diff --git a/src/detection/top/top_sunos.c b/src/detection/top/top_sunos.c new file mode 100644 index 000000000..b00ce50ce --- /dev/null +++ b/src/detection/top/top_sunos.c @@ -0,0 +1,66 @@ +#include "top.h" + +#include "common/io.h" +#include "common/strutil.h" + +#include +#include +#include // DEV_BSIZE + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/proc"); + if (!dirp) { + return "opendir(\"/proc\") failed"; + } + + int procfd = dirfd(dirp); + + struct dirent* dp; + while ((dp = readdir(dirp))) { + if (!ffCharIsDigit(dp->d_name[0])) { // Skip "." and ".." + continue; + } + + FF_AUTO_CLOSE_FD int subfd = openat(procfd, dp->d_name, O_RDONLY | O_DIRECTORY); + if (subfd < 0) { + continue; + } + + // The data model of the returned structure depends on the data model of + // the calling process, not of the observed process. + psinfo_t psinfo; + if (ffReadFileDataRelative(subfd, "psinfo", sizeof(psinfo), &psinfo) != (ssize_t) sizeof(psinfo)) { + continue; // The process may have exited + } + + // pr_nlwp is zero for system processes and zombies + if (psinfo.pr_nlwp == 0 || psinfo.pr_lwp.pr_sname == 'Z') { + continue; + } + + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + + ffStrbufInitS(&item->name, psinfo.pr_fname); + + item->pid = (uint32_t) psinfo.pr_pid; + + item->cpuTime = (uint64_t) psinfo.pr_time.tv_sec * 1000 + + (uint64_t) psinfo.pr_time.tv_nsec / 1000000; + + item->memBytes = (uint64_t) psinfo.pr_rssize * 1024; + + item->startTime = (uint64_t) psinfo.pr_start.tv_sec * 1000 + + (uint64_t) psinfo.pr_start.tv_nsec / 1000000; + + prusage_t usage; + if (ffReadFileDataRelative(subfd, "usage", sizeof(usage), &usage) == (ssize_t) sizeof(usage)) { + item->bytesRead = (uint64_t) usage.pr_inblk * DEV_BSIZE; + item->bytesWritten = (uint64_t) usage.pr_oublk * DEV_BSIZE; + } else { + item->bytesRead = 0; + item->bytesWritten = 0; + } + } + + return nullptr; +} diff --git a/src/detection/top/top_windows.c b/src/detection/top/top_windows.c new file mode 100644 index 000000000..b27ad962e --- /dev/null +++ b/src/detection/top/top_windows.c @@ -0,0 +1,48 @@ +#include "top.h" + +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" + +#include +#include +#include + +const char* ffTopGetProcessSnapshot(FFlist* snapshots) { + FF_AUTO_FREE SYSTEM_PROCESS_INFORMATION* pstart = nullptr; + + // Multiple attempts in case processes change while + // we are in the middle of querying them. + ULONG size = 0; + for (int attempts = 0;; ++attempts) { + if (size) { + pstart = (SYSTEM_PROCESS_INFORMATION*) realloc(pstart, size); + assert(pstart); + } + NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, pstart, size, &size); + if (NT_SUCCESS(status)) { + break; + } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { + size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; + } else { + return "NtQuerySystemInformation(SystemProcessInformation) failed"; + } + } + + for (SYSTEM_PROCESS_INFORMATION* info = pstart;; info = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) info + info->NextEntryOffset)) { + uintptr_t pidValue = (uintptr_t) info->UniqueProcessId; + if (pidValue <= UINT32_MAX && info->CreateTime.QuadPart > 0) { + FFTopProcessSnapshot* item = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); + item->pid = (uint32_t) pidValue; + item->startTime = info->CreateTime.QuadPart > 0 ? (uint64_t) info->CreateTime.QuadPart : 0; + item->cpuTime = ((uint64_t) info->UserTime.QuadPart + (uint64_t) info->KernelTime.QuadPart) / 10000u; + item->memBytes = (uint64_t) info->VirtualMemoryCounters.WorkingSetSize; + item->bytesRead = (uint64_t) info->IoCounters.ReadTransferCount; + item->bytesWritten = (uint64_t) info->IoCounters.WriteTransferCount; + ffStrbufInitNWS(&item->name, info->ImageName.Length / sizeof(wchar_t), info->ImageName.Buffer); + } + if (info->NextEntryOffset == 0) { + break; + } + } + return nullptr; +} diff --git a/src/modules/modules.c b/src/modules/modules.c index bcc4afdf4..ba4c95536 100644 --- a/src/modules/modules.c +++ b/src/modules/modules.c @@ -271,6 +271,9 @@ static FFModuleBaseInfo* T[] = { #if !FF_MODULE_DISABLE_TITLE &ffTitleModuleInfo, #endif +#if !FF_MODULE_DISABLE_TOP + &ffTopModuleInfo, +#endif #if !FF_MODULE_DISABLE_THEME &ffThemeModuleInfo, #endif diff --git a/src/modules/modules.h b/src/modules/modules.h index 37ee242f0..5d2cd99e8 100644 --- a/src/modules/modules.h +++ b/src/modules/modules.h @@ -64,6 +64,7 @@ #include "modules/terminaltheme/terminaltheme.h" #include "modules/theme/theme.h" #include "modules/title/title.h" +#include "modules/top/top.h" #include "modules/tpm/tpm.h" #include "modules/uptime/uptime.h" #include "modules/users/users.h" diff --git a/src/modules/options.h b/src/modules/options.h index 7cf98c127..3c0bd6ee8 100644 --- a/src/modules/options.h +++ b/src/modules/options.h @@ -64,6 +64,7 @@ #include "modules/terminaltheme/option.h" #include "modules/theme/option.h" #include "modules/title/option.h" +#include "modules/top/option.h" #include "modules/tpm/option.h" #include "modules/uptime/option.h" #include "modules/users/option.h" diff --git a/src/modules/top/option.h b/src/modules/top/option.h new file mode 100644 index 000000000..d40cff6fc --- /dev/null +++ b/src/modules/top/option.h @@ -0,0 +1,23 @@ +#pragma once + +#include "common/option.h" +#include "common/percent.h" + +typedef enum FFTopSortType { + FF_TOP_SORT_CPU = 0, + FF_TOP_SORT_MEMORY, + FF_TOP_SORT_DISK_READ, + FF_TOP_SORT_DISK_WRITE, +} FFTopSortType; + +typedef struct FFTopOptions { + FFModuleArgs moduleArgs; + FFTopSortType sort; + uint32_t nProcesses; + uint32_t waitTime; + FFPercentageModuleConfig percent; + bool compact; + bool kernelTask; // Whether to include kernel tasks (kworker, System, kernel_task) in the process list +} FFTopOptions; + +static_assert(sizeof(FFTopOptions) <= FF_OPTION_MAX_SIZE, "FFTopOptions size exceeds maximum allowed size"); diff --git a/src/modules/top/top.c b/src/modules/top/top.c new file mode 100644 index 000000000..a8b924fe3 --- /dev/null +++ b/src/modules/top/top.c @@ -0,0 +1,229 @@ +#include "common/jsonconfig.h" +#include "common/percent.h" +#include "common/printing.h" +#include "common/size.h" +#include "detection/top/top.h" +#include "modules/top/top.h" + +static void printTopResult(FFTopOptions* options, uint32_t index, uint32_t total, FFTopProcessResult* process) { + FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type; + if (options->moduleArgs.outputFormat.length == 0) { + ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Top), total == 1 ? 0 : (uint8_t) (index + 1), &options->moduleArgs, FF_PRINT_TYPE_DEFAULT); + + FF_STRBUF_AUTO_DESTROY output = ffStrbufCreate(); + ffStrbufAppendF(&output, "%s (%u) - ", process->name.chars, process->pid); + if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) { + ffStrbufAppendS(&output, "CPU "); + ffPercentAppendNum(&output, process->cpuPercent, options->percent, false, &options->moduleArgs); + } + if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) { + ffStrbufAppendS(&output, " - MEM "); + ffSizeAppendNum(process->memBytes, &output); + ffStrbufAppendS(&output, " - DSK "); + ffSizeAppendNum(process->bytesRead, &output); + ffStrbufAppendS(&output, "/s / "); + ffSizeAppendNum(process->bytesWritten, &output); + ffStrbufAppendS(&output, "/s"); + } + ffStrbufPutTo(&output, stdout); + } else { + FF_STRBUF_AUTO_DESTROY cpuFormatted = ffStrbufCreate(); + if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) { + ffPercentAppendNum(&cpuFormatted, process->cpuPercent, options->percent, false, &options->moduleArgs); + } + FF_STRBUF_AUTO_DESTROY memFormatted = ffStrbufCreate(); + ffSizeAppendNum(process->memBytes, &memFormatted); + FF_STRBUF_AUTO_DESTROY diskReadFormatted = ffStrbufCreate(); + ffSizeAppendNum(process->bytesRead, &diskReadFormatted); + ffStrbufAppendS(&diskReadFormatted, "/s"); + FF_STRBUF_AUTO_DESTROY diskWriteFormatted = ffStrbufCreate(); + ffSizeAppendNum(process->bytesWritten, &diskWriteFormatted); + ffStrbufAppendS(&diskWriteFormatted, "/s"); + FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Top), (uint8_t) index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]){ + FF_ARG(process->name, "name"), + FF_ARG(process->pid, "pid"), + FF_ARG(process->cpuPercent, "cpu"), + FF_ARG(process->memBytes, "mem"), + FF_ARG(process->bytesRead, "disk-read"), + FF_ARG(process->bytesWritten, "disk-write"), + FF_ARG(cpuFormatted, "cpu-percentage"), + FF_ARG(memFormatted, "mem-formatted"), + FF_ARG(diskReadFormatted, "disk-read-formatted"), + FF_ARG(diskWriteFormatted, "disk-write-formatted"), + })); + } +} + +bool ffPrintTop(FFTopOptions* options) { + FF_LIST_AUTO_DESTROY results = ffListCreate(); + const char* error = ffDetectTopProcesses(options, &results); + if (error) { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Top), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error); + return false; + } + + if (!options->compact) { + FF_LIST_FOR_EACH (FFTopProcessResult, process, results) { + printTopResult(options, (uint32_t) (process - (FFTopProcessResult*) results.data), results.length, process); + } + } else { + ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Top), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT); + FF_LIST_FOR_EACH (FFTopProcessResult, process, results) { + ffStrbufWriteTo(&process->name, stdout); + if ((void*) process != results.data) { + putchar(' '); + } + } + } + + FF_LIST_FOR_EACH (FFTopProcessResult, item, results) { + ffStrbufDestroy(&item->name); + } + return true; +} + +void ffParseTopJsonObject(FFTopOptions* options, yyjson_val* module) { + yyjson_val *key, *val; + size_t idx, max; + yyjson_obj_foreach (module, idx, max, key, val) { + if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) { + continue; + } + if (unsafe_yyjson_equals_str(key, "sort")) { + int value; + const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]){ + { "cpu", FF_TOP_SORT_CPU }, + { "memory", FF_TOP_SORT_MEMORY }, + { "disk-read", FF_TOP_SORT_DISK_READ }, + { "disk-write", FF_TOP_SORT_DISK_WRITE }, + {}, + }); + if (error) { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Top), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error); + } else { + options->sort = (FFTopSortType) value; + } + continue; + } else if (unsafe_yyjson_equals_str(key, "processes")) { + options->nProcesses = (uint32_t) yyjson_get_uint(val); + } else if (unsafe_yyjson_equals_str(key, "waitTime")) { + options->waitTime = (uint32_t) yyjson_get_uint(val); + } else if (unsafe_yyjson_equals_str(key, "compact")) { + options->compact = yyjson_get_bool(val); + } else if (ffPercentParseJsonObject(key, val, &options->percent)) { + continue; + } else { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Top), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key)); + } + } +} + +void ffGenerateTopJsonConfig(FFTopOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { + ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs); + yyjson_mut_obj_add_str(doc, module, "type", "top"); + switch (options->sort) { + case FF_TOP_SORT_CPU: + yyjson_mut_obj_add_str(doc, module, "sort", "cpu"); + break; + case FF_TOP_SORT_MEMORY: + yyjson_mut_obj_add_str(doc, module, "sort", "memory"); + break; + case FF_TOP_SORT_DISK_READ: + yyjson_mut_obj_add_str(doc, module, "sort", "disk-read"); + break; + case FF_TOP_SORT_DISK_WRITE: + yyjson_mut_obj_add_str(doc, module, "sort", "disk-write"); + break; + } + yyjson_mut_obj_add_uint(doc, module, "processes", options->nProcesses); + yyjson_mut_obj_add_uint(doc, module, "waitTime", options->waitTime); + yyjson_mut_obj_add_bool(doc, module, "compact", options->compact); + yyjson_mut_obj_add_bool(doc, module, "kernelTask", options->kernelTask); + ffPercentGenerateJsonConfig(doc, module, options->percent); +} + +bool ffGenerateTopJsonResult(FFTopOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { + FF_LIST_AUTO_DESTROY results = ffListCreate(); + const char* error = ffDetectTopProcesses(options, &results); + if (error) { + yyjson_mut_obj_add_str(doc, module, "error", error); + return false; + } + yyjson_mut_val* array = yyjson_mut_obj_add_arr(doc, module, "result"); + FF_LIST_FOR_EACH (FFTopProcessResult, process, results) { + yyjson_mut_val* item = yyjson_mut_arr_add_obj(doc, array); + yyjson_mut_obj_add_strbuf(doc, item, "name", &process->name); + yyjson_mut_obj_add_uint(doc, item, "pid", process->pid); + yyjson_mut_obj_add_real(doc, item, "cpuPercent", process->cpuPercent); + yyjson_mut_obj_add_uint(doc, item, "mem", process->memBytes); + yyjson_mut_obj_add_uint(doc, item, "bytesRead", process->bytesRead); + yyjson_mut_obj_add_uint(doc, item, "bytesWritten", process->bytesWritten); + yyjson_mut_obj_add_uint(doc, item, "startTime", process->startTime); + } + + FF_LIST_FOR_EACH (FFTopProcessResult, item, results) { + ffStrbufDestroy(&item->name); + } + return true; +} + +void ffInitTopOptions(FFTopOptions* options) { + ffOptionInitModuleArg(&options->moduleArgs, "󰍛"); + options->sort = FF_TOP_SORT_CPU; + options->nProcesses = 5; + options->waitTime = 500; + options->compact = false; + options->kernelTask = false; + options->percent = (FFPercentageModuleConfig){ 50, 80, 0 }; +} + +void ffDestroyTopOptions(FFTopOptions* options) { + ffOptionDestroyModuleArg(&options->moduleArgs); +} + +FFModuleBaseInfo ffTopModuleInfo = { + .name = "Top", + .description = "Print processes with the highest CPU, memory or disk I/O usage", + .displayName = { + .en = "Top Processes", + .ar = "أعلى العمليات", + .cs = "Nejvytíženější procesy", + .de = "Top-Prozesse", + .es = "Procesos principales", + .fr = "Processus principaux", + .gl = "Procesos principais", + .he = "התהליכים המובילים", + .id = "Proses teratas", + .it = "Processi principali", + .ja = "上位プロセス", + .ko = "상위 프로세스", + .pl = "Najaktywniejsze procesy", + .pt = "Processos principais", + .ru = "Топ процессов", + .tr = "En yoğun işlemler", + .uk = "Найактивніші процеси", + .vi = "Tiến trình hàng đầu", + .zh_CN = "进程排行", + .zh_TW = "進程排行", + }, + .initOptions = (void*) ffInitTopOptions, + .destroyOptions = (void*) ffDestroyTopOptions, + .parseJsonObject = (void*) ffParseTopJsonObject, + .printModule = (void*) ffPrintTop, + .generateJsonResult = (void*) ffGenerateTopJsonResult, + .generateJsonConfig = (void*) ffGenerateTopJsonConfig, + .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){ + { "Process name", "name" }, + { "Executable path", "path" }, + { "Process ID", "pid" }, + { "CPU usage", "cpu" }, + { "Memory usage (RSS) in bytes", "mem" }, + { "Disk read bytes per second", "disk-read" }, + { "Disk write bytes per second", "disk-write" }, + { "CPU usage percentage", "cpu-percentage" }, + { "Memory usage (RSS) formatted", "mem-formatted" }, + { "Disk read formatted", "disk-read-formatted" }, + { "Disk write formatted", "disk-write-formatted" }, + })), + .defaultOrder = 36, +}; diff --git a/src/modules/top/top.h b/src/modules/top/top.h new file mode 100644 index 000000000..144130fd6 --- /dev/null +++ b/src/modules/top/top.h @@ -0,0 +1,11 @@ +#pragma once + +#include "option.h" + +void ffPrepareTopProcesses(void); + +bool ffPrintTop(FFTopOptions* options); +void ffInitTopOptions(FFTopOptions* options); +void ffDestroyTopOptions(FFTopOptions* options); + +extern FFModuleBaseInfo ffTopModuleInfo;