#!/bin/sh -u

### BEGIN INIT INFO
# Provides:          kesl-supervisor
# Required-Start:    $local_fs
# Required-Stop:     $remote_fs
# Should-Start:
# Should-Stop:
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Description:       See kesl(5) man page
### END INIT INFO

export LC_ALL=C

readonly KESL_INCLUDE_SH_STANDALONE=true
if [ -z "${__KESL_INCLUSE_SH_INCLUDE_GUARD__:-}" ]; then
readonly __KESL_INCLUSE_SH_INCLUDE_GUARD__=1

if [ -z "${KESL_INCLUDE_SH_STANDALONE:-}" ]; then
    : "${KESL_INCLUDE_SH_PATH:?The 'KESL_INCLUDE_SH_PATH' variable must be set and not null}"
fi

__GetKeslIncludedSpecs__()
{
    local _module="${1%.inc}"
    : "${_module:?Module name must be specified}"

    shift

    eval "${1#@}=\${_module}"
    
    local _hash=$(printf '%s' "${_module}" | md5sum | { IFS=' ' read -r hashValue fileName; printf '%s' "${hashValue}"; })
    eval "${2#@}=__KESL_INCLUDE_SH_MODULE_${_hash}__"
}

StandaloneSh()
{
    : "${KESL_INCLUDE_SH_STANDALONE:?KESL_INCLUDE_SH_STANDALONE variable must be set and not null}"

    local incModuleName=''
    local incGuardVarName=''
    __GetKeslIncludedSpecs__ "${1}" @incModuleName @incGuardVarName

    eval "${incGuardVarName}=1"
}

IncludeSh()
{
    local incModuleName=''
    local incGuardVarName=''
    __GetKeslIncludedSpecs__ "${1}" @incModuleName @incGuardVarName

    if ! eval "test -z \"\${${incGuardVarName}:-}\""; then
        return
    fi

    if [ ! -z "${KESL_INCLUDE_SH_STANDALONE:-}" ]; then
        eval : "\${${incGuardVarName}:?Module ${incModuleName} source code must be subtituted and marked as standalone by the StandaloneSh() function.}"
        return
    fi

    : "${KESL_INCLUDE_SH_PATH:?KESL_INCLUDE_SH_PATH variable must be set and not null}"

    local path="${KESL_INCLUDE_SH_PATH%%:*}"
    local rest="${KESL_INCLUDE_SH_PATH#:*}"

    while :; do
        if [ "${path#/}" = "${path}" ]; then
            echo "ERROR: sh-include path must be an absolute path: '${path}'" 1>&2
            exit 1
        fi

        local moduleFilePath="${path}/${incModuleName}.inc"
        if [ -f "${moduleFilePath}" ]; then
            eval "${incGuardVarName}=1"
            . "${moduleFilePath}" || return $?
            break
        fi

        path="${rest%%:*}"
        rest="${rest#:*}"
        if [ "${path}" = "${rest}" ]; then
            echo "ERROR: module '${incModuleName}' is not found" 1>&2
            exit 1
        fi
    done
}

fi


StandaloneSh 'utils'
DieRc()
{
    local res=1
    if [ "$1" -eq "$1" ] 2>/dev/null; then
        res=$1
        shift
    fi

    printf "\n%s: ERROR: %s\n\n" "$(basename "$0")" "$*" >&2

    exit ${res}
}

DieFmt()
{
    local fmt="$1"
    shift

    printf "\n%s: ERROR: ${fmt}\n\n" "$(basename "$0")" "$@" >&2

    exit 1
}

Die()
{
    DieRc 1 "$@"
}

Echo()
{
    printf "%b\n" "$*"
}

EchoBold()
{
    printf '\033[1m'
    Echo "$@"
    printf '\033[0m'
}

PrintfBold()
{
    printf '\033[1m'
    printf "$@"
    printf '\033[0m'
}

Repeat()
{
    local n="$1"
    local ch="$2"
    printf "%${n}s\n" | tr " " "${ch}"
}

AssignAtVar()
{
    eval "${1#@}=\"\${2}\""
}

RemoveLargestPrefix()
{
    eval "AssignAtVar \"\${3}\" \"\${${1}##\${2}}\""
}

RemoveLargestSuffix()
{
    eval "AssignAtVar \"\${3}\" \"\${${1}%%\${2}}\""
}

PsCommand()
{
    COLUMNS=4096 ps "$@"
}


StandaloneSh 'is-product-ready'
IncludeSh 'utils'

GetPidOfExistingProcessByPidFile()
{
    local _pidFile="${1}"
    local _exeFile="${2}"

    local _pid=''

    {
        test -f "${_pidFile}" ||
            return 1

        IFS='' read -r _pid < "${_pidFile}" ||
            true

        test "${_pid}" -eq "${_pid}" ||
            return 2

        test "/proc/${_pid}/exe" -ef "${_exeFile}" ||
            return 3

    } 1>/dev/null 2>&1

    if [ -n "${3:-}" ]; then
        AssignAtVar "${3}" "${_pid}"
    fi

    return 0
}

IsProductStarting()
{
    GetPidOfExistingProcessByPidFile '/var/run/kesl.pid' '/opt/kaspersky/kesl/libexec/kesl' ||
        return $?
}


StandaloneSh 'upgrade-state'
GetUpgradeState()
{
    local stateVarName="${1:-}"
    local newVersionVarName="${2:-}"
    local oldVersionVarName="${3:-}"

    local current='/var/opt/kaspersky/kesl/install-current'
    local upgrade='/var/opt/kaspersky/kesl/install-upgrade'
    local previous='/var/opt/kaspersky/kesl/install-previous'
    local failed='/var/opt/kaspersky/kesl/install-failed'

    local manifestFilePath='/opt/kaspersky/kesl/doc/manifest.ini'
    local oldIniPath=''
    local newIniPath=''

    local state=''

    if [ ! -h "${current}" ]; then
        state='NotInstalled'
    else
        if [ ! -h "${failed}" ]; then
            if [ ! -h "${upgrade}" ]; then
                if [ ! -h "${previous}" ]; then
                    state='Installed'
                    oldIniPath="${current}"
                    newIniPath="${current}"
                else
                    state='Start'
                    oldIniPath="${previous}"
                    newIniPath="${current}"
                fi
            else
                if [ ! -h "${previous}" ]; then
                    state='Uninstall'
                    oldIniPath="${current}"
                    newIniPath="${upgrade}"
                else
                    state='Upgrade'
                    oldIniPath="${current}"
                    newIniPath="${upgrade}"
                fi
            fi
        else
            if [ ! -h "${previous}" ]; then
                if [ ! -h "${upgrade}" ]; then
                    state='RollingBack'
                    oldIniPath="${current}"
                    newIniPath="${failed}"
                else
                    state='RolledBack'
                    oldIniPath="${current}"
                    newIniPath="${failed}"
                fi
            else
                local curr
                if ! curr=$(readlink "${current}"); then
                    echo "Faild to read 'current' symlink" 1>&2
                    return 255
                fi

                local prev
                if ! prev=$(readlink "${previous}"); then
                    echo "Faild to read 'previous' symlink" 1>&2
                    return 255
                fi

                if [ "${curr}" = "${prev}" ]; then
                    state='UpgradeFailed'
                    oldIniPath="${current}"
                    newIniPath="${failed}"
                else
                    state='StartFailed'
                    oldIniPath="${previous}"
                    newIniPath="${failed}"
                fi
            fi
        fi

        local grepRegEx='^\s*ProductVersion\s*='
        local sedCmd='s:\(.*=\s*\)\(\S*\)\(\s*\):\2:'
        local ver=''

        if [ ! -z "${newVersionVarName}" ]; then
            ver=''
            if [ ! -z "${newIniPath}" ]; then
                if ver=$(grep "${grepRegEx}" "${newIniPath}/${manifestFilePath}"); then
                    ver=$(echo "${ver}" | sed -e "${sedCmd}")
                fi
            fi
            eval "${newVersionVarName#@}=\"\${ver}\""
        fi

        if [ ! -z "${oldVersionVarName}" ]; then
            ver=''
            if [ ! -z "${oldIniPath}" ]; then
                if ver=$(grep "${grepRegEx}" "${oldIniPath}/${manifestFilePath}"); then
                    ver=$(echo "${ver}" | sed -e "${sedCmd}")
                fi
            fi
            eval "${oldVersionVarName#@}=\"\${ver}\""
        fi
    fi

    if [ -z "${stateVarName}" ]; then
        echo "${state}"
    else
        eval "${stateVarName#@}=\"\${state}\""
    fi

    return 0
}


StandaloneSh 'ini-file'
ReadIni()
{

    local _inFile="${1:--}"
    local _section="$2"
    local _key="$3"
    local _outVar="${4:-}"

    local _cmd=''
    _cmd=$(cat <<'AWK_PROGRAM'
BEGIN {
    FS = "=";

    sectionFound = (section == "");
    keyFound = 0;
}

END {
    if (keyFound)
    {
        exit 0;
    }

    exit (sectionFound ? 102 : 101);
}

$0 == "[" section "]" {
    sectionFound = 1;
    next;
}

/^\[/ {
    if (sectionFound)
    {
        exit;
    }
}

sectionFound && $1 == key {
    keyFound = 1;
    print $2;
    exit;
}
AWK_PROGRAM
)
    local _value=''
    _value="$(awk -v section="${_section}" -v key="${_key}" -- "${_cmd}" "${_inFile}" 2>/dev/null)"
    local res=$?
    if [ ${res} -ne 0 ]; then
        return ${res}
    fi

    if [ -n "${_outVar}" ]; then
        eval "${_outVar#@}=\"\${_value}\""
    fi

    return 0
}

WriteIni()
{
    local _inFile="${1:--}"
    local _section="$2"
    local _key="$3"
    local _value="$4"
    local _outFile="${5:-${_inFile}}"

    local _cmd=''
    _cmd=$(cat <<'AWK_PROGRAM'
BEGIN {
    FS = "=";
    sectionFound = 0;
    keySet = 0;
    emptyLines = 0;
}

END {
    if (!keySet)
    {
        if (sectionFound)
        {
            print key "=" value;
            printEmptyLines();
        }
        else
        {
            print "[" section "]";
            print key "=" value;
            printEmptyLines();
        }
    }
}

function printTail(    s)
{
    while ((getline s) > 0)
    {
        print s;
    }
}

function printEmptyLines()
{
    while (emptyLines > 0)
    {
        print "";
        --emptyLines;
    }
}

$0 == "[" section "]" {
    sectionFound = 1;
    print;
    next;
}

/^\[/ {
    if (sectionFound)
    {
        print key "=" value;
        keySet = 1;
        printEmptyLines();
        print;
        printTail();
        exit;
    }
    else
    {
        print;
        next;
    }
}

sectionFound {
    if ($1 == key)
    {
        printEmptyLines();
        print $1 "=" value;
        keySet = 1;
        printTail();
        exit;
    }
    else
    {
        if ($0 == "")
        {
            ++emptyLines;
        }
        else
        {
            printEmptyLines();
            print;
        }
        next;
    }
}

{ print; }
AWK_PROGRAM
)
    local _tmpFilename=''
    local _redirTxt=''

    if [ "${_inFile}" = '-' ] || [ -f "${_inFile}" ]; then
        _redirTxt=' "${_inFile}" '
    else
        _redirTxt=' </dev/null '
    fi

    if [ "${_outFile}" != '-' ]; then
        _tmpFilename="${_outFile}.tmp";
        _redirTxt="${_redirTxt}"' 1>"${_tmpFilename}"'
    fi

    eval 'awk -v section="${_section}" -v key="${_key}" -v value="${_value}" -- "${_cmd}"' ${_redirTxt} 2>/dev/null
    local res=$?
    if [ ${res} -ne 0 ]; then
        [ -n "${_tmpFilename}" ] && rm -f "${_tmpFilename}" 2>/dev/null
    else
        [ -n "${_tmpFilename}" ] && { mv -T "${_tmpFilename}" "${_outFile}" 2>/dev/null; res=$?; }
    fi

    return ${res}
}


readonly PRODUCT_NAME='kesl'
readonly LAUNCHER='/opt/kaspersky/kesl/libexec/kesl_launcher.sh'
readonly SUBSYS_LOCK_DIRECTORY='/var/lock/subsys/'
readonly SUBSYS_LOCK_FILE="${SUBSYS_LOCK_DIRECTORY}kesl-supervisor"
readonly WDSERVER_BIN='/opt/kaspersky/kesl/libexec/wdserver'
readonly WDSERVER_PID='/var/run/wdserver.pid'
readonly INI_FILEPATH='/var/opt/kaspersky/kesl/common/kesl.ini'
readonly EULA_ID='2d2d9fc3-8861-ed95-924f-dad60d5f1847'
readonly SYSTEMD_SERVICE_FILENAME='kesl-supervisor.service'
readonly PRODUCT_RUNNING_STATUS='0'
readonly PRODUCT_DEAD_STATUS='1'
readonly PRODUCT_STOPPED_STATUS='3'
readonly SERVICE_START_TIMEOUT_SEC='600'

readonly MSG_APP_STARTED="${PRODUCT_NAME} started"
readonly MSG_APP_NOT_STARTED="${PRODUCT_NAME} not started"
readonly MSG_APP_ALREADY_STARTED="${PRODUCT_NAME} is already started"
readonly MSG_APP_COULD_NOT_BE_STARTED="${PRODUCT_NAME} could not be started"
readonly MSG_APP_STOPPED="${PRODUCT_NAME} stopped"
readonly MSG_APP_ALREADY_STOPPED="${PRODUCT_NAME} already stopped"
readonly MSG_APP_COULD_NOT_BE_STOPPED="${PRODUCT_NAME} could not be stopped"
readonly MSG_FILE_WAS_NOT_FOUND="${PRODUCT_NAME} was not found"

readonly MSG_ACCEPT_EULA_FIRST_1="Please accept EULA and Privacy Policy first.\n%s not started"
readonly MSG_APP_UPDATE_INSTALLED_1="The application update '%s' has been successfully installed."
readonly MSG_APP_UPDATE_ROOLBACK_PARTIALLY_1="Error occurred while installing the application update '%s'.\nThe application update was partially rolled back.\nTo complete the rollback, restart the operating system.\nPlease contact Kaspersky Lab Technical Support."
readonly MSG_APP_UPDATE_ROOLBACK_1="Error occurred while installing the application update '%s'.\nThe application update was rolled back.\nPlease contact Kaspersky Lab Technical Support."

PrintPidFileError()
{
    local res="${1}"
    local errPrefix="${2:-}"

    local errTxt=''
    case ${res} in
        0)
            return 0
            ;;
        1)
            errTxt='pid file not found'
            ;;
        2)
            errTxt='pid file is empty'
            ;;
        3)
            errTxt='probably is dead but pid file not deleted'
            ;;
        *)
            errTxt="unknown error code ${res}"
    esac

    if [ -n "${errPrefix}" ]; then
        printf "%s\n" "${errPrefix}: ${errTxt}"
    else
        printf "%s\n" "${errTxt}"
    fi
}

IsSystemd()
{
    if [ ! -d '/etc/systemd/system' ]; then
        return 1
    fi

    PsCommand -p 1 -o comm= | grep -F 'systemd' >/dev/null 2>&1 || return $?
}

IsEulaAccepted()
{
    local ids=''
    ReadIni "${INI_FILEPATH}" 'AcceptedAgreement' 'EulaId' @ids || return $?
    printf '%s' "${ids}" | grep -iF "${EULA_ID}" >/dev/null 2>&1 || return $?
}

status()
{
    local statusCode=0
    local statusTxt=''

    local pid=''
    if GetPidOfExistingProcessByPidFile "${WDSERVER_PID}" "${WDSERVER_BIN}" @pid; then
        if kill -0 "${pid}" 2>/dev/null; then
            statusCode=${PRODUCT_RUNNING_STATUS}
            statusTxt='running'
        else
            statusCode=${PRODUCT_DEAD_STATUS}
            statusTxt='dead'
        fi
    else
        statusCode=${PRODUCT_STOPPED_STATUS}
        statusTxt='stopped'
    fi

    Echo "${PRODUCT_NAME} is ${statusTxt}"
    return ${statusCode}
}

DoStart()
{
    if status >/dev/null 2>&1; then
        Echo "${MSG_APP_ALREADY_STARTED}"
        return 0
    fi

    if ! "${LAUNCHER}" >/dev/null 2>&1; then
        Echo "${MSG_APP_COULD_NOT_BE_STARTED}"
        return 255
    fi

    CreateSubsysLockFile

    Echo "${MSG_APP_STARTED}"
    return 0
}

start()
{
    if [ ! -x "${LAUNCHER}" ]; then
        Echo "${MSG_FILE_WAS_NOT_FOUND}" >&2
        return 255
    fi

    if ! IsEulaAccepted; then
        printf "${MSG_ACCEPT_EULA_FIRST_1}\n" "${PRODUCT_NAME}" >&2
        return 1
    fi

    local res=0

    local oldUpgradeState=''
    GetUpgradeState @oldUpgradeState

    if [ ${PPID} -ne 1 ] && IsSystemd; then
        systemctl start "${SYSTEMD_SERVICE_FILENAME}"
        local out=''
        out="$(systemctl is-active "${SYSTEMD_SERVICE_FILENAME}")"
        if [ "${out}" = 'active' ]; then
            res=${PRODUCT_RUNNING_STATUS}
            Echo "${MSG_APP_STARTED}"
        else
            res=255
            Echo "${MSG_APP_NOT_STARTED}"
        fi
    else
       DoStart
       res=$?
    fi

    if [ "${oldUpgradeState}" = 'Uninstall' ]; then
        local i=0
        while :; do
            local newUpgradeState=''
            local newVersion=''
            if ! GetUpgradeState @newUpgradeState @newVersion; then
                break
            fi

            if [ "${newUpgradeState}" != 'Start' ]; then
                case "${newUpgradeState}" in
                    'Installed')
                        PrintfBold "\n${MSG_APP_UPDATE_INSTALLED_1}\n\n" "${newVersion}"
                        ;;
                    'RollingBack')
                        PrintfBold "\n${MSG_APP_UPDATE_ROOLBACK_PARTIALLY_1}\n\n" "${newVersion}"
                        ;;
                    'RolledBack')
                        PrintfBold "\n${MSG_APP_UPDATE_ROOLBACK_1}\n\n" "${newVersion}"
                        ;;
                    *)
                        ;;
                esac
                break
            fi

            if [ ${i} -ge 5 ]; then
                break
            fi

            i=$((i+1))
            sleep 1
        done
    fi

    return ${res}
}

CreateSubsysLockFile()
{
    if [ -d "${SUBSYS_LOCK_DIRECTORY}" ]; then
        touch "${SUBSYS_LOCK_FILE}"
    fi
}

RemoveSubsysLockFile()
{
    if [ -d "${SUBSYS_LOCK_DIRECTORY}" ]; then
        rm -f "${SUBSYS_LOCK_FILE}" 2>/dev/null
    fi
}

ClearStateFiles()
{
    {
        rm -f "${WDSERVER_PID}"
        RemoveSubsysLockFile
    } 1>/dev/null 2>&1
}

DoStop()
{
    local res=0
    local wdPid=''
    GetPidOfExistingProcessByPidFile "${WDSERVER_PID}" "${WDSERVER_BIN}" @wdPid || res=$?
    if [ ${res} -ne 0 ] ; then
        if [ ${res} -eq 1 ]; then
            Echo "${MSG_APP_ALREADY_STOPPED}"
            res=0
        else
            PrintPidFileError ${res} "${MSG_APP_COULD_NOT_BE_STOPPED}"
        fi
        ClearStateFiles
        return ${res}
    fi

    while true; do
        if ! kill -TERM "${wdPid}" 1>/dev/null 2>&1; then
            break
        fi
        sleep 1
    done

    ClearStateFiles

    Echo "${MSG_APP_STOPPED}"
    return ${res}
}

stop()
{
    if [ ${PPID} -ne 1 ] && IsSystemd; then
        systemctl stop "${SYSTEMD_SERVICE_FILENAME}"
        local out=''
        out="$(systemctl is-active "${SYSTEMD_SERVICE_FILENAME}")"
        if [ "${out}" != 'active' ]; then
            Echo "${MSG_APP_STOPPED}"
        else
            Echo "${MSG_APP_COULD_NOT_BE_STOPPED}"
        fi
    else
       DoStop || return $?
    fi
}

restart()
{
    stop
    start
}

Main()
{
    case "${1:-}" in
        start|stop|status|restart)
            "${1}" || return $?
            ;;
        *)
            Echo "Usage: $(basename "$0") {start|stop|status|restart}" >&2
            return 1
            ;;
    esac
}

Main "$@"
