# -*- mode: sh; sh-shell: bash -*-
# vim: set ft=bash:
# shellcheck shell=bash
### Library for managing lockfiles
declare -gA LOCK_REC=()
function lock_wait ## <lockname> - Waits until we have the lock on a lockfile.
{
## '.lock' is automatically appended to the 'lockname'.
## These locks are recursive the same process can lock_wait on the same lockfile multiple
## times which must be paired with the same numbers of 'lock_remove' to unlock it.
# shellcheck disable=SC2155
local lockfile="$(realpath -s "$1.lock")"
if (( LOCK_REC["$lockfile"] > 0 )); then
(( LOCK_REC["$lockfile"]+=1 ))
trace "recursive lock: $1 as $BASHPID"
return 0
fi
# Create a temporary lockfile specific to this process
local tmp_lockfile="${1}.$BASHPID.lock"
# Put our PID in the temporary file
echo -n $BASHPID > "$tmp_lockfile"
# backoff with a binomial sequence up to 100ms
declare -i backoff=1
# Try to atomically acquire the lock
while ! ln -s "$tmp_lockfile" "$lockfile" 2>/dev/null; do
# Lock exists, check if it's stale
if [[ -L "$lockfile" ]]; then
# shellcheck disable=SC2155 # want to ignore errors here
local lockfile_target=$(readlink "$lockfile")
local lockpid="${lockfile_target%.lock}"
lockpid="${lockpid##*.}"
# Check if the process still exists
if kill -0 "$lockpid" 2>/dev/null; then
# Wait for the process to exit
trace "wait: $lockpid to complete"
wait "$lockpid" 2>/dev/null || sleep "${backoff:: -1}.${backoff: -1}"
(( backoff < 100 )) && (( backoff+=1 ))
else
rm -f "$lockfile"
continue
fi
fi
done
# remove stale tmp_lockfiles
local file
for file in "$1."*".lock"; do
local lockpid="${file%.lock}"
lockpid="${lockpid##*.}"
[[ "$lockpid" != "$BASHPID" && "$(cat "$file")" = "$lockpid" ]] &&
! kill -0 "$lockpid" 2>/dev/null &&
rm -f "$file"
done
LOCK_REC["$lockfile"]=1
trace "locked: $1 as $BASHPID"
}
function lock_try_norec ## <lockname> - try to lock a lockfile non recursively.
{
## Will not wait, fails when we already have the lock
# shellcheck disable=SC2155
local lockfile="$(realpath -s "$1.lock")"
if (( LOCK_REC["$lockfile"] > 0 )); then
return 1
fi
# Create a temporary lockfile specific to this process
local tmp_lockfile="${1}.$BASHPID.lock"
# Put our PID in the temporary file
echo $BASHPID > "$tmp_lockfile"
# Try to atomically acquire the lock
while ! ln -s "$tmp_lockfile" "$lockfile" 2>/dev/null; do
# Lock exists, check if it's stale
if [[ -L "$lockfile" ]]; then
# shellcheck disable=SC2155 # want to ignore errors here
local lockfile_target=$(readlink "$lockfile")
local lockpid="${lockfile_target%.lock}"
lockpid="${lockpid##*.}"
# Check if the process still exists
if kill -0 "$lockpid" 2>/dev/null; then
return 1
else
rm -f "$lockfile"
continue
fi
fi
done
LOCK_REC["$lockfile"]=1
trace "locked: $1 as $BASHPID"
}
function lock_next # <old> <new> - hand over hand locking
{
## Locks 'new' and then removes the 'old' locck.
lock_wait "$2"
lock_remove "$1"
}
function lock_send ## <lock> <who> - Sends a 'lock' to another process with pid 'who'.
{
## Locks can be send atomically to another process. 'lock_send' registers the lock to be
## send to a process, the lock will be send at the final lock_remove in the sending
## process when all recursive locks are freed. Note that to obtain the 'who' pid the
## receiver has to be started first.
# shellcheck disable=SC2155
local lockfile="$(realpath -s "$1.lock")"
(( LOCK_REC["$lockfile"] > 0 )) || die "lock $1 is not locked by $BASHPID"
# shellcheck disable=SC2155
local send_to=$(<"$lockfile")
[[ "$send_to" != "$BASHPID" ]] && {
error "lock $1 is already send to $send_to"
return 1
}
echo -n "$2" > "$lockfile"
}
function lock_receive ## <lock> - Wait for lock send by another process.
{
## Blocks until the the sender removed all uses of 'lock',
# shellcheck disable=SC2155
local lockfile="$(realpath -s "$1.lock")"
# Create a temporary lockfile specific to this process
local tmp_lockfile="${1}.$BASHPID.lock"
# Put our PID in the temporary file
echo $BASHPID > "$tmp_lockfile"
# backoff with a binomial sequence up to 100ms
declare -i backoff=1
while true; do
if [[ "$lockfile" -ef "$tmp_lockfile" ]]; then
trace "lock received: $1"
return 0
fi
# TODO: if sender still exists, else die
sleep "${backoff:: -1}.${backoff: -1}"
(( backoff < 100 )) && (( backoff+=1 ))
done
}
function lock_remove ## <lock> - Unlocks 'lock'.
{
## 'lock_remove' must be paired with the same numbers of earlier 'lock_wait'. Only the
## last 'lock_remove' will release the lockfile.
# shellcheck disable=SC2155
local lockfile="$(realpath -s "$1.lock")"
(( LOCK_REC["$lockfile"]-=1 ))
if (( LOCK_REC["$lockfile"] > 0 )); then
trace "recursive unlock $1"
return 0
fi
if [[ -L "$lockfile" ]]; then
unset "LOCK_REC[$lockfile]"
# shellcheck disable=SC2155 # want to ignore errors here
local lockfile_target=$(readlink "$lockfile")
local lockpid="${lockfile_target%.lock}"
lockpid="${lockpid##*.}"
if [[ "$lockpid" = "$BASHPID" ]]; then
# shellcheck disable=SC2155
local send_to=$(<"$lockfile_target")
if [[ "$send_to" != "$BASHPID" ]]; then
trace "send lock $1 to $send_to"
local tmp_lockfile="${1}.$send_to.lock"
[[ -f "$tmp_lockfile" ]] || die "lock receiver $tmp_lockfile does not exist"
ln -sf "$tmp_lockfile" "$lockfile" 2>/dev/null
rm -f "$lockfile_target"
else
trace "unlock $1"
# Remove both the lock and the temporary file
rm -f "$lockfile"
rm -f "$lockfile_target"
fi
return 0
else
error "$lockfile is not ours (we are $BASHPID, owned by $lockpid)"
return 1
fi
elif [[ -f "$lockfile" ]]; then
error "$lockfile is not a symbolic link lockfile"
return 1
else
error "$lockfile does not exist"
return 1
fi
}