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


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


DAEMONIZE_HELPER=/usr/sbin/daemonize


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


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


#/
#/ Format name for PID file of given daemon process. PID files are expected to
#/ be stored in the system's state directory. This function generates a
#/ pathname for the PID file of a daemon which consists of the system's state
#/ directory, the name of the daemon and a file extension. The formatted
#/ filename is echo'ed to stdout.
#/
#/ \param[in] $1  Pathname of daemon.
#/
fmt_pid_pathname()
{
    assert fmt_pid_pathname "-n '$1'"

    exec="$1"
    name=""
    pid_file=""

    name=`basename $exec`
    assert fmt_pid_pathname "-n '$name'"

    pid_file=$STATE_DIR/${name}.$PID_EXT

    $ECHO $pid_file
}


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


#/
#/ Return PID of daemon process. The PID value of the daemon is echo'ed to
#/ stdout if it can be determined (and was valid), otherwise the value 0
#/ echo'ed.
#/
#/ \param[in] $1  Sequence number of daemon. The sequence number references one
#/                of the daemon definitions in the service configuration file.
#/
#/ \todo
#/
#/ * Support for PID files containing multiple PID values.
#/ * Better determination PID values.
#/
get_pid()
{
    assert get_pid "$1 -gt 0 -a $1 -le $MAX_DAEMONS"

    num=$1              # daemon sequence number
    exec=""
    pid_file=""
    pid=0               # PID value from file
    alt_pid=0           # PID value from alternate source
    alt_exec=""         # Executable name from alternate source

    eval exec=\$DAEMON${num}_EXEC
    eval pid_file=\$DAEMON${num}_PID_FILE

    if test -n "$exec"
    then
        #
        # Get PID value from file. Use pre-defined PID file if specified,
        # otherwise "standard" PID file.
        #
        if test -z "$pid_file"
        then
            pid_file=`fmt_pid_pathname $exec`
        fi
        if test -f "$pid_file"
        then
            #
            # PID value should consist of digits only. Leading and
            # trailing whitespace is ignored, as are newlines.
            #
            pid=`head -1 "$pid_file" 2>/dev/null`
            if test -n "`$ECHO $pid | grep '^[0-9]\+$' 2>/dev/null`"
            then
                #
                # Translate PID value to command name and check whether
                # the command name maps to the configured executable name.
                #
                alt_exec=`ps --pid=$pid --no-headers -o cmd | cut -d' ' -f1`
                if test "$alt_exec" != "$exec"
                then
                    pid=0
                fi
            else
                error "invalid PID file: NaN: $pid"
                pid=0
            fi
        fi

        #
        # Get PID value from alternate source. The `pidof' command may
        # return multiple PIDs, PIDs of process' with the same name as
        # our daemon process...
        #
        if test $pid -eq 0
        then
            alt_pid=`pidof -s $exec 2>/dev/null`
            if test $? -ne 0
            then
                alt_pid=0
            fi
            pid=$alt_pid
        fi
    fi

    $ECHO $pid
}


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


#/
#/ Create PID file for given daemon process. The PID file is created in the
#/ system's state directory and contains the PID of the daemon. In case the
#/ PID file already exists, it will be silently overwritten.
#/
#/ \param[in] $1  Pathname of daemon.
#/
#/ \param[in] $2  PID of daemon.
#/
#/ \returns
#/
#/ Return code 0 if PID file was created successfully, otherwise 1.
#/
create_pid_file()
{
    assert create_pid_file "-n '$1'"
    assert create_pid_file "-n '`$ECHO $2 | egrep \"^[0-9]+\"`'"

    exec="$1"
    pid=$2
    pid_file=""
    state_dir=""
    ret=0

    pid_file=`fmt_pid_pathname $exec`

    state_dir=`dirname $pid_file`
    if test -d "$state_dir"
    then
        $ECHO $pid > $pid_file
        ret=$?
    else
        error "directory does not exist: $state_dir"
        ret=1
    fi

    return $ret
}


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


#/ 
#/ Delete PID file of given daemon process. The PID file is removed from the
#/ system's state directory.
#/
#/ \param[in] $1  Pathname of daemon.
#/
#/ \returns
#/
#/ Return code 0 if PID file was removed successfully, otherwise 1.
#/
remove_pid_file()
{
    assert remove_pid_file "-n '$1'"

    exec="$1"
    pid_file=""
    ret=0

    pid_file=`fmt_pid_pathname $exec`
    if test -f "$pid_file"
    then
        rm -f $pid_file
        ret=$?
    else
        error "file not found: $pid_file"
        ret=1
    fi

    return $ret
}


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


#/
#/ Start daemon process.
#/
#/ \param[in] $1  Sequence number of daemon. The sequence number references one
#/                of the daemon definitions in the service configuration file.
#/
#/ \returns
#/
#/ Return code 0 if daemon successfully started, otherwise 1.
#/
#/ \todo
#/
#/ * If daemon started but PID file couldn't be created, abort and kill daemon.
#/
execute_daemon()
{
    assert execute_daemon "$1 -gt 0 -a $1 -le $MAX_DAEMONS"

    num=$1
    exec=""
    args=""
    pid_file=""
    pid=0
    ret=0
    is_daemon=0
    daemonize_helper=""

    eval exec=\$DAEMON${num}_EXEC
    eval args=\$DAEMON${num}_ARGS
    eval pid_file=\$DAEMON${num}_PID_FILE
    eval is_daemon=\$DAEMON${num}_TRUE_DAEMON

    if test -x "$exec"              # ensure executable exists
    then
        if test -z "$is_daemon"     # HACK
        then
            is_daemon=1
        fi
        if test $is_daemon -ne 1
        then
            daemonize_helper=$DAEMONIZE_HELPER
        fi

        $daemonize_helper $exec $args >/dev/null 2>&1
        ret=$?                      # save return value of daemon
        pid=$!                      # save PID of daemon

        if test $ret -eq 0
        then
            # create PID file if necessary:
            if test -z "$pid_file"
            then
                create_pid_file $exec $pid
                if test $? -ne 0
                then
                    ret=1
                fi
            fi
        fi
    else
        ret=1
    fi

    return $ret
}


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


#/
#/ End daemon process. The function sends a series of signals to the daemon
#/ process which should terminate the daemon. Graceful signals are sent first,
#/ but if those don't terminate the daemon, a "hostile" signal is sent. 
#/
#/ \param[in] $1  Sequence number of daemon. The sequence number references one
#/                of the daemon definitions in the service configuration file.
#/
#/ \returns
#/
#/ Return code 0 if the daemon is no longer running. In case the daemon isn't
#/ running when this function is called, the return code is also 0.
#/ If the daemon couldn't be terminated, the return code is 1.
#/
terminate_daemon()
{
    assert terminate_daemon "$1 -gt 0 -a $1 -le $MAX_DAEMONS"

    num=$1
    exec=""
    pid_file=""
    sig=""
    pid=0
    running=1           # daemon running or not (boolean)
    ret=0

    eval exec=\$DAEMON${num}_EXEC
    eval pid_file=\$DAEMON${num}_PID_FILE
    eval sig=\$DAEMON${num}_KILL

    #
    # Try terminating daemon by first sending the specified signal. If
    # that doesn't work, try the KILL signal as a last resort. We'll
    # allow a pause of 1 second to give the daemon time to terminate.
    #
    pid=`get_pid $num`
    if test $pid -gt 0
    then
        # send specified signal:
        kill -$sig $pid 2>/dev/null
        sleep 1

        # check if daemon terminated:
        pid=`get_pid $num`
        if test $pid -gt 0
        then
            # daemon still running, send KILL signal:
            kill -KILL $pid 2>/dev/null
            sleep 1

            # check if daemon terminated:
            pid=`get_pid $num`
            if test $pid -eq 0
            then
                running=0
            fi
        else
            running=0
        fi
    else
        running=0
    fi

    if test $running -eq 0
    then
        if test -n "$pid_file"
        then
            rm -f $pid_file
        else
            remove_pid_file $exec
        fi
        # no error if failing to removed PID file:
        ret=0
    else
        ret=1
    fi

    return $ret
}


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


#/
#/ Return list of daemon pathnames of service. The pathnames are separated by
#/ whitespace and echo'ed to stdout.
#/
#/ The function executes in the context of the current service.
#/
list_service_daemons()
{
    exec=""
    list=""
    num=0

    for num in `list_daemon_defs`
    do
        eval exec=\$DAEMON${num}_EXEC
        list="$list $exec"
    done

    trim "$list"
}


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


#/
#/ List sequence numbers of defined daemons. Up until \c MAX_DAEMONS daemon
#/ definitions are scanned for in the service configuration file to determine
#/ the sequence numbers of valid daemon definitions.
#/
#/ The sequence numbers of the daemon definitions do not need to be sequential
#/ which means gaps are allowed. This function echoes to stdout, a
#/ whitespace-separated list of sequence numbers that contain a valid daemon
#/ definition.
#/
#/ The function executes in the context of the current service.
#/
list_daemon_defs()
{
    assert list_daemon_defs "$MAX_DAEMONS -gt 0"

    exec=""
    defs=""
    num=0

    for num in `seq $MAX_DAEMONS 2>/dev/null`
    do
        # non-empty DAEMON<num>_EXEC definition indicates a configured daemon:
        eval exec=\$DAEMON${num}_EXEC
        if test -n "$exec"
        then
            defs="$defs $num"
        fi
    done

    $ECHO $defs
}


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


#/
#/ Return number of daemons of service. A non-negative number is echo'ed to
#/ stdout indicating the number of daemons defined for the service.
#/
#/ The function executes in the context of the current service.
#/
num_service_daemons()
{
    $ECHO `list_service_daemons` | wc -w 2>/dev/null

    return $?
}


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


#/
#/ Determine whether service is running.
#/
#/ The function executes in the context of the current service.
#/
#/ \returns
#/
#/ Return code 0 if the service is fully running; code 1 if the service isn't
#/ running at all. The return code 2 indicates that the service is running
#/ deficiently, so part of the service is running while another part is not.
#/
#/ \todo
#/
#/ * Add code 3 to indicate more PIDs than there are daemons were found?
#/
is_service_running()
{
    num=0
    pid=0
    num_daemons=0
    num_pids=0
    ret=0

    # determine PID count of service daemons:
    num_pids=0
    for num in `list_daemon_defs`
    do
        pid=`get_pid $num`
        if test $pid -gt 0
        then
            num_pids=`expr "$num_pids" + "1" 2>/dev/null`
        fi
    done

    # translate to return code:
    if test $num_pids -gt 0                     # PIDs -> (partly) running
    then
        num_daemons=`num_service_daemons`
        if test $num_daemons -eq $num_pids      # equal -> fully running
        then
            ret=0
        elif test $num_daemons -gt $num_pids    # less PIDs -> partly running
        then
            ret=2
        else                                    # more PIDs -> not good
            ret=3
        fi
    else                                        # no PIDs -> not running
        ret=1
    fi

    return $ret
}
