diff --git a/Action.c b/Action.c index 1d3bccc51..e3d6deff8 100644 --- a/Action.c +++ b/Action.c @@ -33,6 +33,7 @@ in the source distribution for its full text. #include "ProvideCurses.h" #include "Row.h" #include "RowField.h" +#include "RunScript.h" #include "Scheduling.h" #include "ScreenManager.h" #include "SignalsPanel.h" @@ -646,6 +647,11 @@ static Htop_Reaction actionTogglePauseUpdate(State* st) { return HTOP_REFRESH | HTOP_REDRAW_BAR | HTOP_KEEP_FOLLOWING; } +static Htop_Reaction actionRunScript(State* st) { + RunScript(st); + return HTOP_OK; +} + static const struct { const char* key; bool roInactive; @@ -700,6 +706,7 @@ static const struct { { .key = " F2 C S: ", .roInactive = false, .info = "setup" }, { .key = " F1 h ?: ", .roInactive = false, .info = "show this help screen" }, { .key = " F10 q: ", .roInactive = false, .info = "quit" }, + { .key = " r: ", .roInactive = false, .info = "execute user script on tagged processes"}, { .key = NULL, .info = NULL } }; @@ -939,6 +946,7 @@ void Action_setBindings(Htop_Action* keys) { keys['m'] = actionToggleMergedCommand; keys['p'] = actionToggleProgramPath; keys['q'] = actionQuit; + keys['r'] = actionRunScript; keys['s'] = actionStrace; keys['t'] = actionToggleTreeView; keys['u'] = actionFilterByUser; diff --git a/Makefile.am b/Makefile.am index 4492123f7..6a5151287 100644 --- a/Makefile.am +++ b/Makefile.am @@ -75,11 +75,13 @@ myhtopsources = \ ProcessLocksScreen.c \ ProcessTable.c \ Row.c \ + RunScript.c \ RichString.c \ Scheduling.c \ ScreenManager.c \ ScreensPanel.c \ ScreenTabsPanel.c \ + ScriptOutputScreen.c \ Settings.c \ SignalsPanel.c \ SwapMeter.c \ @@ -148,11 +150,13 @@ myhtopheaders = \ ProvideTerm.h \ RichString.h \ Row.h \ + RunScript.h \ RowField.h \ Scheduling.h \ ScreenManager.h \ ScreensPanel.h \ ScreenTabsPanel.h \ + ScriptOutputScreen.h \ Settings.h \ SignalsPanel.h \ SwapMeter.h \ diff --git a/RunScript.c b/RunScript.c new file mode 100644 index 000000000..01d1db479 --- /dev/null +++ b/RunScript.c @@ -0,0 +1,219 @@ +/* +htop - RunScript.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "RunScript.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Action.h" +#include "InfoScreen.h" +#include "MainPanel.h" +#include "Object.h" +#include "Panel.h" +#include "Process.h" +#include "Row.h" +#include "ScriptOutputScreen.h" +#include "Settings.h" +#include "XUtils.h" + + +static void write_row(Row* row, int write_fd) { + Process* this = (Process*) row; + assert(Object_isA((const Object*) this, (const ObjectClass*) &Process_class)); + + int pid_len = 0; + int pid = row->id; + while (pid > 0) { + pid /= 10; + pid_len++; + } + + char* line; + int user_len = strlen(this->user); + int cmd_len = strlen(Process_getCommand(this)); + // writes pid_len:PID,user_len:User,cmd_len:Command\n in netstring format + xAsprintf(&line, "%d:%d,%d:%s,%d:%s\n", pid_len, row->id, user_len, this->user, cmd_len, Process_getCommand(this)); + + size_t count = strlen(line); + char* line_start = line; + while (count > 0) { + ssize_t res = write(write_fd, line_start, strlen(line_start)); + if ((res == -1 && errno != EINTR) || res == 0) + break; + count -= res; + line_start += res; + } + free(line); +} + +void RunScript(State* st) { + int child_read[2] = {0, 0}; + int child_write[2] = {0, 0}; + + if (pipe(child_read) == -1) + return; + if (pipe(child_write) == -1) { + close(child_read[0]); + close(child_read[1]); + return; + } + + pid_t child = fork(); + if (child == -1) { + close(child_read[0]); + close(child_read[1]); + close(child_write[0]); + close(child_write[1]); + fprintf(stderr, "fork failed\n"); + return; + } else if (child == 0) { + close(child_read[1]); + dup2(child_read[0], STDIN_FILENO); + close(child_read[0]); + + close(child_write[0]); + dup2(child_write[1], STDOUT_FILENO); + dup2(child_write[1], STDERR_FILENO); + close(child_write[1]); + + char* home = getenv("XDG_CONFIG_HOME"); + if (!home) + home = String_cat(getenv("HOME"), "/.config"); + + const char* path = String_cat(home, "/htop/run_script"); + FILE* file = fopen(path, "r"); + if (file) { + // executing script in root's directory, probably not malicious + root_exec(path, false); + // should not reach here unless fexecve fails + fprintf(stderr, "error excuting %s\n", path); + } else { + // check if htoprc has something + const char* htoprc_path = st->host->settings->scriptLocation; + // path can point to anything, so drop sudo for safety + root_exec(htoprc_path, true); + + // only reach here if fexecve fails + fprintf(stderr, "error executing %s from htoprc. ", htoprc_path); + fprintf(stderr, "if you expected your runscript to be executed, htop looked for it at %s", path); + } + exit(127); + } + + close(child_read[0]); + close(child_write[1]); + + bool anyTagged = false; + Panel* super = &st->mainPanel->super; + for (int i = 0; i < Panel_size(super); i++) { + Row* row = (Row*) Panel_get(super, i); + if (row->tag) { + write_row(row, child_read[1]); + anyTagged = true; + } + } + // if nothing was tagged, operate on the highlighted row + if (!anyTagged) { + Row* row = (Row*) Panel_getSelected(super); + if (row) + write_row(row, child_read[1]); + } + + // tell script/child we're done with sending input + close(child_read[1]); + + const Process* p = (Process*) Panel_getSelected((Panel*)st->mainPanel); + if (!p) + return; + + assert(Object_isA((const Object*) p, (const ObjectClass*) &Process_class)); + ScriptOutputScreen* sos = ScriptOutputScreen_new(p); + if (fcntl(child_write[0], F_SETFL, O_NONBLOCK) >= 0) { + ScriptOutputScreen_SetFd(sos, child_write[0]); + InfoScreen_run((InfoScreen*)sos); + } + ScriptOutputScreen_delete((Object*)sos); +} + +void root_exec(const char* path, bool drop_sudo) { + // do not use O_CLOEXEC flag as that will cause fexecve to fail with ENOENT on a script + int fd = open(path, O_RDONLY); + if (fd < 0) { + perror("open"); + return; + } + // check that path is even a file + struct stat st; + if (fstat(fd, &st) == -1) { + perror("fstat"); + return; + } + + uid_t curr_uid = getuid(); + if (drop_sudo) { + // need to remove root from ourselves if we are root + if (curr_uid == 0) { + char* sudo_uid_str = getenv("SUDO_UID"); + if (!sudo_uid_str) { + fprintf(stderr, "sudo uid envar does not exist\n"); + return; + } + uid_t uid = strtoul(sudo_uid_str, NULL, 10); + if (uid == 0) { + fprintf(stderr, "sudo uid envar is root, failed to get uid of invoking user\n"); + return; + } + + char* sudo_gid_str = getenv("SUDO_GID"); + if (!sudo_gid_str) { + fprintf(stderr, "sudo gid envar does not exist\n"); + return; + } + gid_t gid = strtoul(sudo_gid_str, NULL, 10); + if (gid == 0) { + fprintf(stderr, "sudo gid envar is root group, failed to get gid of invoking user\n"); + return; + } + + // remove supplementary groups + if (setgroups(0, NULL) == -1) { + perror("setgroups"); + return; + } + if (setgid(gid) == -1) { + perror("setgid"); + return; + } + if (setuid(uid) == -1) { + perror("setuid"); + return; + } + } + } else if (curr_uid == 0 && (st.st_gid != 0 || st.st_uid != 0)) { + // we are root and script does not belongs to root, consider it unsafe + fprintf(stderr, "%s does not belong to root; has gid %u and uid %u\n", path, st.st_gid, st.st_uid); + return; + } + + static char* argv[] = {NULL, NULL}; + argv[0] = xStrdup(path); + static char* env[] = {NULL}; + fexecve(fd, argv, env); + + perror("fexecve"); +} diff --git a/RunScript.h b/RunScript.h new file mode 100644 index 000000000..fcc84f460 --- /dev/null +++ b/RunScript.h @@ -0,0 +1,25 @@ +#ifndef RUNSCRIPT_Process +#define RUNSCRIPT_Process +/* +htop - RunScript.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include + +#include "Action.h" + + +typedef struct Node_ { + char* line; + struct Node_* next; +} Node; + + +void RunScript(State*); + +void root_exec(const char*, bool); + +#endif diff --git a/ScriptOutputScreen.c b/ScriptOutputScreen.c new file mode 100644 index 000000000..3bcf5b425 --- /dev/null +++ b/ScriptOutputScreen.c @@ -0,0 +1,131 @@ +/* +htop - ScriptOutputScreen.c +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "ScriptOutputScreen.h" + +#include +#include +#include +#include + +#include "Panel.h" +#include "ProvideCurses.h" +#include "XUtils.h" + + +ScriptOutputScreen* ScriptOutputScreen_new(const Process* process) { + ScriptOutputScreen* this = xCalloc(1, sizeof(ScriptOutputScreen)); + Object_setClass(this, Class(ScriptOutputScreen)); + // this fd needs to be set later + this->read_fd = -1; + this->data_head = NULL; + this->data_tail = &this->data_head; + return (ScriptOutputScreen*) InfoScreen_init(&this->super, process, NULL, LINES - 2, " "); +} + +void ScriptOutputScreen_SetFd(ScriptOutputScreen* this, int fd) { + this->read_fd = fd; +} + +void ScriptOutputScreen_delete(Object* this) { + // free the linked list and close fd + assert(Object_isA((const Object*) this, (const ObjectClass*) &ScriptOutputScreen_class)); + Node* walk = ((ScriptOutputScreen*)this)->data_head; + while (walk) { + free(walk->line); + Node* next = walk->next; + free(walk); + walk = next; + } + close(((ScriptOutputScreen*)this)->read_fd); + free(InfoScreen_done((InfoScreen*)this)); +} + +static void ScriptOutputScreen_scan(InfoScreen* super) { + Panel* panel = super->display; + int idx = Panel_getSelectedIndex(panel); + Panel_prune(panel); + + char buffer[8192]; + ScriptOutputScreen* sos = ((ScriptOutputScreen*)super); + assert(Object_isA((const Object*) sos, (const ObjectClass*) &ScriptOutputScreen_class)); + + // redraw existing stuff in the screen first + Node* walk = sos->data_head; + while (walk) { + InfoScreen_addLine(super, walk->line); + walk = walk->next; + } + + for (;;) { + ssize_t res = read(sos->read_fd, buffer, sizeof(buffer) - 1); + if (res < 0) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) + continue; + + break; + } + + if (res == 0) { + break; + } + + size_t start = 0; + int num_tabs = 0; + for (size_t i = 0; i <= res; i++) { + num_tabs += (buffer[i] == '\t'); + // split line when find \n or exhaust buffer + if (i == res || buffer[i] == '\n') { + buffer[i] = '\0'; + char* str; + if (num_tabs > 0) { + // manually replace all \t with TABSIZE spaces + str = xMalloc((num_tabs * TABSIZE + i - start + 1) * sizeof(char)); + size_t index = 0; + for (size_t j = start; j <= i; j++) { + if (buffer[j] == '\t') { + for (int k = 0; k < TABSIZE; k++) { + str[index++] = ' '; + } + } else { + str[index++] = buffer[j]; + } + } + } else { + str = buffer + start; + } + InfoScreen_addLine(super, str); + // store line for next redraw + *(sos->data_tail) = xMalloc(sizeof(Node)); + (*sos->data_tail)->line = xStrdup(str); + (*sos->data_tail)->next = NULL; + *(&sos->data_tail) = &((*sos->data_tail)->next); + + if (num_tabs > 0) + free(str); + start = i + 1; + num_tabs = 0; + } + } + } + Panel_setSelected(panel, idx); +} + +static void ScriptOutputScreen_draw(InfoScreen* this ) { + InfoScreen_drawTitled(this, "Output of script for process %d - %s", Process_getPid(this->process), Process_getCommand(this->process)); +} + +const InfoScreenClass ScriptOutputScreen_class = { + .super = { + .extends = Class(Object), + .delete = ScriptOutputScreen_delete + }, + .scan = ScriptOutputScreen_scan, + .draw = ScriptOutputScreen_draw +}; diff --git a/ScriptOutputScreen.h b/ScriptOutputScreen.h new file mode 100644 index 000000000..6bc7bb18a --- /dev/null +++ b/ScriptOutputScreen.h @@ -0,0 +1,31 @@ +#ifndef HEADER_ScriptOutputScreen +#define HEADER_ScriptOutputScreen +/* +htop - ScriptOutputScreen.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "InfoScreen.h" +#include "Object.h" +#include "Process.h" +#include "RunScript.h" + + +typedef struct ScriptOutputScreen_ { + InfoScreen super; + int read_fd; + Node* data_head; + Node** data_tail; +} ScriptOutputScreen; + +extern const InfoScreenClass ScriptOutputScreen_class; + +ScriptOutputScreen* ScriptOutputScreen_new(const Process* process); + +void ScriptOutputScreen_delete(Object* this); + +void ScriptOutputScreen_SetFd(ScriptOutputScreen*, int); + +#endif diff --git a/Settings.c b/Settings.c index be0019788..d200e66c7 100644 --- a/Settings.c +++ b/Settings.c @@ -53,6 +53,7 @@ void Settings_delete(Settings* this) { free(this->initialFilename); Settings_deleteColumns(this); Settings_deleteScreens(this); + free(this->scriptLocation); free(this); } @@ -552,6 +553,8 @@ static bool Settings_read(Settings* this, const char* fileName, const Machine* h free_and_xStrdup(&screen->dynamic, option[1]); Platform_addDynamicScreen(screen); } + } else if (String_eq(option[0], "script_location")) { + this->scriptLocation = xStrdup(option[1]); } String_freeArray(option); } @@ -736,6 +739,9 @@ int Settings_write(const Settings* this, bool onCrash) { printSettingInteger("tree_view_always_by_pid", this->screens[0]->treeViewAlwaysByPID); printSettingInteger("all_branches_collapsed", this->screens[0]->allBranchesCollapsed); + if (this->scriptLocation) + printSettingString("script_location", this->scriptLocation); + for (unsigned int i = 0; i < this->nScreens; i++) { ScreenSettings* ss = this->screens[i]; const char* sortKey = toFieldName(this->dynamicColumns, ss->sortKey, NULL); @@ -868,7 +874,7 @@ Settings* Settings_new(const Machine* host, Hashtable* dynamicMeters, Hashtable* #endif this->changed = false; this->delay = DEFAULT_DELAY; - + bool ok = Settings_read(this, this->filename, host, /*checkWritability*/true); if (!ok && legacyDotfile) { ok = Settings_read(this, legacyDotfile, host, this->writeConfig); diff --git a/Settings.h b/Settings.h index 01e808e86..b693eae01 100644 --- a/Settings.h +++ b/Settings.h @@ -102,6 +102,7 @@ typedef struct Settings_ { bool headerMargin; bool screenTabs; bool showCachedMemory; + char* scriptLocation; #ifdef HAVE_GETMOUSE bool enableMouse; #endif diff --git a/htop.1.in b/htop.1.in index 8921dedab..eb26587e5 100644 --- a/htop.1.in +++ b/htop.1.in @@ -150,6 +150,12 @@ instead of the currently highlighted one. .B U Untag all processes (remove all tags added with the Space or c keys). .TP +.B r +Execute user script on tagged processes, or selected process if none tagged. +Script is passed each PID, user, and command in TSV format on stdin, and should +not be interactive. htop first searches for a file at $XDG_CONFIG_HOME/htop/run_script +before looking in htoprc for a full path associated with script_location. +.TP .B s Trace process system calls: if strace(1) is installed, pressing this key will attach it to the currently selected process, presenting a live