| /* SPDX-License-Identifier: GPL-2.0-or-later */ |
| |
| #include <getopt.h> |
| #include <stdlib.h> |
| #include <sys/file.h> |
| #include <unistd.h> |
| |
| #include "blockdev-util.h" |
| #include "btrfs-util.h" |
| #include "device-util.h" |
| #include "fd-util.h" |
| #include "fdset.h" |
| #include "main-func.h" |
| #include "parse-util.h" |
| #include "path-util.h" |
| #include "pretty-print.h" |
| #include "process-util.h" |
| #include "signal-util.h" |
| #include "sort-util.h" |
| #include "strv.h" |
| #include "time-util.h" |
| #include "udevadm.h" |
| |
| static usec_t arg_timeout_usec = USEC_INFINITY; |
| static char **arg_devices = NULL; |
| static char **arg_backing = NULL; |
| static char **arg_cmdline = NULL; |
| static bool arg_print = false; |
| |
| STATIC_DESTRUCTOR_REGISTER(arg_devices, strv_freep); |
| STATIC_DESTRUCTOR_REGISTER(arg_backing, strv_freep); |
| STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep); |
| |
| static int help(void) { |
| _cleanup_free_ char *link = NULL; |
| int r; |
| |
| r = terminal_urlify_man("udevadm", "8", &link); |
| if (r < 0) |
| return log_oom(); |
| |
| printf("%s [OPTIONS...] COMMAND\n" |
| "%s [OPTIONS...] --print\n" |
| "\n%sLock a block device and run a command.%s\n\n" |
| " -h --help Print this message\n" |
| " -V --version Print version of the program\n" |
| " -d --device=DEVICE Block device to lock\n" |
| " -b --backing=FILE File whose backing block device to lock\n" |
| " -t --timeout=SECS Block at most the specified time waiting for lock\n" |
| " -p --print Only show which block device the lock would be taken on\n" |
| "\nSee the %s for details.\n", |
| program_invocation_short_name, |
| program_invocation_short_name, |
| ansi_highlight(), |
| ansi_normal(), |
| link); |
| |
| return 0; |
| } |
| |
| static int parse_argv(int argc, char *argv[]) { |
| |
| static const struct option options[] = { |
| { "help", no_argument, NULL, 'h' }, |
| { "version", no_argument, NULL, 'V' }, |
| { "device", required_argument, NULL, 'd' }, |
| { "backing", required_argument, NULL, 'b' }, |
| { "timeout", required_argument, NULL, 't' }, |
| { "print", no_argument, NULL, 'p' }, |
| {} |
| }; |
| |
| int c, r; |
| |
| assert(argc >= 0); |
| assert(argv); |
| |
| while ((c = getopt_long(argc, argv, arg_print ? "hVd:b:t:p" : "+hVd:b:t:p", options, NULL)) >= 0) |
| |
| switch (c) { |
| |
| case 'h': |
| return help(); |
| |
| case 'V': |
| return print_version(); |
| |
| case 'd': |
| case 'b': { |
| _cleanup_free_ char *s = NULL; |
| char ***l = c == 'd' ? &arg_devices : &arg_backing; |
| |
| r = path_make_absolute_cwd(optarg, &s); |
| if (r < 0) |
| return log_error_errno(r, "Failed to make path '%s' absolute: %m", optarg); |
| |
| path_simplify(s); |
| |
| if (strv_consume(l, TAKE_PTR(s)) < 0) |
| return log_oom(); |
| |
| strv_uniq(*l); |
| break; |
| } |
| |
| case 't': |
| r = parse_sec(optarg, &arg_timeout_usec); |
| if (r < 0) |
| return log_error_errno(r, "Failed to parse --timeout= parameter: %s", optarg); |
| break; |
| |
| case 'p': |
| arg_print = true; |
| break; |
| |
| case '?': |
| return -EINVAL; |
| |
| default: |
| assert_not_reached(); |
| } |
| |
| if (arg_print) { |
| if (optind != argc) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No arguments expected"); |
| } else { |
| if (optind + 1 > argc) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments, command to execute."); |
| |
| arg_cmdline = strv_copy(argv + optind); |
| if (!arg_cmdline) |
| return log_oom(); |
| } |
| |
| if (strv_isempty(arg_devices) && strv_isempty(arg_backing)) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No devices to lock specified, refusing."); |
| |
| return 1; |
| } |
| |
| static int find_devno( |
| dev_t **devnos, |
| size_t *n_devnos, |
| const char *device, |
| bool backing) { |
| |
| dev_t devt; |
| int r; |
| |
| assert(devnos); |
| assert(n_devnos); |
| assert(*devnos || *n_devnos == 0); |
| assert(device); |
| |
| r = path_get_whole_disk(device, backing, &devt); |
| if (r < 0) |
| return log_error_errno(r, "Failed to find whole block device for '%s': %m", device); |
| |
| if (typesafe_bsearch(&devt, *devnos, *n_devnos, devt_compare_func)) { |
| log_debug("Device %u:%u already listed for locking, ignoring.", major(devt), minor(devt)); |
| return 0; |
| } |
| |
| if (!GREEDY_REALLOC(*devnos, *n_devnos + 1)) |
| return log_oom(); |
| |
| (*devnos)[(*n_devnos)++] = devt; |
| |
| /* Immediately sort again, to ensure the binary search above will work for the next device we add */ |
| typesafe_qsort(*devnos, *n_devnos, devt_compare_func); |
| return 1; |
| } |
| |
| static int lock_device( |
| const char *path, |
| dev_t devno, |
| usec_t deadline) { |
| |
| _cleanup_close_ int fd = -EBADF; |
| struct stat st; |
| int r; |
| |
| fd = open(path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); |
| if (fd < 0) |
| return log_error_errno(errno, "Failed to open '%s': %m", path); |
| |
| if (fstat(fd, &st) < 0) |
| return log_error_errno(errno, "Failed to stat '%s': %m", path); |
| |
| /* Extra safety: check that the device still refers to what we think it refers to */ |
| if (!S_ISBLK(st.st_mode) || st.st_rdev != devno) |
| return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Path '%s' no longer refers to specified block device %u:%u: %m", path, major(devno), minor(devno)); |
| |
| if (flock(fd, LOCK_EX|LOCK_NB) < 0) { |
| |
| if (errno != EAGAIN) |
| return log_error_errno(errno, "Failed to lock device '%s': %m", path); |
| |
| if (deadline == 0) |
| return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Device '%s' is currently locked.", path); |
| |
| if (deadline == USEC_INFINITY) { |
| |
| log_info("Device '%s' is currently locked, waiting%s", path, special_glyph(SPECIAL_GLYPH_ELLIPSIS)); |
| |
| if (flock(fd, LOCK_EX) < 0) |
| return log_error_errno(errno, "Failed to lock device '%s': %m", path); |
| |
| } else { |
| _cleanup_(sigkill_waitp) pid_t flock_pid = 0; |
| |
| /* flock() doesn't support a time-out. Let's fake one then. The traditional way to do |
| * this is via alarm()/setitimer()/timer_create(), but that's racy, given that the |
| * SIGALRM might already fire between the alarm() and the flock() in which case the |
| * flock() is never cancelled and we lock up (this is a short time window, but with |
| * short timeouts on a loaded machine we might run into it, who knows?). Let's |
| * instead do the lock out-of-process: fork off a child that does the locking, and |
| * that we'll wait on and kill if it takes too long. */ |
| |
| log_info("Device '%s' is currently locked, waiting %s%s", |
| path, FORMAT_TIMESPAN(usec_sub_unsigned(deadline, now(CLOCK_MONOTONIC)), 0), |
| special_glyph(SPECIAL_GLYPH_ELLIPSIS)); |
| |
| BLOCK_SIGNALS(SIGCHLD); |
| |
| r = safe_fork("(timed-flock)", FORK_DEATHSIG|FORK_LOG, &flock_pid); |
| if (r < 0) |
| return r; |
| if (r == 0) { |
| /* Child */ |
| |
| if (flock(fd, LOCK_EX) < 0) { |
| log_error_errno(errno, "Failed to lock device '%s': %m", path); |
| _exit(EXIT_FAILURE); |
| } |
| |
| _exit(EXIT_SUCCESS); |
| } |
| |
| for (;;) { |
| siginfo_t si; |
| sigset_t ss; |
| usec_t n; |
| |
| assert(sigemptyset(&ss) >= 0); |
| assert(sigaddset(&ss, SIGCHLD) >= 0); |
| |
| n = now(CLOCK_MONOTONIC); |
| if (n >= deadline) |
| return log_error_errno(SYNTHETIC_ERRNO(ETIMEDOUT), "Timeout reached."); |
| |
| r = sigtimedwait(&ss, NULL, TIMESPEC_STORE(deadline - n)); |
| if (r < 0) { |
| if (errno != EAGAIN) |
| return log_error_errno(errno, "Failed to wait for SIGCHLD: %m"); |
| |
| return log_error_errno(SYNTHETIC_ERRNO(ETIMEDOUT), "Timeout reached."); |
| } |
| |
| assert(r == SIGCHLD); |
| |
| zero(si); |
| |
| if (waitid(P_PID, flock_pid, &si, WEXITED|WNOHANG|WNOWAIT) < 0) |
| return log_error_errno(errno, "Failed to wait for child: %m"); |
| |
| if (si.si_pid != 0) { |
| assert(si.si_pid == flock_pid); |
| |
| if (si.si_code != CLD_EXITED || si.si_status != EXIT_SUCCESS) |
| return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Unexpected exit status of file lock child."); |
| |
| break; |
| } |
| |
| log_debug("Got SIGCHLD for other child, continuing."); |
| } |
| } |
| } |
| |
| log_debug("Successfully locked %s (%u:%u)%s", path, major(devno), minor(devno), special_glyph(SPECIAL_GLYPH_ELLIPSIS)); |
| |
| return TAKE_FD(fd); |
| } |
| |
| int lock_main(int argc, char *argv[], void *userdata) { |
| _cleanup_(fdset_freep) FDSet *fds = NULL; |
| _cleanup_free_ dev_t *devnos = NULL; |
| size_t n_devnos = 0; |
| usec_t deadline; |
| pid_t pid; |
| int r; |
| |
| r = parse_argv(argc, argv); |
| if (r <= 0) |
| return r; |
| |
| STRV_FOREACH(i, arg_devices) { |
| r = find_devno(&devnos, &n_devnos, *i, /* backing= */ false); |
| if (r < 0) |
| return r; |
| } |
| |
| STRV_FOREACH(i, arg_backing) { |
| r = find_devno(&devnos, &n_devnos, *i, /* backing= */ true); |
| if (r < 0) |
| return r; |
| } |
| |
| assert(n_devnos > 0); |
| |
| fds = fdset_new(); |
| if (!fds) |
| return log_oom(); |
| |
| if (IN_SET(arg_timeout_usec, 0, USEC_INFINITY)) |
| deadline = arg_timeout_usec; |
| else |
| deadline = usec_add(now(CLOCK_MONOTONIC), arg_timeout_usec); |
| |
| for (size_t i = 0; i < n_devnos; i++) { |
| _cleanup_free_ char *node = NULL; |
| |
| r = devname_from_devnum(S_IFBLK, devnos[i], &node); |
| if (r < 0) |
| return log_error_errno(r, "Failed to format block device path: %m"); |
| |
| if (arg_print) |
| printf("%s\n", node); |
| else { |
| _cleanup_close_ int fd = -EBADF; |
| |
| fd = lock_device(node, devnos[i], deadline); |
| if (fd < 0) |
| return fd; |
| |
| r = fdset_put(fds, fd); |
| if (r < 0) |
| return log_oom(); |
| |
| TAKE_FD(fd); |
| } |
| } |
| |
| if (arg_print) |
| return EXIT_SUCCESS; |
| |
| /* Ignore SIGINT and allow the forked process to receive it */ |
| (void) ignore_signals(SIGINT); |
| |
| r = safe_fork("(lock)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_CLOSE_ALL_FDS|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG, &pid); |
| if (r < 0) |
| return r; |
| if (r == 0) { |
| /* Child */ |
| |
| execvp(arg_cmdline[0], arg_cmdline); |
| log_open(); |
| log_error_errno(errno, "Failed to execute %s: %m", arg_cmdline[0]); |
| _exit(EXIT_FAILURE); |
| } |
| |
| return wait_for_terminate_and_check(arg_cmdline[0], pid, 0); |
| } |