#!/usr/bin/bash
#
# Usage: btest-bg-wait [-k] <timeout>
#
# Waits until all of the background process spawned by btest-bg-run
# have finished, or the given timeout (in seconds) has been exceeded.
#
# If the timeout triggers, all remaining processed are killed. If -k
# is not given, this is considered an error and the script abort with
# error code 1. If -k is given, a timeout is not considered an error.
#
# Once all processes have finished (or were killed), the scripts
# merges their stdout and stderr. If one of them returned an error,
# this script does so as well

if [ "$1" == "-k" ]; then
    timeout_ok=1
    shift
else
    timeout_ok=0
fi

if [ $# != 1 ]; then
    echo "usage: $(basename "$0") [-k] <timeout>"
    exit 1
fi

timeout=$(($1 * 100))

procs=$(cat .bgprocs)

rm -f .timeout
touch .timeout

function check_procs {
    for p in $procs; do
        if [ ! -e "$p/.exitcode" ]; then
            return 1
        fi
    done

    # All done.
    return 0
}

is_windows=0
case "$(uname -s)" in
    MINGW* | MSYS* | CYGWIN*) is_windows=1 ;;
esac

function kill_procs {
    for p in $procs; do
        if [ ! -e "$p/.exitcode" ]; then
            kill -1 "$(cat "$p/.pid")" 2>/dev/null
            cat "$p"/.cmdline >>.timeout
            if [ "$1" == "timeout" ]; then
                touch "$p/.timeout"
            fi
        fi
    done

    sleep 1

    for p in $procs; do
        if [ ! -e "$p/.exitcode" ]; then
            kill -9 "$(cat "$p/.pid")" 2>/dev/null
            sleep 1
        fi
    done

    # On Windows, use taskkill as a fallback to ensure child
    # processes and their descendants are terminated even if
    # MSYS signals did not reach them.
    if [ $is_windows -eq 1 ]; then
        local msys_pids=""
        for p in $procs; do
            if [ -f "$p/.winpid" ]; then
                local wpid
                wpid=$(cat "$p/.winpid")
                if [ -n "$wpid" ]; then
                    MSYS_NO_PATHCONV=1 taskkill /F /T /PID "$wpid" &>/dev/null
                fi
            fi

            # The .winpid captured at startup may hold a stale
            # pre-exec Windows PID.  Look up the *current* Windows
            # PID from /proc using the child's MSYS PID, which
            # remains valid and tracks the native process after exec.
            if [ -f "$p/.winchildpid" ]; then
                local cpid
                cpid=$(cat "$p/.winchildpid")
                if [ -n "$cpid" ] && [ -f /proc/"$cpid"/winpid ]; then
                    local live_wpid
                    live_wpid=$(cat /proc/"$cpid"/winpid)
                    if [ -n "$live_wpid" ]; then
                        MSYS_NO_PATHCONV=1 taskkill /F /T /PID "$live_wpid" &>/dev/null
                    fi
                fi
            fi

            if [ -f "$p/.pid" ]; then
                msys_pids="$msys_pids $(cat "$p/.pid")"
            fi
        done

        # Wait for killed processes to fully exit so that ports
        # and file handles are released before the next test.
        if [ -n "$msys_pids" ]; then
            local attempts=0
            while [ $attempts -lt 20 ]; do
                local any_alive=0
                for mpid in $msys_pids; do
                    if kill -0 "$mpid" 2>/dev/null; then
                        any_alive=1
                        break
                    fi
                done
                [ $any_alive -eq 0 ] && break
                sleep 0.1
                attempts=$((attempts + 1))
            done
        fi
    fi
}

function collect_output {
    # Truncate rather than rm: on Windows the parent process (btest)
    # still holds these files open and rm would fail with EBUSY.
    # Truncation is allowed because btest opens them with write-sharing.
    : >.stdout
    : >.stderr

    if [ $timeout_ok != 1 ] && [ -s .timeout ]; then
        {
            echo "The following processes did not terminate:"
            echo

            cat .timeout

            echo
            echo "-----------"
        } >>.stderr
    fi

    for p in $procs; do
        pid=$(cat "$p/.pid")
        cmdline=$(cat "$p/.cmdline")

        {
            printf "<<< [%s] %s\\n" "$pid" "$cmdline"
            cat "$p"/.stdout
            echo ">>>"
        } >>.stdout

        {
            printf "<<< [%s] %s\\n" "$pid" "$cmdline"
            cat "$p"/.stderr
            echo ">>>"
        } >>.stderr
    done
}

trap kill_procs EXIT

while true; do

    if check_procs; then
        # All done.
        break
    fi

    timeout=$((timeout - 1))

    if [ $timeout -le 0 ]; then
        # Timeout exceeded.
        kill_procs timeout

        if [ $timeout_ok == 1 ]; then
            # Just continue.
            break
        fi

        # Exit with error.
        collect_output
        exit 1
    fi

    sleep 0.01
done

trap - EXIT

# All terminated either by themselves, or with a benign timeout.

collect_output

# See if any returned an error.
result=0
for p in $procs; do
    if [ -e "$p/.timeout" ]; then
        # we're here because timeouts are ok, so don't mind the exit code
        # if we initiated killing the process due to timeout
        continue
    fi
    rc=$(cat "$p/.exitcode")
    pid=$(cat "$p/.pid")
    cmdline=$(cat "$p/.cmdline")

    if [ "$rc" != 0 ]; then
        echo ">>> process $pid failed with exitcode $rc: $cmdline" >>.stderr
        result=1
    fi
done

exit $result
