I've recently had to create my own "flock" function as the script I'm writing needs to use TRAP
which can't be used along side the flock command.
Here is the code
#!/bin/bash
flock() {
local lock_name lock_path lock_pid check_pid script_arg script_source script_pid
lock_name="${1}"
script_pid="$$"
script_source="${BASH_SOURCE[0]}"
script_arg=("${BASH}" "${script_source}")
lock_path="$(dirname -- "$(realpath "${script_source[0]}}")")/${lock_name}_flock"
for ((i=0;i<${#BASH_ARGV[@]}; i++)); do
script_arg+=("${BASH_ARGV[~i]}")
done
if [[ -f "${lock_path}" ]]; then
read -r lock_pid < "${lock_path}" > /dev/null 2>&1
if [[ -n "${lock_pid[0]}" ]]; then
check_pid=$(ps -eo pid,cmd \
| awk -v a="${lock_pid}" -v b="${script_arg[*]}" '$1==a && $2!="awk" && index($0,b) {print $1}')
if [[ -n "${check_pid}" ]]; then
printf "%s\n" "Script is already running"
exit 0
fi
fi
fi
read -r check_pid < <(ps -eo pid,cmd | awk -v a="${script_arg[*]}" '$2!="awk" && index($0,a) {print $1}')
if [[ "${script_pid}" -ne "${check_pid[0]}" ]]; then
printf "%s\n" "Race condition prevented"
exit 0
fi
printf '%s' "${script_pid}" | tee "${lock_path}" > /dev/null 2>&1
}
To use this you just add the function followed by whatever you want the lock file to be called.
flock script_name
Explanation
The function uses BASH
, BASH_SOURCE
and BASH_ARGV
to create what the CMD column would look like under the process status.
Example: ./test.sh arg1 arg2 "this arg3"
CMD: /bin/bash ./test.sh arg1 arg2 this arg3
We then use the process status to only show us any item that matches the CMD, except we place all their PID's into an array.
Next we only return the first value of that array and compare it to the scripts PID, if the scripts PID does not match the first PID in our array, then then we exit, otherwise we store the scripts PID in our lock file.
If the lock file already exists and contains an existing PID, then the function will check to see if the script is still runnning by searching for the PID and the CMD values, if it's running then it will exit, otherwise the PID in the lock file will be updated to the new PID.
The function not only prevents race conditioning but will continue to work even if your system crashed as it doesn't rely on the lock file to determin whether or not the script is running.
Expands to the process ID of the shell. In a subshell, it expands to the process ID of the invoking shell, not the subshell.
An array variable whose members are the source filenames where the corresponding shell function names in the FUNCNAME array variable are defined. The shell function ${FUNCNAME[$i]} is defined in the file ${BASH_SOURCE[$i]} and called from ${BASH_SOURCE[$i+1]}
An array variable containing all of the parameters in the current bash execution call stack. The final parameter of the last subroutine call is at the top of the stack; the first parameter of the initial call is at the bottom. When a subroutine is executed, the parameters supplied are pushed onto BASH_ARGV. The shell sets BASH_ARGV only when in extended debugging mode (see The Shopt Builtin for a description of the extdebug option to the shopt builtin). Setting extdebug after the shell has started to execute a script, or referencing this variable when extdebug is not set, may result in inconsistent values.
BASH_ARGV
returns the arguments in reverse. In order to fix this I run the following code in the function thanks to a post from user232326 that works with bash 4.2+
for ((i=0;i<${#BASH_ARGV[@]}; i++)); do
script_arg+=("${BASH_ARGV[~i]}")
done