# Copyright 2012 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
# * Neither the name of Google Inc. nor the names of its contributors
#   may be used to endorse or promote products derived from this software
#   without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

shtk_import cleanup
shtk_import cli


shtk_process_run() {
    local timeout=

    local OPTIND
    while getopts ':t:' arg "${@}"; do
        case "${arg}" in
            t)  # Timeout.
                timeout="${OPTARG}"
                ;;

            :)
                shtk_cli_error "Missing argument to option -${OPTARG} in" \
                    "shtk_process_run"
                ;;

            \?)
                shtk_cli_error "Unknown option -${OPTARG} in shtk_process_run"
                ;;
        esac
    done
    shift $((${OPTIND} - 1))
    OPTIND=1  # Should not be necessary due to the 'local' above.

    shtk_cli_info "Running '${@}' in $(pwd)"

    local ret=
    if [ -z "${timeout}" ]; then
        ret=0
        "${@}" || ret="${?}"
    else
        # Spawning a process with a timeout is tricky because we have to spawn
        # two subprocesses: one to run the actual process and one to act as a
        # watchdog.  Because both of these processes are blocking, we cannot
        # perform their duties on the current process.  These two processes kill
        # themselves mutually on completion regardless of the order in which
        # they finish and, because their interdependency, we need to pass
        # details between them with a file.  Once they are both dead, we have to
        # guess what actually happened.
        #
        # The cookie file below works like this: first, the PID of the
        # subprocess running the desired command is written to it.  The watchdog
        # process uses the content of this file to kill the subprocess if
        # needed.  Once the subprocess has finished, its exit status is written
        # to the cookie.  If the subprocess has been killed, then the magic word
        # TIMEOUT is written to the cookie.

        local pattern="${TMPDIR:-/tmp}/shtk.XXXXXX"
        local cookie="$(mktemp "${pattern}" 2>/dev/null)"
        [ -f "${cookie}" ] || shtk_cli_error "Failed to create temporary file" \
            "using pattern ${pattern}"
        eval "shtk_process_run_remove_cookie() { rm -f '${cookie}'; }"
        shtk_cleanup_register shtk_process_run_remove_cookie

        (
            sleep "${timeout}"
            while [ -z "$(cat "${cookie}")" ]; do
                # Race protection: the process has not yet started.
                sleep 1
            done
            local pid="$(cat "${cookie}")"
            shtk_cli_warning "Timer expired; killing ${pid}"
            kill -9 "${pid}"
            echo "TIMEOUT" >"${cookie}"
        ) &
        local watchdog="${!}"

        (
            local ret=0
            "${@}" || ret="${?}"
            kill -9 "${watchdog}"
            while [ -z "$(cat "${cookie}")" ]; do
                # Race protection: the parent has not yet written the cookie.
                sleep 1
            done
            echo "${ret}" >"${cookie}"
        ) &
        local subprocess="${!}"
        echo "${subprocess}" >"${cookie}"

        wait >/dev/null 2>&1  # Silence 'Killed' notifications.

        ret="$(cat "${cookie}")"
        rm -f "${cookie}"
    fi
    [ -n "${ret}" ] || shtk_cli_error "ret not initialized; something failed"

    if [ "${ret}" = "TIMEOUT" ]; then
        shtk_cli_warning "Command timed out after ${timeout} seconds"
        ret=127
    elif [ "${ret}" -eq 0 ]; then
        shtk_cli_info "Command finished successfully"
    else
        shtk_cli_warning "Command failed with code ${ret}"
    fi
    return "${ret}"
}
