#!/bin/bash
#==============================================================================
#
#  FILE         : autoshutdown
#  DESCRIPTION  : Shuts-down a server based on a set of configurable checks.
#  REQUIREMENTS : Debian / Ubuntu-based system.
#  BUGS         : https://github.com/OpenMediaVault-Plugin-Developers/openmediavault-autoshutdown
#  AUTHOR       : Solo0815 - R. Lindlein (Ubuntu-Port, OMV-Changes)
#                 based on autoshutdown.sh v0.7.008 by chrikai, see:
#                 https://sourceforge.net/apps/phpbb/freenas/viewtopic.php?f=12&t=2158&start=60
#
#==============================================================================

set -euo pipefail
shopt -s extglob

export LC_ALL=C.UTF-8

######## STORAGE DEFINITIONS #########
declare -a NICS=()        # Array for storing active _check_networkconfig
                          # network interface names.
declare -A PORT_MAP=()    # Associative array for storing _check_net_status
                          # custom port to service mappings.
declare -A NIC_2_IPV4     # Associative array for storing _check_networkconfig
                          # network interface to IPv4 address mappings.
declare -A NIC_2_IPV6     # Associative array for storing _check_networkconfig
                          # network interface to IPv6 address mappings.
declare -A PINGLIST_IFACE # Associative array for storing _ping_range network
                          # interface IP addresses.
declare -A P_HDDIO_DEVS   # Associative array for storing _check_hddio read,
                          # wrtn and epoch values.
declare -A P_UL_DL_RATE   # Associative array for storing _check_ul_dl_rate a
                          # network interface tx, rx and epoch values.

######## CONSTANT DEFINITIONS ########
                          # Autoshutdown configuration.
readonly CONFIG="${1:-/etc/autoshutdown.conf}"
                          # Autoshutdown default configuration.
readonly DEFAULT_CONFIG="${2:-/etc/autoshutdown.default}"
                          # Directory containing plugin checks.
readonly PLUGIN_DIR="${3:-/etc/autoshutdown.d}"

### REGULAR EXPRESSION DEFINITIONS ###
                          # Network interface name regular expression.
readonly R_IFACE="[^ /]{1,15}"
                          # FQDN validation regular expression parts.
readonly R_PART="[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]"
readonly R_FQDN="(${R_PART})([.](${R_PART}))"
                          # IPv4 address regular expression parts.
readonly R_OCTET="25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]"
readonly R_IPV4="(((${R_OCTET})[.]){3})(${R_OCTET})"
                          # IPv4 address range regular expression for IPCHECK.
readonly R_RANGE_IPV4="(((${R_OCTET})[.]){3})?((${R_OCTET})[.]{2}(${R_OCTET}))"
                          # IPv6 address regular expression parts.
readonly R_HEXTET="[0-9a-fA-F]{1,4}"
R_IPV6="(${R_HEXTET}:){7}${R_HEXTET}|"
R_IPV6+="(${R_HEXTET}:){1,7}:|"
R_IPV6+="(${R_HEXTET}:){1,6}:${R_HEXTET}|"
R_IPV6+="(${R_HEXTET}:){1,5}(:${R_HEXTET}){1,2}|"
R_IPV6+="(${R_HEXTET}:){1,4}(:${R_HEXTET}){1,3}|"
R_IPV6+="(${R_HEXTET}:){1,3}(:${R_HEXTET}){1,4}|"
R_IPV6+="(${R_HEXTET}:){1,2}(:${R_HEXTET}){1,5}|"
R_IPV6+="${R_HEXTET}:((:${R_HEXTET}){1,6})|"
R_IPV6+=":((:${R_HEXTET}){1,7}|:)|"
R_IPV6+="[fF][eE]80:(:${R_HEXTET}){0,4}%[${R_IFACE}]|"
R_IPV6+="::([fF]{4}(:0{1,4}){0,1}:){0,1}${R_IPV4}|"
R_IPV6+="(${R_HEXTET}:){1,4}:${R_IPV4}"
readonly R_IPV6
                          # IPv6 address range regular expression for IPCHECK.
R_RANGE_IPV6="((${R_HEXTET}:){7}|"
R_RANGE_IPV6+="(${R_HEXTET}:){1,6}:|"
R_RANGE_IPV6+="(${R_HEXTET}:){1,5}(:${R_HEXTET}):|"
R_RANGE_IPV6+="(${R_HEXTET}:){1,4}(:${R_HEXTET}){1,2}:|"
R_RANGE_IPV6+="(${R_HEXTET}:){1,3}(:${R_HEXTET}){1,3}:|"
R_RANGE_IPV6+="(${R_HEXTET}:){1,2}(:${R_HEXTET}){1,4}:|"
R_RANGE_IPV6+="${R_HEXTET}:((:${R_HEXTET}){1,5}):|"
R_RANGE_IPV6+=":((:${R_HEXTET}){1,6}:|:))?"
R_RANGE_IPV6+="((0[xX](${R_HEXTET}))[.]{2}(0[xX](${R_HEXTET})))"
readonly R_RANGE_IPV6

####### FUNCTION DECLARATIONS ########

###############################################################################
#
#   Name       : _log
#   Parameters : $1 : Log message in format "PRIORITY: MESSAGE".
#              : $2 : 'force' will force logging to Syslog and sent to stderr.
#   Globals    : $VERBOSE    : Is verbose mode enabled allowing DEBUG output.
#                              ('true' or 'false')
#              : $FAKE       : Are we running in fake mode. ('true' or 'false')
#              : $SYSLOG     : Should logging to to Syslog. ('true' or 'false')
#              : $LOG_STDOUT : Suppress logging to stdout. Set 'false' in a
#                              systemd service. (default 'true')
#   Return     : 0 on success.
#
_log() {
    local -r user="$(id -nu "$(< "/proc/self/loginuid")" 2>/dev/null || true)"
    local -r tag="$(basename "${0}")"
    local -r priority="${1%%: *}"

    [[ "${priority}" == "DEBUG" && "${VERBOSE:-"true"}" != "true" ]] &&
        return 0

    local message="${FUNCNAME[1]}(): ${1#*: }"
    [ "${FAKE:-}" == "true" ] && message="FAKE-Mode: ${message}"
    local -r message="${user:-${USER}}: ${priority}: '${message}'"

    [ "${LOG_STDOUT:-}" != "false" ] &&
        echo "$(date "+%b %e %H:%M:%S") $(hostname -s
                ) ${tag}[${$}]: ${message}"

    [[ "${SYSLOG:-}" == "true" || "${2:-}" == "force" ]] &&
        logger --id="${$}" -t "${tag}" -p "syslog.${priority,,}" "${message}"

    return 0
}


###############################################################################
#
#   Name       : _ping_range
#   Parameters : $1 : The network interface that is being worked on.
#              : $2 : The network interface IPv4 address.
#              : $3 : The network interface IPv6 address.
#              : $4 : IP addresses found for all network interfaces.
#   Globals    : $PINTLIST       : File reference of IP's/FQDN's to be checked
#                                  (expert setting overrides $RANGE).
#              : $RANGE          : Details of IP's/FQDN's to be checked.
#              : $PINGLIST_IFACE : Associative array storing network interface
#                                  IP addresses.
#              : $R_IFACE        : Network interface name regular expression.
#              : $R_OCTET        : Octet regular expression.
#              : $R_IPV4         : IPv4 address regular expression.
#              : $R_RANGE_IPV4   : IPv4 ping range regular expression.
#              : $R_HEXTET       : Fore hexadecimal digits.
#              : $R_IPV6         : IPv6 address regular expression.
#              : $R_RANGE_IPV6   : IPv6 ping range regular expression.
#              : $R_FQDN         : FQDN regular expression.
#   Return     : Number of active IP on network interfaces.
#
_ping_range() {
    local -r net_iface="${1}"
    local -r srv_ipv4="${2}"
    local -r srv_ipv6="${3}"
    local -r ips_regex="^\\(${4// /\\|}\\)$"

    local ip_list=()
    local pinglist_array=()

    local -r empty="No IP addresses specified"
    local -r v4_c="${srv_ipv4%.*}"

    if [[ "${srv_ipv6}" =~ ((%${R_IFACE})|(${R_IPV4}))$ ]]; then
        local -r v6_c=""
    elif [[ "${srv_ipv6}" =~ ^((${R_HEXTET}:?){1,7})::$ ]]; then
        local -r v6_c="${BASH_REMATCH[1]}"
    else
        local -r v6_c="${srv_ipv6%:*}"
    fi

    _log "DEBUG: INTERFACE: ${net_iface}"
    _log "DEBUG: ADRESSES TO EXTRACT: ${ips_regex}"
    _log "DEBUG: IPv4 CLASS: ${v4_c:-N/A}"
    _log "DEBUG: IPv6 CLASS: ${v6_c:-N/A}"
    _log "DEBUG: PINGLIST: ${PINGLIST:-N/A}"
    _log "DEBUG: RANGE: ${RANGE:-N/A}"

    local -r key="${PINGLIST:-"${net_iface}"}"
    [ -z "${PINGLIST_IFACE["${key}"]:-}" ] && {
        _log "DEBUG: Generating new PINGLIST_IFACE[${key}]"

        if [ -n "${PINGLIST}" ]; then
            mapfile -t ip_list < "${PINGLIST}"
        else
            for rg in ${RANGE//,/ }; do
                if [[ "${rg}" =~ ^(${R_IFACE})[@] &&
                        "${BASH_REMATCH[1]}" != "${net_iface}" ]]; then
                    continue

                elif [[ "${rg}" =~ ^(${R_IFACE}[@])?(${R_FQDN})$ ]]; then
                    mapfile -t -O "${#ip_list[@]}" ip_list <<< \
                        "${BASH_REMATCH[2]}"

                elif [[ -n "${srv_ipv4}" && "${rg}" =~ \
                        ^(${R_IFACE}[@])?((${R_OCTET})|(${R_IPV4}))$ ]]; then
                    mapfile -t -O "${#ip_list[@]}" ip_list <<< \
                        "${BASH_REMATCH[4]:-"${v4_c}."}${BASH_REMATCH[3]}"
                elif [[ -n "${srv_ipv4}" &&
                        "${rg}" =~ ^(${R_IFACE}[@])?(${R_RANGE_IPV4})$ ]]; then
                    local min="${BASH_REMATCH[7]}" max="${BASH_REMATCH[8]}"
                    [ "${BASH_REMATCH[8]}" -lt "${BASH_REMATCH[7]}" ] && {
                        min="${BASH_REMATCH[8]}"; max="${BASH_REMATCH[7]}"; }
                    mapfile -t -O "${#ip_list[@]}" ip_list < <(
                        seq -f "${BASH_REMATCH[3]:-"${v4_c}."}%g" \
                            "${min}" "${max}")

                elif [[ -n "${srv_ipv6}" && "${rg}" =~ \
                        ^(${R_IFACE}[@])?((0[xX](${R_HEXTET}))|(${R_IPV6}))$ ]]
                then
                    mapfile -t -O "${#ip_list[@]}" ip_list < <(
                        awk '{sub(/::0$/,"::",$0); print tolower($0)}' <<< \
                            "$(printf "${BASH_REMATCH[5]:-"${v6_c}:"}%x\\n" \
                                "$((16#${BASH_REMATCH[4]}))")")
                elif [[ -n "${srv_ipv6}" && "${rg}" =~ \
                        ^(${R_IFACE}[@])?(${R_RANGE_IPV6})$ ]]; then
                    local min="${BASH_REMATCH[20]}" max="${BASH_REMATCH[22]}"
                    [ $((16#${BASH_REMATCH[22]})) -lt \
                            $((16#${BASH_REMATCH[20]})) ] && {
                        min="${BASH_REMATCH[22]}"; max="${BASH_REMATCH[20]}"; }
                    mapfile -t -O "${#ip_list[@]}" ip_list < <(
                        awk '{sub(/::0$/,"::",$0); print tolower($0)}' <<< \
                            "$(printf "${BASH_REMATCH[3]:-"${v6_c}:"}%x\\n" \
                                $(seq $((16#${min})) $((16#${max}))))")
                fi
            done
        fi
        PINGLIST_IFACE["${key}"]="$(
            awk '{gsub(/ /,"\n",$0)}
                 /:/{print "z"$0}!/:/{print}' <<< "${ip_list[@]}" | sort -Vu |
            awk -Fz '/:/{print $2}!/:/{print}' |
            grep -v "${ips_regex,,}")"; }
    mapfile -t pinglist_array <<< "${PINGLIST_IFACE["${key}"]:-${empty}}"

    [ "${pinglist_array[0]}" == "${empty}" ] && {
        _log "INFO: No IP addresses specified for: ${net_iface} -> next check"
        return 0; }

    local pinglist_subset; while read -r pinglist_subset; do
        _log "DEBUG: pinglist_array${cont:-}: ${pinglist_subset}"
        local cont=" continued"
    done < <(xargs -n 2048 <<< "${pinglist_array[*]}")

    _log "INFO: Retrieve list of active IPs for: ${net_iface}"
    local -r fping_result="$(LC_ALL=C
        xargs fping --iface="${net_iface}" --quiet --alive \
                    --retry=1 --addr <<< "${pinglist_array[*]}" |
        grep -v "${ips_regex,,}" || true)"

    [ -z "${fping_result}" ] && {
        _log "INFO: No active IPs found in the specified range -> next check"
        return 0; }

    local active_ip; while read -r active_ip; do
        _log "INFO: ${active_ip} is active"
    done < <(awk '/:/{print "z"$0}!/:/{print}' <<< "${fping_result}" |
             sort -Vu | awk -Fz '/:/{print $2}!/:/{print}')

    _log "INFO: Active IPs found in the specified range -> no shutdown"
    return "$(wc -l <<< "${fping_result}")"
}


###############################################################################
#
#   Name       : _shutdown
#   Parameters : None
#   Globals    : $SHUTDOWNCOMMAND : The shutdown command to use.
#              : $FAKE            : In fake mode do not run the shutdown
#                                   command just stop.
#   Return     : None, script exit point.
#
_shutdown() {
    _log "INFO: Shutdown issued: ${SHUTDOWNCOMMAND}" force

    [ "${FAKE}" == "true" ] && {
        _log "WARNING: Shutdown command not executed in FAKE-Mode" force
        _log "INFO: Autoshutdown will end here"
        exit 0; }

    # Write everything to disk/stick and shutdown, hibernate, ....
    local shutdowncmd; mapfile -t shutdowncmd <<< "${SHUTDOWNCOMMAND// /$'\n'}"
    sync && "${shutdowncmd[@]}" && exit 0
    _log "ERR: Shutdown failed. Exiting ..." force
    exit 142
}


###############################################################################
#
#   Name       : _check_processes
#   Parameters : None
#   Globals    : $LOADPROCNAMES : Names of processes with load dependent
#                                 children.
#              : $TEMPPROCNAMES : Names of processes only stated when active.
#   Return     : 0         : If no active process has been found.
#              : 1 or more : If at least one active process has been found.
#
_check_processes() {
    # Check for each given command name in LOADPROCNAMES if it is currently
    # stated active in "top". If the process has been found, there are
    # processes that are always present in "ps" or "top" output (This could be
    # due to the "daemon" mechanism.) So to identify if is something happening
    # with these processes, is to check if "top" states them active hence the
    # "-i" parameter is required.
    local num_load_proc=0
    for process in ${LOADPROCNAMES//,/ }; do
        local lp=0 l_proc=0
        for (( n=0; n < LPREPEAT; n++ )); do
            local lp_result; lp_result="$(LC_ALL=C
                top -i -b -d 1 -n 1 |
                awk -v regex="^${process}$" -v count=0 \
                    '$NF ~ regex {count++} END{print count}')"
            lp=$((lp | lp_result))
            l_proc=$((l_proc + lp_result))
        done
        num_load_proc=$((num_load_proc + lp))
        _log "DEBUG: Load processes: ${process}: Found ${l_proc} active after ${LPREPEAT} cycles"
    done
    [ "${num_load_proc}" -gt 0 ] &&
        _log "INFO: Found ${num_load_proc} active from: ${LOADPROCNAMES}"

    # Check for each given command name in TEMPPROCNAMES if it is currently
    # stated present in "top". It is not guaranteed, that top states these
    # services active, as they usually wait for user input, so no "-i"
    # parameter is used on the command line, but the shear presence of the
    # process is enough.
    local num_temp_proc=0
    for process in ${TEMPPROCNAMES//,/ }; do
        local tp=0 t_proc=0
        for (( n=0; n < TPREPEAT; n++ )); do
            local tp_result; tp_result="$(LC_ALL=C
                top -b -d 1 -n 1 |
                awk -v regex="^${process}$" -v count=0 \
                    '$NF ~ regex {count++} END{print count}')"
            # If there is no process with top, try "pgrep", but not in.tftpd.
            [ "${tp_result}" -eq 0 ] && {
                [ "${process}" == "in.tftpd" ] && {
                    _log "DEBUG: Temp processes: Skipping 'in.tftpd'"
                    break; }
                tp_result="$(LC_ALL=C pgrep -fc "${process}")"; }
            tp=$((tp | tp_result))
            t_proc=$((t_proc + tp_result))
        done
        num_temp_proc=$((num_temp_proc + tp))
        _log "DEBUG: Temp processes: ${process}: Found ${t_proc} active after ${TPREPEAT} cycles"
    done
    [ "${num_temp_proc}" -gt 0 ] &&
        _log "INFO: Found ${num_temp_proc} active from: ${TEMPPROCNAMES}"

    local active=$((num_load_proc + num_temp_proc))
    local msg="INFO: Found ${active} process(es) active -> "
    [ "${active}" -eq 0 ] && {
        _log "${msg}next check"
        return 0; }

    _log "${msg}no shutdown"
    return "${active}"
}


###############################################################################
#
#   Name       : _check_plugin
#   Parameters : None
#   Globals    : None
#   Return     : 0 : If file specified by the plugin has not been found.
#              : 1 : If file specified by the plugin has been found.
#
# With this plugin-system we can check if a specific file exists. If it exists,
# the machine won't shutdown.
#
_check_plugin() {
    local foundvalue=0
    for plugin in "${PLUGIN_DIR}"/*; do
        [ ! -f "${plugin}" ] && {
            _log "DEBUG: Ignoring: ${plugin} as it is not a plugin file"
            continue; }

        local name="${plugin##*/}"
        _log "DEBUG: -----------------------------------------------------"
        _log "DEBUG: ${name}: plugin: ${plugin}"

        for var in "enabled:true" "folder:N/A" "file:N/A" "content:N/A"; do
            local value="${var%:*}"
            declare "${value}"="$(
                awk -F= -v regex="^${var%:*}=" \
                    '$0 ~ regex {gsub(/^["'\'']|["'\'']$/,"",$2);print $2}' \
                    "${plugin}")"
            _log "DEBUG: ${name}: ${value}: ${!value:-"${var#*:}"}"
        done

        # shellcheck disable=SC2154
        [ "${enabled}" == "false" ] && {
            _log "INFO: ${name}: Plugin is disabled skipping -> next check"
            continue; }

        # shellcheck disable=SC2154
        [ -z "$(find "${folder}" -maxdepth 1 -type f \
                    -execdir basename {} ';' 2>/dev/null |
                sed -En "/${file}/p")" ] && {
            _log "INFO: ${name}: File not found -> next check"
            continue; }

        # shellcheck disable=SC2154
        if [ -z "${content}" ]; then
            _log "INFO: ${name}: File found -> no shutdown"
            ((foundvalue+=1))
        elif grep -qe "${content}" "${folder}/${file}"; then
            _log "INFO: ${name}: Content found (${content}) -> no shutdown"
            ((foundvalue+=1))
        else
            _log "INFO: ${name}: Content not found (${content}) -> next check"
        fi
    done

    _log "DEBUG: -----------------------------------------------------"
    _log "DEBUG: All plugins processed: Active plugins: ${foundvalue}"

    [ "${foundvalue}" -gt 0 ] && return 1
    return 0
}


###############################################################################
#
#   Name       : _check_loadaverage
#   Parameters : None
#   Globals    : $LOADAVERAGE : Integer load average.
#   Return     : 0 : If load average is lower than LOADAVERAGE, shutdown.
#              : 1 : If load average is higher than LOADAVERAGE, no shutdown.
#
# Checks if the load average is higher than LOADAVERAGE over the last 1 minute.
# If yes -> no shutdown, next cycle. If no -> next check and shutdown if all
# cycles failed.
#
_check_loadaverage() {
    # Get the load averages.
    local -r loadavg_line="$(< "/proc/loadavg")"
    # Get load average in decimal notation.
    local -r loadavg_dec="$(awk '{print $1}' <<< "${loadavg_line}")"
    # Convert to integer by removing decimal point, assumes two decimal places.
    local -r loadavg="$(sed 's/[.]//;s/^0*//g;s/^$/0/' <<< "${loadavg_dec}")"

    _log "DEBUG: -----------------------------------------------------"
    _log "DEBUG: Output from '/proc/loadavg': ${loadavg_line}"
    _log "DEBUG: load average decimal: ${loadavg_dec}"
    _log "DEBUG: load average: ${loadavg}"

    local msg="INFO: Load average (${loadavg_dec} -> ${loadavg}) is"
    [ "${loadavg}" -gt "${LOADAVERAGE}" ] && {
        _log "${msg} higher than target (${LOADAVERAGE}) -> no shutdown"
        return 1; }

    _log "${msg} lower than target (${LOADAVERAGE}) -> next check"
    return 0
}


###############################################################################
#
#   Name       : _check_net_status
#   Parameters : $1 : IP addresses found for all network interfaces.
#   Globals    : $PORT_MAP            : Associative array for custom port to
#                                       service mappings.
#              : $NSOCKETPORTMAP      : User defined port to service mappings.
#              : $NSOCKETNUMBERS      : Socket number to check for activity.
#              : $NSOCKETIGNORESELF   : If set will ignore all network
#                                       interfaces IPv4/IPv6 addresses.
#              : NSOCKETSCLIENTSCHECK : If true check, if the client connected
#                                       to the socket is online.
#   Return     : 0         : If no active socket has been found, shutdown.
#              : 1 or more : If an active socket has been found, no shutdown.
#
_check_net_status() {
    local -r ips_filter="${1// /] or src [}"

    [ "${NSOCKETIGNORESELF}" == "true" ] &&
        local -r ips_ignore="and not ( dst [${1// /] or dst [}] )"

    local active=0

    [[ "${#PORT_MAP[@]}" -eq 0 && -n "${NSOCKETPORTMAP}" ]] && {
        local map; while read -r map; do
            PORT_MAP["${map%%:*}"]="${map#*:}"
        done <<< "${NSOCKETPORTMAP//,/$'\n'}"; }

    local -r reg_s="^[[]?(::(ffff|FFFF):)?"
    local -r reg_e="[]]?([%][^ /]+)?:[0-9]+$"

    # For each socket see if it is stated active using ss command.
    for nsocket in ${NSOCKETNUMBERS//,/ }; do
        local lines; lines="$(LC_ALL=C
            ss state established --no-header \
                "( sport ${nsocket} and ( src [${ips_filter}] ) \
                    ${ips_ignore:-} )" )"

        local msg="DEBUG: Port ${nsocket}: Found "
        [ -z "${lines}" ] && {
            _log "${msg}no active connections"
            continue; }

        _log "${msg}active connections:"
        local line; while read -r line; do
            _log "DEBUG: ${line//+([[:space:]])/ }"
        done <<< "${lines}"

        local service; service="${PORT_MAP[${nsocket}]:-}"
            [ -z "${service}" ] && service="$(
            awk 'NR==1 && $4!~/:[0-9]*$/{gsub(/.*:/,"",$4);print toupper($4)}
                 NR==1 && $4~/:[0-9]*$/{print "UNKNOWN"}' <<< "${lines}")"
        local conips; conips="$(
            awk -v reg_s="${reg_s}" -v reg_e="${reg_e}" \
                '{sub(reg_s,"",$5); sub(reg_e,"",$5)}
                  $5~/:/{print "z"$5} $5!~/:/{print $5}' <<< "${lines}" |
            sort -Vu | awk -Fz '/:/{print $2}!/:/{print}')"

        [ "${NSOCKETSCLIENTSCHECK}" == "true" ] && {
            local online; online="$(LC_ALL=C
                fping --quiet --retry=1 --alive <<< "${conips}")"
            conips="$(awk -v regex="^${online//$'\n'/|}$" \
                          '$0 ~ regex {print $0 "/online"}
                           $0 !~ regex {print $0 "/offline"}' <<< "${conips}")"
            lines="$(awk -v regex="${reg_s}(${online//$'\n'/|})${reg_e}" \
                         '$5 ~ regex' <<< "${lines}")"; }

        local no_lines=0;
        [ -n "${lines}" ] && no_lines="$(wc -l <<< "${lines}")"
        ((active+="${no_lines}"))

        _log "INFO: Found ${no_lines} active connection(s) on port ${nsocket} (${service}) from: ${conips//$'\n'/, }"
    done

    _log "INFO: Found ${active} active socket(s) from port(s): ${NSOCKETNUMBERS}"

    # Return the number of active sockets found.
    return "${active}"
}


###############################################################################
#
#   Name       : _check_container_status
#   Parameters : $1 : Name of the container engine to use with a compatible
#                     Docker API/Interface.
#   Globals    : $NSOCKETNUMBERS      : Socket number to check for activity.
#              : NSOCKETSCLIENTSCHECK : If true check, if the client connected
#                                       to the socket is online.
#   Return     : 0         : If no active socket has been found, shutdown.
#              : 1 or more : If an active socket is found, no shutdown.
#
_check_container_status() {
    local -r engine="${1}"
    command -v "${engine}" &>/dev/null || return 0
    _log "INFO: Check status of container engine: ${engine}"
    local active=0
    local nsocketnums; mapfile -t nsocketnums <<< "${NSOCKETNUMBERS//,/$'\n'}"
    local -r reg_s="^[[]?(::(ffff|FFFF):)?"
    local -r reg_e="[]]?([%][^ /]+)?:[0-9]+$"
    for container in $("${engine}" container ls --format "{{ .Names }}"); do
        local info; read -ra info <<< "$(# shellcheck disable=SC2016
            LC_ALL=C "${engine}" container inspect --format \
            '{{.State.Pid}}
             {{- ""}} {{range.NetworkSettings.Networks}}{{.IPAddress}},{{end}}
             {{- ""}} {{range $key, $value := .NetworkSettings.Ports}}
                {{- ""}}{{if $value}}
                    {{- ""}} {{(index $value 0).HostPort}}:
                        {{- ""}}{{index (split $key "/") 0}}
                {{- ""}}{{end}}
             {{- ""}}{{end}}' \
            "${container}")"
        [[ "${#info[@]}" -eq 0 || ! ("${info[*]}" =~ :) ]] && continue
        local p_map=()
        for ports in "${info[@]: -2}"; do
            [[ "${ports}" =~ (.*)-(.*):(.*)-(.*) ]] && {
                local h_map; mapfile -t h_map < <(
                    seq "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}")
                local c_map; mapfile -t c_map < <(
                    seq "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}")
                for index in "${!h_map[@]}"; do
                    [[ " ${nsocketnums[*]} " =~ \
                            [[:blank:]]${h_map["${index}"]}[[:blank:]] ]] &&
                        p_map["${h_map["${index}"]}"]="${c_map["${index}"]}"
                done
                continue; }
            [[ " ${nsocketnums[*]} " =~ [[:blank:]]${ports%:*}[[:blank:]] ]] &&
                p_map["${ports%:*}"]="${ports#*:}"
        done
        [ "${#p_map[@]}" -eq 0 ] && continue
        local ips_filter="${info[1]::-1}"
        ips_filter="${ips_filter//,/] or src [}"
        for target in "${!p_map[@]}"; do
            local lines; lines="$(LC_ALL=C
                nsenter --target "${info[0]}" --net \
                    ss state established --no-header --numeric \
                        "sport ${p_map[${target}]} and \
                            ( src [${ips_filter}] )" || true )"
            [ -z "${lines}" ] && continue
            local con_ips; con_ips="$(
                awk -v reg_s="${reg_s}" -v reg_e="${reg_e}" \
                    '{sub(reg_s,"",$5); sub(reg_e,"",$5)}
                      $5~/:/{print "z"$5} $5!~/:/{print $5}' <<< "${lines}" |
                sort -Vu | awk -Fz '/:/{print $2}!/:/{print}')"
            [ "${NSOCKETSCLIENTSCHECK}" == "true" ] && {
                local online; online="$(LC_ALL=C
                    nsenter --target "${info[0]}" --net \
                        fping --quiet --retry=1 --alive <<< "${con_ips}")"
                con_ips="$(awk -v regex="^${online//$'\n'/|}$" \
                               '$0 ~ regex {print $0 "/online"}
                                $0 !~ regex {print $0 "/offline"}' <<< \
                           "${con_ips}")"
                lines="$(awk -v regex="${reg_s}(${online//$'\n'/|})${reg_e}" \
                             '$5 ~ regex' <<< "${lines}")"; }
            local no_lines=0;
            [ -n "${lines}" ] && no_lines="$(wc -l <<< "${lines}")"
            ((active+="${no_lines}"))
            _log "INFO: Found ${no_lines} active connection(s) on port ${target} (${container}) from: ${con_ips//$'\n'/, }"
        done
    done
    _log "INFO: Found ${active} active socket(s) from port(s): ${NSOCKETNUMBERS}"
    return "${active}"
}


###############################################################################
#
#   Name       : _check_users_status
#   Parameters : None
#   Globals    : None
#   Return     : 0 : If there are no connected users, ready for shutdown.
#              : 1 : If there are connected users, no shutdown.
#
_check_users_status() {
    local -r users_connected="$(LC_ALL=C w -hs)"

    [ -z "${users_connected}" ] && {
        _log "INFO: There are no users connected -> next check"
        return 0; }

    _log "DEBUG: Connected users:"
    local user; while read -r user; do
        _log "DEBUG: ${user}"
    done <<< "${users_connected}"

    local connecton; while read -r connecton; do
        _log "$(awk '{sub(/^-$/,"localhost",$3);
                      printf "INFO: User: %s on from %s at %s",$1,$2,$3}' <<< \
                "${connecton}")"
    done <<< "${users_connected}"

    _log "INFO: There are users connected -> no shutdown"
    return 1
}


###############################################################################
#
#   Name       : _check_smb_status
#   Parameters : None
#   Globals    : CHECK_SAMBA_CLIENTS : If true check, if the client is online.
#   Return     : 0 : If there are no SMB client connected, ready for shutdown.
#              : 1 : If there are SMB client connected, no shutdown.
#
_check_smb_status() {
    local -r smb_conns="$(LC_ALL=C smbstatus --brief | awk 'NR>4')"
    local smb_lock_pids; mapfile -t smb_lock_pids <<< "$(LC_ALL=C
        smbstatus --locks | awk '$1 ~ /[0-9]+/{print $1}' | sort -u)"

    [[ -z "${smb_conns}" || ! "${smb_lock_pids[*]}" ]] && {
        _log "INFO: No SMB clients connected with locks -> next check"
        return 0; }

    local clients="false"
    _log "INFO: SMB client(s) with locks:"
    local pid user host prot; while read -r pid user host prot; do
        [[ " ${smb_lock_pids[*]} " =~ [[:blank:]]${pid}[[:blank:]] ]] && {
            [ "${CHECK_SAMBA_CLIENTS}" == "true" ] && {
                local state="/offline"
                ! LC_ALL=C fping --quiet --retry=1 "${host}" || {
                    state="/online"; clients="true"; }; }
            _log "INFO: HOST: ${host}${state:-} PID: ${pid} USER: ${user} PROTO: ${prot}"; }
    done < <(awk '{print $1 " " $2":"$3 " " $4 " " $6}' <<< "${smb_conns}")

    [[ "${CHECK_SAMBA_CLIENTS}" == "true" && "${clients}" == "false" ]] && {
        _log "INFO: SMB clients with locks are offline -> next check"
        return 0; }

    _log "INFO: SMB clients with locks connected -> no shutdown"
    return 1
}


###############################################################################
#
#   Name       : _check_ul_dl_rate
#   Parameters : $1 : The network interface to check.
#   Globals    : $ULDLRATE     : Rate in kB/s of network activity.
#              : $P_UL_DL_RATE : Associative array for strong a network
#                                interface tx, rx and epoch values.
#   Return     : 0 : If not enough network activity, ready for shutdown.
#              : 1 : If enough network activity, no shutdown.
#
_check_ul_dl_rate() {
    local -r net_iface="${1}"

    _log "INFO: Running on interface: ${net_iface}"

    local -r epoch_sec="${EPOCHSECONDS}"
    local -r last_checked_sec=$((
        epoch_sec - ${P_UL_DL_RATE["${net_iface}_epoch_sec"]:-${epoch_sec}}))

    local -r rx_kb="$(awk '{printf("%.0f",($0/1024))}' \
                "/sys/class/net/${net_iface}/statistics/rx_bytes")"
    local -r tx_kb="$(awk '{printf("%.0f",($0/1024))}' \
                "/sys/class/net/${net_iface}/statistics/tx_bytes")"

    _log "DEBUG: Actual: TX kB: ${tx_kb}, RX kB: ${rx_kb}"

    [[ "${last_checked_sec}" -eq 0 ||
          -z "${P_UL_DL_RATE["${net_iface}_epoch_sec"]:-}" ||
          -z "${P_UL_DL_RATE["${net_iface}_tx"]:-}" ||
          -z "${P_UL_DL_RATE["${net_iface}_rx"]:-}" ]] && {
        # Store current value.
        P_UL_DL_RATE["${net_iface}_epoch_sec"]="${epoch_sec}"
        P_UL_DL_RATE["${net_iface}_rx"]="${rx_kb}"
        P_UL_DL_RATE["${net_iface}_tx"]="${tx_kb}"

        _log "DEBUG: Store TX, RX and epoch values"
        return 0; }

    _log "DEBUG: Previous TX kB: ${P_UL_DL_RATE["${net_iface}_tx"]}, RX kB: ${P_UL_DL_RATE["${net_iface}_rx"]}"

    # Calculate threshold limit (defined kB/s multiplied with second since
    # last checked) to get the total value of kB over the 'last checked
    # time' - 'time now'.
    local ul_dl_increase=$((ULDLRATE * last_checked_sec))

    # Calculate the total value.
    local t_rx=$((P_UL_DL_RATE["${net_iface}_rx"] + ul_dl_increase))
    local t_tx=$((P_UL_DL_RATE["${net_iface}_tx"] + ul_dl_increase))

    # Calculate difference between the last and the actual value.
    local diff_rx=$((rx_kb - P_UL_DL_RATE["${net_iface}_rx"]))
    local diff_tx=$((tx_kb - P_UL_DL_RATE["${net_iface}_tx"]))

    # Calculate dl/ul-rate in kB/s - format xx.x.
    local last_dl_rate; last_dl_rate=$(
        awk '{printf("%.1f",($1/$2))}' <<< "${diff_rx} ${last_checked_sec}")
    local last_ul_rate; last_ul_rate=$(
        awk '{printf("%.1f",($1/$2))}' <<< "${diff_tx} ${last_checked_sec}")

    _log "DEBUG: last_checked_sec ${last_checked_sec}"
    _log "DEBUG: ul_dl_increase: ${ul_dl_increase}"
    _log "DEBUG: t_rx: ${t_rx}"
    _log "DEBUG: diff_rx: ${diff_rx}"
    _log "DEBUG: t_tx: ${t_tx}"
    _log "DEBUG: diff_tx: ${diff_tx}"
    _log "DEBUG: Check: rx_kb:${rx_kb} <= t_rx:${t_rx}"
    _log "DEBUG: Check: tx_kb:${tx_kb} <= t_tx:${t_tx}"

    # Store current values.
    P_UL_DL_RATE["${net_iface}_epoch_sec"]="${epoch_sec}"
    P_UL_DL_RATE["${net_iface}_rx"]="${rx_kb}"
    P_UL_DL_RATE["${net_iface}_tx"]="${tx_kb}"

    local msg="INFO: Network interface: ${net_iface} (last ${last_checked_sec}s) "
          msg+="DL: ${last_dl_rate} kB/s, UL: ${last_ul_rate} kB/s"

    [[ "${rx_kb}" -gt "${t_rx}" || "${tx_kb}" -gt "${t_tx}" ]] && {
        _log "${msg} over ${ULDLRATE} kB/s -> no shutdown"
        return 1; }

    _log "${msg} under ${ULDLRATE} kB/s -> next check"
    return 0
}


###############################################################################
#
#   Name       : _check_clock
#   Parameters : None
#   Globals    : $UPHOURS         : Range of hours, where system should go to
#                                   sleep. e.g. 06:00..20:00.
#              : $OVERRIDEUPHOURS : Range of hours overriding default $UPHOURS
#                                   for days of the week. e.g.
#                                   Sat:00:00..23:59,Sun:00:00..21:59
#   Return     : 0 : If the time is in the DOWN range, ready for shutdown.
#              : 1 : If the time is in the UP range, sleep till DOWN time.
#
_check_clock() {
    local -r epoch_sec="${EPOCHSECONDS}"
    local -r current_time="$(date --date @"${epoch_sec}" "+%R")"

    local up_hours="${UPHOURS}"
    for override in ${OVERRIDEUPHOURS//,/ }; do
        local o_hours="${override#*:}"
        local o_start; o_start="$(date --file - "+%R" <<< "${o_hours%..*}")"
        local o_end; o_end="$(date --file - "+%R" <<< "${o_hours#*..}")"
        [[ "$(date --date @"${epoch_sec}" "+%a")" == "${override%%:*}" ||
           ("$(date --date @$((epoch_sec - 86400)) "+%a")" == \
                "${override%%:*}" && "${o_start}" > "${o_end}" &&
            ( "${current_time}" < "${o_end}" ||
              "${current_time}" == "${o_end}" )) ]] && {
                up_hours="${o_hours}"
                _log "INFO: OVERRIDEUPHOURS found applying: ${override}"
                break; }
    done

    local -r range_start="$(date --file - "+%R" <<< "${up_hours%..*}")"
    local -r range_end="$(date --file - "+%R" <<< "${up_hours#*..}")"

    local epoch_start; epoch_start="$(date --file - "+%s" <<< \
            "$(date --date @"${epoch_sec}" "+%D") ${range_start}")"
    local epoch_end; epoch_end="$(date --file - "+%s" <<< \
            "$(date --date @"${epoch_sec}" "+%D") ${range_end}")"
    # Allow epoch to wrapped around midnight using a lexicographic comparison.
    [[ "${range_start}" > "${range_end}" ]] &&
        if [[ "${current_time}" > "${range_start}" ||
              "${current_time}" == "${range_start}" ||
              "${current_time}" > "${range_end}" ||
              "${current_time}" == "${range_end}" ]]; then
            epoch_end=$((epoch_end + 86400))
        else
            epoch_start=$((epoch_start - 86400))
        fi

    # If it negative or zero we already in the shutdown window.
    local seconds_to_sleep=$((epoch_end - epoch_sec))
    local msg; msg="DEBUG: Current time: $(date --date @"${epoch_sec}"), "
          msg+="Stay up range - start: ${range_start}, end: ${range_end} -> "
    [[ "${epoch_sec}" -lt "${epoch_start}" ||
          "${epoch_sec}" -ge "${epoch_end}" ||
          "${seconds_to_sleep}" -le 0 ]] && {
        _log "${msg}allow shutdown"
        _log "INFO: System is in shutdown range -> next check"
        return 0; }

    _log "${msg}stay up"
    _log "INFO: System is in stay up range. Sleeping until: $(
            date --date @$((epoch_sec + seconds_to_sleep))
                ) -> ${seconds_to_sleep}s"
    sleep "${seconds_to_sleep}"
    return 1
}


###############################################################################
#
#   Name       : _set_wakealarm
#   Parameters : None
#   Globals    : $WAKEALARM         : The time the system should wake up. (24
#                                     hour clock value)
#              : $OVERRIDEWAKEALARM : Overriding default $WAKEALARM for days
#                                     of the week. e.g. Sat:06:30,Sun:06:30
#              : $FAKE              : In fake mode do not set a real wakealarm.
#   Return     : 0 on success.
#
_set_wakealarm() {
    local wakealarm="${WAKEALARM}"
    local alarm_time; alarm_time="$(date --date "today ${wakealarm}" "+%s")"
    [ "${EPOCHSECONDS}" -ge "${alarm_time}" ] &&
        alarm_time="$(date --date "tomorrow ${wakealarm}" "+%s")"

    for override in ${OVERRIDEWAKEALARM//,/ }; do
        [[ "$(date --date @"${alarm_time}" "+%a")" == "${override%%:*}" ]] && {
            wakealarm="${override#*:}"
            alarm_time="$(date --date "${override/:/ }" "+%s")"
            _log "INFO: OVERRIDEWAKEALARM found applying: ${override}"
            break; }
    done

    [[ "$(LC_ALL=C timedatectl |
            awk '/RTC in local TZ/{print $NF}')" == 'yes' ]] &&
        alarm_time="$(date --utc --file - "+%s" <<< \
                        "$(date --date @"${alarm_time}" "+c")")"

    if [ "${FAKE}" == "false" ]; then
        echo 0 > "/sys/class/rtc/${RTC}/wakealarm"
        echo "${alarm_time}" > "/sys/class/rtc/${RTC}/wakealarm"
    else
        _log "WARNING: Wakealarm will not be set in FAKE-Mode" force
    fi

    _log "INFO: Wake alarm set: ${wakealarm}, RTC time: ${alarm_time} -> $(
            date --utc --date @"${alarm_time}")"

    _log "DEBUG: Output from: /proc/driver/rtc"
    local line; while read -r line; do
        _log "DEBUG: ${line}"
    done < "/proc/driver/rtc"

    return 0
}


###############################################################################
#
#   Name       : _check_hddio
#   Parameters : None
#   Globals    : $HDDIO_RATE   : Rate in kB/s of HDD-IO.
#              : $P_HDDIO_DEVS : Associative array for storing read, wrtn and
#                                epoch values.
#   Return     : 0 : If hdd IO is lower than defined value, ready for shutdown.
#              : 1 : If hdd IO is higher than defined value, no shutdown.
#
_check_hddio() {
    local rvalue=0

    _log "DEBUG: HDDIO_RATE: ${HDDIO_RATE} kB/s"

    local -r epoch_sec="${EPOCHSECONDS}"
    local -r last_checked_sec=$((
        epoch_sec - ${P_HDDIO_DEVS["epoch_sec"]:-${epoch_sec}}))

    local hdd hdd_in hdd_out; while read -r hdd hdd_in hdd_out; do
        _log "DEBUG: ========== Device: ${hdd} =========="

        ! mount -l | grep -q "${hdd}" && {
            _log "DEBUG: Skipping as no mount point"
            continue; }

        local line; while read -r line; do
            _log "DEBUG: ${line}"
        done < <(blkid -s LABEL -s UUID | grep "${hdd}")
        _log "DEBUG: Actual: Read kB: ${hdd_in}, Write kB: ${hdd_out}"

        [[ "${rvalue}" -eq 1 || "${last_checked_sec}" -eq 0 ||
              -z "${P_HDDIO_DEVS["epoch_sec"]:-}" ||
              -z "${P_HDDIO_DEVS["${hdd}_r"]:-}" ||
              -z "${P_HDDIO_DEVS["${hdd}_w"]:-}" ]] && {
            # Store current value.
            P_HDDIO_DEVS["${hdd}_r"]="${hdd_in}"
            P_HDDIO_DEVS["${hdd}_w"]="${hdd_out}"

            _log "DEBUG: Store new read/write value for device"
            continue; }

        _log "DEBUG: Previous: Read kB: ${P_HDDIO_DEVS["${hdd}_r"]}, Write kB: ${P_HDDIO_DEVS["${hdd}_w"]}"

        # Calculate threshold limit (defined kB/s multiplied with second since
        # last checked) to get the total value of kB over the 'last checked
        # time' - 'time now'.
        local hddio_increase=$((HDDIO_RATE * last_checked_sec))

        # Calculate the total value.
        local t_hddio_read=$((P_HDDIO_DEVS["${hdd}_r"] + hddio_increase))
        local t_hddio_write=$((P_HDDIO_DEVS["${hdd}_w"] + hddio_increase))

        # Calculate difference between the last and the actual value.
        local diff_hddio_read=$((hdd_in - P_HDDIO_DEVS["${hdd}_r"]))
        local diff_hddio_write=$((hdd_out - P_HDDIO_DEVS["${hdd}_w"]))

        # Calculate hddio-rate in kB/s - format xx.x.
        local last_hddio_read_rate; last_hddio_read_rate="$(
            awk '{printf("%.1f",($1/$2))}' <<< \
                 "${diff_hddio_read} ${last_checked_sec}")"
        local last_hddio_write_rate; last_hddio_write_rate="$(
            awk '{printf("%.1f",($1/$2))}' <<< \
                 "${diff_hddio_write} ${last_checked_sec}")"

        _log "DEBUG: last_checked_sec: ${last_checked_sec}"
        _log "DEBUG: hddio_increase: ${hddio_increase}"
        _log "DEBUG: t_hddio_read: ${t_hddio_read}"
        _log "DEBUG: diff_hddio_read: ${diff_hddio_read}"
        _log "DEBUG: t_hddio_write: ${t_hddio_write}"
        _log "DEBUG: diff_hddio_write: ${diff_hddio_write}"
        _log "DEBUG: Check: hdd_in: ${hdd_in} <= t_hddio_read: ${t_hddio_read}"
        _log "DEBUG: Check: hdd_out: ${hdd_out} <= t_hddio_write: ${t_hddio_write}"

        # Store current values.
        P_HDDIO_DEVS["${hdd}_r"]="${hdd_in}"
        P_HDDIO_DEVS["${hdd}_w"]="${hdd_out}"

        local msg="INFO: Device: ${hdd} (last ${last_checked_sec}s) "
              msg+="Read: ${last_hddio_read_rate} kB/s, "
              msg+="Write: ${last_hddio_write_rate} kB/s"

        if [[ "${hdd_in}" -gt "${t_hddio_read}" ||
              "${hdd_out}" -gt "${t_hddio_write}" ]]; then
            _log "${msg} over: ${HDDIO_RATE} kB/s -> no shutdown"
            rvalue=1
        else
            _log "${msg} under: ${HDDIO_RATE} kB/s -> next check"
        fi

    done < <(LC_ALL=C iostat -kdyNz -o JSON |
             jq -r '.sysstat.hosts[].statistics[].disk[] |
                    "\(.disk_device) \(.kB_read) \(.kB_wrtn)"');

    P_HDDIO_DEVS["epoch_sec"]="${epoch_sec}"
    _log "INFO: All checks complete"
    return "${rvalue}"
}


###############################################################################
#
#   Name       : _check_smart
#   Parameters : None
#   Globals    : None
#   Return     : 1 : If any HDD is running smart tests then no shutdown.
#              : 0 : If no HDD is running smart tests.
#
# This checks if a device is running smart tests, if S.M.A.R.T. is supported
# directly by the device and its controller. For example this will not work on
# a HPE smart array. If smart tests are running on a supported device then
# no shutdown. If no smart tests or device/controller is not supported go to
# next check.
#
_check_smart() {
    local hdd; while read -r hdd; do
        _log "DEBUG: Checking device for running smart tests: ${hdd}"

        local smart_output; smart_output="$(LC_ALL=C
            smartctl --all --nocheck=standby "${hdd}" || true)"

        _log "DEBUG: Smartctl output: ${hdd}"
        local line; while read -r line; do
           _log "DEBUG: ${line}"
        done <<< "${smart_output}"

        grep -q "in[ _]progress" <<< "${smart_output}" && {
            _log "INFO: Device ${hdd} is running SMART tests -> no shutdown"
            return 1; }
    done < <(LC_ALL=C lsblk --nodeps --noheadings --paths --output "name,type" |
             awk '/disk/{print $1}');

    _log "INFO: No supported device is running SMART tests -> next check"
    return 0
}


###############################################################################
#
#   Name       : _validate_config
#   Parameters : None
#   Globals    : DEFAULT_CONFIG : Location of the $DEFAULT_CONFIG.
#              : $*             : All options as defined in $CONFIG for
#                                 validation.
#              : $DEFAULT_*     : All options as defined in $DEFAULT_CONFIG for
#                                 default value reset in case of error.
#              : $R_*       : Global regular expression's for validation.
#   Return     : 0 on success.
#
_validate_config() {
    __check_value() {
        local -r var="${1}"
        local -r regex="${2}"
        local -r msg="${3}"
        local -r default="${4:-"true"}"

        [[ "${!var}" =~ ${regex} ]] || {
            _log "WARNING: ${var} not set properly: ${!var}"
            _log "WARNING: ${var} must be ${msg}"
            [ "${default}" == "false" ] && {
                _log "ERR: Exiting ..."
                exit 140; }

            local -r default_var="DEFAULT_${var}"
            _log "WARNING: ${var} set to default: ${!default_var:-\"\"}"
            declare -g "${var}"="${!default_var}"; }
    }

    __check_value "FAKE" "^(true|false)$" "set to: 'true' or 'false'"
    [ "${FAKE}" == "true" ] && VERBOSE="true"

    # VERBOSE (true or false)
    __check_value "VERBOSE" "^(true|false)$" "set to: 'true' or 'false'"

    # SYSLOG (true or false).
    __check_value "SYSLOG" "^(true|false)$" "set to: 'true' or 'false'"

    # ENABLE (true or false).
    __check_value "ENABLE" "^(true|false)$" "set to: 'true' or 'false'" "false"

    # FORCE_NIC.
    __check_value "FORCE_NIC" "^(${R_IFACE})([ ]${R_IFACE})*$" \
        "in a space delimited list and entries less than 15 charters" "false"

    # NET_IFACE_RETRY (1 - 100).
    __check_value "NET_IFACE_RETRY" "^([1-9][0-9]{0,1}|100)$" \
        "in the range: '1' - '100'"

    # NET_IFACE_SLEEP (1 - 60 seconds).
    __check_value "NET_IFACE_SLEEP" "^([1-5][0-9]{0,1}|60)$" \
        "in the range: '1' - '60' seconds"

    # CYCLES (1 - 999).
    __check_value "CYCLES" "^([1-9][0-9]{0,2})$" \
        "in the range: '1' - '999'"

    # SLEEP (1 - 9999 seconds).
    __check_value "SLEEP" "^([1-9][0-9]{0,3})$" \
        "in the range: '1' - '9999' seconds"

    # Validate *CHECK* variables.
    local var; while read -r var; do
        __check_value "${var}" "^(true|false)$" "set to: 'true' or 'false'"
    done < <(awk -F= '$0 !~ /^[ ]*#/ && /(.*CHECK.*|.*_SET)/{print $1}' \
                "${DEFAULT_CONFIG}")

    local -r regex_hours="(2[0-3]|[01]?[0-9])((:[0-5]?[0-9])?)"
    local -r short_days="$(locale abday | sed 's:;:|:g')"

    if [ "${CHECKCLOCKACTIVE}" == "true" ]; then
        local -r regex_uphours="(${regex_hours})[.]{2}(${regex_hours})"
        [[ "${UPHOURS}" =~ ^(${regex_uphours})$ &&
           "${BASH_REMATCH[2]}" != "${BASH_REMATCH[6]}" ]] || {
            _log "WARNING: UPHOURS not set properly: ${UPHOURS}"
            _log "WARNING: UPHOURS must be in the format: <start 24h>..<end 24h> and start != end"
            _log "WARNING: UPHOURS set to default: ${DEFAULT_UPHOURS}"
            UPHOURS="${DEFAULT_UPHOURS}"; }

        [ -n "${OVERRIDEUPHOURS}" ] && {
            local -r regex_o="(${short_days}):(${regex_uphours})"
            __check_value "OVERRIDEUPHOURS" "^${regex_o}(,${regex_o})*$" \
                "in the format: <shot day>:<start 24h>..<end 24h>,<next..> and start != end"
            for override in ${OVERRIDEUPHOURS//,/ }; do
                [[ "${override}" =~ ^${regex_o}$ &&
                   "${BASH_REMATCH[3]}" != "${BASH_REMATCH[7]}" ]] || {
                    _log "WARNING: OVERRIDEUPHOURS invalid at: ${override}"
                    _log "WARNING: OVERRIDEUPHOURS set to default: ${DEFAULT_OVERRIDEUPHOURS:-\"\"}"
                    OVERRIDEUPHOURS="${DEFAULT_OVERRIDEUPHOURS}"
                    break; }
            done; }
    else
        _log "WARNING: CHECKCLOCKACTIVE set false not validating: UPHOURS and OVERRIDEUPHOURS"
    fi

    [ -f "/sys/class/rtc/${RTC}/wakealarm" ] &&
        _log "INFO: ${RTC} supports: wakealarm"
    if [ "${WAKEALARM_SET}" == "true" ]; then
        [ -f "/sys/class/rtc/${RTC}/wakealarm" ] || {
            _log "ERR: ${RTC} does not support wakealarm but it is set"
            exit 140; }

        __check_value "WAKEALARM_UPHOURS" "^(true|false)$" \
            "set to: 'true' or 'false'"

        [ "${WAKEALARM_UPHOURS}" == "true" ] && {
            [ "${CHECKCLOCKACTIVE}" == "true" ] || {
                _log "ERR: WAKEALARM_UPHOURS is set but CHECKCLOCKACTIVE is not"
                exit 140; }

            _log "INFO: Setting WAKEALARM and OVERRIDEWAKEALARM using UPHOURS and OVERRIDEUPHOURS start times"
            WAKEALARM="${UPHOURS%..*}"
            OVERRIDEWAKEALARM="$(
                sed -E "s/[.]{2}${regex_hours}//g" <<< "${OVERRIDEUPHOURS}")"; }

        __check_value "WAKEALARM" "^${regex_hours}$" \
            "a valid 24 hour clock time" "false"

        [ -n "${OVERRIDEWAKEALARM}" ] && {
            local -r regex_a_o="(${short_days}):(${regex_hours})"
            __check_value "OVERRIDEWAKEALARM" "^${regex_a_o}(,${regex_a_o})*$"\
                "in the format: <shot day>:<time 24h>,<next..>"; }
    else
        _log "WARNING: WAKEALARM_SET set false not validating: WAKEALARM and OVERRIDEWAKEALARM"
    fi

    if [ "${CHECKPROCNAMES}" == "true" ]; then
        for var in "LOADPROCNAMES" "TEMPPROCNAMES"; do
            [ "${!var}" == "-" ] && {
                _log "INFO: ${var} disabled, processes not checked"
                declare -g "${var}"=''
                continue; }

            __check_value "${var}" "^([A-Za-z0-9_.-]+)(,[A-Za-z0-9_.-]+)*$" \
                "in the format: [proc1,proc2,...,procN]" "false"
        done

        for var in "LPREPEAT" "TPREPEAT"; do
            __check_value "${var}" "^([1-9][0-9]{0,2})$" \
                "in the range: '1' - '999'"
        done
    else
        _log "WARNING: CHECKPROCNAMES set false not validating: LOADPROCNAMES, LPREPEAT, TEMPPROCNAMES and TPREPEAT"
    fi

    if [ "${CHECKSOCKETS}" == "true" ]; then
        __check_value "NSOCKETNUMBERS" "^([0-9]{2,5})(,[0-9]{2,5})*$" \
            "at least 2 to 5 digits in the format: [nsocket1,nsocket2,...,nsocketN]"

        __check_value "NSOCKETPORTMAP" "^((([0-9]{2,5}:[^,]+)(,[0-9]{2,5}:[^,]+)*)|)$" \
            "a port of 2 to 5 digits with an associated service name in the format: [<port1>:<service1>,<port2>:<service2>,...,<portN>:<serviceN>]" \
            "false"

        __check_value "NSOCKETIGNORESELF" "^(true|false)$" \
            "set to: 'true' or 'false'"

        __check_value "NSOCKETSCLIENTSCHECK" "^(true|false)$" \
            "set to: 'true' or 'false'"
    else
        _log "WARNING: CHECKSOCKETS set false not validating: NSOCKETNUMBERS, NSOCKETPORTMAP, NSOCKETIGNORESELF and NSOCKETSCLIENTSCHECK"
    fi

    # PINGLIST or RANGE set-up.
    local regex_ipcheck="(${R_IFACE}[@])?"
    regex_ipcheck+="((${R_OCTET})|(${R_IPV4})|(${R_RANGE_IPV4})|"
    regex_ipcheck+="(0[xX]${R_HEXTET})|(${R_IPV6})|(${R_RANGE_IPV6})|(${R_FQDN}))"
    local -r regex_ipcheck="${regex_ipcheck}"
    if [[ -n "${PINGLIST}" && -f "${PINGLIST}" ]]; then
        _log "INFO: PINGLIST is set, reading IPs from: ${PINGLIST}"
        _log "INFO: Overidding IPCHECK to: 'true'"
        unset RANGE
        IPCHECK="true"
    elif [[ -n "${PINGLIST}" && ! -f "${PINGLIST}" ]]; then
        _log "WARNING: PINGLIST is set, but file not found: ${PINGLIST}"
        _log "WARNING: Set IPCHECK to: 'false'"
        IPCHECK="false"
    elif [ "${IPCHECK}" == "false" ]; then
        _log "WARNING: IPCHECK set false not validating: RANGE"
    elif [ "${RANGE}" == "-" ]; then
        _log "WARNING: RANGE is set to '${RANGE}' -> no IP-Check"
        IPCHECK="false"
    elif [[ ! "${RANGE}" =~ ^(${regex_ipcheck})(,(${regex_ipcheck}))*$ ]]; then
        _log "WARNING: RANGE not set properly: ${RANGE}"
        _log "WARNING: The 'RANGE' should be a comma delimited list of the following:"
        _log "WARNING: Define an IPv4 range: <START>..<END> | iface@<START>..<END> | www.xxx.yyy.<START>..<END> | iface@xxx.yyy.zzz.<START>..<END>"
        _log "WARNING: Define a single IPv4: Last octet of IPv4 zzz | iface@zzz | www.xxx.yyy.zzz | iface@www.xxx.yyy.zzz"
        _log "WARNING: Define an IPv6 range: 0x<START>..0x<END> | iface@0x<START>..0x<END> | s:t:u:v:w:x:y:0x<START>..0x<END> | ifaces@s:t:u:v:w:x:y:0x<START>..0x<END>"
        _log "WARNING: Define a single IPv6: Last hextet of IPv6 0xzzzz | iface@0xzzzz | s:t:u:v:w:x:y:z | iface@s:t:u:v:w:x:y:z"
        _log "WARNING: Define by FQDN: fqdn | iface@fqdn"
        _log "WARNING: If '<START>..<END>' or 'Last octet of IPv4' is set the first three octets of the iface IPv4 address is used"
        _log "WARNING: If '0x<START>..0x<END>' or 'Last hextet of IPv6' is set the first seven hextets of the iface IPv6 address is used"
        _log "WARNING: Setting RANGE to default: ${DEFAULT_RANGE}"
        RANGE="${DEFAULT_RANGE}"
    fi

    if [ "${HDDIOCHECK}" == "true" ]; then
        # HDDIO_RATE (1 - 999999 kB/s).
        __check_value "HDDIO_RATE" "^([1-9][0-9]{0,5})$" \
            "in the range: '1' - '999999' kB/s"
    else
        _log "WARNING: HDDIOCHECK set false not validating: HDDIO_RATE"
    fi

    if [ "${ULDLCHECK}" == "true" ]; then
        # ULDLRATE (1 - 999999 kB/s).
        __check_value "ULDLRATE" "^([1-9][0-9]{0,5})$" \
            "in the range: '1' - '999999' kB/s"
    else
        _log "WARNING: ULDLCHECK set false not validating: ULDLRATE"
    fi

    if [ "${LOADAVERAGECHECK}" == "true" ]; then
        # LOADAVERAGE (max 3 digits).
        __check_value "LOADAVERAGE" "^([1-9][0-9]{0,2})$" \
            "a maximum of 3 digits"
    else
        _log "WARNING: LOADAVERAGECHECK set false not validating: LOADAVERAGE"
    fi

    # SHUTDOWNCOMMAND - Check acpi power states with pm-is-supported.
    _log "INFO: Kernel $(uname -r) supports power management modes:"
    declare -A pm_map=( ["suspend"]="--suspend:suspend to RAM" \
                        ["hibernate"]="--hibernate:suspend to DISK" \
                        ["hybrid-sleep"]="--suspend-hybrid:suspend to RAM and DISK" )
    for check in "${!pm_map[@]}"; do
        pm-is-supported "${pm_map["${check}"]%:*}" &&
            _log "INFO: Kernel supports: ${check^^} (${pm_map["${check}"]#*:})"
    done
    if [ -z "${SHUTDOWNCOMMAND}" ]; then
        _log "WARNING: SHUTDOWNCOMMAND not set"
        _log "WARNING: SHUTDOWNCOMMAND set to default: ${DEFAULT_SHUTDOWNCOMMAND}"
        SHUTDOWNCOMMAND="${DEFAULT_SHUTDOWNCOMMAND}"
    # Check, if shutdown hybrid-sleep, suspend and hibernate are supported.
    elif ( [[ "${SHUTDOWNCOMMAND}" =~ \
                    ^systemctl[[:blank:]]+(hybrid-sleep|suspend|hibernate)$ ]] &&
              ! pm-is-supported \
                    "${pm_map["${BASH_REMATCH[1]}"]%:*}" 2>/dev/null ) ||
         ( [[  "${SHUTDOWNCOMMAND}" =~ \
                    ^systemctl[[:blank:]]+suspend-then-hibernate$ ]] &&
               ( ! pm-is-supported --suspend 2>/dev/null ||
                 ! pm-is-supported --hibernate 2>/dev/null ) ); then
        _log "WARNING: SHUTDOWNCOMMAND not supported: ${SHUTDOWNCOMMAND}"
        _log "WARNING: SHUTDOWNCOMMAND set to default: ${DEFAULT_SHUTDOWNCOMMAND}"
        SHUTDOWNCOMMAND="${DEFAULT_SHUTDOWNCOMMAND}"
    else
        _log "INFO: SHUTDOWNCOMMAND is set to: ${SHUTDOWNCOMMAND}"
    fi

    # Set all vars defined in DEFAULT_CONFIG to readonly.
    . <(awk -F= '$0!~/^[ ]*#/ && $0!~/^$/ {print "readonly "$1}' \
            "${DEFAULT_CONFIG}")

    return 0
}


###############################################################################
#
#   Name       : _get_network_details
#   Parameters : None
#   Globals    : $FORCE_NIC       : Force network interface lookup using a
#                                   space delimited list of network interface
#                                   or partial interface names to be checked.
#              : $NICS            : Array for storing active network interface
#                                   names.
#              : $NET_IFACE_RETRY : How many times to retry the interface to
#                                   check if it is online.
#              : $NET_IFACE_SLEEP : The amount of time to sleep between
#                                   re-checking if the interface is online.
#              : $NIC_2_IPV4      : Associative array for storing network
#                                   interface to IPv4 address mappings.
#              : $NIC_2_IPV6      : Associative array for storing network
#                                 : interface to IPv6 address mappings.
#              : $R_IPV4          : IPv4 address regular expression.
#              : $R_IPV6          : IPv6 address regular expression.
#   Return     : 0 on success.
#
_get_network_details() {
    local _already="_"
    local net_iface; while true; do
        local _alldevices; _alldevices=$(LC_ALL=C ip -br link show |
                awk -v regex="^(${FORCE_NIC// /|})" \
                    '$1 ~ regex && !/SLAVE/{sub(/@.*/,"",$1); print $1}')
        net_iface=$(echo -e "$_alldevices" | awk -v regex="${_already// /|}" '$1 !~ regex  { print $1; exit }')
        [ -z "$net_iface" ] && break
        for (( n=0; n < NET_IFACE_RETRY; n++ )); do
            local msg=""
            local state; state="$(< "/sys/class/net/${net_iface}/operstate")"
            _log "DEBUG: Network interfaces: ${net_iface}"
            _log "DEBUG: Network interfaces status: ${state}"
            for ip_type in ipv4 ipv6; do
                local valid="false" regex="R_${ip_type^^}"
                local ip; ip="$(LC_ALL=C
                    ip -br -"${ip_type: -1}" addr show dev "${net_iface}" | sed 's/.*  //' |
                    awk '{tolower($0);gsub(/\/[0-9]+/,"");gsub(/ metric [0-9]+/,"");sub(" fe80.* ",""); $1=$1; print}')"
                msg+="IPv${ip_type: -1} address: ${ip:-N/A}, "
                [[ "${state}" =~ ^(unknown|dormant|up)$ ]] && {
                    declare -n ip_ref="NIC_2_${ip_type^^}[${net_iface}]"
                    # shellcheck disable=SC2034
                    ip_ref="${ip}"
                    valid="true"; }
                _log "DEBUG: Network interfaces IPv${ip_type: -1} address: ${ip:-N/A}"
                _log "DEBUG: Network interfaces IPv${ip_type: -1} address valid: ${valid}"
            done

            [[ -n "${NIC_2_IPV4["${net_iface}"]:-}" ||
               -n "${NIC_2_IPV6["${net_iface}"]:-}" ]] && {
                _log "INFO: '${net_iface}' has ${msg::-2}"
                NICS+=("${net_iface}")
                break; }

            _log "INFO: '${net_iface}' status: ${state}, ${msg}retrying in ${NET_IFACE_SLEEP}s ..."
            sleep "${NET_IFACE_SLEEP}"
        done
        _already+=" $net_iface"
    done

    [ "${#NICS[@]}" -eq 0 ] && {
        _log "ERR: No valid network interface found, exiting ..."
        exit 138; }
    return 0
}


###############################################################################
#
#   Name       : _check_system_active
#   Parameters : None
#   Globals    : $*          : All options defined in $CONFIG for validation.
#              : $NICS       : Array for storing active network interface
#                              names.
#              : $NIC_2_IPV4 : Associative array for storing network interface
#                              to IPv4 address mappings.
#              : $NIC_2_IPV6 : Associative array for storing network interface
#                              to IPv6 address mappings.
#   Return     : 0 : If no active host has been found.
#              : 1 : If at least one active host has been found, no shutdown.
#
_check_system_active() {
    __run_check() {
        [ "${1}" == "true" ] || return 0
        _log "DEBUG: Calling: ${2}()"
        "${2}" "${@:3}"
    }

    local ips="${NIC_2_IPV4[*]:+" ${NIC_2_IPV4[*]}"}"
          ips+="${NIC_2_IPV6[*]:+" ${NIC_2_IPV6[*]}"}"
          ips="${ips:1}"

    # PRIO 0: Check if time is within the STAYUP range e.g. 06:00 - 20:00.
    __run_check "${CHECKCLOCKACTIVE}" _check_clock || true

    for net_iface in "${NICS[@]}"; do
        # PRIO 1: Ping IP address specified across active network interfaces.
        __run_check "${IPCHECK}" _ping_range "${net_iface}" \
            "${NIC_2_IPV4["${net_iface}"]:-}" \
            "${NIC_2_IPV6["${net_iface}"]%% *}" \
            "${ips}" || return 1
    done

    # PRIO 2: Do a check for active network sockets.
    __run_check "${CHECKSOCKETS}" _check_net_status "${ips}" || return 1
    # PRIO 2: Do a check docker sockets for active network sockets.
    __run_check "${CHECKSOCKETS}" _check_container_status "docker" || return 1
    # PRIO 2: Do a check podman sockets for active network sockets.
    __run_check "${CHECKSOCKETS}" _check_container_status "podman" || return 1
    # PRIO 2: Check for connected users on the CLI.
    __run_check "${CHECK_CLI}" _check_users_status || return 1
    # PRIO 2: Check Samba for connected clients.
    __run_check "${CHECK_SAMBA}" _check_smb_status || return 1

    for net_iface in "${NICS[@]}"; do
        # PRIO 3: Do a check for ul-dl-rate across active network interfaces.
        __run_check "${ULDLCHECK}" _check_ul_dl_rate "${net_iface}" ||
            return 1
    done

    # PRIO 4: Do a hdd IO check.
    __run_check "${HDDIOCHECK}" _check_hddio || return 1
    # PRIO 4: Do a hdd check to see if S.M.A.R.T. tests are running.
    __run_check "${SMARTCHECK}" _check_smart || return 1

    # PRIO 5: Do a check for some active processes.
    __run_check "${CHECKPROCNAMES}" _check_processes || return 1

    # PRIO 6: Do a load average check.
    __run_check "${LOADAVERAGECHECK}" _check_loadaverage || return 1

    # PRIO 7: Do a PlugIn-Check for any existing files, setup in plugins.
    __run_check "${PLUGINCHECK}" _check_plugin || return 1

    return 0
}


###############################################################################
################# Start of body function script autoshutdown ##################
###############################################################################
_log "INFO: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" force
_log "INFO: Openmediavault-autoshutdown version: $(
    dpkg-query -f='${Version}' --show openmediavault-autoshutdown)" force
_log "INFO: Script md5sum: $(md5sum "${0}" | awk '{print $1}')" force

for config_file in "${DEFAULT_CONFIG}" "${CONFIG}"; do
    [ ! -f "${config_file}" ] && {
    _log "ERR: File not found! Check ${config_file} exits ..." force
    exit 139; }
done

. "${DEFAULT_CONFIG}"
. <(awk '$0 !~ /^[ ]*#/ && $0 !~ /^$/{print "readonly DEFAULT_"$0}' \
        "${DEFAULT_CONFIG}")
. "${CONFIG}"

_log "INFO: ${DEFAULT_CONFIG} loaded" force
_log "INFO: ${CONFIG} loaded" force
_log "INFO: ---------------- Validate config ---------------------"
_log "INFO: Checking config: ${CONFIG}"
_validate_config

# Enable / disable check here.
[ "${ENABLE}" == "false" ] && {
    _log "INFO: Script disabled, exiting here ..."
    exit 0; }

_log "DEBUG: ------------ Current config values ------------------"
while read -r var; do
    value="N/A"
    [[ "${var}" == "LOADPROCNAMES" || "${var}" == "TEMPPROCNAMES" ]] &&
        value="disabled"
    _log "DEBUG: ${var}: ${!var:-${value}}"
done < <(awk -F= '$0!~/^[ ]*#/ && $0!~/^$/ {print $1}' "${DEFAULT_CONFIG}")
_log "DEBUG: Available plugins found in: ${PLUGIN_DIR}"
for _plugin in "${PLUGIN_DIR}"/*; do
    [ ! -f "${_plugin}" ] || _log "DEBUG: Plugin: ${_plugin}"
done

_log "INFO: ---------------- Network details ---------------------"
_log "INFO: Find active network interfaces and their IP addresses"
_get_network_details

_log "INFO: --------------- Initialise states --------------------"

[ "${HDDIOCHECK}" == "true" ] && ! _check_hddio && {
    _log "ERR: Initialising _check_hddio(): Check using verbose mode"
    exit 141; }

for net_iface in "${NICS[@]}"; do
    [ "${ULDLCHECK}" == "true" ] && ! _check_ul_dl_rate "${net_iface}" && {
        _log "ERR: Initialising _check_ul_dl_rate(): Check using verbose mode"
        exit 141; }
done

_log "INFO: ---------------- Script started ----------------------"

_log "INFO: Registered trap on signal EXIT"
trap  '{ _log "INFO: Calling signal EXIT trap" &&
         [ "${WAKEALARM_SET}" == "true" ] && _set_wakealarm; }' EXIT

for net_iface in "${NICS[@]}"; do
    ips="${NIC_2_IPV4[${net_iface}]:+", IPv4: ${NIC_2_IPV4[${net_iface}]}"}"
    ips+="${NIC_2_IPV6[${net_iface}]:+", IPv6: ${NIC_2_IPV6[${net_iface}]}"}"
    _log "INFO: Checking network interface: ${net_iface} - ${ips:2}"
done

_log "INFO: ${CYCLES} cycles until shutdown is issued"
fcnt="${CYCLES}"
while : ; do
    _log "INFO: ------------------------------------------------------"
    [ "${FAKE}" == "true" ] && [ "${fcnt}" -eq "${CYCLES}" ] && seconds=1 || seconds="${SLEEP}"
    _log "INFO: New supervision cycle started, waiting ${seconds}s until checking system activity"
    sleep "${seconds}"
    fcnt=$((fcnt - 1))
    msg="All active checks passed"
    _check_system_active || { fcnt="${CYCLES}"; msg="Active check failed"; }
    [ "${fcnt}" -eq 0 ] && break
    _log "INFO: ${msg}, ${fcnt} cycles until shutdown ..."
done
[ "${fcnt}" -ne 0 ] && {
    _log "ERR: Script error detected in supervision cycle"
    exit 143; }

_log "INFO: All active checks passed, Shutting down system ..."
[ "${WAKEALARM_SET}" == "true" ] && _set_wakealarm
trap - EXIT
_shutdown
