/var/lib/sorcery/modules/libtrack
1 #!/bin/bash
2 #---------------------------------------------------------------------
3 ## @Synopsis Functions for dealing with tracking of files, and other installwatch related things.
4 ## @Copyright Copyright (C) 2004 The Source Mage Team <http://www.sourcemage.org>
5 ## Functions for dealing with tracking of files, and other installwatch related things.
6 #---------------------------------------------------------------------
7
8 #---------------------------------------------------------------------
9 ##
10 ## Initialize libtrack, currently this is just the modified files list.
11 ## This is used by cast to exclude files it's modified from the md5 list.
12 ##
13 #---------------------------------------------------------------------
14 function libtrack_init() {
15 [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
16 }
17
18
19 #---------------------------------------------------------------------
20 ## @Stdin list of files
21 ## @Stdout list of files
22 ## Reads a list of files from standard in, and returns a list of the
23 ## files that exist.
24 ##
25 #---------------------------------------------------------------------
26 function exists() {
27 local item
28 while read item; do
29 [[ -e $item ]] && echo $item
30 done
31 }
32
33
34 #---------------------------------------------------------------------
35 ##
36 ## Given a list of files it will notify installwatch of them.
37 ## Useful for spells whose components are not dynamically linked
38 ## to glibc. Uses simple hack of touching files while
39 ## installwatch is running.
40 ##
41 #---------------------------------------------------------------------
42 function real_track_manual() {
43 if [[ -z "$STAGED_INSTALL" || $STAGED_INSTALL == off ]] &&
44 [[ -z "$INSTALLWATCHFILE" && -z "$INSTW_LOGFILE" ]] ; then
45 echo "Can't tell installwatch to manually track... installwatch isn't running."
46 return 1
47 fi
48 touch -c "$@"
49 return 0
50 }
51
52 #--------------------------------------------------------------------
53 ## Some simple castfs sanity checking
54 ## make a file, check to see if it exists, check to see if the
55 ## contents of the file is what we made it
56 #--------------------------------------------------------------------
57 function run_castfs_sanity()
58 {
59 export CASTFS_LOGFILE=${CASTFS_DBGLOG}
60 export CASTFS_DBGLVL=${CASTFS_DEBUG_LEVEL}
61 debug "libtrack" "$FUNCNAME on $SPELL"
62
63 # first check if /dev/null is really a character device #13315
64 if [[ ! -c /dev/null ]]; then
65 debug "libtrack" "$FUNCNAME failure: damaged /dev/null"
66 return 1
67 fi
68
69 mkdir -p "${TMP_DIR}/test-mount" &&
70 mkdir -p "${TMP_DIR}/test-stage" &&
71 castfs "${TMP_DIR}/test-mount" -o "stage=${TMP_DIR}/test-stage" -o "ignore=/tmp" &&
72 pushd "${TMP_DIR}/test-mount" > /dev/null &&
73 (
74 (echo stuff > foo) &&
75 (ls foo > /dev/null 2>&1) &&
76 (test "stuff" = "`cat foo`") &&
77 rm -f foo
78 )
79 local rc=$?
80 popd > /dev/null
81 if [[ $rc == 0 ]]
82 then
83 debug "libtrack" "$FUNCNAME success"
84 else
85 debug "libtrack" "$FUNCNAME failure"
86 fi
87 umount -l "${TMP_DIR}/test-mount"
88 return $rc
89 }
90
91 #---------------------------------------------------------------------
92 ##
93 ## Starts Translation Stage Root
94 ##
95 #---------------------------------------------------------------------
96 function real_invoke_stage_root()
97 {
98 export CASTFS_LOGFILE=${CASTFS_DBGLOG}
99 export CASTFS_DBGLVL=${CASTFS_DEBUG_LEVEL}
100 local CASTFS_IGNORE_LIST=""
101 local LOCAL_IGNORE_DIRS="${CCACHE_DIR} ${DISTCC_DIR}"
102 # this forks and daemonizes so it will exit you simply unmount the dir to stop the process
103 for dir in ${CASTFS_UNSTAGED_PATHS} ${LOCAL_IGNORE_DIRS}
104 do
105 if [[ -h $dir ]]; then
106 CASTFS_IGNORE_LIST="${CASTFS_IGNORE_LIST} -o ignore=$(readlink $dir)"
107 elif [[ -d $dir ]]; then
108 CASTFS_IGNORE_LIST="${CASTFS_IGNORE_LIST} -o ignore=$dir"
109 else
110 debug "libtrack" "User wanted $FUNCNAME to treat $dir as a directory!"
111 fi
112 done &&
113 castfs "${STAGE_DIRECTORY}/MOUNT" -o "stage=${STAGE_DIRECTORY}/TRANSL" $CASTFS_IGNORE_LIST &&
114 for dir in /dev /dev/pts /proc /sys
115 do
116 mount -o bind "$dir" "${STAGE_DIRECTORY}/MOUNT$dir"
117 done
118 }
119
120 #---------------------------------------------------------------------
121 ##
122 ## Stops Translation Stage Root
123 ##
124 #---------------------------------------------------------------------
125 function real_devoke_stage_root()
126 {
127 unset CASTFS_LOGFILE
128 unset CASTFS_DBGLVL
129 for dir in /dev/pts /dev /proc /sys
130 do
131 umount -l "${STAGE_DIRECTORY}/MOUNT$dir"
132 done &&
133 umount -l "${STAGE_DIRECTORY}/MOUNT"
134 }
135
136 #---------------------------------------------------------------------
137 ##
138 ## Prepare Stage Root
139 ##
140 #---------------------------------------------------------------------
141 function prepare_stage_root()
142 {
143 mk_source_dir "${STAGE_DIRECTORY}" &&
144 mk_source_dir "${STAGE_DIRECTORY}/TRANSL" &&
145 mk_source_dir "${STAGE_DIRECTORY}/MOUNT"
146 }
147
148 #---------------------------------------------------------------------
149 ##
150 ## Destroy Stage Root
151 ##
152 #---------------------------------------------------------------------
153 function destroy_stage_root()
154 {
155 rm_source_dir $STAGE_DIRECTORY
156 }
157
158 #---------------------------------------------------------------------
159 ##
160 ## Sets up installwatch.
161 ##
162 #---------------------------------------------------------------------
163 function real_invoke_installwatch() {
164 if [[ -e $INSTALLWATCH_SO ]]; then
165 export INSTALLWATCHFILE=$IW_LOG
166 export INSTW_LOGFILE=$IW_LOG
167 export LD_PRELOAD=$INSTALLWATCH_SO
168 fi
169 }
170
171 #---------------------------------------------------------------------
172 ##
173 ## Stops using installwatch
174 ##
175 #---------------------------------------------------------------------
176 function real_devoke_installwatch() {
177 unset LD_PRELOAD
178 unset INSTALLWATCHFILE INSTW_LOGFILE
179 }
180
181 function is_castfs_installed()
182 {
183 local loc
184 if smgl_which castfs loc >/dev/null 2>&1 &&
185 (
186 modprobe fuse > /dev/null 2>&1 ||
187 grep -q '^nodev[[:space:]]*fuse$' /proc/filesystems
188 ) &&
189 [[ -c /dev/fuse ]]
190 then
191 message "${MESSAGE_COLOR}Staging enabled${DEFAULT_COLOR}"
192 return 0
193 else
194 message "${MESSAGE_COLOR}Staging disabled${DEFAULT_COLOR}"
195 return 1
196 fi
197 }
198
199 #---------------------------------------------------------------------
200 ##
201 ## Parses the installwatch log for files installed by a spell.
202 ##
203 #---------------------------------------------------------------------
204 function parse_iw() {
205 local INPUT=$1
206
207 # it is EXTREMELY IMPORTANT that this variable contains an actual
208 # tab character and not some number of spaces. Otherwise BAD THINGS
209 # will happen.
210 local TAB=$'\t'
211 OMIT_IN="${TAB}rename\|${TAB}symlink\|${TAB}unlink\|${TAB}access\|${TAB}utimes"
212
213 grep -v "$OMIT_IN" $INPUT | cut -f3 | grep "^/" | sed 's#^//#/#g'
214 cat $INPUT | cut -f4 | grep "^/" | sed 's#^//#/#g'
215 }
216
217
218 #---------------------------------------------------------------------
219 ##
220 ## Creates the install log containing all files installed by the spell.
221 ##
222 #---------------------------------------------------------------------
223 function create_install_log() {
224 debug "libtrack" "$FUNCNAME on $SPELL"
225 local INPUT=$1
226 local OUTPUT=$2
227
228 rm -f $OUTPUT
229 if [[ $STAGED_INSTALL != off ]]
230 then
231 get_all_package_files |
232 filter_excluded > $OUTPUT
233 else
234 parse_iw $INPUT |
235 sed "s#/\(\./\)\+#/#g" |
236 sort -u |
237 install_log_filter $INSTALL_ROOT "" |
238 grep -v -x "" |
239 filter_excluded |
240 install_log_filter "" $INSTALL_ROOT |
241 exists > $OUTPUT
242 fi
243
244 echo "$C_LOG_COMP" >> $OUTPUT
245 echo "$MD5_LOG" >> $OUTPUT
246 echo "$INST_LOG" >> $OUTPUT
247
248 }
249
250 #---------------------------------------------------------------------
251 ##
252 ## Creates the install log containing all staged files. It reuses
253 ## the existing install log and just prepends $STAGE_DIRECTORY/TRANSL
254 ## to every line
255 ##
256 ## @param input log file
257 ## @param output log file
258 ##
259 #---------------------------------------------------------------------
260 function create_stage_install_log() {
261 debug "libtrack" "$FUNCNAME on $SPELL"
262 local input="$1"
263 local output="$2"
264
265 # treat the logs specially, as they weren't staged
266 grep -v "$C_LOG_COMP\|$MD5_LOG\|$INST_LOG" "$input" > "$output"
267 sed -i "s#^#$STAGE_DIRECTORY/TRANSL#" "$output"
268 echo "$C_LOG_COMP" >> "$output"
269 echo "$MD5_LOG" >> "$output"
270 echo "$INST_LOG" >> "$output"
271
272 }
273
274 #---------------------------------------------------------------------
275 ## Makes a list of files with the md5sum
276 #---------------------------------------------------------------------
277 function create_md5list() {
278 local INPUT=$1
279 local OUTPUT=$2
280 debug "libtrack" "$FUNCNAME on $SPELL"
281
282 [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
283 touch $__MODIFIED_FILES
284 filter "$__MODIFIED_FILES" < $INPUT | while read LINE ; do
285 debug "libtrack" "Checking file $LINE"
286 if [ -f "$LINE" ] ; then
287 debug "libtrack" "Running md5 on $LINE"
288 md5sum "$LINE"
289 fi
290 done 2>/dev/null > $OUTPUT
291 }
292
293 #---------------------------------------------------------------------
294 ## External api to note config files
295 #---------------------------------------------------------------------
296 function real_note_config_file() {
297 if check_if_modified "$1"; then
298 mark_file_modified "$1"
299 fi
300 }
301
302
303 #---------------------------------------------------------------------
304 ## Notes that a file was previously modified so that its md5 is
305 ## deliberatly munged
306 #---------------------------------------------------------------------
307 function mark_file_modified() {
308 [[ "$1" ]] || return 1
309 [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
310 echo "^$1\$" >> $__MODIFIED_FILES
311 }
312
313
314 #---------------------------------------------------------------------
315 ## @param file to check
316 ## @param md5 file (optional)
317 #---------------------------------------------------------------------
318 function check_if_modified() {
319 local to=$1
320 local md5_log=$2
321 if ! [[ $2 ]] ; then
322 md5_log="$TMP_DIR/$SPELL.md5"
323 if [[ $OLD_SPELL_VERSION ]] ; then
324 old_md5_log="$MD5SUM_LOGS/$SPELL-$OLD_SPELL_VERSION"
325 # log must be in filterable form
326 log_adjuster "$old_md5_log" "$md5_log" filterable root
327 else
328 old_md5_log=/dev/null
329 fi
330 fi
331 local my_md5=$(md5sum "$to")
332 if test -f "$md5_log" && grep -qx "$my_md5" "$md5_log"; then
333 false
334 else
335 true
336 fi
337 }
338
339 #---------------------------------------------------------------------
340 ## Constructs the to-be cache name depending on the ARCHIVEBIN
341 ## @param archive name
342 ## @param upvar
343 #---------------------------------------------------------------------
344 function construct_cache_name() {
345 local name=$1
346 if [[ -n $ARCHIVEBIN ]]; then
347 # just use the bin, currently there is no need for another extension
348 name="$name.$ARCHIVEBIN"
349 fi
350 upvar $2 "$name"
351 }
352
353 #---------------------------------------------------------------------
354 ## Given a filename, will return the actual filename if a similar
355 ## filename with a different extension exists. A more thorough version
356 ## of guess_filename used for finding caches
357 ## @param archive name (can be right trimmed, globbing will be done)
358 ## @param upvar (optional)
359 #---------------------------------------------------------------------
360 function find_cache() {
361 debug "libtrack" "$FUNCNAME $@"
362 local filename=$1
363 local real_name
364
365 if [[ -f $filename ]]; then
366 real_name=$filename
367 else
368 # use the first if more were found
369 local basename
370 smgl_basename "$filename" basename
371 read real_name < <(find $INSTALL_CACHE -type f -name "$basename*")
372 fi
373 [[ -z $real_name ]] && return 1
374 debug "libtrack" "$FUNCNAME found $real_name"
375
376 if [[ -z $2 ]]; then
377 echo $real_name
378 else
379 upvar $2 "$real_name"
380 fi
381 return 0
382 }
383
384 #---------------------------------------------------------------------
385 ##
386 ## Creates a bzip/gzip'ed tar file containing an archived backup of
387 ## file specified on standard input into the target dir.
388 ##
389 ## Input files are relative to install root for regular files and
390 ## state root for state files
391 ## @param files, one per line, to put into the archive
392 ## @param archive name
393 ## @param compressed archive name
394 ##
395 #---------------------------------------------------------------------
396 function create_cache_archive() {
397
398 debug "libtrack" "$FUNCNAME on $SPELL"
399 if [ "$ARCHIVE" == "off" ]; then
400 debug "libtrack" "$FUNCNAME - ARCHIVE=$ARCHIVE, aborting archival."
401 return
402 fi
403 debug "libtrack" "$FUNCNAME - ARCHIVE=$ARCHIVE, archiving."
404 local input=$1
405 local CACHE=$2
406 local CACHE_COMP=$3
407
408 message "${MESSAGE_COLOR}Creating cache file" \
409 "${FILE_COLOR}${CACHE_COMP}${DEFAULT_COLOR}"
410 # gather the queuing factors, so we can include them in the label, saving us the
411 # need to compute the spell name and a few untars when dealing with just a cache
412 local label
413 label="$SPELL ${VERSION:-0} ${PATCHLEVEL:-0} ${SECURITY_PATCH:-0} ${UPDATED:-0}"
414
415 local TMP_DATA=$TMP_DIR/foo.data
416 local TMP_MDATA=$TMP_DIR/foo.mdata
417 seperate_state_files $input $TMP_DATA $TMP_MDATA
418
419 case "$ARCHIVEBIN" in
420 tar)
421 pushd $STATE_ROOT/ &>/dev/null
422 install_log_filter $STATE_ROOT "." < $TMP_MDATA |
423 tar --no-recursion -cPf "$CACHE" -T - -V "$label"
424 popd &>/dev/null
425
426 pushd $INSTALL_ROOT/ &>/dev/null
427 install_log_filter $INSTALL_ROOT "." < $TMP_DATA |
428 tar --no-recursion -rPf "$CACHE" -T -
429 popd &>/dev/null
430 ;;
431 esac
432 rm $TMP_DATA $TMP_MDATA
433
434 case "$COMPRESSBIN" in
435 gzip|bzip2|pbzip2|xz)
436 $COMPRESSBIN -c $CACHE > $CACHE_COMP
437 rm $CACHE
438 ;;
439 esac
440 }
441
442 #---------------------------------------------------------------------
443 ## this is to filter the install log from one form to another
444 ## for install_root/track_root conversions
445 #---------------------------------------------------------------------
446 function install_log_filter() {
447 sed "s:^$1:$2:"
448 }
449 function md5_log_filter() {
450 sed "s: $1: $2:"
451 }
452
453 #---------------------------------------------------------------------
454 ## @param input file (can be /dev/stdin)
455 ## @param output file (can be /dev/stdout)
456 ## @param input format (root/log/filterable)
457 ## @param output format (root/log/filterable)
458 ## @param filter callback (optional install_log_filter, could be md5_log_filter)
459 ##
460 ## This filters an install log from a given format into another format
461 ## <pre>
462 ## root: relative to / all paths are relative to / file existence tests
463 ## should work, INSTALL_ROOT and STATE_ROOT are prepended to data and
464 ## state files respectively
465 ##
466 ## log: relative to track_root etc, format used in the logs (see note on
467 ## special behavior below)
468 ##
469 ## filterable: track_root/install_root/state_root stripped out files can
470 ## have filters applied to them
471 ##
472 ## "Special" handling applies depending on whether STATE_ROOT is inside
473 ## or outside INSTALL_ROOT.
474 ## For converting into log format:
475 ## If STATE_ROOT is within INSTALL_ROOT
476 ## eg: STATE_ROOT=/opt/stuff INSTALL_ROOT=/opt/stuff
477 ## or STATE_ROOT=/opt/stuff/state INSTALL_ROOT=/opt/stuff
478 ## the portion of INSTALL_ROOT within STATE_ROOT is replaced with TRACK_ROOT
479 ## if STATE_ROOT is outside of INSTALL_ROOT (eg /opt/stuff and /opt/state)
480 ## then STATE_ROOT is left as is
481 ##
482 ## Converting from log to root format is the inverse, and of course going
483 ## to filterable format just requires removing whatever the expected prefix is.
484 ## </pre>
485 #---------------------------------------------------------------------
486 function log_adjuster() {
487 local input=$1
488 local output=$2
489 local informat=$3
490 local outformat=$4
491 local callback=${5:-install_log_filter}
492
493 local data_in data_out metadata_in metadata_out cat_metadata
494
495
496 if [[ "$informat" == root ]] ; then
497 data_in=$INSTALL_ROOT
498 if [[ "$outformat" == log ]] ; then
499 # root to log
500 data_out=$TRACK_ROOT
501
502 # if the STATE_ROOT is within the install root, then the state files are
503 # adjusted relative to track_root, otherwise they are left as is
504 if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
505 metadata_in=$STATE_ROOT
506 metadata_out=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
507 else
508 cat_metadata=yes
509 fi
510 elif [[ $outformat == filterable ]] ; then
511 # root to filterable
512 data_out=""
513 metadata_in=$STATE_ROOT
514 metadata_out=""
515 fi
516 elif [[ "$informat" == log ]] ; then
517 data_in=$TRACK_ROOT
518 if [[ "$outformat" == root ]] ; then
519 # log to root
520 data_out=$INSTALL_ROOT
521 if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
522 # we actually could do this another way by stripping off
523 # $TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}, and replacing
524 # it with $STATE_ROOT, but i think below is simpler and equivalent
525 metadata_in=$TRACK_ROOT
526 metadata_out=$INSTALL_ROOT
527 else
528 cat_metadata=yes
529 fi
530 elif [[ "$outformat" == filterable ]] ; then
531 # log to filterable
532 data_out=""
533 metadata_out=""
534 if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
535 metadata_in=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
536 else
537 metadata_in=$STATE_ROOT
538 fi
539 fi
540 elif [[ "$informat" == filterable ]] ; then
541 data_in=""
542 metadata_in=""
543 if [[ "$outformat" == root ]] ; then
544 # filterable to root
545 data_out=$INSTALL_ROOT
546 metadata_out=$STATE_ROOT
547 elif [[ "$outformat" == log ]] ; then
548 # filterable to log
549 data_out=$TRACK_ROOT
550 if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
551 metadata_out=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
552 else
553 metadata_out=$STATE_ROOT
554 fi
555 fi
556 fi
557
558 local TMP_SSF=$(make_safe_dir)
559 local TMP_DATA=$TMP_SSF/foo.data
560 local TMP_MDATA=$TMP_SSF/foo.mdata
561
562 seperate_state_files $input $TMP_DATA $TMP_MDATA
563 {
564 if [[ $cat_metadata ]] ; then
565 cat $TMP_MDATA
566 else
567 eval "$callback \"$metadata_in\" \"$metadata_out\" < $TMP_MDATA"
568 fi
569 eval "$callback \"$data_in\" \"$data_out\" < $TMP_DATA"
570 } > $output
571 rm $TMP_DATA $TMP_MDATA
572 rmdir $TMP_SSF
573 return 0
574 }
575
576 #---------------------------------------------------------------------
577 ## Split a log file into data that should be TRACK_ROOT'd versus
578 ## STATE_ROOT'd.
579 ##
580 ## @param filename or - (or /dev/stdin) for a pipe. This routine will read the input only once in-order to work with pipes.
581 ## @param filename for non-state files, possibly /dev/stdout or /dev/stderr
582 ## @param filename for state files, possibly /dev/stdout or /dev/stderr don't use the same stream for both types.
583 ##
584 #---------------------------------------------------------------------
585 function seperate_state_files() {
586 local REAL_LOG_DIR=${LOG_DIRECTORY#$STATE_ROOT}
587 local REAL_STATE_DIR=${STATE_DIRECTORY#$STATE_ROOT}
588
589 # the input file is almost certainly a pipe, and things get weird
590 # since we have to grep twice, so just dump the data into a unique file
591
592 local TMP_SSF=$(make_safe_dir)
593 local FILE=$TMP_SSF/ssf
594 cat $1 > $FILE
595 grep -v "$REAL_LOG_DIR\|$REAL_STATE_DIR" $FILE | grep -xv '' > $2
596 grep "$REAL_LOG_DIR\|$REAL_STATE_DIR" $FILE | grep -xv '' > $3
597 rm $FILE
598 rmdir $TMP_SSF
599 return 0
600 }
601
602 #---------------------------------------------------------------------
603 ## Try to make a unique directory using $RANDOM, leverage the fact that
604 ## two simultaneous mkdir's will have one succeed and the other fail.
605 ##
606 ## This is run from log_adjuster primarily which may be invoked several
607 ## times in a pipe, when this happens, bash does a fork, but does not
608 ## seem to reseed the random number generator, causing a high rate of
609 ## collisions. This is not easily reproducable outside of sorcery at
610 ## the time of writing, but inside it happens nearly everytime with bash
611 ## 3.1. This may actually be a bash 3.1 bug.
612 ##
613 ## The collisions aren't bad necessarily, but they result in un-necesary
614 ## delay.
615 ##
616 ## Setting RANDOM re-seeds the random number generator.
617 ##
618 ## Despite the fact that the subshell and invocation of date are slow
619 ## the consequence for not doing them are worse. The nano-seconds
620 ## are usually going to be different between forks so the liklihood
621 ## of a collision is greatly reduced.
622 #---------------------------------------------------------------------
623 function make_safe_dir() { (
624 RANDOM=$(date "+%N")
625 local TMP_SSF=$TMP_DIR/$RANDOM
626 while ! mkdir "$TMP_SSF" &> /dev/null; do
627 RANDOM=$(date "+%N")
628 TMP_SSF=$TMP_DIR/$RANDOM
629 sleep .1
630 debug "libtrack" "safe dir collision on $TMP_SSF"
631 done
632 echo $TMP_SSF
633 return 0
634 ) }
635
636 #---------------------------------------------------------------------
637 ## @License
638 ##
639 ## This software is free software; you can redistribute it and/or modify
640 ## it under the terms of the GNU General Public License as published by
641 ## the Free Software Foundation; either version 2 of the License, or
642 ## (at your option) any later version.
643 ##
644 ## This software is distributed in the hope that it will be useful,
645 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
646 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
647 ## GNU General Public License for more details.
648 ##
649 ## You should have received a copy of the GNU General Public License
650 ## along with this software; if not, write to the Free Software
651 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
652 ##
653 #---------------------------------------------------------------------