/var/lib/sorcery/modules/liblock

     1	#!/bin/bash
     2	
     3	#----------------------
     4	## @Author : Benoit PAPILLAULT <benoit.papillault@free.fr>
     5	## @Creation : 29/10/2003
     6	## This source code is based on the fact that mkdir will create a
     7	## directory atomically. If two processes try to create a dir, one and only one
     8	## will succeed immediately
     9	## when interrupting the current process, we need to unlock all current
    10	## locks. Therefor, we need a list of locks for the current
    11	## process. And we need to update this list at the same time we got the
    12	## lock.
    13	## liblock is used to lock and unlock resources. It should be deadlock
    14	## free, and clean up locks after processes that had locks die without
    15	## unlocking the files. It uses the directory $LOCK_DIR defined in
    16	## /etc/sorcery/config
    17	#-----------------------
    18	
    19	# global options for external users; sorcery defines these in its own config
    20	[ -z "$LOCK_DIR"  ] && LOCK_DIR=/tmp/lockdir
    21	[ -z "$MAX_SLEEP" ] && MAX_SLEEP=2
    22	
    23	#---------------------------------------------------------------------
    24	## lock_resources
    25	## @param Type of resource
    26	## @param Name of resource
    27	## <pre>
    28	## Type of resource | Name of resource | Usage
    29	##     cast         | spell name       | when casting a spell
    30	##     file         | file name        | to lock a file
    31	##     summon       | spell name       | when downloading a spell
    32	##     network      | network          | ?
    33	##     solo         | resource name    | when using a resource exclusively
    34	##     libgrimoire  | install          | when a spell is in the install stage
    35	## </pre>
    36	## locking solo will not cause these routines to behave any different.
    37	## catching it has to be done externally (see cast for an example)
    38	##
    39	## this function locks the specified resource. this is a blocking
    40	## function that can take an indeterminate period of time to acquire
    41	## the lock. If no other processes have locked the resource, this
    42	## function will return immediately
    43	#---------------------------------------------------------------------
    44	lock_resources()
    45	{
    46	#    if [ $# -ne 2 ]; then
    47	#        echo "usage: lock_resources type_of_resource name_of_resource"
    48	#        exit -1
    49	#    fi
    50	
    51	    while ! trylock_resources "$@";
    52	    do
    53	      local SLEEP=$(( ${RANDOM} % ${MAX_SLEEP} ))
    54	      debug "lock_resources" "process $$ is sleeping ${SLEEP} seconds"
    55	      sleep "${SLEEP}"
    56	    done
    57	}
    58	
    59	#---------------------------------------------------------------------
    60	## trylock_resources
    61	## @param Type of resource
    62	## @param Name of resource
    63	##
    64	## this function tries to lock the specified resource. On success, it
    65	## immediately returns 0 (true). On failure (the lock cannot be
    66	## acquired), it returns 1 (false).
    67	#---------------------------------------------------------------------
    68	trylock_resources()
    69	{
    70	#    if [ $# -ne 2 ]; then
    71	#        echo "usage: trylock_resources type_of_resource name_of_resource"
    72	#        exit -1
    73	#    fi
    74	
    75	    local lockfile="$1.$2"
    76	    # we replace / with ^
    77	    lockfile="${LOCK_DIR}/${lockfile//\//^}"
    78	
    79	    debug "trylock_resources" "lockfile=${lockfile}"
    80	
    81	    [[ -d $LOCK_DIR ]] || mkdir -p "$LOCK_DIR"
    82	
    83	    if [[ -d ${lockfile} ]] || ! mkdir "${lockfile}" 2>/dev/null; then
    84	    # we try to remove stale locks here and try again
    85	        sleep 0.1
    86	        global_clean_resources
    87	        if ! mkdir "${lockfile}" 2>/dev/null; then
    88	            return 1
    89	        fi
    90	    fi
    91	    [[ -e ${lockfile}/$$ ]] || ln -s /proc/$$ "${lockfile}/$$"
    92	    return 0
    93	}
    94	
    95	#---------------------------------------------------------------------
    96	## excllock_resources
    97	## @param Type of resource
    98	## @param Name of resource
    99	##
   100	## this function works like lock_resources except that it waits for
   101	## all resources of the given type to unlock before locking.
   102	## This will not stop any other lock from being created, that has to
   103	## be done in the code
   104	#---------------------------------------------------------------------
   105	excllock_resources()
   106	{
   107	#  if [ $# -ne 2 ]; then
   108	#    echo "usage: excllock_resources type_of_resource name_of_resource"
   109	#    exit -1
   110	#  fi
   111	
   112	  while find $LOCK_DIR -mindepth 1 -maxdepth 1 -name "$1.*" | grep -q '';
   113	  do
   114	    #^^^ look if a lock exists of the given type (ugly code)
   115	    global_clean_resources
   116	    local SLEEP=$(( ${RANDOM} % ${MAX_SLEEP} ))
   117	    sleep "$SLEEP"
   118	  done
   119	  #theoretically, someone could create a lock now... tiny race
   120	  trylock_resources "$@"
   121	}
   122	
   123	#---------------------------------------------------------------------
   124	## unlock_resources
   125	## @param Type of resource
   126	## @param Name of resource
   127	##
   128	## this function unlocks the specified resource and returns immediately
   129	## if the specified resource is :
   130	##  - unlocked : nothing is done
   131	##  - locked by this process : it's unlocked
   132	##  - locked by another process : it's kept locked.
   133	## In all cases and for compatibility reasons with the old liblock, we
   134	## return true
   135	#---------------------------------------------------------------------
   136	unlock_resources()
   137	{
   138	#    if [ $# -ne 2 ]; then
   139	#        echo "usage: unlock_resources type_of_resource name_of_resource"
   140	#        exit -1
   141	#    fi
   142	
   143	    local lockfile="$1.$2"
   144	    lockfile="${LOCK_DIR}/${lockfile//\//^}"
   145	
   146	    rm "${lockfile}/$$" 2>/dev/null &&
   147	    rmdir "${lockfile}" 2>/dev/null
   148	
   149	    return 0
   150	}
   151	
   152	#---------------------------------------------------------------------
   153	## clean_resources
   154	##
   155	## this function removes all locks acquired by the current process.
   156	## you can call this function at the end of your script to check for
   157	## missing call to unlock_resources
   158	#---------------------------------------------------------------------
   159	clean_resources()
   160	{
   161	# we remove locks owned by this process. there is no guarantee since
   162	# locks that have been acquired, but where no pidfile have been
   163	# created will not be released.
   164	
   165	  if [ -d "${LOCK_DIR}" ]; then
   166	    find "${LOCK_DIR}" -maxdepth 2 -mindepth 2 -name $$ | \
   167	    while read file; do
   168	      debug "clean_resources" "removing forgotten lock ${file}"
   169	      rm -f "${file}" 2>/dev/null &&
   170	      rmdir $(smgl_dirname "${file}") 2>/dev/null
   171	    done
   172	  fi
   173	}
   174	
   175	#---------------------------------------------------------------------
   176	## global_clean_resources
   177	##
   178	## this function is used internaly to remove all stale locks.
   179	#---------------------------------------------------------------------
   180	global_clean_resources()
   181	{
   182	  if [ -d "${LOCK_DIR}" ]; then
   183	    # we remove old locks (>1 minute) that are not owned by any process.
   184	    find "${LOCK_DIR}" -maxdepth 1 -mindepth 1 -mmin +1 -type d -empty -exec rmdir {} \;
   185	
   186	    # we remove locks that are owned by dead processes
   187	    find "${LOCK_DIR}" -maxdepth 2 -mindepth 2 | \
   188	
   189	    while read file; do
   190	      # check if the process still exist (we use procfs mounted on /proc)
   191	      if [ ! -d "${file}" ]; then
   192	          debug "global_clean_resources" "removing stale lock ${file}"
   193	          rm -f "${file}" 2>/dev/null &&
   194	          rmdir $(smgl_dirname "${file}") 2>/dev/null
   195	      fi
   196	    done
   197	  fi
   198	}
   199	
   200	#---------------------------------------------------------------------
   201	## @param Name of file to lock
   202	## @param <file> ... (optional)
   203	##
   204	## Locks files for access to only this PID. Will cause attempts by
   205	## other processes that want to lock the file(s) to block until this
   206	## PID unlocks the file, or this PID dies.
   207	## It cannot prevent bad programs from modifying the file w/o
   208	## permission.
   209	## Note: Files with funky chars may break things. No colons or stuff.
   210	## @Blocking
   211	##
   212	#---------------------------------------------------------------------
   213	function lock_file()
   214	{
   215	  debug "liblock" "$FUNCNAME - $*"
   216	  lock_resources file "$1"
   217	}
   218	
   219	
   220	#---------------------------------------------------------------------
   221	## @param Name of file to unlock
   222	## @param <file> ... (optional)
   223	##
   224	## Unlocks a file so that another process can lock and modify it.
   225	##
   226	#---------------------------------------------------------------------
   227	function unlock_file()
   228	{
   229	  debug "liblock" "$FUNCNAME - $*"
   230	  unlock_resources file "$1"
   231	}
   232	
   233	#---------------------------------------------------------------------
   234	## @param File to start transaction on
   235	## @param <file> ... (optional)
   236	## @param variable to set (optional)
   237	##
   238	## A transaction locks a file and ensures that changes made to the
   239	## file are all made at once. No changes are made to the file until
   240	## the transaction is commited. Furthermore, a transaction can be
   241	## killed before it is commited, ending the transaction and not
   242	## making any changes to the files. All files are locked at once.
   243	## If not all can be locked, they will be unlocked, and another try
   244	## will be made.
   245	##
   246	## @Stdout The name of a temporary file that should be written to instead of the real file.
   247	## @return 0 If successful
   248	## @return 1 If not successful
   249	#---------------------------------------------------------------------
   250	function lock_start_transaction()
   251	{
   252	  debug "liblock" "$FUNCNAME - $*"
   253	  local NUMTRANS transaction_file file="$1"
   254	
   255	  if lock_file "$file" ; then
   256	
   257	    lock_file $LOCK_TRANSACTIONS
   258	    [ -s $LOCK_TRANSACTIONS ] || echo "0:::" > $LOCK_TRANSACTIONS
   259	
   260	    NUMTRANS=$(tail -n 1 $LOCK_TRANSACTIONS | cut -d : -f 1)
   261	    NUMTRANS=${NUMTRANS:-1} # TODO: check if this should fallback to 0
   262	
   263	    let NUMTRANS++
   264	    transaction_file="$LOCK_TRANSACTIONS.$NUMTRANS"
   265	    # cp file to temp file or if doesn't exist, create an empty file.
   266	    if [[ -e $file ]]; then
   267	      cp "$file" "$transaction_file"
   268	    else
   269	      echo -n "" > "$transaction_file"
   270	    fi
   271	    echo "$NUMTRANS:$file:$$" >> $LOCK_TRANSACTIONS
   272	
   273	    unlock_file $LOCK_TRANSACTIONS
   274	    if [[ $2 ]]; then
   275	      eval "$2=\"\$transaction_file\""
   276	    else
   277	      echo "$transaction_file"
   278	    fi
   279	    return 0
   280	
   281	  else
   282	    return 1
   283	  fi
   284	}
   285	
   286	#---------------------------------------------------------------------
   287	## @param File to commit transaction of
   288	## @param <file> ... (optional)
   289	##
   290	## Commits the changes made to the files as atomicaly as possible
   291	## Before this func is called, the files remain unchanged.
   292	##
   293	#---------------------------------------------------------------------
   294	function lock_commit_transaction()
   295	{
   296	  debug "liblock" "$FUNCNAME - $*"
   297	  local TMP_FILE unlockList=""
   298	
   299	  lock_file $LOCK_TRANSACTIONS
   300	  for i in $* ; do
   301	    TMP_FILE=${LOCK_TRANSACTIONS}.$(grep ".*:$i:$$" $LOCK_TRANSACTIONS | cut -d : -f 1)
   302	    [ -e $TMP_FILE ] && cp $TMP_FILE $i
   303	    if [ $? -ne 0 ] ; then
   304	      message "${PROBLEM_COLOR}Transaction commit failed on $i."
   305	      message "${DEFAULT_COLOR}The modified file is stored in $TMP_FILE."
   306	      message "This normally happens when the disk runs out of space."
   307	      if ! query "Do you wish to continue?" "n" ; then
   308	        exit 1
   309	      fi
   310	    else
   311	      rm $TMP_FILE
   312	    fi
   313	    unlock_file $i
   314	    cp $LOCK_TRANSACTIONS $LOCK_TRANSACTIONS.new
   315	    if [ $? -ne 0 ] ; then
   316	      message "${PROBLEM_COLOR}Transaction commit failed on $LOCK_TRANSACTIONS."
   317	      message "${DEFAULT_COLOR}"
   318	      message "This normally happens when the disk runs out of space."
   319	      if ! query "Do you wish to continue?" "n" ; then
   320	        exit 1
   321	      fi
   322	    fi
   323	    grep -v ".*:$i:$$" $LOCK_TRANSACTIONS.new >$LOCK_TRANSACTIONS &&
   324	    if ! rm $LOCK_TRANSACTIONS.new; then
   325	      message "${PROBLEM_COLOR}Very odd case. Quitting."
   326	      exit 1
   327	    fi
   328	  done
   329	  unlock_file $LOCK_TRANSACTIONS
   330	
   331	}
   332	
   333	#---------------------------------------------------------------------
   334	## @param File to stop the transaction of
   335	## @param file ... (optional)
   336	##
   337	## Stops a transaction. Causes the changes to the file(s) to be
   338	## ignored/undone.
   339	##
   340	#---------------------------------------------------------------------
   341	function lock_kill_transaction()
   342	{
   343	  debug "liblock" "$FUNCNAME - $*"
   344	  local TMP_FILE string
   345	
   346	  lock_file $LOCK_TRANSACTIONS
   347	  for i in $* ; do
   348	    esc_str ".*:$i:$$" string
   349	    TMP_FILE=$(grep "$string" $LOCK_TRANSACTIONS | cut -d : -f 1)
   350	    unlock_file $i
   351	    cp $LOCK_TRANSACTIONS $LOCK_TRANSACTIONS.1
   352	    grep -v "$string" $LOCK_TRANSACTIONS.1 >$LOCK_TRANSACTIONS
   353	  done
   354	  unlock_file $LOCK_TRANSACTIONS
   355	}
   356	
   357	#---------------------------------------------------------------------
   358	## Increment a counter
   359	##
   360	## @param Type of resource
   361	## @param Name of resource
   362	##
   363	## Processes should call counter_down when done with the resource
   364	## however counter_down and counter_check will clean up after misbehaving
   365	## processes.
   366	#---------------------------------------------------------------------
   367	function counter_up() {
   368	  local counterfile="$1.$2"
   369	  # we replace / with ^
   370	  counterfile="${LOCK_DIR}/${counterfile//\//^}"
   371	
   372	  touch $counterfile
   373	  lock_file $counterfile
   374	  echo $$ > $counterfile
   375	  unlock_file $counterfile
   376	}
   377	#---------------------------------------------------------------------
   378	## Decrement a counter
   379	##
   380	## @param Type of resource
   381	## @param Name of resource
   382	#---------------------------------------------------------------------
   383	function counter_down() {
   384	  local counterfile="$1.$2"
   385	  local foo each
   386	  # we replace / with ^
   387	  counterfile="${LOCK_DIR}/${counterfile//\//^}"
   388	
   389	  touch $counterfile
   390	  local tc
   391	  lock_start_transaction "$counterfile" tc
   392	  for each in $(counter_clean $tc); do
   393	    [[ $foo ]] && echo $each && continue
   394	    if [[ $each == $$ ]] ; then foo=found ; else echo $each; fi
   395	  done > $tc
   396	  lock_commit_transaction $counterfile
   397	}
   398	#---------------------------------------------------------------------
   399	##
   400	## Destroy a counter
   401	##
   402	## @param Type of resource
   403	## @param Name of resource
   404	##
   405	#---------------------------------------------------------------------
   406	function counter_reset() {
   407	  local counterfile="$1.$2"
   408	  local foo each
   409	  # we replace / with ^
   410	  counterfile="${LOCK_DIR}/${counterfile//\//^}"
   411	  lock_file $counterfile
   412	  rm "${LOCK_DIR}/${counterfile//\//^}"
   413	  unlock_file $counterfile
   414	}
   415	
   416	#---------------------------------------------------------------------
   417	##
   418	## Check the value of a counter
   419	##
   420	## @param Type of resource
   421	## @param Name of resource
   422	##
   423	## Also cleans stale items
   424	##
   425	#---------------------------------------------------------------------
   426	function counter_check() {
   427	  local counterfile="$1.$2"
   428	  # we replace / with ^
   429	  counterfile="${LOCK_DIR}/${counterfile//\//^}"
   430	  if test -f $counterfile ; then
   431	    local tc
   432	    lock_start_transaction "$counterfile" tc
   433	    # sort saves a temp file since it has to read all the input
   434	    counter_clean $tc |sort > $tc
   435	    lock_commit_transaction $counterfile
   436	    cat $counterfile|wc -l
   437	  else
   438	    echo 0
   439	  fi
   440	}
   441	
   442	#---------------------------------------------------------------------
   443	##
   444	## @Internal
   445	## @param counter file
   446	##
   447	## Print out the file with non-existent pids removed
   448	## Caller must hold a lock on the file
   449	##
   450	#---------------------------------------------------------------------
   451	function counter_clean() {
   452	  for each in $(cat $1) ; do
   453	    ps -A|grep -q $each && echo $each
   454	  done
   455	}
   456	
   457	#---------------------------------------------------------------------
   458	##=item SYNCHRONIZE
   459	##
   460	## Prevents more than one process from entering a section of code
   461	## at a time
   462	## @NOTE This assumes that two functions of the same name will
   463	## @NOTE not havethe SYNCHRONIZE command on the same line in different
   464	## @NOTE files.
   465	## <pre>
   466	## Example:
   467	## #!/bin/bash
   468	## echo "Script started with PID=$$
   469	## eval "$SYNCHRONIZE" && {
   470	## echo "PID $$ is in the synched section of code."
   471	## sleep 10
   472	## } && eval "$UNSYNCHRONIZE"
   473	## echo "PID $$ done."
   474	## </pre>
   475	## Note: SYNCHRONIZE and UNSYNCHRONIZE must be in the same local
   476	## scope. Nested SYNCHs are not allowed in the same local scopes.
   477	## Local scope usualy being a function.
   478	## @Blocking
   479	##
   480	#---------------------------------------------------------------------
   481	SYNCHRONIZE='
   482	  __llSYNCH_LINE=$LINENO
   483	  debug "liblock" "+++ in synch code"
   484	  lock_resources "lockfunction" "${FUNCNAME}/${__llSYNCH_LINE}"'
   485	
   486	#---------------------------------------------------------------------
   487	##=item UNSYNCHRONIZE
   488	##
   489	## Terminates a section of SYNCHRONIZED code
   490	##
   491	#---------------------------------------------------------------------
   492	UNSYNCHRONIZE='
   493	  debug "liblock" "+++ in unsynch code"
   494	  unlock_resources "lockfunction" "${FUNCNAME}/${__llSYNCH_LINE}"
   495	  unset __llSYNCH_LINE'