diff --git a/src/udev/udevd.c b/src/udev/udevd.c index d6415ad57e..18079d363d 100644 --- a/src/udev/udevd.c +++ b/src/udev/udevd.c @@ -459,6 +459,43 @@ static int worker_process_device(Manager *manager, sd_device *dev) { return -ENOMEM; r = worker_lock_block_device(dev, &fd_lock); + if (r == -EAGAIN) { + /* So this is a block device and the device is locked currently via the BSD advisory locks — + * someone else is exclusively using it. This means we don't run our udev rules now, to not + * interfere. However we want to know when the device is unlocked again, and retrigger the + * device again then, so that the rules are run eventually. For that we use IN_CLOSE_WRITE + * inotify watches (which isn't exactly the same as waiting for the BSD locks to release, but + * not totally off, as long as unlock+close() is done together, as it usually is). + * + * (The user-facing side of this: https://systemd.io/BLOCK_DEVICE_LOCKING) + * + * There's a bit of a chicken and egg problem here for this however: inotify watching is + * supposed to be enabled via an option set via udev rules (OPTIONS+="watch"). If we skip the + * udev rules here however (as we just said we do), we would thus never see that specific + * udev rule, and thus never turn on inotify watching. But in order to catch up eventually + * and run them we we need the inotify watching: hence a classic chicken and egg problem. + * + * Our way out here: if we see the block device locked, unconditionally watch the device via + * inotify, regardless of any explicit request via OPTIONS+="watch". Thus, a device that is + * currently locked via the BSD file locks will be treated as if we ran a single udev rule + * only for it: the one that turns on inotify watching for it. If we eventually see the + * inotify IN_CLOSE_WRITE event, and then run the rules after all and we then realize that + * this wasn't actually requested (i.e. no OPTIONS+="watch" set) we'll simply turn off the + * watching again (see below). Effectively this means: inotify watching is now enabled either + * a) when the udev rules say so, or b) while the device is locked. + * + * Worst case scenario hence: in the (unlikely) case someone locked the device and we clash + * with that we might do inotify watching for a brief moment for a device where we actually + * weren't supposed to. But that shouldn't be too bad, in particular as BSD locks being taken + * on a block device is kinda an indication that the inotify logic is desired too, to some + * degree — they go hand-in-hand after all. */ + + log_device_debug(dev, "Block device is currently locked, installing watch to wait until the lock is released."); + (void) udev_watch_begin(dev); + + /* Now the watch is installed, let's lock the device again, maybe in the meantime things changed */ + r = worker_lock_block_device(dev, &fd_lock); + } if (r < 0) return r; @@ -475,13 +512,14 @@ static int worker_process_device(Manager *manager, sd_device *dev) { /* in case rtnl was initialized */ manager->rtnl = sd_netlink_ref(udev_event->rtnl); - /* apply/restore inotify watch */ + /* apply/restore/end inotify watch */ if (udev_event->inotify_watch) { (void) udev_watch_begin(dev); r = device_update_db(dev); if (r < 0) return log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/: %m"); - } + } else + (void) udev_watch_end(dev); log_device_debug(dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) processed", seqnum, device_action_to_string(action));