# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#               Do not run this script directly!
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# \version
#	pre-alpha
# \author
#	Gil Ran
# \date
#	2004-10-14
#
#GEN* This bash library provides a set of locking tools.
#GEN* The locking is done by creating a directory named `.lock'.
#GEN* After the master-lock named `.lock' is set, a proccess can
#GEN* create its .lock.$$ file, and remove the master-lock.
#GEN* Each non-safe action requires a master-lock.
#GEN* Locking, unlocking, cleaning leftover locks and cleaning
#GEN* left over spin-files are non-safe action and require
#GEN* a master lock.
#GEN* Creating or removing my own spin-file and creating a
#GEN* directory that should be locked are the only actions
#GEN* done on the directory, that are considered safe.

#EXPORT=dirInitLock dirTryLock dirLock dirUnlock dirDestroyLock
#REQUIRE=

prefix=/usr
__locks_LOCKS_DIR=/var/lock/dirlocks
__locks_DEFAULT_SLEEP_TIME=0.01

__locks_ERR_DIR_IS_LOCKED=1
__locks_ERR_NO_SPIN_FILE=2
__locks_ERR_COULD_NOT_RESOLVE_DIR=3

#__locks_DEBUG_MODE=1

#############################################################
######################   FUNCTIONS   ########################
#############################################################

#FUNC*
#FUNC*\subsection locks_findEffectiveDirname findEffectiveDirname <DirName>
#FUNC*	Finds the full path that we should lock, i.e. the effective
#FUNC*	dirname we should work on.
#FUNC*	This function does not change files (and therefore does not lock anything).
#FUNC*	\param DirName
#FUNC*		The name of the directory we wish to lock. You can choose
#FUNC*		any name if you want to
__locks_findEffectiveDirname()
{
	local DIR_NAME=$1

	# Check if it a file/dir exists
	if [ -e "$DIR_NAME" ] ; then 
		if [[ -d "$DIR_NAME" ]] ; then
			retval=$(cd "$DIR_NAME" > /dev/null 2>&1 && pwd) || \
					return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
		else
			# Get the perent directory for $DIR_NAME (full path)
			retval=$(cd $(dirname "$DIR_NAME") > /dev/null 2>&1 && pwd) || \
					return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
			# Add the basename of $DIR_NAME
			retval=$(ls -d $retval/$(basename "$DIR_NAME") 2>/dev/null) || \
					return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
		fi
		return 0
	else
		# It does not exist:
		#    Expand `\'
		#    Check that it does not contain `..' - it can be very dangerous
		while echo "$DIR_NAME" |grep -q '\\'; do DIR_NAME=$(echo "$DIR_NAME" | xargs echo); done
		echo "$DIR_NAME" | grep -q '\.\.' && return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
		retval="$DIR_NAME" && return 0
	fi

	# If we got here something's wrong
	return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
}

#FUNC*
#FUNC*\subsection locks_dirInitLock dirInitLock <DirName> [Spin]
#FUNC*	Initializes a lock for a DirName object, for current process
#FUNC*	
#FUNC*	\param DirName
#FUNC*		The name of the directory we wish to lock. You can choose
#FUNC*		any name if you want to - i.e It does not have to be a real dir
#FUNC*	\param Spin
#FUNC*		Spin period - we'll retry to aquire lock each Spin seconds.
#FUNC*		Spin may (and generally should) be less then 1. 
#FUNC*		Default Spin is 0.01 seconds.
#FUNC*	\return
#FUNC*		Return value:
#FUNC*		0 - The lock was initialized and is ready for use
#FUNC*		non-zero - The lock was not initialized
#FUNC*
dirInitLock()
{
	local DIR_NAME=$1
	local SPIN=${2:-${__locks_DEFAULT_SLEEP_TIME}}
	
	# Resolve effective directory name
	__locks_findEffectiveDirname "$DIR_NAME" || return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
	local EFFECTIVE_DIRNAME=$retval

	__locks_setMasterLock "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME"

	__locks_spinCleanup "$DIR_NAME"

	# Creating our spin-files - safe action
	echo $SPIN > "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/$$.spin"
	status=$?

	# Free the master lock
	rmdir "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/.lock"
	
	# Return the return value of the spin-file creation command
	return $status
}

#FUNC*
#FUNC*\subsection locks_dirTryLock dirTryLock <DirName>
#FUNC*	Tries to set a lock on a directory. If lock is 
#FUNC*	already set - returns immediately with error.
#FUNC*	\param DirName
#FUNC*		The name of the directory we wish to lock.
#FUNC*	\return
#FUNC*		The function exits with:
#FUNC*		0 - The directory was successfully locked
#FUNC*		1 - Lock was already set
#FUNC*		2 - The lock object is not initialized
#FUNC*
dirTryLock()
{
	local DIR_NAME=$1
	
	# Resolve effective directory name
	__locks_findEffectiveDirname "$DIR_NAME" || return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
	local EFFECTIVE_DIRNAME=$retval
	
	# Check if the directory-lock is initialized
	[[ -e "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/$$.spin" ]] || return ${__locks_ERR_NO_SPIN_FILE}
	
	__locks_setMasterLock "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME"
	
	__locks_deadlockCleanup "$DIR_NAME"

	# Check if there is a lock on the directory
	if ls -d "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME"/.lock.* > /dev/null 2>&1 ; then
		# Remove the master-lock
		rmdir "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/.lock"
		
		return ${__locks_ERR_DIR_IS_LOCKED}
	fi

	# Create a lock file for this proccess
	(umask 0 && touch "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/.lock.$$")

	# Remove the master-lock
	rmdir "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/.lock"
	
	# If we got here, everything worked just file.
	return 0
}

#FUNC*
#FUNC*\subsection locks_dirLock dirLock <DirName>
#FUNC*	Sets a lock on a directory.
#FUNC*	\param DirName
#FUNC*		The name of the directory we wish to lock.
#FUNC*	\return
#FUNC*		The function exits with:
#FUNC*		0 - The directory was successfully locked
#FUNC*		2 - The lock object was not initialized
#FUNC*
dirLock()
{
	local DIR_NAME=$1
	
	# Resolve effective directory name
	__locks_findEffectiveDirname "$DIR_NAME" || return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
	local EFFECTIVE_DIRNAME=$retval
	
	#  Loop trying to lock the directory
	while true ; do
		dirTryLock "$DIR_NAME"
		local status=$?

		# If the result of the TryLock is other than an error that says that the directory is locked, return it
		[[ $status != ${__locks_ERR_DIR_IS_LOCKED} ]] && return $status

		# The return value was the __locks_ERR_DIR_IS_LOCKED error. Sleep and try again.
		sleep $(cat "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/$$.spin" 2> /dev/null) > /dev/null 2>&1 || sleep ${__locks_DEFAULT_SLEEP_TIME}
	done
}

#FUNC*
#FUNC*\subsection locks_dirUnlock dirUnlock <DirName>
#FUNC*	Free a directory from its lock.
#FUNC*	\param DirName
#FUNC*		The name of the directory we wish to unlock.
#FUNC*	\return
#FUNC*		The function exits with:
#FUNC*		0 - The directory was successfully unlocked
#FUNC*		non-zero - Unlocking failed
#FUNC*
dirUnlock()
{
	local DIR_NAME=$1
	
	# Resolve effective directory name
	__locks_findEffectiveDirname "$DIR_NAME" || return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
	local EFFECTIVE_DIRNAME=$retval
	
	# Check if the directory-lock is initialized
	[[ -e "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/$$.spin" ]] || return ${__locks_ERR_NO_SPIN_FILE}
	
	__locks_setMasterLock "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME"

	__locks_deadlockCleanup "$DIR_NAME"

	# Remove my lock file
	rm -f "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/.lock.$$" > /dev/null 2>&1

	# Remove the master lock
	rmdir "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/.lock" > /dev/null 2>&1

	ls -la "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/".lock.* > /dev/null 2>&1 && return ${__locks_ERR_DIR_IS_LOCKED}
	
	return 0
}

dirDestroyLock()
{
	local DIR_NAME=$1
	
	# Resolve effective directory name
	__locks_findEffectiveDirname "$DIR_NAME" || return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
	local EFFECTIVE_DIRNAME=$retval
	
	# Make sure that the directory is not locked
	dirTryLock "$DIR_NAME"
	if [[ "$?" == "${__locks_ERR_DIR_IS_LOCKED}" ]] ; then
		# Check if the directory is locked by this proccess
		if [[ -e "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/.lock.$$" ]] ; then
			# We can't destroy our lock while it is locked!!!
			return ${__locks_ERR_DIR_IS_LOCKED}
		fi
		
		# The directory is locked by someone else. We remove our spin file,
		# and do not touch anything else.
		rm -f "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/$$.spin"
		return 0
	fi
	dirUnlock "$DIR_NAME"

	__locks_setMasterLock "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME"

	# Remove my spoin file
	rm -f "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/$$.spin"
	# Clean the locks directories tree of this lock, as possible
	# The first thing we clean is the master-lock.
	pushd "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME" > /dev/null
	local curr_subdir=".lock"
	while rmdir "$curr_subdir" 2>/dev/null ; do
		curr_subdir=$PWD
		[[ "$curr_subdir" == ${__locks_LOCKS_DIR} ]] && break

		cd ..

		__locks_spinCleanup "$PWD"
		__locks_deadlockCleanup "$PWD"

	done
	popd > /dev/null

	return 0
}

#FUNC*
#FUNC*\subsection locks_spinCleanup spinCleanup <DirName>
#FUNC*	Cleans all unnedded spinfiles from DirName.
#FUNC*	This function removes spin-files. 
#FUNC*	This function does not lock the directory, even when removing
#FUNC*	the files (non-safe action). That is because it is an internal
#FUNC*	function that is to be used only by this library.
#FUNC*	When maintaining this file notice that this is a function that
#FUNC*	does not protect you from doing nasty stuff.
#FUNC*	
#FUNC*	If this function will be used without locking first, it can
#FUNC*	cause problams. This is an internal function, so nothing like
#FUCN*	that should never happand. If it does, sorry, we do not suply
#FUNC*	`Stupid user protection'.
#FUNC*	
#FUNC*	\param DirName
#FUNC*		The name of the directory we wish to clean from spinfiles.
#FUNC*	\return
#FUNC*		The function exits with:
#FUNC*		0 - The directory was successfully unlocked
#FUNC*		non-zero - Unlocking failed
#FUNC*
__locks_spinCleanup()
{
	local DIR_NAME=$1
	
	# Resolve effective directory name
	__locks_findEffectiveDirname "$DIR_NAME" || return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
	local EFFECTIVE_DIRNAME=$retval
	
	# Clean dead proccesses spin files
	local spinfile
	for spinfile in $(ls "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME"/*.spin 2> /dev/null) ; do
		local pid=${spinfile%.spin}
		pid=${pid##*/}
		ps -ef | awk '{print $2}' | grep -q "^$pid$" || rm -f $spinfile
	done
}

__locks_deadlockCleanup()
{
	local DIR_NAME=$1
	
	# Resolve effective directory name
	__locks_findEffectiveDirname "$DIR_NAME" || return ${__locks_ERR_COULD_NOT_RESOLVE_DIR}
	local EFFECTIVE_DIRNAME=$retval
	
	# Clean dead proccesses spin files
	local lockfile
	for lockfile in $(ls "${__locks_LOCKS_DIR}/$EFFECTIVE_DIRNAME/".lock.* 2> /dev/null) ; do
		local pid=${lockfile##*/.lock.}
		if ! ps -ef | awk '{print $2}' | grep -q "^$pid$"  ; then
			rm -rf "$lockfile"
		fi
	done
}

__locks_setMasterLock()
{
	local DIR_FULL_PATH=$1

	local tries=0
	while [[ $tries -lt 50 ]] ; do
		# Creating the directory to lock
		mkdir -p "$DIR_FULL_PATH"
		# Try to set the master lock
		mkdir "$DIR_FULL_PATH/.lock" >/dev/null 2>&1 && return
		sleep 0.1
		let "tries++"
	done

	if [[ $tries -eq 50 ]] ; then
		rmdir "$DIR_FULL_PATH/.lock"
		sleep $(echo "" | awk '{srand(); print rand()}')
		__locks_setMasterLock "$DIR_FULL_PATH"
	fi
}

