udevadm: introduce new 'wait' command

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.
This commit is contained in:
Yu Watanabe 2022-03-26 05:01:40 +09:00
parent 209294ad24
commit aa2b0d8d29
7 changed files with 495 additions and 18 deletions

View 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>
@ -405,15 +408,9 @@
<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.
@ -694,6 +691,73 @@
<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>

View 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[*]}
;;

View 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

View file

@ -13,6 +13,7 @@ udevadm_sources = files(
'udevadm-trigger.c',
'udevadm-util.c',
'udevadm-util.h',
'udevadm-wait.c',
'udevd.c',
)

378
src/udev/udevadm-wait.c Normal file
View file

@ -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;
}

View file

@ -19,13 +19,14 @@
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 },
{}

View 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 */