/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'