#!/bin/sh
#/
#/ \file
#/
#/ \brief       check and mount local filesystems
#/
#/ \author      Arthur
#/
#/ \date        January 27, 2013
#/
#/ \version     0.2
#/
#/ \remarks
#/
#/ This script must only rely on utilities found in <tt>/bin</tt> and
#/ <tt>/sbin</tt>.
#/


################################################################################
#                                                                              #
#                              C O N S T A N T S                               #
#                                                                              #
################################################################################


RC_NAME=`ls $0 2>/dev/null | sed -e 's@.*/\+\([^ \t/]\+\)@\1@'`
RC_DIR=/sbin/init.d
RC_VER="0.2"
RC_ID="mount local filesystems"

HOSTNAME_FILE=/etc/hostname
MTAB_FILE=/etc/mtab
IGNORE_SIG="INT QUIT TSTP"              # signal names/numbers to ignore

HC_IS_UTC=0                             # hardware clock in UTC?


################################################################################
#                                                                              #
#                               I N C L U D E S                                #
#                                                                              #
################################################################################


. $RC_DIR/include/rc_utils
. $RC_DIR/include/fs


################################################################################
#                                                                              #
#                              F U N C T I O N S                               #
#                                                                              #
################################################################################


#-------------------------------------------------------------------------------


#/
#/ Output script usage and exit.
#/
#/ \returns
#/
#/ Always exit code 1.
#/
usage()
{
    $ECHO "Usage: $RC_NAME [-n][-q][-v][-h][-V]"

    exit 1
}


#-------------------------------------------------------------------------------


#/
#/ Output script version and exit.
#/
#/ \returns
#/
#/ Always exit code 1.
#/
version()
{
    $ECHO "$RC_NAME $RC_VER"

    exit 1
}


#-------------------------------------------------------------------------------


#/
#/ Return list of unmounted, local filesystems. The local filesystems that have
#/ \em not been mounted are echo'ed to stdout (separated by whitespace). A
#/ filesystem is considered local if the \c comment option of the filesystem in
#/ <tt>/etc/fstab</tt> is set to <tt>local</tt>.
#/
list_unmounted_local_filesystems()
{
    #
    # Do a fake mount of the filesystems marked as "local".
    # Errors or warnings `mount' may output begin with the
    # name of the executable followed by a colon (e.g.
    # "mount:"); these lines will be filtered out. The
    # remaining lines signify unmounted filesystems where
    # the first column indicates the device file of the.
    # filesystem.
    #
    list=`/bin/mount -v -f -n -a -O comment=local 2>/dev/null | grep -v ':' | sed -ne 's@^[ \t]*\([^ \t]\+\)[ \t]\+.*@\1@p'`

    $ECHO $list
}


#-------------------------------------------------------------------------------


#/
#/ Return list of unmounted, pseudo filesystems. The pseudo filesystems that
#/ have \em not been mounted are echo'ed to stdout (separated by whitespace).
#/ A filesystem is considered pseudo if the \c comment option of the filesystem
#/ in <tt>/etc/fstab</tt> is set to <tt>pseudo</tt>.
#/
list_unmounted_pseudo_filesystems()
{
    list=`/bin/mount -v -f -n -a -O comment=pseudo 2>/dev/null | grep -v ':' | sed -ne 's@^[[:space:]]*\([^[:space:]]\+\)[[:space:]]\+.*[[:space:]]\+type[[:space:]]\+\([^[:space:]]\+\).*@\1,\2@p'`

    $ECHO $list
}


#-------------------------------------------------------------------------------


#/
#/ Mount given filesystem. The specified filesystem conforms to the \c fs_spec
#/ field of the <tt>/etc/fstab</tt> file and will be mounted according to the
#/ settings in that same file. Optionally the filesystem is checked by the
#/ \c fsck utility and repaired as needed. In case the filesystem check fails
#/ with some sort of error, the filesystem will not be mounted.
#/
#/ \param[in] $1  Filesystem specification.
#/
#/ \param[in] $2  Boolean indicating whether to check filesystem before
#/                mounting.
#/
#/ \returns
#/
#/ Return code 0 if filesystem was mounted successfully, otherwise 1.
#/
mount_filesystem()
{
    assert mount_filesystem "-n $1"
    assert mount_filesystem "$2 -eq 0 -o $2 -eq 1"

    fs=$1
    check=$2
    fs_ok=0
    ret=0
    
    # check filesystem, if requested:
    if test $check -eq 1
    then
        $DRY_RUN /sbin/fsck -V -a $fs
        ret=$?
        if test $ret -eq 0 -o $ret -eq 1
        then
            fs_ok=1         # f/s is clean
        fi
    else
        fs_ok=1             # consider f/s clean
    fi

    if test "$VERBOSE" = "1"
    then
        if test $fs_ok -eq 1
        then
            $ECHO "$fs appears to be clean."
        else
            $ECHO "$fs is dirty."
        fi
    fi
    
    # mount filesystem, but only if it's clean:
    if test $fs_ok -eq 1
    then
        $DRY_RUN /bin/mount $fs >/dev/null 2>&1
        ret=$?
        if test $ret -eq 0 -o $ret -eq 16
        then
            [ "$VERBOSE" = "1" ] && $ECHO "Mounted filesystem $fs"
        else
            error "$fs: mount failed ($ret)"
            fs_ok=0
        fi
    else
        warning "$fs: not mounting dirty filesystem"
    fi

    # transform boolean value to Unix return code:
    test $fs_ok -eq 1
    return $?
}


#-------------------------------------------------------------------------------


#/
#/ Remount root filesystem R/W. The filesystem is checked (if necessary),
#/ remounted, and the mount table file (<tt>MTAB_FILE</tt>) is prepared to list
#/ the mounted root filesystem. When the filesystem is being checked, all
#/ errors that can be safely fixed will be fixed automatically. In case of
#/ serious errors, requiring operator intervention, an emergency shell is 
#/ started to allow manual repairs. In the event the emergency shell fails to
#/ execute, the script enters an infinite loop thereby simulating a \c halt.
#/
#/ \returns
#/   
#/ Return code 0 if root filesystem was successfully mounted, otherwise 1.
#/ 
remount_rootfs_rw()
{
    fs_ok=0
    fsck_ret=0
    line=""

    $ECHO "Checking root filesystem..."

    $DRY_RUN /sbin/fsck -T -V -a /
    fsck_ret=$?
    if test $fsck_ret -eq 0 -o $fsck_ret -eq 1
    then
        fs_ok=1         # f/s is clean
        
        $DRY_RUN mount -o remount,rw /
        if test $? -eq 0 -o $? -eq 16
        then
            $ECHO "Remounted root filesystem for read-write access."
        else
            error "failed remounting root filesystem R/W"
        fi
    else
        $ECHO
        $ECHO "***            BOOT FAILURE             ***"
        $ECHO "*** FILESYSTEM CONSISTENCY CHECK FAILED ***"
        $ECHO
        $ECHO_N "fsck command returned: "
        case "$fsck_ret" in
          2) $ECHO "$fsck_ret (reboot system)" ;;
          4) $ECHO "$fsck_ret (uncorrected errors)" ;;
          8) $ECHO "$fsck_ret (operational error)" ;;
         16) $ECHO "$fsck_ret (usage error)" ;;
         32) $ECHO "$fsck_ret (user interuption)" ;;
        128) $ECHO "$fsck_ret (shared library error)" ;;
          *) $ECHO "$fsck_ret (unknown)" ;;
        esac
        $ECHO "Normal boot sequence aborted."
        $ECHO "System requires operator intervention."
        $ECHO
        $ECHO "Spawning emergency shell..."
        spawn_emergency_shell

        #
        # Failing to start an emergency shell leaves
        # us no other option then to halt the system.
        #
        $ECHO "Failed executing emergency shell."
        $ECHO "Halting the system."
        #
        # Simulate a halt by waiting for keypress.
        # The system may be in bad shape, so we'll
        # only use builtin commands. Also, at the
        # start of the script we've set to ignore
        # certain signals, so the `while' loop below
        # cannot be interrupted.
        #
        while read line
        do
            :
        done
        exit 1
    fi

    if test $fs_ok -eq 1
    then
        [ "$VERBOSE" = "1" ] && $ECHO "Generating fresh mount table: $MTAB_FILE"

        $DRY_RUN $ECHO_N > $MTAB_FILE
        $DRY_RUN mount -f /
    fi

    # boolean to Unix return code:
    test $fs_ok -eq 1
    return $?
}


#-------------------------------------------------------------------------------


#/
#/ Start an emergency shell. Spawning an emergency shell implies the system is
#/ in a vulnerable state, so we want to be very careful but still allow the
#/ operator to login somehow. Files may be damaged so we try to omit using
#/ scripts and binaries as much as possible.
#/ First we attempt to launch <tt>/sbin/sulogin</tt> to enable a safe login.
#/ If \c sulogin fails to start due to being corrupted, we allow unauthorized
#/ access to the system by executing <tt>/bin/sh</tt>. This should work because
#/ this script, \c bcheckrc, is being interpreted by that shell. However as a
#/ last resort we offer the possibility for the operator to input an
#/ alternative shell. If that one fails, it's over.
#/
#/ Note that exiting the emergency shell just respawns an emergency shell
#/ again. This is because we want to omit using the \c halt script or any
#/ other script or binaries as much as possible. Also, we don't want to the
#/ operator to exit the emergency shell while s/he accidently forgets to
#/ unmount filesystems (which may just have been repaired).
#/
#/ \remark
#/
#/ If this function returns, halt the system.
#/
#/ \todo
#/
#/ Need to find a way to determine whether the interpreter executable loaded
#/ successfully or not, and subsequently ignore the exit code of the
#/ interpreter. Now we consider exit codes other than 0 to be an error
#/ although the interpreter did load into memory correctly.
#/
spawn_emergency_shell()
{
    sh_list=""      # list of shells we can try
    next_sh=""      # shell we're going to try
    ret=0

    sh_list="/sbin/sulogin /bin/sh ask"

    #
    # Set list of shells we're going to try to spawn, from left to right.
    # The word "ask" indicates that the user must enter the pathname of
    # the shell s/he wants to spawn.
    # If spawning a shell fails horribly, we try the next one until we
    # run out of shells. In this case the function returns. If a shell
    # exits reasonably normal, the list of shells to try is restored
    # and the cycle starts over again.
    #
    set $sh_list
    while test $# -gt 0
    do
        next_sh=$1
        if test "$next_sh" = "ask"
        then
            $ECHO "Enter command interpreter to use:"
            read next_sh
        fi

        # ensure the alleged file (i.e. shell executable) exists:
        if test -s "$next_sh"
        then
            $ECHO "Attempting to execute command interpreter $next_sh"

            $next_sh            # execute shell
            ret=$?
            #
            # Exit codes other than 0 indicate is a problem with
            # the shell, in which case we'll try the next one.
            # Otherwise, we'll re-set the shell list.
            #
            if test $ret -eq 0
            then
                set $sh_list    # restore shell list
            else
                $ECHO "Failed executing command interpreter $next_sh ($ret)"
                shift           # try next shell
            fi
        else
            $ECHO "Command interpreter not found or invalid: $next_sh"
            shift               # try next shell
        fi
    done
}


#-------------------------------------------------------------------------------


#/
#/ Mount all local filesystems. In addition to mounting the filesystems, a
#/ consistency check is run, if necessary. All filesystem errors that can be
#/ safely fixed will be fixed automatically.
#/ A filesystem is only mounted if it has not yet been mounted, otherwise it
#/ is silently skipped.
#/
#/ \returns
#/
#/ Return code 0 if all umounted local filesystems were successfully mounted,
#/ otherwise 1.
#/
#/ \todo
#/
#/ * What to do when filesystem errors cannot be fixed automatically?
#/ * Hard to do logging here as all filesystems are handle in one stroke.
#/
mount_local_filesystems()
{
    unmounted=`list_unmounted_local_filesystems`
    fs_ok=0
    ret=0
    
    $ECHO_N "Mounting local filesystems..."

    if test -n "$unmounted"
    then
        $DRY_RUN /sbin/fsck -T -V -a $unmounted
        ret=$?
        if test $ret -eq 0 -o $ret -eq 1
        then
            fs_ok=1         # f/s is clean
        fi
    fi

    # mount filesystem, but only if it's clean:
    if test $fs_ok -eq 1
    then
        $DRY_RUN /bin/mount -a -O comment=local >/dev/null 2>&1
        ret=$?
        if test $ret -ne 0 -a $ret -ne 16
        then
            error "mount failed ($ret)"
            fs_ok=0
        fi
    fi

    $ECHO

    # transform boolean value to Unix return code:
    test $fs_ok -eq 1
    return $?
}


#-------------------------------------------------------------------------------


#/
#/ Mount pseudo filesystems. Since pseudo filesystems differ between major
#/ kernel versions, external pre- and post-mount functions can be defined for
#/ each filesystem type. The functions reside in the \c fs include script and
#/ implement all the necessary operations to make a pseudo filesystem usable.
#/ A filesystem is only mounted if it has not yet been mounted, otherwise it is
#/ silently skipped (including its pre- and post-mount function).
#/
#/ \returns
#/
#/ Return code 0 if all unmounted pseudo filesystems were successfully mounted,
#/ otherwise 1.
#/
mount_pseudo_filesystems()
{
    unmounted=""
    fs=""
    fs_spec=""
    fs_type=""
    premount_fn=""
    postmount_fn=""
    ret=0
    err=0
    
    $ECHO_N "Mounting pseudo filesystems..."

    unmounted=`list_unmounted_pseudo_filesystems`

    for fs in $unmounted
    do
        set `$ECHO $fs | sed -ne 's@,@ @p' 2>/dev/null`
        fs_spec=$1
        fs_type=$2

        $ECHO_N " $fs_spec ($fs_type)"

        # do pre-mount function, if any:
        premount_fn=premount_${fs_type}
        type "$premount_fn" >/dev/null 2>&1
        if test $? -eq 0
        then
            [ "$VERBOSE" = "1" ] && $ECHO "Executing pre-mount function for $fs_type"
            
            eval $premount_fn $fs_spec
            ret=$?
            if test $ret -ne 0
            then
                error "$fs_type: pre-mount function failed ($ret); skipping mount"
                err=1
            fi
        else
            ret=0                   # no pre-mount function; is OK
        fi

        # mount the filesystem, but only
        # if pre-mount function succeeded:
        if test $ret -eq 0
        then
            mount_filesystem $fs_spec 0
            ret=$?
            # do post-mount function, if any,
            # and only if `mount' succeeded:
            if test $ret -eq 0
            then
                postmount_fn=postmount_${fs_type}
                type "$postmount_fn" >/dev/null 2>&1
                if test $? -eq 0
                then
                    [ "$VERBOSE" = "1" ] && $ECHO "Executing post-mount function for $fs_type"
                    
                    eval $postmount_fn $fs_spec
                    ret=$?
                    if test $ret -ne 0
                    then
                        error "$fs_type: post-mount function failed ($ret)"
                        err=1
                    fi
                else
                    ret=0           # no post-mount function; is OK
                fi
            else
                err=1
            fi
        fi
    done

    $ECHO

    test $err -eq 0
    return $?
}


#-------------------------------------------------------------------------------


#/
#/ Set and echo system hostname.
#/
#/ \returns
#/
#/ Return code 0 if hostname was set, otherwise 1.
#/
set_hostname()
{
    host_name=""
    ret=0

    $ECHO_N "Setting hostname: "

    if test -f "$HOSTNAME_FILE"
    then
        host_name=`cat $HOSTNAME_FILE`
    else
        host_name="(none)"
    fi
    hostname $host_name
    ret=$?

    $ECHO `hostname`

    return $ret
}


#-------------------------------------------------------------------------------


#/
#/ Re-set and echo system time.
#/
#/ \returns
#/
#/ Return code 0 if successfull, otherwise 1.
#/
set_system_time()
{
    assert set_system_time "$HC_IS_UTC -eq 0 -o $HC_IS_UTC -eq 1"
    
    tz_opt=""
    ret=0

    $ECHO_N "Setting system time..."

    case "$HC_IS_UTC" in
    0) tz_opt=--localtime ;;
    1) tz_opt=--utc       ;;
    esac
    $DRY_RUN hwclock --adjust  $tz_opt >/dev/null 2>&1
    $DRY_RUN hwclock --hctosys $tz_opt >/dev/null 2>&1
    ret=$?

    $ECHO
    # finished "Setting system time..."

    $ECHO_N "Local system time: "
    date

    return $ret
}


#-------------------------------------------------------------------------------


################################################################################
#                                                                              #
#                         S T A R T  O F  S C R I P T                          #
#                                                                              #
#                                                                              #
# Operation:                                                                   #
#                                                                              #
# o Process command line arguments.                                            #
# o Mount all local and pseudo filesystems.                                    #
#                                                                              #
################################################################################

QUIET=0                 # quiet flag (0 or 1)
VERBOSE=0               # verbose flag (0 or 1)
DRY_RUN=                # default is to actually execute commands

trap "" $IGNORE_SIG     # prevent interruption of the script


#
# Handle command line OPTIONS.
#
while test $# -gt 0
do
    case "$1" in
    -n|--dry-run)
        DRY_RUN="$ECHO -e \n> "     # ensure we begin on a new line
        ;;
    -q|--quiet)
        QUIET=1
        exec >/dev/null             # mute stdout
        ;;
    -v|--verbose)
        VERBOSE=1
        ECHO_N=$ECHO                # don't inhibit newline;
                                    # this prevents clobbering of text
        ;;
    -h|--help)
        usage
        ;;
    -V|--version)
        version
        ;;
    -*)
        fatal "unknown option: $1"
        ;;
    *)                              # first non-OPTIONS argument
        break
        ;;
    esac
    shift
done

#
# Handle influential environment variables.
#
if test "$RC_VERBOSE" = "0" -o "$RC_VERBOSE" = "1"
then
    VERBOSE=$RC_VERBOSE
fi


#
# Begin the first startup procedures.
#

#remount_rootfs_rw

set_system_time

mount_local_filesystems
mount_pseudo_filesystems

set_hostname
