#!/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 that can be changed to suit your needs.
#Perhaps this should be moved to the config? (Duff, 04/07)

[ -z "$LOCK_DIR"  ] && LOCK_DIR=/tmp/lockdir
[ -z "$MAX_SLEEP" ] && MAX_SLEEP=2

#---------------------------------------------------------------------
## lock_resources
## @param Type of resource
## @param Name of resource
## 
## 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
## 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}"

    mkdir -p "${LOCK_DIR}"

    if ! mkdir "${lockfile}" 2>/dev/null; then
    # we try to remove stale locks here and try again
        global_clean_resources
        if ! mkdir "${lockfile}" 2>/dev/null; then
            return 1
        fi
    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 `dirname "${file}"` 2>/dev/null
    done
  fi
}

#---------------------------------------------------------------------
## global_clean_resources
##
## this function is used internaly to remove all stale locks.
#---------------------------------------------------------------------
global_clean_resources()
{
  if [ -d "${LOCK_DIR}" ]; then
    # we remove old locks (>1 minute) that are not owned by any process.
    find "${LOCK_DIR}" -maxdepth 1 -mindepth 1 -mmin +1 -type d -empty -exec rmdir {} \;

    # we remove locks that are owned by dead processes
    find "${LOCK_DIR}" -maxdepth 2 -mindepth 2 | \
    
		while read file; do
		  # check if the process still exist (we use procfs mounted on /proc)
      if [ ! -d "${file}" ]; then
          debug "global_clean_resources" "removing stale lock ${file}"
          rm -f "${file}" 2>/dev/null &&
          rmdir `dirname "${file}"` 2>/dev/null
      fi
    done
  fi
}

#---------------------------------------------------------------------
##	@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
##
#---------------------------------------------------------------------
function lock_file()
{
	debug "liblock" "$FUNCNAME - $*"
	local FILE
	lock_resources $( 
		for FILE in "$@" ; do
			echo file $FILE
		done
	)
}


#---------------------------------------------------------------------
##	@param Name of file to unlock
##	@param <file> ... (optional)
##
## Unlocks a file so that another process can lock and modify it.
##
#---------------------------------------------------------------------
function unlock_file()
{
	debug "liblock" "$FUNCNAME - $*"
	local FILE
	unlock_resources $(
		for FILE in "$@" ; do
			echo file $FILE
		done
	)
}

#---------------------------------------------------------------------
##	@param File to start transaction on
##	@param <file> ... (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.
##
##	@return The name of a temporary file that should be written to
## instead of the real file.
##
#---------------------------------------------------------------------
function lock_start_transaction()
{
  debug "liblock" "$FUNCNAME - $*"
  local i TRANSNAME NUMTRANS RET=""
  
  if lock_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}
    RET=()
    
    for i in $* ; do
      let NUMTRANS++
      # cp file to temp file, if doesn't exist, create empty file.
      if [ -e "$i" ]   ; then
        cp "$i" "$LOCK_TRANSACTIONS.$NUMTRANS" 
      else
        echo -n "" > $LOCK_TRANSACTIONS.$NUMTRANS
      fi
      echo "$NUMTRANS:$i:$$" >$LOCK_TRANSACTIONS
      RET[$NUMTRANS]="${LOCK_TRANSACTIONS}.${NUMTRANS}"
    done
    
    unlock_file $LOCK_TRANSACTIONS
    echo ${RET[*]}
    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.
##
#---------------------------------------------------------------------
function 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 normaly 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 normaly 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 &&
    { rm $LOCK_TRANSACTIONS.new ||
      message "${PROBLEM_COLOR}Very odd case. Quitting."; exit 1; }
  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.
##
#---------------------------------------------------------------------
function lock_kill_transaction()
{
  debug "liblock" "$FUNCNAME - $*"
  local TMP_FILE
  
  lock_file $LOCK_TRANSACTIONS
  for i in $* ; do
    TMP_FILE=$(grep `esc_str ".*:$i:$$"` $LOCK_TRANSACTIONS | cut -d : -f 1)
    unlock_file $i
    cp $LOCK_TRANSACTIONS $LOCK_TRANSACTIONS.1
    grep -v `esc_str ".*:$i:$$"` $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.
#---------------------------------------------------------------------
function 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
#---------------------------------------------------------------------
function counter_down() {
  local counterfile="$1.$2"
  local foo each
  # we replace / with ^
  counterfile="${LOCK_DIR}/${counterfile//\//^}"

  touch $counterfile
  local tc=$(lock_start_transaction $counterfile)
  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
##
#---------------------------------------------------------------------
function 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
##
#---------------------------------------------------------------------
function counter_check() {
  local counterfile="$1.$2"
  # we replace / with ^
  counterfile="${LOCK_DIR}/${counterfile//\//^}"
  if test -f $counterfile ; then
    local tc=$(lock_start_transaction $counterfile)
    # 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
##
#---------------------------------------------------------------------
function 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
## not havethe SYNCHRONIZE command on the same line in different
## files.
##
## 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."
##
## 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'

