]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
systemd-analyze: added the verb unit-shell to spawn and attach shell
authorZIHCO <chizobajames21@gmail.com>
Fri, 13 Jun 2025 18:38:55 +0000 (19:38 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Fri, 4 Jul 2025 15:09:07 +0000 (16:09 +0100)
man/systemd-analyze.xml
shell-completion/bash/systemd-analyze
src/analyze/analyze-unit-shell.c [new file with mode: 0644]
src/analyze/analyze-unit-shell.h [new file with mode: 0644]
src/analyze/analyze.c
src/analyze/meson.build
test/units/TEST-65-ANALYZE.sh

index e1c575757bd484f3e457ea6654faf803d9698ba6..38063f6d4c45e96a45a48f0417a3de5253db910a 100644 (file)
       <arg choice="opt" rep="repeat">OPTIONS</arg>
       <arg choice="plain">unit-paths</arg>
     </cmdsynopsis>
+    <cmdsynopsis>
+      <command>systemd-analyze</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">unit-shell</arg>
+      <arg choice="plain"><replaceable>SERVICE</replaceable></arg>
+      <arg choice="opt" rep="repeat"><replaceable>Command</replaceable></arg>
+    </cmdsynopsis>
     <cmdsynopsis>
       <command>systemd-analyze</command>
       <arg choice="opt" rep="repeat">OPTIONS</arg>
@@ -1160,6 +1167,22 @@ LEGEND: M → sys_vendor (LENOVO) ┄ F → product_family (ThinkPad X1 Carbon G
 
       <xi:include href="version-info.xml" xpointer="v258"/>
     </refsect2>
+
+    <refsect2>
+      <title><command>systemd-analyze unit-shell <replaceable>SERVICE</replaceable> <optional><replaceable>command</replaceable>...</optional></command></title>
+
+      <para>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.</para>
+
+      <example>
+        <title>Example output</title>
+        <programlisting>$ 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</programlisting>
+      </example>
+
+      <xi:include href="version-info.xml" xpointer="v258"/>
+    </refsect2>
   </refsect1>
 
   <refsect1>
index e4ecafeddca27744c2b2c275dd5bc81fcc0c3664..b909a954e47f5c6300f82cf5b01e252fdd8d25ca 100644 (file)
@@ -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 (file)
index 0000000..576a420
--- /dev/null
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#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 (file)
index 0000000..7c15e08
--- /dev/null
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_unit_shell(int argc, char *argv[], void *userdata);
index 8745db63f5fbd78f65c825e9982a8f12900a3ff3..a43ae3bdb450e48e2f31a63d15c7b9321ae8a9d0 100644 (file)
@@ -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       },
index 7d437953d9b601d8aa7a54c2f4e6a9a2c4b9d172..283378a2f3c4e7219aee6b4826e112f92eb3ab27 100644 (file)
@@ -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',
 )
index 468c9ed09392842a75503907de33215168dff039..e5a9aba4e72a712d7abab2228607e119a90535f1 100755 (executable)
@@ -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" <<EOF
+[Unit]
+Description=Test unit for systemd-analyze unit-shell
+[Service]
+Type=notify
+NotifyAccess=all
+ExecStart=/bin/sh -c "echo 'Hello from test unit' >/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