blob: d19e7561f81f166cabefc3007f732d8ee3d56e6c [file] [log] [blame]
/* 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);
}