#!/bin/bash

# 17 Aug 2020 Charles for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=968171
#   * Creation

# Function call tree
#    +
#    |
#    +-- initialise
#    |   |
#    |   +-- usage
#    |   |
#    |   +-- do_pid
#    |
#    |-- watch
#    |
#    +-- finalise
#
# Utility functions called from various places:
#    ck_file ck_uint fct msg

#--------------------------
# Name: ck_file
# Purpose: for each file listed in the argument list: checks that it is 
#   * reachable and exists
#   * is of the type specified (block special, ordinary file or directory)
#   * has the requested permission(s) for the user
#   * optionally, is absolute (begins with /)
# Usage: ck_file [ path <file_type>:<permissions>[:[a]] ] ...
#   where 
#     file  is a file name (path)
#     file_type  is b (block special file), f (file) or d (directory)
#     permissions  is none or more of r, w and x
#     a  requests an absoluteness test (that the path begins with /)
#   Example:
#     buf=$(ck_file foo f:rw 2>&1)
#     if [[ $buf != '' ]]; then
#          msg W "$buf"
#          fct "${FUNCNAME[0]}" 'returning 1'
#          return 1
#     fi
# Outputs:
#   * For the first requested property each file does not have, a message to
#     stderr
#   * For the first detected programminng error, a message to
#     stderr
# Returns: 
#   0 when all files have the requested properties
#   1 when at least one of the files have the requested properties
#   2 when a programming error is detected
#--------------------------
function ck_file {

    local absolute_flag buf file_name file_type perm perms retval

    # For each file ...
    # ~~~~~~~~~~~~~~~~~
    retval=0
    while [[ $# -gt 0 ]]
    do  
        file_name=$1
        file_type=${2%%:*}
        buf=${2#$file_type:}
        perms=${buf%%:*}
        absolute=${buf#$perms:}
        [[ $absolute = $buf ]] && absolute=
        case $absolute in 
            '' | a )
                ;;
            * )
                echo "ck_file: invalid absoluteness flag in '$2' specified for file '$file_name'" >&2
                return 2
        esac
        shift 2

        # Is the file reachable and does it exist?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        case $file_type in
            b ) 
                if [[ ! -b $file_name ]]; then
                    echo "file '$file_name' is unreachable, does not exist or is not a block special file" >&2
                    retval=1
                    continue
                fi  
                ;;  
            f ) 
                if [[ ! -f $file_name ]]; then
                    echo "file '$file_name' is unreachable, does not exist or is not an ordinary file" >&2
                    retval=1
                    continue
                fi  
                ;;  
            d ) 
                if [[ ! -d $file_name ]]; then
                    echo "directory '$file_name' is unreachable, does not exist or is not a directory" >&2
                    retval=1
                    continue
                fi
                ;;
            * )
                echo "Programming error: ck_file: invalid file type '$file_type' specified for file '$file_name'" >&2
                return 2
        esac

        # Does the file have the requested permissions?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        buf="$perms"
        while [[ $buf ]]
        do
            perm="${buf:0:1}"
            buf="${buf:1}"
            case $perm in
                r )
                    if [[ ! -r $file_name ]]; then
                        echo "$file_name: no read permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                w )
                    if [[ ! -w $file_name ]]; then
                        echo "$file_name: no write permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                x )
                    if [[ ! -x $file_name ]]; then
                        echo "$file_name: no execute permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                * )
                    echo "Programming error: ck_file: invalid permisssion '$perm' requested for file '$file_name'" >&2
                    return 2
            esac
        done

        # Does the file have the requested absoluteness?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        if [[ $absolute = a && ${file_name:0:1} != / ]]; then
            echo "$file_name: does not begin with /" >&2
            retval=1
        fi

    done

    return $retval

}  #  end of function ck_file

#--------------------------
# Name: ck_uint
# Purpose: checks for a valid unsigned integer
# Usage: ck_uint <putative uint>
# Outputs: none
# Returns: 
#   0 when $1 is a valid unsigned integer
#   1 otherwise
#--------------------------
function ck_uint {
    local regex='^[[:digit:]]+$'
    [[ $1 =~ $regex ]] && return 0 || return 1
}  #  end of function ck_uint

#--------------------------
# Name: do_pid
# Purpose:
#   * If can take an exclusive lock on a PID file
#         Writes a record to it identifying the current process
#     Else
#         Exits via writing an error message via msg
# Arguments:
#    $1 - the script's argument list
# Outputs: none except via msg()
# Global variables
#    Read:
#        pid_dir: read
#        my_name: read
#    Set:
#        pid_file_locked_flag: set
#        pid_fn: set
# Returns: 
#   0 on success
#   Does not return otherwise
# Usage notes:
#   * Caller should ensure the PID directory exists and has rwx permissions
#--------------------------
function do_pid {
    fct "${FUNCNAME[0]}" 'started'
    local pid_contents
    
    pid_fn=$pid_dir/${my_name%.*}.pid

    [[ -r $pid_fn ]] && pid_contents=$(< "$pid_fn")
    exec 9>>"$pid_fn"
    if flock --exclusive --timeout 1 9; then
        pid_file_locked_flag=$true
        msg D "Taken lock on PID file $pid_fn"
        echo "$(date '+%b %e %X') $my_name [$$]: arguments: $1" >> "$pid_fn"
        msg I "Created PID file $pid_fn containing:$msg_lf$(< "$pid_fn")"
    else
        msg E "Another instance is running.  Contents of $pid_fn: $pid_contents"
    fi  
        
    fct "${FUNCNAME[0]}" 'returning'
}  #  end of function do_pid

#--------------------------
# Name: fct
# Purpose: function call trace (for debugging)
# $1 - name of calling function 
# $2 - message.  If it starts with "started" or "returning" then the output is prettily indented
#--------------------------
function fct {

    if [[ ! $debugging_flag ]]; then
        return 0
    fi  

    fct_indent="${fct_indent:=}"

    case $2 in
        'started'* )
            fct_indent="$fct_indent  "
            msg D "$fct_indent$1: $2"
            ;;  
        'returning'* )
            msg D "$fct_indent$1: $2"
            fct_indent="${fct_indent#  }"  
            ;;  
        * ) 
            msg D "$fct_indent$1: $2"
    esac

}  # end of function fct

#--------------------------
# Name: finalise
# Purpose: cleans up and exits
# Arguments:
#    $1  return value
# Return code (on exit): 
#   The sum of zero plus
#      1 if any warnings
#      2 if any errors
#      4,8,16 unused
#      32 if terminated by a signal
#--------------------------
function finalise {
    fct "${FUNCNAME[0]}" "started with args $*"

    finalising_flag=$true

    # Temporary directory and PID file removal
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    if [[ $pid_file_locked_flag && -f "$pid_fn" ]]; then
        msg I "Removing PID file $pid_fn"
        rm "$pid_fn"
    fi

    # Remove old logs
    # ~~~~~~~~~~~~~~~
    if [[ ${log_fn%/*} != /dev ]]; then
        msg I 'Removing old logs'
        buf=$(
            find "$log_dir" \
                -maxdepth 1 \
                -regextype posix-egrep \
                -regex ".*/$log_fn_pat" \
                -mtime +7 \
                -delete \
                2>&1
        )
        [[ $buf != '' ]] && msg W "Problem removing old logs: $buf"
    fi

    # Interrupted?  Message and exit return value
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    my_retval=0
    if ck_uint "${1:-}" && (($1>128)); then
        ((my_retval+=32))
        case $1 in
            129 )
                buf=SIGHUP
                ;;
            130 )
                buf=SIGINT
                ;;
            131 )
                buf=SIGQUIT
                ;;
            132 )
                buf=SIGILL
                ;;
            134 )
                buf=SIGABRT
                ;;
            135 )
                buf=SIGBUS
                ;;
            136 )
                buf=SIGFPE
                ;;
            138 )
                buf=SIGUSR1
                ;;
            139 )
                buf=SIGSEGV
                ;;
            140 )
                buf=SIGUSR2
                ;;
            141 )
                buf=SIGPIPE
                ;;
            142 )
                buf=SIGALRM
                ;;
            143 )
                buf=SIGTERM
                ;;
            146 )
                buf=SIGCONT
                ;;
            147 )
                buf=SIGSTOP
                ;;
            148 )
                buf=SIGTSTP
                ;;
            149 )
                buf=SIGTTIN
                ;;
            150 )
                buf=SIGTTOU
                ;;
            151 )
                buf=SIGURG
                ;;
            152 )
                buf=SIGCPU
                ;;
            153 )
                buf=SIGXFSZ
                ;;
            154 )
                buf=SIGVTALRM
                ;;
            155 )
                buf=SIGPROF
                ;;
            * )
                msg E "${FUNCNAME[0]}: programming error: \$1 ($1) not serviced"
                ;;
        esac
        interrupt_flag=$true
        msg="Finalising on $buf"
        msg E "$msg"    # Returns because finalising_flag is set
    fi

    # Exit return value adjustment
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    if [[ $warning_flag ]]; then
        msg I "There was at least one WARNING"
        ((my_retval+=1))
    fi
    if [[ $error_flag ]]; then
        msg I "There was at least one ERROR"
        ((my_retval+=2))
    fi
    [[ $interrupt_flag ]] && msg I 'There was at least one interrupt'
    if ((my_retval==0)) && ((${1:-0}!=0)); then
        msg E 'There was an error not reported in detail (probably by ... || finalise 1)'
        my_retval=2
    fi
    msg I "Exiting with return value $my_retval"

    # Exit
    # ~~~~
    fct "${FUNCNAME[0]}" 'exiting'
    exit $my_retval
}  # end of function finalise

#--------------------------
# Name: initialise
# Purpose: sets up environment, parses command line, reads config file
#--------------------------
function initialise {
    local args buf conf_fn emsg opt opt_l_flag usage_reports_csv_fn

    # Configure shell environment
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    export LANG=en_GB.UTF-8
    export LANGUAGE=en_GB.UTF-8
    for var_name in LC_ADDRESS LC_ALL LC_COLLATE LC_CTYPE LC_IDENTIFICATION \
        LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER \
        LC_TELEPHONE LC_TIME 
    do
        unset $var_name
    done

    export PATH=/usr/sbin:/sbin:/usr/bin:/bin
    IFS=$' \n\t'
    set -o nounset
    shopt -s extglob            # Enable extended pattern matching operators
    unset CDPATH                # Ensure cd behaves as expected
    umask 022

    # Initialise some global logic variables
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    readonly false=
    readonly true=true
    
    debugging_flag=$false
    error_flag=$false
    finalising_flag=$false
    interrupt_flag=$false
    logging_flag=$false
    pid_file_locked_flag=$false
    warning_flag=$false

    # Initialise some global string variables
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    readonly my_name=${0##*/}
    readonly msg_lf=$'\n    '              # Message linefeed and indent
    readonly pid_dir=/run
    log_dir=/var/log/${my_name%.*}
    log_file_name_date_format='%Y-%m-%d'
    log_msg_timestamp_format='%H:%M:%S'
    syslog_dir=/srv/syslog

    # Parse command line
    # ~~~~~~~~~~~~~~~~~~
    args=("$@")
    args_org="$*"
    conf_fn=/usr/local/etc/${my_name%.*}.conf
    emsg=
    log_fn=/dev/stderr
    opt_f_flag=$false
    while getopts :c:dhl opt "$@"
    do
        case $opt in
            c )
                conf_fn=$OPTARG
                ;;
            d )
                debugging_flag=$true
                ;;
            h )
                debugging_flag=$false
                usage verbose
                exit 0
                ;;
            l )
                log_fn=/dev/tty
                ;;
            : )
                emsg+=$msg_lf"Option $OPTARG must have an argument"
                [[ $OPTARG = c ]] && { opt_c_flag=$true; conf_fn=/bin/bash; }
                ;;
            * )
                emsg+=$msg_lf"Invalid option '-$OPTARG'"
        esac
    done

    # Check for mandatory options missing
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # There are no mandatory options

    # Test for mutually exclusive options
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # There are no mutually exclusive options

    # Validate option values
    # ~~~~~~~~~~~~~~~~~~~~~~
    [[ ! -r "$conf_fn" ]] \
        && emsg+=$msg_lf"$conf_fn does not exist or is not readable"

    # Test for extra arguments
    # ~~~~~~~~~~~~~~~~~~~~~~~~
    shift $(($OPTIND-1))
    if [[ $* != '' ]]; then
        emsg+=$msg_lf"Invalid extra argument(s) '$*'"
    fi

    # Report any errors
    # ~~~~~~~~~~~~~~~~~
    if [[ $emsg != '' ]]; then
        msg E "$emsg"
    fi

    # Read conffile
    # ~~~~~~~~~~~~~
    source "$conf_fn"
    buf=$(ck_file "$log_dir" d:rwx 2>&1)
    [[ $buf != '' ]] && emsg+="$msg_lf$buf"
    buf=$(ck_file "$syslog_dir" d:rwx 2>&1)
    [[ $buf != '' ]] && emsg+="$msg_lf$buf"
    if [[ $emsg != '' ]]; then
        msg E "$conf_fn errors:$emsg"
    fi

    # Set up logging
    # ~~~~~~~~~~~~~~
    if [[ $log_fn = /dev/stderr ]]; then
        # -l option not used to log to /dev/tty so log to file
        buf=$(ck_file "$log_dir" d:rwx 2>&1)
        [[ $buf != '' ]] && msg E "$buf"
        log_fn=$log_dir/${my_name%.*}.not_by_msg.log
    fi
    readonly log_fn_pat="${my_name%.*}.[^.]+\.log\$"
    exec &>>"$log_fn"    # For output not by function msg such as bash errors
    fct "${FUNCNAME[0]}" 'started (this message delayed until logging set up)'
    msg I "$my_name: PID: $$, PPID: $PPID, args: $args_org"
    msg I "Configuration file: $conf_fn"

    # Set traps
    # ~~~~~~~~~
    trap 'finalise 129' 'HUP'
    trap 'finalise 130' 'INT'
    trap 'finalise 131' 'QUIT'
    trap 'finalise 132' 'ILL'
    trap 'finalise 134' 'ABRT'
    trap 'finalise 135' 'BUS'
    trap 'finalise 136' 'FPE'
    trap 'finalise 138' 'USR1'
    trap 'finalise 139' 'SEGV'
    trap 'finalise 140' 'USR2'
    trap 'finalise 141' 'PIPE'
    trap 'finalise 142' 'ALRM'
    trap 'finalise 143' 'TERM'
    trap 'finalise 146' 'CONT'
    trap 'finalise 147' 'STOP'
    trap 'finalise 148' 'TSTP'
    trap 'finalise 149' 'TTIN'
    trap 'finalise 150' 'TTOU'
    trap 'finalise 151' 'URG'
    trap 'finalise 152' 'XCPU'
    trap 'finalise 153' 'XFSZ'
    trap 'finalise 154' 'VTALRM'
    trap 'finalise 155' 'PROF'

    # Create and lock the PID file
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    buf=$(ck_file "$pid_dir" d:rwx 2>&1)
    [[ $buf != '' ]] && msg E "$buf"
    do_pid "$(printf '%q ' "${args[@]}")"

    # Change working directory
    # ~~~~~~~~~~~~~~~~~~~~~~~~
    cd "$syslog_dir"
    (($?!=0)) && msg E "Unable to cd to $syslog_dir"
    msg I "Changed working directory to $PWD"

    fct "${FUNCNAME[0]}" 'returning'
}  # end of function initialise

#--------------------------
# Name: msg
# Purpose: generalised messaging interface
# Arguments:
#    $1 class: D, E, I or W indicating Debug, Error, Information or Warning
#    $2 message text
# Global variables read:
#     debugging_flag
# Global variables written:
#     error_flag
#     warning_flag
# Output: information messages to stdout; the rest to stderr
# Returns: 
#   Does not return (calls finalise) when class is E for error
#   Otherwise returns 0
#--------------------------
function msg {
    local buf class logger_flag message_text prefix

    # Process arguments
    # ~~~~~~~~~~~~~~~~~
    class="${1:-}"
    message_text="${2:-}"

    # Class-dependent set-up
    # ~~~~~~~~~~~~~~~~~~~~~~
    logger_flag=$false
    case "$class" in  
        D ) 
            [[ ! $debugging_flag ]] && return
            prefix='DEBUG: '
            ;;  
        E ) 
            error_flag=$true
            prefix='ERROR: '
            logger_flag=$true
            ;;  
        I ) 
            prefix=
            ;;  
        W ) 
            warning_flag=$true
            prefix='WARN: '
            ;;  
        * ) 
            msg E "msg: invalid class '$class': '$*'"
    esac
    message_text="$prefix$message_text"

    # Write to syslog
    # ~~~~~~~~~~~~~~~
    if [[ $logger_flag ]]; then
        buf=$(logger -t "$my_name[$$]" -- "$message_text" 2>&1)
        [[ $buf != '' ]] && msg W "${FUNCNAME[0]}: problem writing to syslog: $buf"
    fi

    # Write to log
    # ~~~~~~~~~~~~
    message_text="$(date "+$log_msg_timestamp_format") $message_text"
    [[ ${log_fn%/*} != /dev ]] \
        && log_fn="$log_dir/${my_name%.*}-$(date +$log_file_name_date_format).log"
    if [[ $class = I ]]; then
        echo "$message_text" >> "$log_fn"
    else
        echo "$message_text"  >> "$log_fn"
        if [[ $class = E ]]; then
            [[ ! $finalising_flag ]] && finalise 1 
        fi
    fi  

    return 0
}  #  end of function msg

#--------------------------
# Name: usage
# Purpose: prints usage message
#--------------------------
function usage {
    local msg usage

    # Build the messages
    # ~~~~~~~~~~~~~~~~~~
    usage="usage: $my_name [-c <conf_file>] [-d] [-h] [-l]"
    msg='  where:'
    msg+=$'\n    -c specifies the configuration file, default '"$conf_fn"
    msg+=$'\n    -d turns debugging on'
    msg+=$'\n    -h prints this help and exits'
    msg+=$'\n    -l logs to /dev/tty, normally terminal'

    # Display the message(s)
    # ~~~~~~~~~~~~~~~~~~~~~~
    echo "$usage" >&2
    if [[ ${1:-} != 'verbose' ]]; then
        echo "(use -h for help)" >&2
    else
        echo "$msg" >&2
    fi
}  # end of function usage

#--------------------------
# Name: watch
# Purpose:
#   * Watch for a new directory with name like an IPv4 address
#   * On detecting one, restart rsyslog.service
#--------------------------
function watch {
    fct "${FUNCNAME[0]}" started
    local buf msg 
    local -r ipv4_regex='^[.0-9]{7,15}$'    # Not exact but good enough

    msg I "Starting inotifywait"
    inotifywait --event create --monitor --quiet --format '%f' "$syslog_dir" \
        | while read fn
    do
        msg I "inotifywait reported new file '$fn'"
        if [[ -d "$fn" && "$fn" =~ $ipv4_regex ]]; then
            msg="$fn is a directory with name like an IPv4 address"
            msg I "$msg.  Restarting rsyslog"
            systemctl restart rsyslog.service
        else
            msg="$fn is not a directory with name like an IPv4 address"
            msg I "$msg.  Not restarting rsyslog"
        fi
    done

    fct "${FUNCNAME[0]}" returning
}  # end of function watch

#--------------------------
# Name: main
# Purpose: the main sequence; execution starts here
#--------------------------
initialise "${@:-}"
watch
msg E 'Function watch unexpectedly returned'
