diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml
index e1c575757bd..38063f6d4c4 100644
--- a/man/systemd-analyze.xml
+++ b/man/systemd-analyze.xml
@@ -70,6 +70,13 @@
OPTIONS
unit-paths
+
+ systemd-analyze
+ OPTIONS
+ unit-shell
+ SERVICE
+ Command
+
systemd-analyze
OPTIONS
@@ -1160,6 +1167,22 @@ LEGEND: M → sys_vendor (LENOVO) ┄ F → product_family (ThinkPad X1 Carbon G
+
+
+ systemd-analyze unit-shell SERVICE command...
+
+ The given command runs on the namespace of the specified running service. If no command is given,
+ spawn and attach a shell with the namespace to the service.
+
+
+ Example output
+ $ systemd-analyze unit-shell systemd-resolved.service ls
+bin dev etc home lib lib64 lost+found mnt proc run srv tmp var vmlinuz.old
+boot efi exitrd init lib32 libx32 media opt root sbin sys usr vmlinuz work
+
+
+
+
diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze
index e4ecafeddca..b909a954e47 100644
--- a/shell-completion/bash/systemd-analyze
+++ b/shell-completion/bash/systemd-analyze
@@ -82,6 +82,7 @@ _systemd_analyze() {
[FDSTORE]='fdstore'
[CAPABILITY]='capability'
[TRANSIENT_SETTINGS]='transient-settings'
+ [UNIT_SHELL]='unit-shell'
)
local CONFIGS='locale.conf systemd/bootchart.conf systemd/coredump.conf systemd/journald.conf
@@ -233,6 +234,13 @@ _systemd_analyze() {
else
comps="$(systemctl --no-legend --no-pager -t help)"
fi
+
+ elif __contains_word "$verb" ${VERBS[UNIT_SHELL]}; then
+ if [[ $cur = -* ]]; then
+ comps='--help --version'
+ else
+ comps=$( __get_services $mode )
+ fi
fi
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
diff --git a/src/analyze/analyze-unit-shell.c b/src/analyze/analyze-unit-shell.c
new file mode 100644
index 00000000000..576a4200343
--- /dev/null
+++ b/src/analyze/analyze-unit-shell.c
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "analyze.h"
+#include "analyze-unit-shell.h"
+#include "bus-error.h"
+#include "bus-util.h"
+#include "fd-util.h"
+#include "log.h"
+#include "namespace-util.h"
+#include "process-util.h"
+#include "runtime-scope.h"
+#include "strv.h"
+#include "unit-def.h"
+#include "unit-name.h"
+
+int verb_unit_shell(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *unit = NULL;
+ int r;
+
+ if (arg_transport != BUS_TRANSPORT_LOCAL)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot spawn a unit shell for a remote service");
+
+ r = unit_name_mangle_with_suffix(argv[1], "as unit", UNIT_NAME_MANGLE_WARN, ".service", &unit);
+ if (r < 0)
+ return log_error_errno(r, "Failed to mangle name '%s': %m", argv[1]);
+
+ r = acquire_bus(&bus, /* use_full_bus= */ NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport, arg_runtime_scope);
+
+ _cleanup_free_ char *object = unit_dbus_path_from_name(unit);
+ if (!object)
+ return log_oom();
+
+ r = sd_bus_get_property(
+ bus,
+ "org.freedesktop.systemd1",
+ object,
+ "org.freedesktop.systemd1.Service",
+ "MainPID",
+ &error,
+ &reply,
+ "u");
+ if (r < 0)
+ return log_error_errno(r, "Failed to get the main PID of %s: %s", unit, bus_error_message(&error, r));
+
+ pid_t pid;
+ r = sd_bus_message_read(reply, "u", &pid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read the main PID of %s from reply: %m", unit);
+
+ _cleanup_close_ int mntns_fd = -EBADF, root_fd = -EBADF, pidns_fd = -EBADF, netns_fd = -EBADF, userns_fd = -EBADF;
+ r = namespace_open(
+ pid,
+ &pidns_fd,
+ &mntns_fd,
+ &netns_fd,
+ &userns_fd,
+ &root_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to retrieve FDs of namespaces of %s: %m", unit);
+
+ _cleanup_strv_free_ char **args = NULL;
+ if (argc > 2) {
+ args = strv_copy(strv_skip(argv, 2));
+ if (!args)
+ return log_oom();
+ }
+
+ pid_t child;
+ r = namespace_fork(
+ "(unit-shell-ns)",
+ "(unit-shell)",
+ /* except_fds= */ NULL,
+ /* n_except_fds */ 0,
+ FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL,
+ pidns_fd,
+ mntns_fd,
+ netns_fd,
+ userns_fd,
+ root_fd,
+ &child);
+ if (r < 0)
+ return log_error_errno(r, "Failed to fork and enter the namespace of %s: %m", unit);
+
+ if (r == 0) {
+ if (args) {
+ r = execvp(args[0], args);
+ if (r < 0)
+ log_error_errno(errno, "Failed to execute '%s': %m", *args);
+ } else {
+ r = execl(DEFAULT_USER_SHELL, "-" DEFAULT_USER_SHELL_NAME, NULL);
+ if (r < 0)
+ log_debug_errno(errno, "Failed to execute '" DEFAULT_USER_SHELL "', ignoring: %m");
+ if (!streq(DEFAULT_USER_SHELL, "/bin/bash")) {
+ r = execl("/bin/bash", "-bash", NULL);
+ if (r < 0)
+ log_debug_errno(errno, "Failed to execute '/bin/bash', ignoring: %m");
+ }
+ if (!streq(DEFAULT_USER_SHELL, "/bin/sh")) {
+ r = execl("/bin/sh", "-sh", NULL);
+ if (r < 0)
+ log_debug_errno(errno, "Failed to execute '/bin/sh', ignoring: %m");
+ }
+ log_error_errno(errno, "Failed to execute '" DEFAULT_USER_SHELL "', '/bin/bash', and '/bin/sh': %m");
+ }
+ _exit(EXIT_FAILURE);
+ }
+
+ return wait_for_terminate_and_check(
+ "(unit-shell)",
+ child,
+ WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS);
+}
diff --git a/src/analyze/analyze-unit-shell.h b/src/analyze/analyze-unit-shell.h
new file mode 100644
index 00000000000..7c15e083a8e
--- /dev/null
+++ b/src/analyze/analyze-unit-shell.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_unit_shell(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c
index 8745db63f5f..a43ae3bdb45 100644
--- a/src/analyze/analyze.c
+++ b/src/analyze/analyze.c
@@ -44,6 +44,7 @@
#include "analyze-timestamp.h"
#include "analyze-unit-files.h"
#include "analyze-unit-paths.h"
+#include "analyze-unit-shell.h"
#include "analyze-verify.h"
#include "analyze-verify-util.h"
#include "build.h"
@@ -241,6 +242,8 @@ static int help(int argc, char *argv[], void *userdata) {
" security [UNIT...] Analyze security of unit\n"
" fdstore SERVICE... Show file descriptor store contents of service\n"
" malloc [D-BUS SERVICE...] Dump malloc stats of a D-Bus service\n"
+ " unit-shell SERVICE [Command]\n"
+ " Run command on the namespace of the service\n"
"\n%3$sExecutable Analysis:%4$s\n"
" inspect-elf FILE... Parse and print ELF package metadata\n"
"\n%3$sTPM Operations:%4$s\n"
@@ -383,14 +386,68 @@ static int parse_argv(int argc, char *argv[]) {
{}
};
- int r, c;
+ bool reorder = false;
+ int r, c, unit_shell = -1;
assert(argc >= 0);
assert(argv);
- while ((c = getopt_long(argc, argv, "hqH:M:U:m", options, NULL)) >= 0)
+ /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
+ * that checks for GNU extensions in optstring ('-' or '+; at the beginning). */
+ optind = 0;
+
+ for (;;) {
+ static const char option_string[] = "-hqH:M:U:m";
+
+ c = getopt_long(argc, argv, option_string + reorder, options, NULL);
+ if (c < 0)
+ break;
+
switch (c) {
+ case 1: /* getopt_long() returns 1 if "-" was the first character of the option string, and a
+ * non-option argument was discovered. */
+
+ assert(!reorder);
+
+ /* We generally are fine with the fact that getopt_long() reorders the command line, and looks
+ * for switches after the main verb. However, for "unit-shell" we really don't want that, since we
+ * want that switches specified after the service name are passed to the program to execute,
+ * and not processed by us. To make this possible, we'll first invoke getopt_long() with
+ * reordering disabled (i.e. with the "-" prefix in the option string), looking for the first
+ * non-option parameter. If it's the verb "unit-shell" we remember its position and continue
+ * processing options. In this case, as soon as we hit the next non-option argument we found
+ * the service name, and stop further processing. If the first non-option argument is any other
+ * verb than "unit-shell" we switch to normal reordering mode and continue processing arguments
+ * normally. */
+
+ if (unit_shell >= 0) {
+ optind--; /* don't processs this argument, go one step back */
+ goto done;
+ }
+ if (streq(optarg, "unit-shell"))
+ /* Remember the position of the "unit_shell" verb, and continue processing normally. */
+ unit_shell = optind - 1;
+ else {
+ int saved_optind;
+
+ /* Ok, this is some other verb. In this case, turn on reordering again, and continue
+ * processing normally. */
+ reorder = true;
+
+ /* We changed the option string. getopt_long() only looks at it again if we invoke it
+ * at least once with a reset option index. Hence, let's reset the option index here,
+ * then invoke getopt_long() again (ignoring what it has to say, after all we most
+ * likely already processed it), and the bump the option index so that we read the
+ * intended argument again. */
+ saved_optind = optind;
+ optind = 0;
+ (void) getopt_long(argc, argv, option_string + reorder, options, NULL);
+ optind = saved_optind - 1; /* go one step back, process this argument again */
+ }
+
+ break;
+
case 'h':
return help(0, NULL, NULL);
@@ -602,6 +659,22 @@ static int parse_argv(int argc, char *argv[]) {
default:
assert_not_reached();
}
+ }
+
+done:
+ if (unit_shell >= 0) {
+ char *t;
+
+ /* We found the "unit-shell" verb while processing the argument list. Since we turned off reordering of the
+ * argument list initially let's readjust it now, and move the "unit-shell" verb to the back. */
+
+ optind -= 1; /* place the option index where the "unit-shell" verb will be placed */
+
+ t = argv[unit_shell];
+ for (int i = unit_shell; i < optind; i++)
+ argv[i] = argv[i+1];
+ argv[optind] = t;
+ }
if (arg_offline && !streq_ptr(argv[optind], "security"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
@@ -684,6 +757,7 @@ static int run(int argc, char *argv[]) {
{ "cat-config", 2, VERB_ANY, 0, verb_cat_config },
{ "unit-files", VERB_ANY, VERB_ANY, 0, verb_unit_files },
{ "unit-paths", 1, 1, 0, verb_unit_paths },
+ { "unit-shell", 2, VERB_ANY, 0, verb_unit_shell },
{ "exit-status", VERB_ANY, VERB_ANY, 0, verb_exit_status },
{ "syscall-filter", VERB_ANY, VERB_ANY, 0, verb_syscall_filters },
{ "capability", VERB_ANY, VERB_ANY, 0, verb_capabilities },
diff --git a/src/analyze/meson.build b/src/analyze/meson.build
index 7d437953d9b..283378a2f3c 100644
--- a/src/analyze/meson.build
+++ b/src/analyze/meson.build
@@ -33,6 +33,7 @@ systemd_analyze_sources = files(
'analyze-timestamp.c',
'analyze-unit-files.c',
'analyze-unit-paths.c',
+ 'analyze-unit-shell.c',
'analyze-verify.c',
'analyze.c',
)
diff --git a/test/units/TEST-65-ANALYZE.sh b/test/units/TEST-65-ANALYZE.sh
index 468c9ed0939..e5a9aba4e72 100755
--- a/test/units/TEST-65-ANALYZE.sh
+++ b/test/units/TEST-65-ANALYZE.sh
@@ -1116,6 +1116,29 @@ systemd-analyze transient-settings mount | grep CPUQuotaPeriodSec
(! systemd-analyze transient-settings service | grep ConditionKernelVersion )
(! systemd-analyze transient-settings service | grep AssertKernelVersion )
+# check systemd-analyze unit-shell with a namespaced unit
+UNIT_NAME="test-unit-shell.service"
+UNIT_FILE="/run/systemd/system/$UNIT_NAME"
+cat >"$UNIT_FILE" </tmp/testfile; systemd-notify --ready; sleep infinity"
+PrivateTmp=disconnected
+EOF
+# Start the service
+systemctl start "$UNIT_NAME"
+# Wait for the service to be active
+systemctl is-active --quiet "$UNIT_NAME"
+# Verify the service is active and has a MainPID
+MAIN_PID=$(systemctl show -p MainPID --value "$UNIT_NAME")
+[ "$MAIN_PID" -gt 0 ]
+# Test systemd-analyze unit-shell with a command (cat /tmp/testfile)
+OUTPUT=$(systemd-analyze unit-shell "$UNIT_NAME" cat /tmp/testfile)
+assert_in "Hello from test unit" "$OUTPUT"
+
systemd-analyze log-level info
touch /testok