From 079bd0ee0799ccdc105649ac848399c0bffeb9bd Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 10 Aug 2021 22:58:31 -0500 Subject: [PATCH] Implement locking --- src/g.bash | 183 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 105 insertions(+), 78 deletions(-) diff --git a/src/g.bash b/src/g.bash index 99bb642..5b5e57a 100644 --- a/src/g.bash +++ b/src/g.bash @@ -66,9 +66,12 @@ g__APP_LAST_ALIAS='' g__LOCKING_FILE_DIR="/tmp" -g__LOCKING_NAME_X_DESCRIPTOR=() +g__LOCKING_HOLDS=() + +g__LOCKING_INTERVAL=0.25 + +g__DEBUG_EVAL=yes -g__LOCKING_NEXT_DESCRIPTOR=100 ## @@ -106,6 +109,18 @@ function g::internals::cleanup() { ## ====================== UTILITIES ====================== ## +function g::eval() { + if [[ "$g__DEBUG_EVAL" == yes ]]; then + g::log::stderr "eval: $*" + fi + + eval "$@" +} + +function g::silence() { + "$@" > /dev/null 2>&1 +} + # # Check if an input value is numeric. (Supports decimals.) # @@ -147,6 +162,20 @@ function g::arr::includes() { return 1 } +function g::arr::assoc::hasKey() { + local key="$1" + shift + local array="$@" + + for possibleKey in "${!array[@]}"; do + if [[ "$key" == "$possibleKey" ]]; then + return $(g::util::true) + fi + done + + return $(g::util::false) +} + ## # Join an array of values by some delimiter. # @@ -187,7 +216,7 @@ function g::util::false() { function g::util::returnToEcho() { local cmd="$@" - if eval "$cmd"; then + if g::eval "$cmd"; then echo 1 else echo 0 @@ -475,7 +504,7 @@ function g::str::quote() { function g::str::quote::eval() { local cmd="$@" - local str="$(eval "$cmd")" + local str="$(g::eval "$cmd")" echo "$(g::str::quote "$str")" } @@ -612,10 +641,25 @@ function g::str::indexOf() { fi done - printf 'none' return $(g::util::false) } +function g::str::replace() { + local string="$1" + local find="$2" + local replaceWith="${3:-}" + + printf "${string//${find}/${replaceWith}}" +} + +function g::str::replace::once() { + local string="$1" + local find="$2" + local replaceWith="${3:-}" + + printf "${string/${find}/${replaceWith}}" +} + @@ -810,7 +854,7 @@ function g::log::write() { if $g__LOGGING_TO_STDOUT; then g::log::stdout "$output" fi - + if $g__LOGGING_TO_STDERR; then g::log::stderr "$output" fi @@ -1430,12 +1474,12 @@ function g::app::set() { local resolved="$(g::app::var "$varname")" local escaped="$(g::util::escape "$value")" g::internal "[g::app::set] $resolved" g::app::vars - eval "export ${resolved}=${escaped}" + g::eval "export ${resolved}=${escaped}" } function g::app::has() { local varname="$1" - + local resolved="$(g::app::var "$varname")" if [[ -v "$resolved" ]]; then g::internal "[g::app::has] $resolved - YES" g::app::vars @@ -1460,7 +1504,7 @@ function g::app::command::exists() { local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" - if g::arr::includes "$name" $(eval "echo \"\${$cmdsVar[@]}\""); then + if g::arr::includes "$name" $(g::eval "echo \"\${$cmdsVar[@]}\""); then return $(g::util::true) fi @@ -1482,10 +1526,10 @@ function g::app::command() { local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" if ! [[ -v $cmdsVar ]]; then - eval "export ${cmdsVar}=()" + g::eval "export ${cmdsVar}=()" fi - eval "${cmdsVar}+=(\"$name\")" + g::eval "${cmdsVar}+=(\"$name\")" g::app::set "$(g::app::command::var DESCRIPTION)" "$description" } @@ -1843,7 +1887,7 @@ function g::app::usage() { local commandsString="" local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" - for cmdName in $(eval "echo \"\${$cmdsVar[@]}\""); do + for cmdName in $(g::eval "echo \"\${$cmdsVar[@]}\""); do local cmdDescVar="$(g::app::command::var DESCRIPTION "${cmdName}")" local cmdDescription="$(g::app::get "$cmdDescVar")" cmdString=" ${cmdName}: ${cmdDescription}" @@ -1871,7 +1915,7 @@ function g::app::invoke() { fi local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" - for registeredCommandName in $(eval "echo \"\${$cmdsVar[@]}\""); do + for registeredCommandName in $(g::eval "echo \"\${$cmdsVar[@]}\""); do if [[ "$registeredCommandName" == "$commandName" ]]; then g::internal "Matched command: $registeredCommandName" @@ -1902,11 +1946,11 @@ function g::app::invoke() { function g::args::buildAlias() { local uuid="$1" - alias "g::args::alias::${uuid}"="g::args "$uuid"" - alias "g::args::alias::${uuid}::arg"="g::arg "$uuid"" - alias "g::args::alias::${uuid}::arg::has"="g::arg:has "$uuid"" - alias "g::args::alias::${uuid}::flag"="g::flag "$uuid"" - alias "g::args::alias::${uuid}::flag::has"="g::flag:has "$uuid"" + alias g::args::alias::${uuid}="g::args \"$uuid\"" + alias g::args::alias::${uuid}::arg="g::arg \"$uuid\"" + alias g::args::alias::${uuid}::arg::has="g::arg:has \"$uuid\"" + alias g::args::alias::${uuid}::flag="g::flag \"$uuid\"" + alias g::args::alias::${uuid}::flag::has="g::flag:has \"$uuid\"" g__APP_LAST_ALIAS="g::args::alias::${uuid}" } @@ -1991,82 +2035,69 @@ function g::flag::has() { } -g::log::all # TODO remove -function g::lock::file() { - local callerName="$(basename "$g__CALLER_PATH")" - local name="${1:-$callerName}" - g::internal "Resolving lock file path for name: $name" g::lock - g::path::resolve "$g__LOCKING_FILE_DIR" "${name}.lock" -} +function g::lock::try() { + local lockName="$1" -function g::arr::assoc::hasKey() { - local key="$1" - shift - local array="$@" + if g::lock::holds "$lockName"; then + return $(g::util::true) + fi - for possibleKey in "${!array[@]}"; do - if [[ "$key" == "$possibleKey" ]]; then - return $(g::util::true) - fi - done + local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")" + local tempFile="$(mktemp --suffix .lock)" + if g::silence ln "$tempFile" "$lockFile"; then + g__LOCKING_HOLDS+=("$lockName") + return $(g::util::true) + fi return $(g::util::false) } -function g::lock::descriptor() { - local callerName="$(basename "$g__CALLER_PATH")" - local name="${1:-$callerName}" - local file="$(g::lock::file "$name")" - - g::internal "Resolving lock file descriptor for file: $file" g::lock - if g::arr::assoc::hasKey "$name" "${g__LOCKING_NAME_X_DESCRIPTOR[@]}"; then - printf $fd - return $(g::util::true) - fi - - local fd=$g__LOCKING_NEXT_DESCRIPTOR - g__LOCKING_NEXT_DESCRIPTOR=$(($g__LOCKING_NEXT_DESCRIPTOR + 1)) - g__LOCKING_NAME_X_DESCRIPTOR+=( ["$name"]=$fd ) - - g::internal "Mapped FD $fd to lock file: $file" g::lock +function g::lock::acquire() { + local lockName="$1" + while ! g::lock::try "$lockName"; do + sleep "$g__LOCKING_INTERVAL" + done +} - exec $fd>"$file" || return $(g::util::false) - printf $fd +function g::lock::holds() { + local lockName="$1" + return $(g::arr::includes "$lockName" "${g__LOCKING_HOLDS[@]}") } -function g::lock::acquire() { - local callerName="$(basename "$g__CALLER_PATH")" - local name="${1:-$callerName}" - local descriptor=$(g::lock::descriptor "$name") +function g::lock::release() { + local lockName="$1" - flock $descriptor || g::error::throw "Unable to acquire lock: ${name} (${descriptor})" -} + if ! g::lock::holds "$lockName"; then + g::error::throw "Cannot release lock '$lockName': not held" + fi -function g::lock::try() { - local callerName="$(basename "$g__CALLER_PATH")" - local name="${1:-$callerName}" - local descriptor=$(g::lock::descriptor "$name") + local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")" + rm -f "$lockFile" - g::internal "Got descriptor for lock: ${descriptor}" + g__LOCKING_HOLDS=("${g__LOCKING_HOLDS[@]/$lockName}") +} - flock -n $descriptor || return $(g::util::false) - return $(g::util::true) +function g::lock::cleanupForExit() { + for lockFile in "${g__LOCKING_HOLDS[@]}"; do + g::internal "Releasing lockfile: $lockFile" + rm -f "$lockFile" + done } -function g::lock::release() { - local callerName="$(basename "$g__CALLER_PATH")" - local name="${1:-$callerName}" - local file=$(g::lock::file "$name") +function g::lock::singleton() { + if ! g::lock::try "$(g::lock::uniqueCallerName)"; then + g::error::throw "Another instance of this script is already running." + fi +} - rm -rf "$file" +function g::lock::singleton::acquire() { + g::lock::acquire "$(g::lock::uniqueCallerName)" } -function g::lock::cleanupForExit() { - for file in "${!g__LOCKING_NAME_X_DESCRIPTOR[@]}"; do - rm -rf "$file" - done +function g::lock::uniqueCallerName() { + g::str::replace "$g__CALLER_PATH" '/' '__' } # dependency declarations @@ -2079,9 +2110,5 @@ function g::lock::cleanupForExit() { # sendmail support -# locking - -# application definition - # Bootstrap the framework g::internals::bootstrap