]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
lock-util: add a new lock_generic_with_timeout() helper
authorLennart Poettering <lennart@poettering.net>
Thu, 2 Nov 2023 10:07:14 +0000 (11:07 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 2 Nov 2023 13:19:32 +0000 (14:19 +0100)
This is just like lock_generic(), but applies the lock with a timeout.
This requires jumping through some hoops by executing things in a child
process, so that we can abort if necessary via a timer. Linux after all
has no native way to take file locks with a timeout.

src/basic/lock-util.c
src/basic/lock-util.h
src/basic/meson.build
src/test/test-lock-util.c

index 7e67c2d583850ad61978533c42c94b2e7f2fccad..047fd0184d606513262313965b886c499e2d4a82 100644 (file)
@@ -2,10 +2,12 @@
 
 #include <errno.h>
 #include <fcntl.h>
+#include <signal.h>
 #include <stdio.h>
 #include <string.h>
 #include <sys/file.h>
 #include <sys/stat.h>
+#include <sys/wait.h>
 
 #include "alloc-util.h"
 #include "fd-util.h"
@@ -14,6 +16,7 @@
 #include "macro.h"
 #include "missing_fcntl.h"
 #include "path-util.h"
+#include "process-util.h"
 
 int make_lock_file_at(int dir_fd, const char *p, int operation, LockFile *ret) {
         _cleanup_close_ int fd = -EBADF, dfd = -EBADF;
@@ -186,3 +189,89 @@ int lock_generic(int fd, LockType type, int operation) {
                 assert_not_reached();
         }
 }
+
+int lock_generic_with_timeout(int fd, LockType type, int operation, usec_t timeout) {
+        _cleanup_(sigkill_waitp) pid_t pid = 0;
+        int r;
+
+        assert(fd >= 0);
+
+        /* A version of lock_generic(), but with a time-out. We do this in a child process, since the kernel
+         * APIs natively don't support a timeout. We set a SIGALRM timer that will kill the child after the
+         * timeout is hit. Returns -ETIMEDOUT if the time-out is hit, and 0 on success.
+         *
+         * This only works for BSD and UNPOSIX locks, as only those are fd-bound, and hence can be acquired
+         * from any process that has access to the fd. POSIX locks OTOH are process-bound, and hence if we'd
+         * acquire them in a child process they'd remain unlocked in the parent. */
+
+        if (type == LOCK_NONE)
+                return 0;
+        if (!IN_SET(type, LOCK_BSD, LOCK_UNPOSIX)) /* Not for POSIX locks, see above. */
+                return -EOPNOTSUPP;
+
+        /* First, try without forking anything off */
+        r = lock_generic(fd, type, operation | (timeout == USEC_INFINITY ? 0 : LOCK_NB));
+        if (r != -EAGAIN || timeout == 0 || FLAGS_SET(operation, LOCK_NB))
+                return r;
+
+        /* If that didn't work, try with a child */
+
+        r = safe_fork("(sd-flock)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL, &pid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to flock block device in child process: %m");
+        if (r == 0) {
+                struct sigevent sev = {
+                        .sigev_notify = SIGEV_SIGNAL,
+                        .sigev_signo = SIGALRM,
+                };
+                timer_t id = 0;
+
+                if (timer_create(CLOCK_MONOTONIC, &sev, &id) < 0) {
+                        log_error_errno(errno, "Failed to allocate CLOCK_MONOTONIC timer: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                struct itimerspec its = {};
+                timespec_store(&its.it_value, timeout);
+
+                if (timer_settime(id, /* flags= */ 0, &its, NULL) < 0) {
+                        log_error_errno(errno, "Failed to start CLOCK_MONOTONIC timer: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                if (lock_generic(fd, type, operation) < 0) {
+                        log_error_errno(errno, "Unable to get an exclusive lock on the device: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                _exit(EXIT_SUCCESS);
+        }
+
+        siginfo_t status;
+        r = wait_for_terminate(pid, &status);
+        if (r < 0)
+                return r;
+
+        TAKE_PID(pid);
+
+        switch (status.si_code) {
+
+        case CLD_EXITED:
+                if (status.si_status != EXIT_SUCCESS)
+                        return -EPROTO;
+
+                return 0;
+
+        case CLD_KILLED:
+                if (status.si_status == SIGALRM)
+                        return -ETIMEDOUT;
+
+                _fallthrough_;
+
+        case CLD_DUMPED:
+                return -EPROTO;
+
+        default:
+                assert_not_reached();
+        }
+}
index b96ad85e2106269ee74375305c3165348c6ae3d3..91b332f803cab4145a85cf530608fa85867a6302 100644 (file)
@@ -41,3 +41,5 @@ typedef enum LockType {
 } LockType;
 
 int lock_generic(int fd, LockType type, int operation);
+
+int lock_generic_with_timeout(int fd, LockType type, int operation, usec_t timeout);
index 438e8493b82f94651c2c0a688f1c8ece3aaa3872..d7450d8b445ec37d0d05459d80be2747a58fb309 100644 (file)
@@ -273,6 +273,7 @@ libbasic = static_library(
         include_directories : basic_includes,
         dependencies : [libcap,
                         libm,
+                        librt,
                         threads,
                         userspace],
         c_args : ['-fvisibility=default'],
index 28fc54a5d6ee4e7eeef8a2a65e1128b42f84d8af..5edd0875822c8b17cea3719b9a2e504a3e180472 100644 (file)
@@ -35,4 +35,31 @@ TEST(make_lock_file) {
         assert_se(make_lock_file("lock", LOCK_EX|LOCK_NB, &lock2) == -EBUSY);
 }
 
+static void test_lock_generic_with_timeout_for_type(LockType type) {
+        _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+        _cleanup_close_ int tfd = -EBADF, tfd2 = -EBADF;
+
+        tfd = mkdtemp_open(NULL, 0, &t);
+        assert_se(tfd >= 0);
+
+        tfd2 = fd_reopen(tfd, O_CLOEXEC|O_DIRECTORY);
+        assert_se(tfd2 >= 0);
+
+        assert_se(lock_generic(tfd, LOCK_BSD, LOCK_EX) >= 0);
+        assert_se(lock_generic(tfd2, LOCK_BSD, LOCK_EX|LOCK_NB) == -EWOULDBLOCK);
+
+        usec_t start = now(CLOCK_MONOTONIC);
+        assert_se(lock_generic_with_timeout(tfd2, LOCK_BSD, LOCK_EX, 200 * USEC_PER_MSEC) == -ETIMEDOUT);
+        assert_se(usec_sub_unsigned(now(CLOCK_MONOTONIC), start) >= 200 * USEC_PER_MSEC);
+
+        assert_se(lock_generic(tfd, LOCK_BSD, LOCK_UN) >= 0);
+        assert_se(lock_generic_with_timeout(tfd2, LOCK_BSD, LOCK_EX, 200 * USEC_PER_MSEC) == 0);
+        assert_se(lock_generic(tfd, LOCK_BSD, LOCK_EX|LOCK_NB) == -EWOULDBLOCK);
+}
+
+TEST(lock_generic_with_timeout) {
+        test_lock_generic_with_timeout_for_type(LOCK_BSD);
+        test_lock_generic_with_timeout_for_type(LOCK_UNPOSIX);
+}
+
 DEFINE_TEST_MAIN(LOG_INFO);