]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
udevadm: introduce new 'wait' command
authorYu Watanabe <watanabe.yu+github@gmail.com>
Fri, 25 Mar 2022 20:01:40 +0000 (05:01 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Fri, 1 Apr 2022 06:13:18 +0000 (15:13 +0900)
Prompted by https://github.com/systemd/systemd/pull/22717#issuecomment-1067348496.

The new command 'udevadm wait' waits for device or device symlink being
created. This may be useful to wait for a device is processed by udevd
after e.g. formatting or partitioning the device.

man/udevadm.xml
shell-completion/bash/udevadm
shell-completion/zsh/_udevadm
src/udev/meson.build
src/udev/udevadm-wait.c [new file with mode: 0644]
src/udev/udevadm.c
src/udev/udevadm.h

index af485711222e21a9e9844f3f50123e7f5b605188..e299a7587945146eda74689f68bbc4ad3dc40f3c 100644 (file)
@@ -48,6 +48,9 @@
     <cmdsynopsis>
       <command>udevadm test-builtin <optional>options</optional> <replaceable>command</replaceable> <replaceable>devpath</replaceable></command>
     </cmdsynopsis>
+    <cmdsynopsis>
+      <command>udevadm wait <optional>options</optional> <replaceable>device|syspath</replaceable></command>
+    </cmdsynopsis>
   </refsynopsisdiv>
 
   <refsect1><title>Description</title>
             <para>When <option>--initialized-nomatch</option> is specified, trigger events for devices
             that are not initialized by <command>systemd-udevd</command> yet, and skip devices that
             are already initialized.</para>
-            <para>Here, initialized devices are those for which at least one udev rule already
-            completed execution – for any action but <literal>remove</literal> — that set a property
-            or other device setting (and thus has an entry in the udev device database). Devices are
-            no longer considered initialized if a <literal>remove</literal> action is seen for them
-            (which removes their entry in the udev device database). Note that devices that have no
-            udev rules are never considered initialized, but might still be announced via the sd-device
-            API (or similar). Typically, it is thus essential that applications which intend to use
-            such a match, make sure a suitable udev rule is installed that sets at least one property
-            on devices that shall be matched.</para>
+            <para>Typically, it is essential that applications which intend to use such a match, make
+            sure a suitable udev rule is installed that sets at least one property on devices that
+            shall be matched. See also Initialized Devices section below for more details.</para>
             <para>WARNING: <option>--initialized-nomatch</option> can potentially save a significant
             amount of time compared to re-triggering all devices in the system and e.g. can be used to
             optimize boot time. However, this is not safe to be used in a boot sequence in general.
         <xi:include href="standard-options.xml" xpointer="help" />
       </variablelist>
     </refsect2>
+
+    <refsect2>
+      <title>udevadm wait
+      <arg choice="opt"><replaceable>options</replaceable></arg>
+      <arg choice="opt"><replaceable>device|syspath</replaceable></arg>
+      …
+      </title>
+
+      <para>Wait for devices or device symlinks being created and initialized by
+      <command>systemd-udevd</command>. Each device path must start with
+      <literal>/dev/</literal> or <literal>/sys/</literal>, e.g. <literal>/dev/sda</literal>,
+      <literal>/dev/disk/by-path/pci-0000:3c:00.0-nvme-1-part1</literal>,
+      <literal>/sys/devices/pci0000:00/0000:00:1f.6/net/eth0</literal>, or
+      <literal>/sys/class/net/eth0</literal>. This can take multiple devices. This may be useful for
+      waiting for devices being processed by <command>systemd-udevd</command> after e.g. partitioning
+      or formatting the devices.</para>
+
+      <variablelist>
+        <varlistentry>
+          <term><option>-t</option></term>
+          <term><option>--timeout=<replaceable>SECONDS</replaceable></option></term>
+          <listitem>
+            <para>Maximum number of seconds to wait for the specified devices or device symlinks being
+            created, initialized, or removed. The default value is <literal>infinity</literal>.</para>
+          </listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term><option>--initialized=<replaceable>BOOL</replaceable></option></term>
+          <listitem>
+            <para>Check if <command>systemd-udevd</command> initialized devices. Defaults to true. When
+            false, the command only checks if the specified devices exist. Set false to this setting if
+            there is no udev rules for the specified devices, as the devices will never be considered
+            as initialized in that case. See Initialized Devices section below for more details.</para>
+          </listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term><option>--removed</option></term>
+          <listitem>
+            <para>When specified, the command wait for devices being removed instead of created or
+            initialized. If this is specified, <option>--initialized=</option> will be ignored.</para>
+          </listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term><option>--settle</option></term>
+          <listitem>
+            <para>When specified, also watches the udev event queue, and wait for all queued events
+            being processed by <command>systemd-udevd</command>.</para>
+          </listitem>
+        </varlistentry>
+
+        <xi:include href="standard-options.xml" xpointer="help" />
+      </variablelist>
+    </refsect2>
+  </refsect1>
+
+  <refsect1>
+    <title>Initialized Devices</title>
+
+    <para>Initialized devices are those for which at least one udev rule already completed execution
+    – for any action but <literal>remove</literal> — that set a property or other device setting (and
+    thus has an entry in the udev device database). Devices are no longer considered initialized if a
+    <literal>remove</literal> action is seen for them (which removes their entry in the udev device
+    database). Note that devices that have no udev rules are never considered initialized, but might
+    still be announced via the sd-device API (or similar).</para>
   </refsect1>
 
   <refsect1>
index 23ce02365c7f0fe3bb782daa239219d963e2b6e4..08446b2d5df5b84a5f22e8cc95fb4045c5a267e0 100644 (file)
@@ -32,7 +32,7 @@ __get_all_sysdevs() {
 
 __get_all_devs() {
     local i
-    for i in /dev/* /dev/*/*; do
+    for i in /dev/* /dev/*/* /dev/*/*/*; do
         echo $i
     done
 }
@@ -64,9 +64,10 @@ _udevadm() {
         [MONITOR_ARG]='-s --subsystem-match -t --tag-match'
         [TEST]='-a --action -N --resolve-names'
         [TEST_BUILTIN]='-a --action'
+        [WAIT]='-t --timeout --initialized=no --removed --settle'
     )
 
-    local verbs=(info trigger settle control monitor test-builtin test)
+    local verbs=(info trigger settle control monitor test-builtin test wait)
     local builtins=(blkid btrfs hwdb input_id keyboard kmod net_id net_setup_link path_id usb_id uaccess)
 
     for ((i=0; i < COMP_CWORD; i++)); do
@@ -245,6 +246,25 @@ _udevadm() {
             fi
             ;;
 
+        'wait')
+            if __contains_word "$prev" ${OPTS[WAIT]}; then
+                case $prev in
+                    *)
+                        comps=''
+                        ;;
+                esac
+                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+                return 0
+            fi
+
+            if [[ $cur = -* ]]; then
+                comps="${OPTS[COMMON]} ${OPTS[WAIT]}"
+            else
+                comps=$( __get_all_devs )
+                local IFS=$'\n'
+            fi
+            ;;
+
         *)
             comps=${VERBS[*]}
             ;;
index 63df8b7c9eccf9468d6cd73a80cfb304c0d6a5ba..a277ad7929a16c5fb6fd7f12655d5f97a5769566 100644 (file)
@@ -104,6 +104,17 @@ _udevadm_test-builtin(){
     fi
 }
 
+(( $+functions[_udevadm_wait] )) ||
+_udevadm_wait(){
+    _arguments \
+        '--timeout=[Maximum number of seconds to wait for the devices being created.]' \
+        '--initialized=[Wait for devices being initialized by systemd-udevd.]:boolean:(yes no)' \
+        '--removed[Wait for devices being removed.]' \
+        '--settle[Also wait for udev queue being empty.]' \
+        '--help[Print help text.]' \
+        '*::devpath:_files -P /dev/ -W /dev'
+}
+
 (( $+functions[_udevadm_mounts] )) ||
 _udevadm_mounts(){
     local dev_tmp dpath_tmp mp_tmp mline
index a4648be3b49ae220967ef897acc15af3b1dc3302..8a2926db308d1408295fff480cc744d2161d6d8b 100644 (file)
@@ -13,6 +13,7 @@ udevadm_sources = files(
         'udevadm-trigger.c',
         'udevadm-util.c',
         'udevadm-util.h',
+        'udevadm-wait.c',
         'udevd.c',
 )
 
diff --git a/src/udev/udevadm-wait.c b/src/udev/udevadm-wait.c
new file mode 100644 (file)
index 0000000..0ea0ed5
--- /dev/null
@@ -0,0 +1,378 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "chase-symlinks.h"
+#include "device-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "inotify-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "static-destruct.h"
+#include "string-table.h"
+#include "strv.h"
+#include "udev-util.h"
+#include "udevadm.h"
+
+typedef enum WaitUntil {
+        WAIT_UNTIL_INITIALIZED,
+        WAIT_UNTIL_ADDED,
+        WAIT_UNTIL_REMOVED,
+        _WAIT_UNTIL_MAX,
+        _WAIT_UNTIL_INVALID = -EINVAL,
+} WaitUntil;
+
+static WaitUntil arg_wait_until = WAIT_UNTIL_INITIALIZED;
+static usec_t arg_timeout_usec = USEC_INFINITY;
+static bool arg_settle = false;
+static char **arg_devices = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_devices, strv_freep);
+
+static const char * const wait_until_table[_WAIT_UNTIL_MAX] = {
+        [WAIT_UNTIL_INITIALIZED] = "initialized",
+        [WAIT_UNTIL_ADDED]       = "added",
+        [WAIT_UNTIL_REMOVED]     = "removed",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(wait_until, WaitUntil);
+
+static int check_device(const char *path) {
+        _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+        int r;
+
+        assert(path);
+
+        r = sd_device_new_from_path(&dev, path);
+        if (r == -ENODEV)
+                return arg_wait_until == WAIT_UNTIL_REMOVED;
+        if (r < 0)
+                return r;
+
+        switch (arg_wait_until) {
+        case WAIT_UNTIL_INITIALIZED:
+                return sd_device_get_is_initialized(dev);
+        case WAIT_UNTIL_ADDED:
+                return true;
+        case WAIT_UNTIL_REMOVED:
+                return false;
+        default:
+                assert_not_reached();
+        }
+}
+
+static bool check(void) {
+        int r;
+
+        if (arg_settle) {
+                r = udev_queue_is_empty();
+                if (r == 0)
+                        return false;
+                if (r < 0)
+                        log_warning_errno(r, "Failed to check if udev queue is empty, assuming empty: %m");
+        }
+
+        STRV_FOREACH(p, arg_devices) {
+                r = check_device(*p);
+                if (r <= 0) {
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to check if device \"%s\" is %s, assuming not %s: %m",
+                                                  *p,
+                                                  wait_until_to_string(arg_wait_until),
+                                                  wait_until_to_string(arg_wait_until));
+                        return false;
+                }
+        }
+
+        return true;
+}
+
+static int check_and_exit(sd_event *event) {
+        assert(event);
+
+        if (check())
+                return sd_event_exit(event, 0);
+
+        return 0;
+}
+
+static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, void *userdata) {
+        const char *name;
+        int r;
+
+        assert(monitor);
+        assert(device);
+
+        if (device_for_action(device, SD_DEVICE_REMOVE) != (arg_wait_until == WAIT_UNTIL_REMOVED))
+                return 0;
+
+        if (arg_wait_until == WAIT_UNTIL_REMOVED)
+                /* On removed event, the received device may not contain enough information.
+                 * Let's unconditionally check all requested devices are removed. */
+                return check_and_exit(sd_device_monitor_get_event(monitor));
+
+        /* For other events, at first check if the received device matches with the requested devices,
+         * to avoid calling check() so many times within a short time. */
+
+        r = sd_device_get_sysname(device, &name);
+        if (r < 0) {
+                log_device_warning_errno(device, r, "Failed to get sysname of received device, ignoring: %m");
+                return 0;
+        }
+
+        STRV_FOREACH(p, arg_devices) {
+                const char *s;
+
+                if (!path_startswith(*p, "/sys"))
+                        continue;
+
+                r = path_find_last_component(*p, false, NULL, &s);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to extract filename from \"%s\", ignoring: %m", *p);
+                        continue;
+                }
+                if (r == 0)
+                        continue;
+
+                if (strneq(s, name, r))
+                        return check_and_exit(sd_device_monitor_get_event(monitor));
+        }
+
+        r = sd_device_get_devname(device, &name);
+        if (r < 0) {
+                if (r != -ENOENT)
+                        log_device_warning_errno(device, r, "Failed to get devname of received device, ignoring: %m");
+                return 0;
+        }
+
+        if (path_strv_contains(arg_devices, name))
+                return check_and_exit(sd_device_monitor_get_event(monitor));
+
+        STRV_FOREACH(p, arg_devices) {
+                const char *link;
+
+                if (!path_startswith(*p, "/dev"))
+                        continue;
+
+                FOREACH_DEVICE_DEVLINK(device, link)
+                        if (path_equal(*p, link))
+                                return check_and_exit(sd_device_monitor_get_event(monitor));
+        }
+
+        return 0;
+}
+
+static int setup_monitor(sd_event *event, sd_device_monitor **ret) {
+        _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL;
+        int r;
+
+        assert(event);
+        assert(ret);
+
+        r = sd_device_monitor_new(&monitor);
+        if (r < 0)
+                return r;
+
+        (void) sd_device_monitor_set_receive_buffer_size(monitor, 128*1024*1024);
+
+        r = sd_device_monitor_attach_event(monitor, event);
+        if (r < 0)
+                return r;
+
+        r = sd_device_monitor_start(monitor, device_monitor_handler, NULL);
+        if (r < 0)
+                return r;
+
+        r = sd_event_source_set_description(sd_device_monitor_get_event_source(monitor),
+                                            "device-monitor-event-source");
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(monitor);
+        return 0;
+}
+
+static int on_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) {
+        return check_and_exit(sd_event_source_get_event(s));
+}
+
+static int setup_inotify(sd_event *event) {
+        _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+        int r;
+
+        assert(event);
+
+        if (!arg_settle)
+                return 0;
+
+        r = sd_event_add_inotify(event, &s, "/run/udev" , IN_CREATE | IN_DELETE, on_inotify, NULL);
+        if (r < 0)
+                return r;
+
+        r = sd_event_source_set_description(s, "inotify-event-source");
+        if (r < 0)
+                return r;
+
+        return sd_event_source_set_floating(s, true);
+}
+
+static int setup_timer(sd_event *event) {
+        _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+        int r;
+
+        assert(event);
+
+        if (arg_timeout_usec == USEC_INFINITY)
+                return 0;
+
+        r = sd_event_add_time_relative(event, &s, CLOCK_BOOTTIME, arg_timeout_usec, 0,
+                                       NULL, INT_TO_PTR(-ETIMEDOUT));
+        if (r < 0)
+                return r;
+
+        r = sd_event_source_set_description(s, "timeout-event-source");
+        if (r < 0)
+                return r;
+
+        return sd_event_source_set_floating(s, true);
+}
+
+static int help(void) {
+        printf("%s wait [OPTIONS] DEVICE [DEVICE…]\n\n"
+               "Wait for devices or device symlinks being created.\n\n"
+               "  -h --help             Print this message\n"
+               "  -V --version          Print version of the program\n"
+               "  -t --timeout=SEC      Maximum time to wait for the device\n"
+               "     --initialized=BOOL Wait for devices being initialized by systemd-udevd\n"
+               "     --removed          Wait for devices being removed\n"
+               "     --settle           Also wait for all queued events being processed\n",
+               program_invocation_short_name);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        enum {
+                ARG_INITIALIZED = 0x100,
+                ARG_REMOVED,
+                ARG_SETTLE,
+        };
+
+        static const struct option options[] = {
+                { "timeout",     required_argument, NULL, 't'             },
+                { "initialized", required_argument, NULL, ARG_INITIALIZED },
+                { "removed",     no_argument,       NULL, ARG_REMOVED     },
+                { "settle",      no_argument,       NULL, ARG_SETTLE      },
+                { "help",        no_argument,       NULL, 'h'             },
+                { "version",     no_argument,       NULL, 'V'             },
+                {}
+        };
+
+        int c, r;
+
+        while ((c = getopt_long(argc, argv, "t:hV", options, NULL)) >= 0)
+                switch (c) {
+                case 't':
+                        r = parse_sec(optarg, &arg_timeout_usec);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse -t/--timeout= parameter: %s", optarg);
+                        break;
+
+                case ARG_INITIALIZED:
+                        r = parse_boolean(optarg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --initialized= parameter: %s", optarg);
+                        arg_wait_until = r ? WAIT_UNTIL_INITIALIZED : WAIT_UNTIL_ADDED;
+                        break;
+
+                case ARG_REMOVED:
+                        arg_wait_until = WAIT_UNTIL_REMOVED;
+                        break;
+
+                case ARG_SETTLE:
+                        arg_settle = true;
+                        break;
+
+                case 'V':
+                        return print_version();
+
+                case 'h':
+                        return help();
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached();
+                }
+
+        if (optind >= argc)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Too few arguments, expected at least one device path or device symlink.");
+
+        arg_devices = strv_copy(argv + optind);
+        if (!arg_devices)
+                return log_oom();
+
+        return 1; /* work to do */
+}
+
+int wait_main(int argc, char *argv[], void *userdata) {
+        _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL;
+        _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+        int r;
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r;
+
+        STRV_FOREACH(p, arg_devices) {
+                path_simplify(*p);
+
+                if (!path_is_safe(*p))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Device path cannot contain \"..\".");
+
+                if (!is_device_path(*p))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Specified path \"%s\" does not start with \"/dev/\" or \"/sys/\".", *p);
+        }
+
+        /* Check before configuring event sources, as devices may be already initialized. */
+        if (check())
+                return 0;
+
+        r = sd_event_default(&event);
+        if (r < 0)
+                return log_error_errno(r, "Failed to initialize sd-event: %m");
+
+        r = setup_timer(event);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set up timeout: %m");
+
+        r = setup_inotify(event);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set up inotify: %m");
+
+        r = setup_monitor(event, &monitor);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set up device monitor: %m");
+
+        /* Check before entering the event loop, as devices may be initialized during setting up event sources. */
+        if (check())
+                return 0;
+
+        r = sd_event_loop(event);
+        if (r == -ETIMEDOUT)
+                return log_error_errno(r, "Timed out for waiting devices being %s.",
+                                       wait_until_to_string(arg_wait_until));
+        if (r < 0)
+                return log_error_errno(r, "Event loop failed: %m");
+
+        return 0;
+}
index ba17d9348b6865947f039e426134bcf376c7b141..df23a60c1a0999d39b796fc3d278310f5927d2fe 100644 (file)
 
 static int help(void) {
         static const char *const short_descriptions[][2] = {
-                { "info",         "Query sysfs or the udev database" },
-                { "trigger",      "Request events from the kernel"   },
-                { "settle",       "Wait for pending udev events"     },
-                { "control",      "Control the udev daemon"          },
-                { "monitor",      "Listen to kernel and udev events" },
-                { "test",         "Test an event run"                },
-                { "test-builtin", "Test a built-in command"          },
+                { "info",         "Query sysfs or the udev database"  },
+                { "trigger",      "Request events from the kernel"    },
+                { "settle",       "Wait for pending udev events"      },
+                { "control",      "Control the udev daemon"           },
+                { "monitor",      "Listen to kernel and udev events"  },
+                { "test",         "Test an event run"                 },
+                { "test-builtin", "Test a built-in command"           },
+                { "wait",         "Wait for device or device symlink" },
         };
 
         _cleanup_free_ char *link = NULL;
@@ -101,6 +102,7 @@ static int udevadm_main(int argc, char *argv[]) {
                 { "hwdb",         VERB_ANY, VERB_ANY, 0, hwdb_main    },
                 { "test",         VERB_ANY, VERB_ANY, 0, test_main    },
                 { "test-builtin", VERB_ANY, VERB_ANY, 0, builtin_main },
+                { "wait",         VERB_ANY, VERB_ANY, 0, wait_main    },
                 { "version",      VERB_ANY, VERB_ANY, 0, version_main },
                 { "help",         VERB_ANY, VERB_ANY, 0, help_main    },
                 {}
index 75ce633632bf663098aecbf3ca947ebed4dd6bdb..808294ec9d6b1ec69b50cf96da13770a74fa9d55 100644 (file)
@@ -13,6 +13,7 @@ int monitor_main(int argc, char *argv[], void *userdata);
 int hwdb_main(int argc, char *argv[], void *userdata);
 int test_main(int argc, char *argv[], void *userdata);
 int builtin_main(int argc, char *argv[], void *userdata);
+int wait_main(int argc, char *argv[], void *userdata);
 
 static inline int print_version(void) {
         /* Dracut relies on the version being a single integer */