A flock of fish: locking functions for the fish-shell
As I was hacking away at a shell-script the other day, I realised the script should prevent multiple instances from running at the same time. In my case, I wanted a simple egg-timer on the command line that runs for X minutes, shows a notification and beeps annoyingly, then exits. Obviously, there’s no point in having multiple timers stack up, so there should only ever be one ticking away.
There are multiple other reasons you may only want a single instance running: a long-running backup-task should complete before the next iteration starts, or all writes to a file should be synced to disk before modifying the file again.
Enter flock
Luckily, most Unixes come with the
flock(2) syscall,
conveniently wrapped by
flock(1) provided by
the util-linux package. flock
manages locks from shell scripts, allowing us
to express the notion of critical sections in our scripts: while the lock is
held by another process, the critical section cannot be entered. A “lock” in
this case is simply a file or directory on the filesystem.
This is the basic invocation:
# flock /tmp/timer.lock sleep 10m
In this basic form, flock
will…
- create
/tmp/timer.lock
if it doesn’t exist, - acquire a lock on this file,
- execute the specified command,
sleep 10m
This will acquire an exclusive lock on /tmp/timer.lock
, meaning only one
process may hold the lock at any given time1. If the lock is being held, flock
will wait until it is free again; this is called a “blocking wait”.
Don’t block the lock
While this blocking and waiting is all nice and dandy, I want my script to fail
if the lock is being held. flock
has us covered:
-n, --nb, --nonblock
Fail rather than wait if the lock cannot be immediately acquired.
See the -E option for the exit status used.
Putting it all together
Armed with all of this info, I perused the manpage a little more and stumbled upon this gem:
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
If the variable FLOCKER
is not set to the name of the current script, it is
explicitly set, and flock
re-executes the script (identified by $0
) with
all arguments ($@
), using the script itself as a lock. If you think about it,
this is pretty nifty, as it doesn’t clutter the filesystem with useless
lock-files!
Teach the script to fish
Anyway, this bash oneliner won’t work in fish, so some translation is necessary. In the end, I came up with this:
function timer
set numargs $(count $argv)
if test $numargs -eq 0
echo "missing duration: 60s, 10m, 1h"
else
set FILE (status filename)
if test "{$FLOCKER}" != "{$FILE}"
set me fish -c "timer $argv"
env FLOCKER=$FILE flock -n $FILE $me || echo "Timer already running"
return
end
sleep {$argv[1]}
notify-send -t 6000 "$argv[1] over!"
paplay /usr/share/sounds/freedesktop/stereo/bell.oga
end
end
Some argument handling first, then a transliteration from bash to fish
beginning at line 7. One thing to note here is that I’ve got this thing as an
autoloaded function
because I didn’t want to mess around with $PATH
and wellwhynotafterall.
Therefore, line 9 has fish -c "timer $argv"
rather than fish -c "$FILE $argv"
, because the new instance of fish knows that timer
is an
autofunction, but doesn’t know what to do with the file itself2.
Wrapping up
This post got longer than I thought, but I learned a couple of things about
flock
, and yet more stuff about fish functions and syntax. If we learned
anything, then it’s that we don’t need to futz around creating, testing, and
removing files by hand to implement lockfiles: just use flock
and be merry :)
-
flock
also supports shared locks, which I haven’t completely grokked yet. From what I understand, a shared lock can be held by multiple processes, iff all are acquired shared; attempting to acquire a held shared lock in exclusive-mode will fail. However, I believe the semantics of when to share and when to exclude is up to the programmer and is not enforced in any way by the command itself. ↩︎ -
This is incredibly handwavy, because this post is getting too long already. Suffice to say, you can just as well put your logic in a file with a shebang and have fish execute that. ↩︎
Go to top