diff options
Diffstat (limited to 'usr.bin/lockf')
-rw-r--r-- | usr.bin/lockf/Makefile | 8 | ||||
-rw-r--r-- | usr.bin/lockf/Makefile.depend | 14 | ||||
-rw-r--r-- | usr.bin/lockf/lockf.1 | 286 | ||||
-rw-r--r-- | usr.bin/lockf/lockf.c | 429 | ||||
-rw-r--r-- | usr.bin/lockf/tests/Makefile | 5 | ||||
-rw-r--r-- | usr.bin/lockf/tests/Makefile.depend | 10 | ||||
-rw-r--r-- | usr.bin/lockf/tests/lockf_test.sh | 344 |
7 files changed, 1096 insertions, 0 deletions
diff --git a/usr.bin/lockf/Makefile b/usr.bin/lockf/Makefile new file mode 100644 index 000000000000..36740dbd3a95 --- /dev/null +++ b/usr.bin/lockf/Makefile @@ -0,0 +1,8 @@ +.include <src.opts.mk> + +PROG= lockf + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include <bsd.prog.mk> diff --git a/usr.bin/lockf/Makefile.depend b/usr.bin/lockf/Makefile.depend new file mode 100644 index 000000000000..93249906da4f --- /dev/null +++ b/usr.bin/lockf/Makefile.depend @@ -0,0 +1,14 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + include \ + lib/${CSU_DIR} \ + lib/libc \ + lib/libcompiler_rt \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/usr.bin/lockf/lockf.1 b/usr.bin/lockf/lockf.1 new file mode 100644 index 000000000000..40b4497bc80c --- /dev/null +++ b/usr.bin/lockf/lockf.1 @@ -0,0 +1,286 @@ +.\" +.\" Copyright (C) 1998 John D. Polstra. All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY JOHN D. POLSTRA AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL JOHN D. POLSTRA OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd June 24, 2025 +.Dt LOCKF 1 +.Os +.Sh NAME +.Nm lockf +.Nd execute a command while holding a file lock +.Sh SYNOPSIS +.Nm +.Op Fl knpsTw +.Op Fl t Ar seconds +.Ar file +.Ar command +.Op Ar arguments +.Nm +.Op Fl s +.Op Fl t Ar seconds +.Ar fd +.Sh DESCRIPTION +The +.Nm +utility acquires an exclusive lock on a +.Ar file , +creating it if necessary, +.Bf Em +and removing the file on exit unless explicitly told not to. +.Ef +While holding the lock, it executes a +.Ar command +with optional +.Ar arguments . +After the +.Ar command +completes, +.Nm +releases the lock, and removes the +.Ar file +unless the +.Fl k +option is specified. +.Bx Ns -style +locking is used, as described in +.Xr flock 2 ; +the mere existence of the +.Ar file +is not considered to constitute a lock. +.Pp +.Nm +may also be used to operate on a file descriptor instead of a file. +If no +.Ar command +is supplied, then +.Ar fd +must be a file descriptor. +The version with a +.Ar command +may also be used with a file descriptor by supplying it as a path +.Pa /dev/fd/N , +where N is the desired file descriptor. +The +.Fl k +option is implied when a file descriptor is in use, and the +.Fl n +and +.Fl w +options are silently ignored. +This can be used to lock inside a shell script. +.Pp +If the +.Nm +utility is being used to facilitate concurrency between a number +of processes, it is recommended that the +.Fl k +option be used. +This will guarantee lock ordering, as well as implement +a performance enhanced algorithm which minimizes CPU load associated +with concurrent unlink, drop and re-acquire activity. +It should be noted +that if the +.Fl k +option is not used, then no guarantees around lock ordering can be made. +.Pp +The following options are supported: +.Bl -tag -width ".Fl t Ar seconds" +.It Fl k +Causes the lock file to be kept (not removed) after the command +completes. +.It Fl s +Causes +.Nm +to operate silently. +Failure to acquire the lock is indicated only in the exit status. +.It Fl n +Causes +.Nm +to fail if the specified lock +.Ar file +does not exist. +If +.Fl n +is not specified, +.Nm +will create +.Ar file +if necessary. +.It Fl p +Write the pid of the +.Ar command +to +.Ar file . +This option will cause +.Nm +to open +.Ar file +for writing rather than reading. +.It Fl T +Upon receipt of a +.Dv SIGTERM , +forward a +.Dv SIGTERM +along to the +.Ar command +before cleaning up the +.Ar file +and exiting. +By default, +.Nm +effectively orphans the +.Ar command +after cleaning up the +.Ar file . +.It Fl t Ar seconds +Specifies a timeout for waiting for the lock. +By default, +.Nm +waits indefinitely to acquire the lock. +If a timeout is specified with this option, +.Nm +will wait at most the given number of +.Ar seconds +before giving up. +A timeout of 0 may be given, in which case +.Nm +will fail unless it can acquire the lock immediately. +When a lock times out, +.Ar command +is +.Em not +executed. +.It Fl w +Causes +.Nm +to open +.Ar file +for writing rather than reading. +This is necessary on filesystems (including NFSv4) where a file which +has been opened read-only cannot be exclusively locked. +.El +.Pp +In no event will +.Nm +break a lock that is held by another process. +.Sh EXIT STATUS +If +.Nm +successfully acquires the lock, it returns the exit status produced by +.Ar command . +Otherwise, it returns one of the exit codes defined in +.Xr sysexits 3 , +as follows: +.Bl -tag -width ".Dv EX_CANTCREAT" +.It Dv EX_TEMPFAIL +The specified lock file was already locked by another process. +.It Dv EX_CANTCREAT +The +.Nm +utility +was unable to create the lock file, e.g., because of insufficient access +privileges. +.It Dv EX_UNAVAILABLE +The +.Fl n +option is specified and the specified lock file does not exist. +.It Dv EX_USAGE +There was an error on the +.Nm +command line. +.It Dv EX_OSERR +A system call (e.g., +.Xr fork 2 ) +failed unexpectedly. +.It Dv EX_SOFTWARE +The +.Ar command +did not exit normally, +but may have been signaled or stopped. +.El +.Sh EXAMPLES +The first job takes a lock and sleeps for 5 seconds in the background. +The second job tries to get the lock and timeouts after 1 second (PID numbers +will differ): +.Bd -literal -offset indent +$ lockf mylock sleep 5 & lockf -t 1 mylock echo "Success" +[1] 94410 +lockf: mylock: already locked +.Ed +.Pp +The first job takes a lock and sleeps for 1 second in the background. +The second job waits up to 5 seconds to take the lock and echoes the message on +success (PID numbers will differ): +.Bd -literal -offset indent +$ lockf mylock sleep 1 & lockf -t 5 mylock echo "Success" +[1] 19995 +Success +[1]+ Done lockf mylock sleep 1 +.Ed +Lock a file and run a script, return immediately if the lock is not +available. Do not delete the file afterward so lock order is +guaranteed. +.Pp +.Dl $ lockf -t 0 -k /tmp/my.lock myscript +.Pp +Protect a section of a shell script with a lock, wait up to 5 seconds +for it to become available. +Note that the shell script has opened the lock file +.Fa /tmp/my.lock , +and +.Nm +is performing the lock call exclusively via the passed in file descriptor (9). +In this case +.Fl k +is implied, and +.Fl w +has no effect because the file has already been opened by the shell. +This example assumes that +.Ql > +is implemented in the shell by opening and truncating +.Pa /tmp/my.lock , +rather than by replacing the lock file. +.Bd -literal -offset indent +( + lockf -s -t 5 9 + if [ $? -ne 0 ]; then + echo "Failed to obtain lock" + exit 1 + fi + + echo Start + # Do some stuff + echo End +) 9>/tmp/my.lock +.Ed +.Sh SEE ALSO +.Xr flock 2 , +.Xr lockf 3 , +.Xr sysexits 3 +.Sh HISTORY +A +.Nm +utility first appeared in +.Fx 2.2 . +.Sh AUTHORS +.An John Polstra Aq Mt jdp@polstra.com diff --git a/usr.bin/lockf/lockf.c b/usr.bin/lockf/lockf.c new file mode 100644 index 000000000000..16bae36a21e0 --- /dev/null +++ b/usr.bin/lockf/lockf.c @@ -0,0 +1,429 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (C) 1997 John D. Polstra. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JOHN D. POLSTRA AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL JOHN D. POLSTRA OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/types.h> +#include <sys/wait.h> + +#include <assert.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#include <stdatomic.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +#define FDLOCK_PREFIX "/dev/fd/" + +union lock_subject { + long subj_fd; + const char *subj_name; +}; + +static int acquire_lock(union lock_subject *subj, int flags, int silent); +static void cleanup(void); +static void killed(int sig); +static void sigchld(int sig); +static void timeout(int sig); +static void usage(void) __dead2; +static void wait_for_lock(const char *name); + +static const char *lockname; +_Static_assert(sizeof(sig_atomic_t) >= sizeof(pid_t), + "PIDs cannot be managed safely from a signal handler on this platform."); +static sig_atomic_t child = -1; +static int lockfd = -1; +static bool keep; +static bool fdlock; +static int status; +static bool termchild; +static sig_atomic_t timed_out; + +/* + * Check if fdlock is implied by the given `lockname`. We'll write the fd that + * is represented by it out to ofd, and the caller is expected to do any + * necessary validation on it. + */ +static bool +fdlock_implied(const char *name, long *ofd) +{ + char *endp; + long fd; + + if (strncmp(name, FDLOCK_PREFIX, sizeof(FDLOCK_PREFIX) - 1) != 0) + return (false); + + /* Skip past the prefix. */ + name += sizeof(FDLOCK_PREFIX) - 1; + errno = 0; + fd = strtol(name, &endp, 10); + if (errno != 0 || *endp != '\0') + return (false); + + *ofd = fd; + return (true); +} + +/* + * Execute an arbitrary command while holding a file lock. + */ +int +main(int argc, char **argv) +{ + struct sigaction sa_chld = { + .sa_handler = sigchld, + .sa_flags = SA_NOCLDSTOP, + }, sa_prev; + sigset_t mask, omask; + long long waitsec; + const char *errstr; + union lock_subject subj; + int ch, flags; + bool silent, writepid; + + silent = writepid = false; + flags = O_CREAT | O_RDONLY; + waitsec = -1; /* Infinite. */ + while ((ch = getopt(argc, argv, "knpsTt:w")) != -1) { + switch (ch) { + case 'k': + keep = true; + break; + case 'n': + flags &= ~O_CREAT; + break; + case 's': + silent = true; + break; + case 'T': + termchild = true; + break; + case 't': + waitsec = strtonum(optarg, 0, UINT_MAX, &errstr); + if (errstr != NULL) + errx(EX_USAGE, + "invalid timeout \"%s\"", optarg); + break; + case 'p': + writepid = true; + flags |= O_TRUNC; + /* FALLTHROUGH */ + case 'w': + flags = (flags & ~O_RDONLY) | O_WRONLY; + break; + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + if (argc == 0) + usage(); + + lockname = argv[0]; + + argc--; + argv++; + + /* + * If there aren't any arguments left, then we must be in fdlock mode. + */ + if (argc == 0 && *lockname != '/') { + fdlock = true; + subj.subj_fd = -1; + } else { + fdlock = fdlock_implied(lockname, &subj.subj_fd); + if (argc == 0 && !fdlock) { + fprintf(stderr, "Expected fd, got '%s'\n", lockname); + usage(); + } + } + + if (fdlock) { + if (subj.subj_fd < 0) { + char *endp; + + errno = 0; + subj.subj_fd = strtol(lockname, &endp, 10); + if (errno != 0 || *endp != '\0') { + fprintf(stderr, "Expected fd, got '%s'\n", + lockname); + usage(); + } + } + + if (subj.subj_fd < 0 || subj.subj_fd > INT_MAX) { + fprintf(stderr, "fd '%ld' out of range\n", + subj.subj_fd); + usage(); + } + } else { + subj.subj_name = lockname; + } + + if (waitsec > 0) { /* Set up a timeout. */ + struct sigaction act; + + act.sa_handler = timeout; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; /* Note that we do not set SA_RESTART. */ + sigaction(SIGALRM, &act, NULL); + alarm((unsigned int)waitsec); + } + /* + * If the "-k" option is not given, then we must not block when + * acquiring the lock. If we did, then the lock holder would + * unlink the file upon releasing the lock, and we would acquire + * a lock on a file with no directory entry. Then another + * process could come along and acquire the same lock. To avoid + * this problem, we separate out the actions of waiting for the + * lock to be available and of actually acquiring the lock. + * + * That approach produces behavior that is technically correct; + * however, it causes some performance & ordering problems for + * locks that have a lot of contention. First, it is unfair in + * the sense that a released lock isn't necessarily granted to + * the process that has been waiting the longest. A waiter may + * be starved out indefinitely. Second, it creates a thundering + * herd situation each time the lock is released. + * + * When the "-k" option is used, the unlink race no longer + * exists. In that case we can block while acquiring the lock, + * avoiding the separate step of waiting for the lock. This + * yields fairness and improved performance. + */ + lockfd = acquire_lock(&subj, flags | O_NONBLOCK, silent); + while (lockfd == -1 && !timed_out && waitsec != 0) { + if (keep || fdlock) { + lockfd = acquire_lock(&subj, flags, silent); + } else { + wait_for_lock(lockname); + lockfd = acquire_lock(&subj, flags | O_NONBLOCK, + silent); + } + + /* timed_out */ + atomic_signal_fence(memory_order_acquire); + } + if (waitsec > 0) + alarm(0); + if (lockfd == -1) { /* We failed to acquire the lock. */ + if (silent) + exit(EX_TEMPFAIL); + errx(EX_TEMPFAIL, "%s: already locked", lockname); + } + + /* At this point, we own the lock. */ + + /* Nothing else to do for FD lock, just exit */ + if (argc == 0) { + assert(fdlock); + return 0; + } + + if (atexit(cleanup) == -1) + err(EX_OSERR, "atexit failed"); + + /* + * Block SIGTERM while SIGCHLD is being processed, so that we can safely + * waitpid(2) for the child without a concurrent termination observing + * an invalid pid (i.e., waited-on). If our setup between here and the + * sigsuspend loop gets any more complicated, we should rewrite it to + * just use a pipe to signal the child onto execvp(). + * + * We're blocking SIGCHLD and SIGTERM here so that we don't do any + * cleanup before we're ready to (after the pid is written out). + */ + sigemptyset(&mask); + sigaddset(&mask, SIGCHLD); + sigaddset(&mask, SIGTERM); + (void)sigprocmask(SIG_BLOCK, &mask, &omask); + + memcpy(&sa_chld.sa_mask, &omask, sizeof(omask)); + sigaddset(&sa_chld.sa_mask, SIGTERM); + (void)sigaction(SIGCHLD, &sa_chld, &sa_prev); + + if ((child = fork()) == -1) + err(EX_OSERR, "cannot fork"); + if (child == 0) { /* The child process. */ + (void)sigprocmask(SIG_SETMASK, &omask, NULL); + close(lockfd); + execvp(argv[0], argv); + warn("%s", argv[0]); + _exit(1); + } + /* This is the parent process. */ + signal(SIGINT, SIG_IGN); + signal(SIGQUIT, SIG_IGN); + signal(SIGTERM, killed); + + fclose(stdin); + fclose(stdout); + fclose(stderr); + + /* Write out the pid before we sleep on it. */ + if (writepid) + (void)dprintf(lockfd, "%d\n", (int)child); + + /* Just in case they were blocked on entry. */ + sigdelset(&omask, SIGCHLD); + sigdelset(&omask, SIGTERM); + while (child >= 0) { + (void)sigsuspend(&omask); + /* child */ + atomic_signal_fence(memory_order_acquire); + } + + return (WIFEXITED(status) ? WEXITSTATUS(status) : EX_SOFTWARE); +} + +/* + * Try to acquire a lock on the given file/fd, creating the file if + * necessary. The flags argument is O_NONBLOCK or 0, depending on + * whether we should wait for the lock. Returns an open file descriptor + * on success, or -1 on failure. + */ +static int +acquire_lock(union lock_subject *subj, int flags, int silent) +{ + int fd; + + if (fdlock) { + assert(subj->subj_fd >= 0 && subj->subj_fd <= INT_MAX); + fd = (int)subj->subj_fd; + + if (flock(fd, LOCK_EX | LOCK_NB) == -1) { + if (errno == EAGAIN || errno == EINTR) + return (-1); + err(EX_CANTCREAT, "cannot lock fd %d", fd); + } + } else if ((fd = open(subj->subj_name, O_EXLOCK|flags, 0666)) == -1) { + if (errno == EAGAIN || errno == EINTR) + return (-1); + else if (errno == ENOENT && (flags & O_CREAT) == 0) { + if (!silent) + warn("%s", subj->subj_name); + exit(EX_UNAVAILABLE); + } + err(EX_CANTCREAT, "cannot open %s", subj->subj_name); + } + return (fd); +} + +/* + * Remove the lock file. + */ +static void +cleanup(void) +{ + + if (keep || fdlock) + flock(lockfd, LOCK_UN); + else + unlink(lockname); +} + +/* + * Signal handler for SIGTERM. Cleans up the lock file, then re-raises + * the signal. + */ +static void +killed(int sig) +{ + + if (termchild && child >= 0) + kill(child, sig); + cleanup(); + signal(sig, SIG_DFL); + if (raise(sig) == -1) + _Exit(EX_OSERR); +} + +/* + * Signal handler for SIGCHLD. Simply waits for the child and ensures that we + * don't end up in a sticky situation if we receive a SIGTERM around the same + * time. + */ +static void +sigchld(int sig __unused) +{ + int ostatus; + + while (waitpid(child, &ostatus, 0) != child) { + if (errno != EINTR) + _exit(EX_OSERR); + } + + status = ostatus; + child = -1; + atomic_signal_fence(memory_order_release); +} + +/* + * Signal handler for SIGALRM. + */ +static void +timeout(int sig __unused) +{ + + timed_out = 1; + atomic_signal_fence(memory_order_release); +} + +static void +usage(void) +{ + + fprintf(stderr, + "usage: lockf [-knsw] [-t seconds] file command [arguments]\n" + " lockf [-s] [-t seconds] fd\n"); + exit(EX_USAGE); +} + +/* + * Wait until it might be possible to acquire a lock on the given file. + * If the file does not exist, return immediately without creating it. + */ +static void +wait_for_lock(const char *name) +{ + int fd; + + if ((fd = open(name, O_RDONLY|O_EXLOCK, 0666)) == -1) { + if (errno == ENOENT || errno == EINTR) + return; + err(EX_CANTCREAT, "cannot open %s", name); + } + close(fd); +} diff --git a/usr.bin/lockf/tests/Makefile b/usr.bin/lockf/tests/Makefile new file mode 100644 index 000000000000..a7c6f45290c9 --- /dev/null +++ b/usr.bin/lockf/tests/Makefile @@ -0,0 +1,5 @@ +PACKAGE= tests + +ATF_TESTS_SH+= lockf_test + +.include <bsd.test.mk> diff --git a/usr.bin/lockf/tests/Makefile.depend b/usr.bin/lockf/tests/Makefile.depend new file mode 100644 index 000000000000..11aba52f82cf --- /dev/null +++ b/usr.bin/lockf/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/usr.bin/lockf/tests/lockf_test.sh b/usr.bin/lockf/tests/lockf_test.sh new file mode 100644 index 000000000000..823b5673a176 --- /dev/null +++ b/usr.bin/lockf/tests/lockf_test.sh @@ -0,0 +1,344 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2023 Klara, Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +# sysexits(3) +: ${EX_USAGE:=64} +: ${EX_UNAVAILABLE:=69} +: ${EX_CANTCREAT:=73} +: ${EX_TEMPFAIL:=75} + +waitlock() +{ + local cur lockfile tmo + + lockfile="$1" + + cur=0 + tmo=20 + + while [ "$cur" -lt "$tmo" -a ! -f "$lockfile" ]; do + sleep 0.1 + cur=$((cur + 1)) + done + + atf_check_not_equal "$cur" "$tmo" +} + + +atf_test_case badargs +badargs_body() +{ + atf_check -s exit:${EX_USAGE} -e not-empty lockf + atf_check -s exit:${EX_USAGE} -e not-empty lockf "testlock" +} + +atf_test_case basic +basic_body() +{ + # Something innocent so that it does eventually go away without our + # intervention. + lockf "testlock" sleep 10 & + lpid=$! + + # Make sure that the lock exists... + while ! test -e "testlock"; do + sleep 0.1 + done + + # Attempt both verbose and silent re-lock + atf_check -s exit:${EX_TEMPFAIL} -e not-empty \ + lockf -t 0 "testlock" sleep 0 + atf_check -s exit:${EX_TEMPFAIL} -e empty \ + lockf -t 0 -s "testlock" sleep 0 + + # Make sure it cleans up after the initial sleep 10 is over. + wait "$lpid" + atf_check test ! -e "testlock" +} + +atf_test_case bubble_error +bubble_error_body() +{ + # Ensure that lockf bubbles up the error as expected. + atf_check -s exit:9 lockf testlock sh -c 'exit 9' +} + +atf_test_case fdlock +fdlock_body() +{ + # First, make sure we don't get a false positive -- existing uses with + # numeric filenames shouldn't switch to being fdlocks automatically. + atf_check lockf -k "9" sleep 0 + atf_check test -e "9" + rm "9" + + subexit_lockfail=1 + subexit_created=2 + subexit_lockok=3 + subexit_concurrent=4 + ( + lockf -s -t 0 9 + if [ $? -ne 0 ]; then + exit "$subexit_lockfail" + fi + + if [ -e "9" ]; then + exit "$subexit_created" + fi + ) 9> "testlock1" + rc=$? + + atf_check test "$rc" -eq 0 + + sub_delay=5 + + # But is it actually locking? Child 1 will acquire the lock and then + # signal that it's ok for the second child to try. The second child + # will try to acquire the lock and fail immediately, signal that it + # tried, then try again with an indefinite timeout. On that one, we'll + # just check how long we ended up waiting -- it should be at least + # $sub_delay. + ( + lockf -s -t 0 /dev/fd/9 + if [ $? -ne 0 ]; then + exit "$subexit_lockfail" + fi + + # Signal + touch ".lock_acquired" + + while [ ! -e ".lock_attempted" ]; do + sleep 0.5 + done + + sleep "$sub_delay" + + if [ -e ".lock_acquired_again" ]; then + exit "$subexit_concurrent" + fi + ) 9> "testlock2" & + lpid1=$! + + ( + while [ ! -e ".lock_acquired" ]; do + sleep 0.5 + done + + # Got the signal, try + lockf -s -t 0 9 + if [ $? -ne "${EX_TEMPFAIL}" ]; then + exit "$subexit_lockok" + fi + + touch ".lock_attempted" + start=$(date +"%s") + lockf -s 9 + touch ".lock_acquired_again" + now=$(date +"%s") + elapsed=$((now - start)) + + if [ "$elapsed" -lt "$sub_delay" ]; then + exit "$subexit_concurrent" + fi + ) 9> "testlock2" & + lpid2=$! + + wait "$lpid1" + status1=$? + + wait "$lpid2" + status2=$? + + atf_check test "$status1" -eq 0 + atf_check test "$status2" -eq 0 +} + +atf_test_case keep +keep_body() +{ + lockf -k "testlock" sleep 10 & + lpid=$! + + # Make sure that the lock exists now... + while ! test -e "testlock"; do + sleep 0.5 + done + + kill "$lpid" + wait "$lpid" + + # And it still exits after the lock has been relinquished. + atf_check test -e "testlock" +} + +atf_test_case needfile +needfile_body() +{ + # Hopefully the clock doesn't jump. + start=$(date +"%s") + + # Should fail if the lockfile does not yet exist. + atf_check -s exit:"${EX_UNAVAILABLE}" lockf -sn "testlock" sleep 30 + + # It's hard to guess how quickly we should have finished that; one would + # hope that it exits fast, but to be safe we specified a sleep 30 under + # lock so that we have a good margin below that duration that we can + # safely test to make sure we didn't actually execute the program, more + # or less. + now=$(date +"%s") + tpass=$((now - start)) + atf_check test "$tpass" -lt 10 +} + +atf_test_case termchild +termchild_body() +{ + lockf -kp testlock sleep 30 & + lpid=$! + + waitlock testlock + + atf_check -o file:testlock pgrep -F testlock + + start=$(date +"%s") + atf_check kill -TERM "$lpid" + wait "$lpid" + end=$(date +"%s") + elapsed=$((end - start)) + + if [ "$elapsed" -gt 5 ]; then + atf_fail "lockf seems to have dodged the SIGTERM ($elapsed passed)" + fi + + # We didn't start lockf with -T this time, so the process should not + # have been terminated. + atf_check -o file:testlock pgrep -F testlock + + lockf -kpT testlock sleep 30 & + lpid=$! + + waitlock testlock + + atf_check -o file:testlock pgrep -F testlock + + start=$(date +"%s") + atf_check kill -TERM "$lpid" + wait "$lpid" + end=$(date +"%s") + elapsed=$((end - start)) + + if [ "$elapsed" -gt 5 ]; then + atf_fail "lockf -T seems to have dodged the SIGTERM ($elapsed passed)" + fi + + # This time, it should have terminated (notably much earlier than our + # 30 second timeout). + atf_check -o empty -e not-empty -s not-exit:0 pgrep -F testlock +} + +atf_test_case timeout +timeout_body() +{ + lockf "testlock" sleep 30 & + lpid=$! + + while ! test -e "testlock"; do + sleep 0.5 + done + + start=$(date +"%s") + timeout=2 + atf_check -s exit:${EX_TEMPFAIL} lockf -st "$timeout" "testlock" sleep 0 + + # We should have taken no less than our timeout, at least. + now=$(date +"%s") + tpass=$((now - start)) + atf_check test "$tpass" -ge "$timeout" + + kill "$lpid" + wait "$lpid" || true +} + +atf_test_case writepid +writepid_body() +{ + lockf -p "testlock" sleep 10 & + lpid=$! + + waitlock "testlock" + + atf_check test -s testlock + atf_check -o file:testlock pgrep -F testlock + atf_check -o file:testlock pgrep -F testlock -fx "sleep 10" + atf_check pkill -TERM -F testlock + + wait + + atf_check test ! -f testlock +} + +atf_test_case writepid_keep +writepid_keep_body() +{ + # Check that we'll clobber any existing contents (a pid, usually) + # once we acquire the lock. + jot -b A -s "" 64 > testlock + atf_check lockf -kp testlock sleep 0 + atf_check -o not-match:"A" cat testlock +} + +atf_test_case wrlock +wrlock_head() +{ + atf_set "require.user" "unprivileged" +} +wrlock_body() +{ + touch "testlock" + chmod -w "testlock" + + # Demonstrate that we can lock the file normally, but -w fails if we + # can't write. + atf_check lockf -kt 0 "testlock" sleep 0 + atf_check -s exit:${EX_CANTCREAT} -e not-empty \ + lockf -wt 0 "testlock" sleep 0 +} + +atf_init_test_cases() +{ + atf_add_test_case badargs + atf_add_test_case basic + atf_add_test_case bubble_error + atf_add_test_case fdlock + atf_add_test_case keep + atf_add_test_case needfile + atf_add_test_case termchild + atf_add_test_case timeout + atf_add_test_case writepid + atf_add_test_case writepid_keep + atf_add_test_case wrlock +} |