#!/bin/bash

#----------------------
## @Author : Benoit PAPILLAULT <benoit.papillault@free.fr>
## @Creation : 29/10/2003
## This source code is based on the fact that mkdir will create a
## directory atomically. If two processes try to create a dir, one and only one
## will succeed immediately
## when interrupting the current process, we need to unlock all current
## locks. Therefor, we need a list of locks for the current
## process. And we need to update this list at the same time we got the
## lock.
## liblock is used to lock and unlock resources. It should be deadlock
## free, and clean up locks after processes that had locks die without
## unlocking the files. It uses the directory $LOCK_DIR defined in
## /etc/sorcery/config
#-----------------------

# global options for external users; sorcery defines these in its own config
[ -z "$LOCK_DIR"  ] && LOCK_DIR=/tmp/lockdir
[ -z "$MAX_SLEEP" ] && MAX_SLEEP=2

#---------------------------------------------------------------------
## lock_resources
## @param Type of resource
## @param Name of resource
## <pre>
## Type of resource | Name of resource | Usage
##     cast         | spell name       | when casting a spell
##     file         | file name        | to lock a file
##     summon       | spell name       | when downloading a spell
##     network      | network          | ?
##     solo         | resource name    | when using a resource exclusively
##     libgrimoire  | install          | when a spell is in the install stage
## </pre>
## locking solo will not cause these routines to behave any different.
## catching it has to be done externally (see cast for an example)
##
## this function locks the specified resource. this is a blocking
## function that can take an indeterminate period of time to acquire
## the lock. If no other processes have locked the resource, this
## function will return immediately
#---------------------------------------------------------------------
lock_resources()
{
#    if [ $# -ne 2 ]; then
#        echo "usage: lock_resources type_of_resource name_of_resource"
#        exit -1
#    fi

    while ! trylock_resources "$@";
    do
      local SLEEP=$(( RANDOM % MAX_SLEEP ))
      debug "lock_resources" "process $$ is sleeping ${SLEEP} seconds"
      sleep "${SLEEP}"
    done
}

#---------------------------------------------------------------------
## trylock_resources
## @param Type of resource
## @param Name of resource
##
## this function tries to lock the specified resource. On success, it
## immediately returns 0 (true). On failure (the lock cannot be
## acquired), it returns 1 (false).
#---------------------------------------------------------------------
trylock_resources()
{
#    if [ $# -ne 2 ]; then
#        echo "usage: trylock_resources type_of_resource name_of_resource"
#        exit -1
#    fi

    local lockfile="$1.$2"
    # we replace / with ^
    lockfile="${LOCK_DIR}/${lockfile//\//^}"

    debug "trylock_resources" "lockfile=${lockfile}"

    [ -e "$lockfile/$$" ] && return

    mkdir -p "$LOCK_DIR"
    if ! mkdir "${lockfile}" 2>/dev/null; then
      # we try to remove stale locks here and try again
      sleep 0.1
      global_clean_resources
      mkdir "${lockfile}" 2>/dev/null || return
    fi
    ln -sf /proc/$$ "${lockfile}/$$"
    return 0
}

#---------------------------------------------------------------------
## excllock_resources
## @param Type of resource
## @param Name of resource
##
## this function works like lock_resources except that it waits for
## all resources of the given type to unlock before locking.
## This will not stop any other lock from being created, that has to
## be done in the code
#---------------------------------------------------------------------
excllock_resources()
{
#  if [ $# -ne 2 ]; then
#    echo "usage: excllock_resources type_of_resource name_of_resource"
#    exit -1
#  fi

  while find $LOCK_DIR -mindepth 1 -maxdepth 1 -name "$1.*" | grep -q '';
  do
    #^^^ look if a lock exists of the given type (ugly code)
    global_clean_resources
    local SLEEP=$(( RANDOM % MAX_SLEEP ))
    sleep "$SLEEP"
  done
  #theoretically, someone could create a lock now... tiny race
  trylock_resources "$@"
}

#---------------------------------------------------------------------
## unlock_resources
## @param Type of resource
## @param Name of resource
##
## this function unlocks the specified resource and returns immediately
## if the specified resource is :
##  - unlocked : nothing is done
##  - locked by this process : it's unlocked
##  - locked by another process : it's kept locked.
## In all cases and for compatibility reasons with the old liblock, we
## return true
#---------------------------------------------------------------------
unlock_resources()
{
#    if [ $# -ne 2 ]; then
#        echo "usage: unlock_resources type_of_resource name_of_resource"
#        exit -1
#    fi

    local lockfile="$1.$2"
    lockfile="${LOCK_DIR}/${lockfile//\//^}"

    rm "${lockfile}/$$" 2>/dev/null &&
    rmdir "${lockfile}" 2>/dev/null

    return 0
}

#---------------------------------------------------------------------
## clean_resources
##
## this function removes all locks acquired by the current process.
## you can call this function at the end of your script to check for
## missing call to unlock_resources
#---------------------------------------------------------------------
clean_resources()
{
# we remove locks owned by this process. there is no guarantee since
# locks that have been acquired, but where no pidfile have been
# created will not be released.

  if [ -d "${LOCK_DIR}" ]; then
    find "${LOCK_DIR}" -maxdepth 2 -mindepth 2 -name $$ | \
    while read file; do
      debug "clean_resources" "removing forgotten lock ${file}"
      rm -f "${file}" 2>/dev/null &&
      rmdir $(smgl_dirname "${file}") 2>/dev/null
    done
  fi
}

#---------------------------------------------------------------------
## global_clean_resources
##
## this function is used internaly to remove all stale locks.
#---------------------------------------------------------------------
global_clean_resources()
{
  [ -d "$LOCK_DIR" ] || return 0

  # Release locks from dead processes
  find -L "$LOCK_DIR" -mindepth 2 -maxdepth 2 \
       -type l -print -exec rm -f {} + |
  debug_stream global_clean_resources 'removing stale lock'
  # Try to clean up empty locks
  2>/dev/null \
   find "$LOCK_DIR" -d -mindepth 1 -maxdepth 1 -type d \
   -exec rmdir {} +
}

#---------------------------------------------------------------------
## @param Name of file to lock
## @param <file> ... (optional)
##
## Locks files for access to only this PID. Will cause attempts by
## other processes that want to lock the file(s) to block until this
## PID unlocks the file, or this PID dies.
## It cannot prevent bad programs from modifying the file w/o
## permission.
## Note: Files with funky chars may break things. No colons or stuff.
## @Blocking
##
#---------------------------------------------------------------------
lock_file()
{
  debug "liblock" "$FUNCNAME - $*"
  lock_resources file "$1"
}


#---------------------------------------------------------------------
## @param Name of file to unlock
## @param <file> ... (optional)
##
## Unlocks a file so that another process can lock and modify it.
##
#---------------------------------------------------------------------
unlock_file()
{
  debug "liblock" "$FUNCNAME - $*"
  unlock_resources file "$1"
}

#---------------------------------------------------------------------
## @param File to start transaction on
## @param <file> ... (optional)
## @param variable to set (optional)
##
## A transaction locks a file and ensures that changes made to the
## file are all made at once. No changes are made to the file until
## the transaction is commited. Furthermore, a transaction can be
## killed before it is commited, ending the transaction and not
## making any changes to the files. All files are locked at once.
## If not all can be locked, they will be unlocked, and another try
## will be made.
##
## @Stdout The name of a temporary file that should be written to instead of the real file.
## @return 0 If successful
## @return 1 If not successful
#---------------------------------------------------------------------
lock_start_transaction()
{
  debug "liblock" "$FUNCNAME - $*"
  local NUMTRANS transaction_file file="$1"

  if lock_file "$file" ; then

    lock_file $LOCK_TRANSACTIONS
    [ -s $LOCK_TRANSACTIONS ] || echo "0:::" > $LOCK_TRANSACTIONS

    NUMTRANS=$(tail -n 1 $LOCK_TRANSACTIONS | cut -d : -f 1)
    NUMTRANS=${NUMTRANS:-1} # TODO: check if this should fallback to 0

    let NUMTRANS++
    transaction_file="$LOCK_TRANSACTIONS.$NUMTRANS"
    # cp file to temp file or if doesn't exist, create an empty file.
    if [[ -e $file ]]; then
      cp "$file" "$transaction_file"
    else
      echo -n "" > "$transaction_file"
    fi
    echo "$NUMTRANS:$file:$$" >> $LOCK_TRANSACTIONS

    unlock_file $LOCK_TRANSACTIONS
    if [[ $2 ]]; then
      eval "$2=\"\$transaction_file\""
    else
      echo "$transaction_file"
    fi
    return 0

  else
    return 1
  fi
}

#---------------------------------------------------------------------
## @param File to commit transaction of
## @param <file> ... (optional)
##
## Commits the changes made to the files as atomicaly as possible
## Before this func is called, the files remain unchanged.
##
#---------------------------------------------------------------------
lock_commit_transaction()
{
  debug "liblock" "$FUNCNAME - $*"
  local TMP_FILE unlockList=""

  lock_file $LOCK_TRANSACTIONS
  for i in $* ; do
    TMP_FILE=${LOCK_TRANSACTIONS}.$(grep ".*:$i:$$" $LOCK_TRANSACTIONS | cut -d : -f 1)
    [ -e $TMP_FILE ] && cp $TMP_FILE $i
    if [ $? -ne 0 ] ; then
      message "${PROBLEM_COLOR}Transaction commit failed on $i."
      message "${DEFAULT_COLOR}The modified file is stored in $TMP_FILE."
      message "This normally happens when the disk runs out of space."
      if ! query "Do you wish to continue?" "n" ; then
        exit 1
      fi
    else
      rm $TMP_FILE
    fi
    unlock_file $i
    cp $LOCK_TRANSACTIONS $LOCK_TRANSACTIONS.new
    if [ $? -ne 0 ] ; then
      message "${PROBLEM_COLOR}Transaction commit failed on $LOCK_TRANSACTIONS."
      message "${DEFAULT_COLOR}"
      message "This normally happens when the disk runs out of space."
      if ! query "Do you wish to continue?" "n" ; then
        exit 1
      fi
    fi
    grep -v ".*:$i:$$" $LOCK_TRANSACTIONS.new >$LOCK_TRANSACTIONS &&
    if ! rm $LOCK_TRANSACTIONS.new; then
      message "${PROBLEM_COLOR}Very odd case. Quitting."
      exit 1
    fi
  done
  unlock_file $LOCK_TRANSACTIONS

}

#---------------------------------------------------------------------
## @param File to stop the transaction of
## @param file ... (optional)
##
## Stops a transaction. Causes the changes to the file(s) to be
## ignored/undone.
##
#---------------------------------------------------------------------
lock_kill_transaction()
{
  debug "liblock" "$FUNCNAME - $*"
  local TMP_FILE string

  lock_file $LOCK_TRANSACTIONS
  for i in $* ; do
    esc_str ".*:$i:$$" string
    TMP_FILE=$(grep "$string" $LOCK_TRANSACTIONS | cut -d : -f 1)
    unlock_file $i
    cp $LOCK_TRANSACTIONS $LOCK_TRANSACTIONS.1
    grep -v "$string" $LOCK_TRANSACTIONS.1 >$LOCK_TRANSACTIONS
  done
  unlock_file $LOCK_TRANSACTIONS
}

#---------------------------------------------------------------------
## Increment a counter
##
## @param Type of resource
## @param Name of resource
##
## Processes should call counter_down when done with the resource
## however counter_down and counter_check will clean up after misbehaving
## processes.
#---------------------------------------------------------------------
counter_up() {
  local counterfile="$1.$2"
  # we replace / with ^
  counterfile="${LOCK_DIR}/${counterfile//\//^}"

  touch $counterfile
  lock_file $counterfile
  echo $$ > $counterfile
  unlock_file $counterfile
}
#---------------------------------------------------------------------
## Decrement a counter
##
## @param Type of resource
## @param Name of resource
#---------------------------------------------------------------------
counter_down() {
  local counterfile="$1.$2"
  local foo each
  # we replace / with ^
  counterfile="${LOCK_DIR}/${counterfile//\//^}"

  touch $counterfile
  local tc
  lock_start_transaction "$counterfile" tc
  for each in $(counter_clean $tc); do
    [[ $foo ]] && echo $each && continue
    if [[ $each == $$ ]] ; then foo=found ; else echo $each; fi
  done > $tc
  lock_commit_transaction $counterfile
}
#---------------------------------------------------------------------
##
## Destroy a counter
##
## @param Type of resource
## @param Name of resource
##
#---------------------------------------------------------------------
counter_reset() {
  local counterfile="$1.$2"
  local foo each
  # we replace / with ^
  counterfile="${LOCK_DIR}/${counterfile//\//^}"
  lock_file $counterfile
  rm "${LOCK_DIR}/${counterfile//\//^}"
  unlock_file $counterfile
}

#---------------------------------------------------------------------
##
## Check the value of a counter
##
## @param Type of resource
## @param Name of resource
##
## Also cleans stale items
##
#---------------------------------------------------------------------
counter_check() {
  local counterfile="$1.$2"
  # we replace / with ^
  counterfile="${LOCK_DIR}/${counterfile//\//^}"
  if test -f $counterfile ; then
    local tc
    lock_start_transaction "$counterfile" tc
    # sort saves a temp file since it has to read all the input
    counter_clean $tc |sort > $tc
    lock_commit_transaction $counterfile
    cat $counterfile|wc -l
  else
    echo 0
  fi
}

#---------------------------------------------------------------------
##
## @Internal
## @param counter file
##
## Print out the file with non-existent pids removed
## Caller must hold a lock on the file
##
#---------------------------------------------------------------------
counter_clean() {
  for each in $(cat $1) ; do
    ps -A|grep -q $each && echo $each
  done
}

#---------------------------------------------------------------------
##=item SYNCHRONIZE
##
## Prevents more than one process from entering a section of code
## at a time
## @NOTE This assumes that two functions of the same name will
## @NOTE not havethe SYNCHRONIZE command on the same line in different
## @NOTE files.
## <pre>
## Example:
## #!/bin/bash
## echo "Script started with PID=$$
## eval "$SYNCHRONIZE" && {
## echo "PID $$ is in the synched section of code."
## sleep 10
## } && eval "$UNSYNCHRONIZE"
## echo "PID $$ done."
## </pre>
## Note: SYNCHRONIZE and UNSYNCHRONIZE must be in the same local
## scope. Nested SYNCHs are not allowed in the same local scopes.
## Local scope usualy being a function.
## @Blocking
##
#---------------------------------------------------------------------
SYNCHRONIZE='
  __llSYNCH_LINE=$LINENO
  debug "liblock" "+++ in synch code"
  lock_resources "lockfunction" "${FUNCNAME}/${__llSYNCH_LINE}"'

#---------------------------------------------------------------------
##=item UNSYNCHRONIZE
##
## Terminates a section of SYNCHRONIZED code
##
#---------------------------------------------------------------------
UNSYNCHRONIZE='
  debug "liblock" "+++ in unsynch code"
  unlock_resources "lockfunction" "${FUNCNAME}/${__llSYNCH_LINE}"
  unset __llSYNCH_LINE'
