summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2021-03-30 19:42:36 +0200
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2021-05-05 13:59:23 +0200
commit61977664e9d186287d0b85a26256cf82ae89fcbf (patch)
tree269287a6d30fd8b451a2b30d8257012a56a22023 /src
parenttest-utf8: hide most output by default (diff)
downloadsystemd-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.c7
-rw-r--r--src/basic/process-util.c131
-rw-r--r--src/basic/process-util.h1
-rw-r--r--src/test/test-process-util.c45
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);