diff options
author | Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> | 2021-03-30 19:42:36 +0200 |
---|---|---|
committer | Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> | 2021-05-05 13:59:23 +0200 |
commit | 61977664e9d186287d0b85a26256cf82ae89fcbf (patch) | |
tree | 269287a6d30fd8b451a2b30d8257012a56a22023 /src | |
parent | test-utf8: hide most output by default (diff) | |
download | systemd-61977664e9d186287d0b85a26256cf82ae89fcbf.tar.xz systemd-61977664e9d186287d0b85a26256cf82ae89fcbf.zip |
basic/process-util: allow quoting of commandlines
Since the new functionality is controlled by an option, this causes no change
in output yet, except tests.
The login in the old branch of !(flags & PROCESS_CMDLINE_QUOTE) is essentially
unmodified. But there is an important difference in behaviour: instead of
unconditionally reading the whole virtual file, we now read only 'max_columns'
bytes. This makes out code to write process lists quite a bit more efficient
when there are processes with long command lines.
Diffstat (limited to 'src')
-rw-r--r-- | src/basic/fileio.c | 7 | ||||
-rw-r--r-- | src/basic/process-util.c | 131 | ||||
-rw-r--r-- | src/basic/process-util.h | 1 | ||||
-rw-r--r-- | src/test/test-process-util.c | 45 |
4 files changed, 148 insertions, 36 deletions
diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 90484a98c2..024eb29bb9 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -368,6 +368,7 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents _cleanup_close_ int fd = -1; size_t n, size; int n_retries; + bool truncated = false; assert(ret_contents); @@ -381,7 +382,8 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents * * max_size specifies a limit on the bytes read. If max_size is SIZE_MAX, the full file is read. If * the the full file is too large to read, an error is returned. For other values of max_size, - * *partial contents* may be returned. (Though the read is still done using one syscall.) */ + * *partial contents* may be returned. (Though the read is still done using one syscall.) + * Returns 0 on partial success, 1 if untruncated contents were read. */ fd = open(filename, O_RDONLY|O_CLOEXEC); if (fd < 0) @@ -454,6 +456,7 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents /* Accept a short read, but truncate it appropropriately. */ n = MIN(n, max_size); + truncated = true; break; } @@ -484,7 +487,7 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents buf[n] = 0; *ret_contents = TAKE_PTR(buf); - return 0; + return !truncated; } int read_full_stream_full( diff --git a/src/basic/process-util.c b/src/basic/process-util.c index d7afc4fe5a..fd708eed98 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -123,64 +123,133 @@ int get_process_comm(pid_t pid, char **ret) { return 0; } -int get_process_cmdline(pid_t pid, size_t max_columns, ProcessCmdlineFlags flags, char **line) { - _cleanup_free_ char *t = NULL, *ans = NULL; +static int get_process_cmdline_nulstr( + pid_t pid, + size_t max_size, + ProcessCmdlineFlags flags, + char **ret, + size_t *ret_size) { + const char *p; + char *t; size_t k; int r; - assert(line); - assert(pid >= 0); - - /* Retrieves a process' command line. Replaces non-utf8 bytes by replacement character (�). If - * max_columns is != -1 will return a string of the specified console width at most, abbreviated with - * an ellipsis. If PROCESS_CMDLINE_COMM_FALLBACK is specified in flags and the process has no command - * line set (the case for kernel threads), or has a command line that resolves to the empty string - * will return the "comm" name of the process instead. This will use at most _SC_ARG_MAX bytes of - * input data. + /* Retrieves a process' command line as a "sized nulstr", i.e. possibly without the last NUL, but + * with a specified size. * - * Returns -ESRCH if the process doesn't exist, and -ENOENT if the process has no command line (and - * comm_fallback is false). Returns 0 and sets *line otherwise. */ + * If PROCESS_CMDLINE_COMM_FALLBACK is specified in flags and the process has no command line set + * (the case for kernel threads), or has a command line that resolves to the empty string, will + * return the "comm" name of the process instead. This will use at most _SC_ARG_MAX bytes of input + * data. + * + * Returns an error, 0 if output was read but is truncated, 1 otherwise. + */ p = procfs_file_alloca(pid, "cmdline"); - r = read_full_virtual_file(p, &t, &k); + r = read_virtual_file(p, max_size, &t, &k); /* Let's assume that each input byte results in >= 1 + * columns of output. We ignore zero-width codepoints. */ if (r == -ENOENT) return -ESRCH; if (r < 0) return r; - if (k > 0) { - /* Arguments are separated by NULs. Let's replace those with spaces. */ - for (size_t i = 0; i < k - 1; i++) - if (t[i] == '\0') - t[i] = ' '; - } else { + if (k == 0) { + t = mfree(t); + if (!(flags & PROCESS_CMDLINE_COMM_FALLBACK)) return -ENOENT; /* Kernel threads have no argv[] */ - _cleanup_free_ char *t2 = NULL; + _cleanup_free_ char *comm = NULL; - r = get_process_comm(pid, &t2); + r = get_process_comm(pid, &comm); if (r < 0) return r; - free(t); - t = strjoin("[", t2, "]"); + t = strjoin("[", comm, "]"); if (!t) return -ENOMEM; + + k = strlen(t); + r = k <= max_size; + if (r == 0) /* truncation */ + t[max_size] = '\0'; } - delete_trailing_chars(t, WHITESPACE); + *ret = t; + *ret_size = k; + return r; +} - bool eight_bit = (flags & PROCESS_CMDLINE_USE_LOCALE) && !is_locale_utf8(); +int get_process_cmdline(pid_t pid, size_t max_columns, ProcessCmdlineFlags flags, char **line) { + _cleanup_free_ char *t = NULL; + size_t k; + char *ans; - ans = escape_non_printable_full(t, max_columns, eight_bit * XESCAPE_8_BIT); - if (!ans) - return -ENOMEM; + assert(line); + assert(pid >= 0); + + /* Retrieve adn format a commandline. See above for discussion of retrieval options. + * + * There are two main formatting modes: + * + * - when PROCESS_CMDLINE_QUOTE is specified, output is quoted in C/Python style. If no shell special + * characters are present, this output can be copy-pasted into the terminal to execute. UTF-8 + * output is assumed. + * + * - otherwise, a compact non-roundtrippable form is returned. Non-UTF8 bytes are replaced by �. The + * returned string is of the specified console width at most, abbreviated with an ellipsis. + * + * Returns -ESRCH if the process doesn't exist, and -ENOENT if the process has no command line (and + * PROCESS_CMDLINE_COMM_FALLBACK is not specified). Returns 0 and sets *line otherwise. */ + + int full = get_process_cmdline_nulstr(pid, max_columns, flags, &t, &k); + if (full < 0) + return full; + + if (flags & PROCESS_CMDLINE_QUOTE) { + assert(!(flags & PROCESS_CMDLINE_USE_LOCALE)); + + _cleanup_strv_free_ char **args = NULL; + + args = strv_parse_nulstr(t, k); + if (!args) + return -ENOMEM; + + for (size_t i = 0; args[i]; i++) { + char *e; + + e = shell_maybe_quote(args[i], SHELL_ESCAPE_EMPTY); + if (!e) + return -ENOMEM; + + free_and_replace(args[i], e); + } + + ans = strv_join(args, " "); + if (!ans) + return -ENOMEM; + + } else { + /* Arguments are separated by NULs. Let's replace those with spaces. */ + for (size_t i = 0; i < k - 1; i++) + if (t[i] == '\0') + t[i] = ' '; + + delete_trailing_chars(t, WHITESPACE); + + bool eight_bit = (flags & PROCESS_CMDLINE_USE_LOCALE) && !is_locale_utf8(); + + ans = escape_non_printable_full(t, max_columns, + eight_bit * XESCAPE_8_BIT | !full * XESCAPE_FORCE_ELLIPSIS); + if (!ans) + return -ENOMEM; + + ans = str_realloc(ans); + } - ans = str_realloc(ans); - *line = TAKE_PTR(ans); + *line = ans; return 0; } diff --git a/src/basic/process-util.h b/src/basic/process-util.h index ddce7bd272..3121d82d3f 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -35,6 +35,7 @@ typedef enum ProcessCmdlineFlags { PROCESS_CMDLINE_COMM_FALLBACK = 1 << 0, PROCESS_CMDLINE_USE_LOCALE = 1 << 1, + PROCESS_CMDLINE_QUOTE = 1 << 2, } ProcessCmdlineFlags; int get_process_comm(pid_t pid, char **name); diff --git a/src/test/test-process-util.c b/src/test/test-process-util.c index 7f0a771ba7..5f148c1522 100644 --- a/src/test/test-process-util.c +++ b/src/test/test-process-util.c @@ -248,9 +248,15 @@ static void test_get_process_cmdline_harder(void) { assert_se(get_process_cmdline(0, SIZE_MAX, 0, &line) == -ENOENT); assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0); + log_info("'%s'", line); assert_se(streq(line, "[testa]")); line = mfree(line); + assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK | PROCESS_CMDLINE_QUOTE, &line) >= 0); + log_info("'%s'", line); + assert_se(streq(line, "\"[testa]\"")); /* quoting is enabled here */ + line = mfree(line); + assert_se(get_process_cmdline(0, 0, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0); log_info("'%s'", line); assert_se(streq(line, "")); @@ -288,6 +294,8 @@ static void test_get_process_cmdline_harder(void) { assert_se(streq(line, "[testa]")); line = mfree(line); + /* Test with multiple arguments that don't require quoting */ + assert_se(write(fd, "foo\0bar", 8) == 8); assert_se(get_process_cmdline(0, SIZE_MAX, 0, &line) >= 0); @@ -390,6 +398,32 @@ static void test_get_process_cmdline_harder(void) { assert_se(streq(line, "[aaaa bbbb …")); line = mfree(line); + /* Test with multiple arguments that do require quoting */ + +#define CMDLINE1 "foo\0'bar'\0\"bar$\"\0x y z\0!``\0" +#define EXPECT1 "foo \"'bar'\" \"\\\"bar\\$\\\"\" \"x y z\" \"!\\`\\`\" \"\"" + assert_se(lseek(fd, SEEK_SET, 0) == 0); + assert_se(write(fd, CMDLINE1, sizeof CMDLINE1) == sizeof CMDLINE1); + assert_se(ftruncate(fd, sizeof CMDLINE1) == 0); + + assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE, &line) >= 0); + log_info("got: ==%s==", line); + log_info("exp: ==%s==", EXPECT1); + assert_se(streq(line, EXPECT1)); + line = mfree(line); + +#define CMDLINE2 "foo\0\1\2\3\0\0" +#define EXPECT2 "foo \"\\001\\002\\003\" \"\" \"\"" + assert_se(lseek(fd, SEEK_SET, 0) == 0); + assert_se(write(fd, CMDLINE2, sizeof CMDLINE2) == sizeof CMDLINE2); + assert_se(ftruncate(fd, sizeof CMDLINE2) == 0); + + assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE, &line) >= 0); + log_info("got: ==%s==", line); + log_info("exp: ==%s==", EXPECT2); + assert_se(streq(line, EXPECT2)); + line = mfree(line); + safe_close(fd); _exit(EXIT_SUCCESS); } @@ -403,6 +437,8 @@ static void test_rename_process_now(const char *p, int ret) { (ret == 0 && r >= 0) || (ret > 0 && r > 0)); + log_info_errno(r, "rename_process(%s): %m", p); + if (r < 0) return; @@ -425,9 +461,12 @@ static void test_rename_process_now(const char *p, int ret) { if (r == 0 && detect_container() > 0) log_info("cmdline = <%s> (not verified, Running in unprivileged container?)", cmdline); else { - log_info("cmdline = <%s>", cmdline); - assert_se(strneq(p, cmdline, STRLEN("test-process-util"))); - assert_se(startswith(p, cmdline)); + log_info("cmdline = <%s> (expected <%.*s>)", cmdline, (int) strlen("test-process-util"), p); + + bool skip = cmdline[0] == '"'; /* A shortcut to check if the string is quoted */ + + assert_se(strneq(cmdline + skip, p, strlen("test-process-util"))); + assert_se(startswith(cmdline + skip, p)); } } else log_info("cmdline = <%s> (not verified)", cmdline); |