#!/bin/sh
#/
#/ \file
#/
#/ \brief       utility functions and definitions for startup
#/
#/ \author      Arthur
#/
#/ \date        December 6, 2012
#/
#/ \version     0.2
#/


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


ECHO="/bin/echo -e"             # prevent using a shell's builtin `echo'
ECHO_N="$ECHO -n"               # `echo' w/o trailing newline


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


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


#/
#/ Output message of severity "warning". The message is prefixed with the name
#/ of the script followed a colon and the literal \"<tt>warning: </tt>\". The
#/ resulting warning message is output to the standard error stream.
#/
#/ \param[in] $1  Message text.
#/
warning()
{
    $ECHO "$RC_NAME: warning: $1" >/dev/stderr
}


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


#/
#/ Output message of severity "error". The message is prefixed with the name
#/ of the script followed a colon and the literal \"<tt>error: </tt>\". The
#/ resulting error message is output to the standard error stream.
#/
#/ \param[in] $1  Message text.
#/
error()
{
    $ECHO "$RC_NAME: error: $1" >/dev/stderr
}


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


#/ 
#/ Output message of severity "fatal". The message is prefixed with the name
#/ of the script followed a colon and the literal \"<tt>fatal: </tt>\". The
#/ resulting message is output to the standard error stream.
#/ In additional to outputting the fatal error message, the usage() function
#/ is called to output the help text of the script. After that the script is
#/ terminated.
#/
#/ \param[in] $1  Message text.
#/
#/ \returns
#/
#/ Return code of usage(), otherwise 1.
#/
fatal()
{
    $ECHO "$RC_NAME: fatal: $1" >/dev/stderr
    $ECHO
    usage

    exit 1
}


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


#/
#/ Test given expression and abort if evaluates to boolean false. Return
#/ normally if expression evaluates to boolean true. The expression to
#/ evaluate follows the syntax of the \c test command.
#/
#/ \param[in] $1  Name of originating function.
#/
#/ \param[in] $2  Expression to evaluate.
#/
#/ \returns
#/
#/ Return code 0 if assert was true. Otherwise, the script will abort with
#/ exit code 1.
#/
assert()
{
    func=$1
    expr=$2

    if test $expr
    then
        return 0
    else
        $ECHO "$RC_NAME: assert: $func(): $expr" >/dev/stderr
        exit 1
    fi
}


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


#/   
#/ Remove leading and trailing whitespace from string. The trimmed string is
#/ echo'ed to stdout.
#/
#/ \param[in] $1  String to be trimmed.
#/
trim()
{
    #
    # The shell automatically removes leading and trailing whitespace
    # from unquoted parameters, so we only need to echo the string.
    #
    $ECHO $1
}


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


#/
#/ Sort words alphanumerically. A list of words is sorted alphabetically and
#/ numerically, and is echo'ed to stdout. Letters are sorted before numbers,
#/ non-alphanumeric characters are sorted according to their ASCII value.
#/
#/ \param[in] $1..$N  Words to be sorted. Whitespace within a parameter is not
#/                    allowed and causes the parameter to be interpreted as two
#/                    or more separate words.
#/
sort_list()
{
    unsorted=$*         # list of unsorted words
    sorted=""

    #
    # When sorting the list we have to make sure each word is on a separate
    # line since the `sort' command operates on *lines* of text. To this end,
    # we use the `fmt' command with an impossible small line width, say 1,
    # which will force each word on a separate line. Afterwards, we have to
    # join all lines into a single line of words, separated by whitespace.
    #
    sorted=`$ECHO $unsorted | fmt --width=1 | sort -n | tr '\n' ' '`

    #
    # Remove leading and trailing whitespace and output.
    #
    trim "$sorted"
}


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


#/
#/ Return S- or K-script name parts. The given name is assumed to be the name
#/ of an S- or K-script and the function attempts to split the name into the
#/ following three parts:
#/
#/     <S|K> <2-digit seq-nr> <service name>
#/
#/ If the script name cannot be separated in these three parts, the name was
#/ not an S- or K-script name and the function returns an empty string.
#/
#/ \param[in] $1  Name of S- or K-script.
#/
split_SK_script_name()
{
    name=$1
    parts=""

    # try to match S- or K-script name; only echo if there's an actual match:
    parts=`$ECHO $name | sed -ne 's@^\([SK]\)\([0-9][0-9]\)\([^ \t]\+\)@\1 \2 \3@p' 2>/dev/null`

    assert split_SK_script_name "'`$ECHO $parts | wc -w`' = '3' -o '`$ECHO $parts | wc -w`' = '0'"

    $ECHO $parts
}


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


#/
#/ Translate SK-initial to ACTION type. The translation is echo'ed to stdout
#/ if one was found, otherwise an empty string is output. The S- and K-initial
#/ translate to the following strings:
#/
#/     S -> start
#/     K -> stop
#/
#/ \param[in] $1  Name of S- or K-script.
#/
#/ \returns
#/
#/ Code 0 if a translation was found and echo'ed, otherwise 1.
#/
get_action()
{
    name=$1
    name_parts=""
    action_part=""
    action=""
    ret=1
    
    name_parts=`split_SK_script_name $name`
    if test -n "$name_parts"
    then
        action_part=`$ECHO $name_parts | cut -d' ' -f1 2>/dev/null`
        case "$action_part" in
        S) action=start; ret=0 ;;
        K) action=stop;  ret=0 ;;
        esac
    fi
    $ECHO $action

    return $ret
}


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


#/
#/ Extract service/facility name from SK-script. The service or facility
#/ sub-string is echo'ed to stdout if one was found, otherwise an empty string
#/ is output.
#/
#/ \param[in] $1  Name of S- or K-script.
#/
#/ \returns
#/
#/ Code 0 if a service or facility name was extracted, otherwise 1.
#/
get_facility()
{
    name=$1
    name_parts=""
    facl_part=""
    ret=0
    
    name_parts=`split_SK_script_name $name`
    facl_part=`$ECHO $name_parts | cut -d' ' -f3 2>/dev/null`
    if test -n "$facl_part"
    then
        ret=0
    else
        ret=1
    fi
    $ECHO $facl_part

    return $ret
}


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


#/
#/ Search for configuration files. The directory searched for config files is
#/ determined by the \c CONFIG_DIR constant. Depending on the script calling
#/ this function, the value of \c CONFIG_DIR may be different and therefore the
#/ config directory being searched may be different. If config files are found
#/ in the \c CONFIG_DIR directory, the filenames are echo'ed to stdout in a
#/ whitespace-separated list. If no config files were found, an empty string
#/ is echo'ed.
#/
#/ The \c CONFIG_DIR directory is not searched recursively, so any configs in
#/ sub-directories are ignored. Also, hidden files and files with an extension
#/ are ignored.
#/
#/ \returns
#/
#/ Code 0 if a search of \c CONFIG_DIR has been performed, although this does
#/ not mean any files were found; it merely indicates the search ran without
#/ any errors. Code 1 is returned if an actual error occurred during the search.
#/
find_configs()
{
    cfgs=""
    ret=0

    if test -d "$CONFIG_DIR"
    then
        cfgs=`find $CONFIG_DIR -maxdepth 1 -type f -regex '.*/[^.]+$' -printf '%P\n' 2>/dev/null | sort -d`
        if test $? -eq 0
        then
            $ECHO $cfgs
            ret=0
        else
            ret=1
        fi
    else
        error "config directory not found: $CONFIG_DIR"
        ret=1
    fi

    return $ret
}


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


#/
#/ Read configuration file. The function formats the full path to the config
#/ file of the current service or facility and checks whether the file exists.
#/ If it does, the contents of the config file are read and evaluated.
#/
#/ The function executes in the context of the current service/facility.
#/
#/ \param[in] $1  Name of config file relative to the service/facility's
#/                \c CONFIG_DIR.
#/
#/ \returns
#/
#/ Code 0 if config file existed and was evaluated, otherwise 1.
#/
read_config()
{
    assert read_config "-n '$1'"

    cfg=$CONFIG_DIR/$1
    if test ! -r "$cfg"
    then
        error "config not found or no permission: $cfg"
        return 1
    fi
    
    [ "$VERBOSE" = "1" ] && $ECHO "Reading configuration: $cfg"
    
    . $cfg

    return 0
}


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


#/
#/ Run selected commands of service/facility. A service or facility can define
#/ commands which should be called at a certain moment during startup or
#/ shutdown of the service or facility. These commands are referred to as pre-
#/ or post-start commands and pre- or post-stop commands and can be defined at
#/ the "global" level of a service or facility, or on per-daemon basis. This
#/ function allows these commands to be called by specifying the service or
#/ facility along with the appropriate parameters to select the commands. The
#/ pre- and post-commands however, are optional, so if they are not defined
#/ for a service or facility, this function does nothing.
#/
#/ \param[in] $1  Name of service or facility.
#/
#/ \param[in] $2  Daemon sequence number. A value larger than 0 addresses the
#/                commands of the corresponding daemon. A value of 0 indicates
#/                the service/facility's "global" commands.
#/
#/ \param[in] $3  Command type. Two types of identifiers are recognized:
#/
#/                - START: startup commands
#/                - STOP:  shutdown commands
#/
#/ \param[in] $4  Command qualifier. This indicates when a command is supposed
#/                to be executed. Two qualifiers are recognized:
#/
#/                - PRE:  commands executed upfront
#/                - POST: commands executed afterwards
#/
#/ \param[in] $5  Abort when a command fails (1) or continue (0).
#/
#/ \returns
#/
#/ Code 0 if all commands executed successfully, or 1 if any one failed. The
#/ return code is also 0 if no commands were defined. Return code 2 indicates
#/ a usage error.
#/
do_commands()
{
    assert do_commands "-n '$1'"
    assert do_commands "$2 -ge 0"
    assert do_commands "-n '$3'"
    assert do_commands "-n '$4'"
    assert do_commands "$5 -ne 0 -o $5 -ne 1"
    
    name=$1
    daemon_num=$2
    type=$3
    qual=$4
    abort_on_err=$5
    cmds=""
    daemon_note=""

    #
    # Evaluate parameters and construct the approriate
    # commands variable of the service/facility. Each
    # parameter adds a small part to the variable name.
    #
    if test $daemon_num -eq 0       # "global" or daemon-specific commands
    then
        cmds=""
    elif test $daemon_num -le $MAX_DAEMONS
    then
        cmds=DAEMON${daemon_num}_
        daemon_note=" (DAEMON:$daemon_num)"
    else
        error "illegal daemon sequence number: $daemon_num"
        return 2
    fi

    case "$qual" in                 # pre- or post-command
    PRE)  cmds="${cmds}PRE_"  ;;
    POST) cmds="${cmds}POST_" ;;
    *) error "unsupported command qualifier: $qual"; return 2 ;;
    esac

    case "$type" in                 # start- or stop-command
    START) cmds="${cmds}START" ;;
    STOP)  cmds="${cmds}STOP"  ;;
    *) error "unsupported command type: $type"; return 2 ;;
    esac

    eval cmds=\$$cmds               # evaluate commands variable

    if test -n "$cmds"
    then
        [ "$VERBOSE" = "1" ] && $ECHO "Executing ${qual}_${type} commands \`$name'$daemon_note:"
        
        do_command_list "$cmds" $abort_on_err
        ret=$?
    else
        [ "$VERBOSE" = "1" ] && $ECHO "No ${qual}_${type} commands for \`$name'$daemon_note."
        ret=0
    fi

    return $ret
}


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


#/
#/ Execute list of commands. The command list is split into simple commands
#/ which are executed in sequence. Simple commands are separated from each
#/ other by an \em unquoted semicolon (`;'). In addition, a flag can be
#/ specified which controls whether to abort the command list if one of the
#/ simple commands fail, or ignore such a condition and just continue
#/ executing commands.
#/
#/ \param[in] $1  Command list to execute.
#/
#/ \param[in] $2  Abort as soon as a command fails (1) or just continue (0).
#/
#/ \returns
#/
#/ Return code 0 if all commands executed successfully, or 1 if a command(s)
#/ failed. Note that the value of $2 does not affect the return code. So, if
#/ $2 is set to 0 ("execute unconditionally") and a command fails, then the
#/ return code will still be 1.
#/
do_command_list()
{
    assert do_command_list "$2 -ne 0 -o $2 -ne 1"
    
    cmdlist=$1      # list of commands
    abort_on_err=$2 # abort executing command on first failure
    cmd=""          # single command
    result=""       # result code of command
    ret=0           # overall return code
    
    $ECHO $cmdlist | awk -f $RC_DIR/include/split_list.awk | while read cmd
    do
        [ "$VERBOSE" = "1" ] && $ECHO "> $cmd"
        
        $DRY_RUN eval $cmd >/dev/null 2>&1
        result=$?
        if test $result -ne 0
        then
            [ "$VERBOSE" = "1" ] && $ECHO "Exit code: $result"
            ret=1
            
            if test $abort_on_err -eq 1
            then
                break
            fi
        fi
    done

    return $ret
}
