#!/bin/bash
#---------------------------------------------------------------------
## @Synopsis Functions for dealing with tracking of files, and other installwatch related things.
## @Copyright Copyright (C) 2004 The Source Mage Team <http://www.sourcemage.org>
## Functions for dealing with tracking of files, and other installwatch related things.
#---------------------------------------------------------------------

#---------------------------------------------------------------------
##
##  Initialize libtrack, currently this is just the modified files list.
##  This is used by cast to exclude files it's modified from the md5 list.
##
#---------------------------------------------------------------------
function libtrack_init() {
  [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
}


#---------------------------------------------------------------------
## @Stdin list of files
## @Stdout list of files
## Reads a list of files from standard in, and returns a list of the
## files that exist.
##
#---------------------------------------------------------------------
function exists()  {
  while  read  ITEM;  do  [  -e  "$ITEM"  ]  &&  echo  "$ITEM";  done;
}


#---------------------------------------------------------------------
##
## Given a list of files it will notify installwatch of them.
## Useful for spells whose components are not dynamically linked
## to glibc. Uses simple hack of touching files while
## installwatch is running.
##
#---------------------------------------------------------------------
function real_track_manual()  {
  if  [[ -z "$STAGED_INSTALL" || $STAGED_INSTALL == off ]] &&
      [[ -z "$INSTALLWATCHFILE"  ]] ;  then
    echo "Can't tell installwatch to manually track... installwatch isn't running."
    return 1
  fi
  local  i
  for i in $* ; do
    [ -e $i ] && touch $i
  done
  return 0
}

#--------------------------------------------------------------------
## Some simple castfs sanity checking
## make a file, check to see if it exists, check to see if the
## contents of the file is what we made it
#--------------------------------------------------------------------
function run_castfs_sanity()
{
  export CASTFS_LOGFILE=${CASTFS_DBGLOG}
  export CASTFS_DBGLVL=${CASTFS_DEBUG_LEVEL}
  debug  "libtrack" "$FUNCNAME on $SPELL"

  # first check if /dev/null is really a character device #13315
  if [[ ! -c /dev/null ]]; then
    debug "libtrack" "$FUNCNAME failure: damaged /dev/null"
    return 1
  fi

  mkdir -p "${TMP_DIR}/test-mount" &&
  mkdir -p "${TMP_DIR}/test-stage" &&
  castfs "${TMP_DIR}/test-mount" -o "stage=${TMP_DIR}/test-stage" -o "ignore=/tmp" &&
  pushd "${TMP_DIR}/test-mount" > /dev/null &&
  (
    (echo stuff > foo) &&
    (ls foo > /dev/null 2>&1) &&
    (test "stuff" = "`cat foo`") &&
    rm -f foo
  )
  local rc=$?
  popd > /dev/null
  if [[ $rc == 0 ]]
  then
    debug "libtrack" "$FUNCNAME success"
  else
    debug "libtrack" "$FUNCNAME failure"
  fi
  umount -l "${TMP_DIR}/test-mount"
  return $rc
}

#---------------------------------------------------------------------
##
## Starts Translation Stage Root
##
#---------------------------------------------------------------------
function real_invoke_stage_root()
{
  export CASTFS_LOGFILE=${CASTFS_DBGLOG}
  export CASTFS_DBGLVL=${CASTFS_DEBUG_LEVEL}
  local CASTFS_IGNORE_LIST=""
  local LOCAL_IGNORE_DIRS="${CCACHE_DIR} ${DISTCC_DIR}"
  # this forks and daemonizes so it will exit you simply unmount the dir to stop the process
  for dir in ${CASTFS_UNSTAGED_PATHS} ${LOCAL_IGNORE_DIRS}
  do
    CASTFS_IGNORE_LIST="${CASTFS_IGNORE_LIST} -o ignore=$dir"
  done &&
  castfs "${STAGE_DIRECTORY}/MOUNT" -o "stage=${STAGE_DIRECTORY}/TRANSL" $CASTFS_IGNORE_LIST &&
  for dir in /dev /dev/pts /proc /sys
  do
    mount -o bind "$dir" "${STAGE_DIRECTORY}/MOUNT$dir"
  done
}

#---------------------------------------------------------------------
##
## Stops Translation Stage Root
##
#---------------------------------------------------------------------
function real_devoke_stage_root()
{
  unset CASTFS_LOGFILE
  unset CASTFS_DBGLVL
  for dir in /dev/pts /dev /proc /sys
  do
    umount -l "${STAGE_DIRECTORY}/MOUNT$dir"
  done &&
  umount -l "${STAGE_DIRECTORY}/MOUNT"
}

#---------------------------------------------------------------------
##
## Prepare Stage Root
##
#---------------------------------------------------------------------
function prepare_stage_root()
{
  mk_source_dir "${STAGE_DIRECTORY}" &&
  mk_source_dir "${STAGE_DIRECTORY}/TRANSL" &&
  mk_source_dir "${STAGE_DIRECTORY}/MOUNT"
}

#---------------------------------------------------------------------
##
## Destroy Stage Root
##
#---------------------------------------------------------------------
function destroy_stage_root()
{
  rm_source_dir $STAGE_DIRECTORY
}

#---------------------------------------------------------------------
##
## Sets up installwatch.
##
#---------------------------------------------------------------------
function real_invoke_installwatch()  {
  if  [  -e  /usr/lib/installwatch.so  ];  then
    export  INSTALLWATCHFILE=$IW_LOG
    export  LD_PRELOAD=/usr/lib/installwatch.so
  fi
}

#---------------------------------------------------------------------
##
## Stops using installwatch
##
#---------------------------------------------------------------------
function real_devoke_installwatch()  {
  unset  LD_PRELOAD
  unset  INSTALLWATCHFILE
}

function is_castfs_installed()
{
  local loc
  if smgl_which castfs loc >/dev/null 2>&1 &&
     (
       modprobe fuse > /dev/null 2>&1 ||
       grep -q '^nodev[[:space:]]*fuse$' /proc/filesystems
     ) &&
     [[ -c /dev/fuse ]]
  then
    message "${MESSAGE_COLOR}staging enabled${DEFAULT_COLOR}"
    return 0
  else
    message "${MESSAGE_COLOR}staging disabled${DEFAULT_COLOR}"
    return 1
  fi
}

#---------------------------------------------------------------------
##
## Parses the installwatch log for files installed by a spell.
##
#---------------------------------------------------------------------
function parse_iw()  {
  local INPUT=$1

  # it is EXTREMELY IMPORTANT that this variable contains an actual
  # tab character and not some number of spaces. Otherwise BAD THINGS
  # will happen.
  local TAB=$'\t'
  OMIT_IN="${TAB}rename\|${TAB}symlink\|${TAB}unlink"

  grep -v "$OMIT_IN" $INPUT | cut -f3 | grep "^/" | sed 's#^//#/#g'
  cat                $INPUT | cut -f4 | grep "^/" | sed 's#^//#/#g'
}


#---------------------------------------------------------------------
##
## Creates the install log containing all files installed by the spell.
##
#---------------------------------------------------------------------
function create_install_log()  {
  debug  "libtrack" "$FUNCNAME on $SPELL"
  local INPUT=$1
  local OUTPUT=$2

  rm -f $OUTPUT
  if [[ $STAGED_INSTALL != off ]]
  then
    find "${STAGE_DIRECTORY}/TRANSL/"   |
    sed "s#${STAGE_DIRECTORY}/TRANSL##g"|
    grep -v -x '/'                      |
    filter_excluded                     > $OUTPUT
  else
    parse_iw $INPUT                     |
    sed "s#/\(\./\)\+#/#g"              |
    sort                                |
    uniq                                |
    install_log_filter $INSTALL_ROOT "" |
    grep -v -x ""                       |
    filter_excluded                     |
    install_log_filter "" $INSTALL_ROOT |
    exists                              >  $OUTPUT
  fi

  echo "$C_LOG_COMP"                  >> $OUTPUT
  echo "$MD5_LOG"                     >> $OUTPUT
  echo "$INST_LOG"                    >> $OUTPUT

}

#---------------------------------------------------------------------
##
## Creates the install log containing all staged files. It reuses
## the existing install log and just prepends $STAGE_DIRECTORY/TRANSL
## to every line
##
## @param input log file
## @param output log file
##
#---------------------------------------------------------------------
function create_stage_install_log()  {
  debug  "libtrack" "$FUNCNAME on $SPELL"
  local input="$1"
  local output="$2"

  # treat the logs specially, as they weren't staged
  grep -v "$C_LOG_COMP\|$MD5_LOG\|$INST_LOG" "$input" > "$output"
  sed -i "s#^#$STAGE_DIRECTORY/TRANSL#" "$output"
  echo "$C_LOG_COMP"                 >> "$output"
  echo "$MD5_LOG"                    >> "$output"
  echo "$INST_LOG"                   >> "$output"

}

#---------------------------------------------------------------------
## Makes a list of files with the md5sum
#---------------------------------------------------------------------
function create_md5list() {
  local INPUT=$1
  local OUTPUT=$2
  debug  "libtrack" "$FUNCNAME on $SPELL"

  [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
  touch $__MODIFIED_FILES
  filter "$__MODIFIED_FILES" < $INPUT | while read LINE ; do
    debug "libtrack" "Checking file $LINE"
    if  [ -f "$LINE" ] ; then
      debug "libtrack" "Running md5 on $LINE"
      md5sum "$LINE"
    fi
  done 2>/dev/null > $OUTPUT
}

#---------------------------------------------------------------------
## External api to note config files
#---------------------------------------------------------------------
function real_note_config_file() {
  if check_if_modified "$1"; then
    mark_file_modified "$1"
  fi
}


#---------------------------------------------------------------------
## Notes that a file was previously modified so that its md5 is
## deliberatly munged
#---------------------------------------------------------------------
function mark_file_modified() {
  [[ "$1" ]] || return 1
  [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
  echo "^$1\$" >> $__MODIFIED_FILES
}


#---------------------------------------------------------------------
## @param file to check
## @param md5 file (optional)
#---------------------------------------------------------------------
function check_if_modified() {
  local to=$1
  local md5_log=$2
  if ! [[ $2 ]] ; then
    md5_log="$TMP_DIR/$SPELL.md5"
    if [[ $OLD_SPELL_VERSION ]] ; then
      old_md5_log="$MD5SUM_LOGS/$SPELL-$OLD_SPELL_VERSION"
      # log must be in filterable form
      log_adjuster "$old_md5_log" "$md5_log" filterable root
    else
      old_md5_log=/dev/null
    fi
  fi
  local my_md5=$(md5sum "$to")
  if test -f "$md5_log" && grep -q "$my_md5" "$md5_log"; then
    false
  else
    true
  fi
}


#---------------------------------------------------------------------
##
## Creates a bzip/gzip'ed tar file containing an archived backup of
## file specified on standard input into $CACHE_COMP
## @stdin files, one per line, to put into the archive
## @param directory input files are relative to install root for regular files state root for state files
##
#---------------------------------------------------------------------
function create_cache_archive()  {

  debug  "libtrack" "$FUNCNAME on $SPELL"
  if    [  "$ARCHIVE"  ==  "off"  ]; then
    debug "libtrack" "$FUNCNAME - ARCHIVE=$ARCHIVE, aborting archival."
    return
  fi
  debug "libtrack" "$FUNCNAME - ARCHIVE=$ARCHIVE, archiving."
  local input=$1
  local CACHE=$2
  local CACHE_COMP=$3

  message  "${MESSAGE_COLOR}Creating cache file" \
           "${FILE_COLOR}${CACHE_COMP}${DEFAULT_COLOR}"
  # gather the queuing factors, so we can include them in the label, saving us the
  # need to compute the spell name and a few untars when dealing with just a cache
  local label
  label="$SPELL ${VERSION:-0} ${PATCHLEVEL:-0} ${SECURITY_PATCH:-0} ${UPDATED:-0}"

  local TMP_DATA=$TMP_DIR/foo.data
  local TMP_MDATA=$TMP_DIR/foo.mdata
  seperate_state_files $input $TMP_DATA $TMP_MDATA

  pushd $STATE_ROOT/ &>/dev/null
  install_log_filter $STATE_ROOT "." < $TMP_MDATA |
  tar --no-recursion -cPf "$CACHE" -V "$label" -T -
  popd &>/dev/null

  pushd $INSTALL_ROOT/ &>/dev/null
  install_log_filter $INSTALL_ROOT "." < $TMP_DATA |
  tar --no-recursion -rPf "$CACHE" -T -
  rm $TMP_DATA $TMP_MDATA
  popd &>/dev/null
  if  [  "$COMPRESSBIN" != "tar"  ]; then
    $COMPRESSBIN -c $CACHE > $CACHE_COMP
    rm $CACHE
  fi
}

#---------------------------------------------------------------------
## this is to filter the install log from one form to another
## for install_root/track_root conversions
#---------------------------------------------------------------------
function install_log_filter() {
  sed "s:^$1:$2:"
}
function md5_log_filter() {
  sed "s:  $1:  $2:"
}

#---------------------------------------------------------------------
## @param input file (can be /dev/stdin)
## @param output file (can be /dev/stdout)
## @param input format (root/log/filterable)
## @param output format (root/log/filterable)
## @param filter callback (optional install_log_filter, could be md5_log_filter)
##
## This filters an install log from a given format into another format
## <pre>
## root: relative to / all paths are relative to / file existence tests
## should work, INSTALL_ROOT and STATE_ROOT are prepended to data and
## state files respectively
##
## log: relative to track_root etc, format used in the logs (see note on
## special behavior below)
##
## filterable: track_root/install_root/state_root stripped out files can
## have filters applied to them
##
## "Special" handling applies depending on whether STATE_ROOT is inside
## or outside INSTALL_ROOT.
## For converting into log format:
## If STATE_ROOT is within INSTALL_ROOT
## eg: STATE_ROOT=/opt/stuff INSTALL_ROOT=/opt/stuff
## or  STATE_ROOT=/opt/stuff/state INSTALL_ROOT=/opt/stuff
## the portion of INSTALL_ROOT within STATE_ROOT is replaced with TRACK_ROOT
## if STATE_ROOT is outside of INSTALL_ROOT (eg /opt/stuff and /opt/state)
## then STATE_ROOT is left as is
##
## Converting from log to root format is the inverse, and of course going
## to filterable format just requires removing whatever the expected prefix is.
## </pre>
#---------------------------------------------------------------------
function log_adjuster() {
  local input=$1
  local output=$2
  local informat=$3
  local outformat=$4
  local callback=${5:-install_log_filter}

  local data_in data_out metadata_in metadata_out cat_metadata


  if [[ "$informat" == root ]] ; then
    data_in=$INSTALL_ROOT
    if [[ "$outformat" == log ]] ; then
      # root to log
      data_out=$TRACK_ROOT

      # if the STATE_ROOT is within the install root, then the state files are
      # adjusted relative to track_root, otherwise they are left as is
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        metadata_in=$STATE_ROOT
        metadata_out=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
      else
        cat_metadata=yes
      fi
    elif [[ $outformat == filterable ]] ; then
      # root to filterable
      data_out=""
      metadata_in=$STATE_ROOT
      metadata_out=""
    fi
  elif [[ "$informat" == log ]] ; then
    data_in=$TRACK_ROOT
    if [[ "$outformat" == root ]] ; then
      # log to root
      data_out=$INSTALL_ROOT
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        # we actually could do this another way by stripping off
        # $TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}, and replacing
        # it with $STATE_ROOT, but i think below is simpler and equivalent
        metadata_in=$TRACK_ROOT
        metadata_out=$INSTALL_ROOT
      else
        cat_metadata=yes
      fi
    elif [[ "$outformat" == filterable ]] ; then
      # log to filterable
      data_out=""
      metadata_out=""
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        metadata_in=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
      else
        metadata_in=$STATE_ROOT
      fi
    fi
  elif [[ "$informat" == filterable ]] ; then
    data_in=""
    metadata_in=""
    if [[ "$outformat" == root ]] ; then
      # filterable to root
      data_out=$INSTALL_ROOT
      metadata_out=$STATE_ROOT
    elif [[ "$outformat" == log ]] ; then
      # filterable to log
      data_out=$TRACK_ROOT
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        metadata_out=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
      else
        metadata_out=$STATE_ROOT
      fi
    fi
  fi

  local TMP_SSF=$(make_safe_dir)
  local TMP_DATA=$TMP_SSF/foo.data
  local TMP_MDATA=$TMP_SSF/foo.mdata

  seperate_state_files $input $TMP_DATA $TMP_MDATA
  {
    if [[ $cat_metadata ]] ; then
      cat $TMP_MDATA
    else
      eval "$callback \"$metadata_in\" \"$metadata_out\" < $TMP_MDATA"
    fi
    eval "$callback \"$data_in\" \"$data_out\" < $TMP_DATA"
  } > $output
  rm $TMP_DATA $TMP_MDATA
  rmdir $TMP_SSF
  return 0
}

#---------------------------------------------------------------------
## Split a log file into data that should be TRACK_ROOT'd versus
## STATE_ROOT'd.
##
## @param filename or - (or /dev/stdin) for a pipe. This routine will read the input only once in-order to work with pipes.
## @param filename for non-state files, possibly /dev/stdout or /dev/stderr
## @param filename for state files, possibly /dev/stdout or /dev/stderr don't use the same stream for both types.
##
#---------------------------------------------------------------------
function seperate_state_files() {
  local REAL_LOG_DIR=${LOG_DIRECTORY#$STATE_ROOT}
  local REAL_STATE_DIR=${STATE_DIRECTORY#$STATE_ROOT}

  # the input file is almost certainly a pipe, and things get weird
  # since we have to grep twice, so just dump the data into a unique file

  local TMP_SSF=$(make_safe_dir)
  local FILE=$TMP_SSF/ssf
  cat $1 > $FILE
  grep -v "$REAL_LOG_DIR\|$REAL_STATE_DIR" $FILE | grep -xv '' > $2
  grep    "$REAL_LOG_DIR\|$REAL_STATE_DIR" $FILE | grep -xv '' > $3
  rm $FILE
  rmdir $TMP_SSF
  return 0
}

#---------------------------------------------------------------------
## Try to make a unique directory using $RANDOM, leverage the fact that
## two simultaneous mkdir's will have one succeed and the other fail.
##
## This is run from log_adjuster primarily which may be invoked several
## times in a pipe, when this happens, bash does a fork, but does not
## seem to reseed the random number generator, causing a high rate of
## collisions. This is not easily reproducable outside of sorcery at
## the time of writing, but inside it happens nearly everytime with bash
## 3.1. This may actually be a bash 3.1 bug.
##
## The collisions aren't bad necessarily, but they result in un-necesary
## delay.
##
## Setting RANDOM re-seeds the random number generator.
##
## Despite the fact that the subshell and invocation of date are slow
## the consequence for not doing them are worse. The nano-seconds
## are usually going to be different between forks so the liklihood
## of a collision is greatly reduced.
#---------------------------------------------------------------------
function make_safe_dir() { (
  RANDOM=$(date "+%N")
  local TMP_SSF=$TMP_DIR/$RANDOM
  while ! mkdir "$TMP_SSF" &> /dev/null; do
    RANDOM=$(date "+%N")
    TMP_SSF=$TMP_DIR/$RANDOM
    sleep .1
    debug "libtrack" "safe dir collision on $TMP_SSF"
  done
  echo $TMP_SSF
  return 0
) }

#---------------------------------------------------------------------
## @License
##
## This software is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This software is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this software; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
##
#---------------------------------------------------------------------
