#!/usr/bin/env bash

# fail hard
set -o pipefail
# fail harder
set -eu
# for ${DOCUMENT_ROOT%%*(/)} pattern further down
shopt -s extglob
# for detecting when -l 'logs/*.log' matches nothing
shopt -s nullglob

if ! type -p "realpath" > /dev/null; then
	# cedar-14 and macOS don't have realpath
	# readlink is not an option because BSD readlink does not have the GNU -f option
	# must be a function so subshells, including $(…), can use it
	realpath() {
		python -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$@"
	}
fi

# we very likely got called via a symlink, so we have to realpath $0 first to find the base buildpack directory
bp_dir=$(cd $(dirname $(realpath $0)); cd ..; pwd)

verbose=

php_passthrough() {
	local dir=$(dirname "$1")
	local file=$(basename "$1")
	local out=$(basename "$file" .php)
	if [[ "$out" != "$file" ]]; then
		[[ $verbose ]] && echo "Interpreting ${1#$HEROKU_APP_DIR/} to $out" >&2
		out="$dir/$out"
		hhvm "$1" > "$out"
		echo "$out"
	else
		echo "$1"
	fi
}

check_exists() {
	if [[ ! -f "$HEROKU_APP_DIR/$1" ]]; then
		echo "Cannot read -$2 '$1' (relative to '$HEROKU_APP_DIR')" >&2
		exit 1
	else
		echo "$HEROKU_APP_DIR/$1"
	fi
}

touch_log() {
	mkdir -p $(dirname "$1") && touch "$1"
}

findconfig() {
	local dir=$(dirname "$2")
	local file=$(basename "$2")
	IFS='.' read -r -a version <<< "$1" # read the parts of $1 (version number) into an array, e.g. (7 2 33)

	# iterate "backwards" over version parts so we try "7/2/11" first, then "7/2", then "7" for a version "7.2.11"
	# we go down to 0, which will yield an empty string, for the last fallback layer without any version number
	for (( i = ${#version[@]}; i >= 0; i--)); do
		version_dir=$(IFS=/; echo "${version[*]:0:$i}") # set IFS to "/" for merging, but echo is a builtin, so it must be a subshell and a separate command
		full_path="${dir}/${version_dir}${version_dir:+/}${file}" # concat, but if scandir is empty (last fallback), don't produce a double slash
		if [[ -f "$full_path" ]]; then
			echo "$full_path";
			return 0;
		fi
	done
	echo "$2"
	return 1;
}

wait_pidfile() {
	i=0
	while ! test -f "$1" && (( i < 25 )); do
		[[ $verbose && ${2:-} ]] && echo "$2" >&2
		sleep 0.1
		((++i)) # don't do i++, that is a post increment operation, and will return status 1, since the expression evaluates to 0...
	done
	if (( i == 25 )); then
		# we timed out
		return 1;
	fi
}

wait_pid_and_pidfile() {
	i=0
	while ! test -f "$2" && (( i < 25 )); do
		if ! kill -0 "$1" 2> /dev/null; then # kill -0 checks if process exists
			break;
		fi
		[[ $verbose && ${3:-} ]] && echo "$3" >&2
		sleep 0.1
		((++i)) # don't do i++, that is a post increment operation, and will return status 1, since the expression evaluates to 0...
	done
	if (( i == 25 )); then
		# we timed out
		return 1;
	fi
}

print_help() {
	cat >&2 <<-EOF
		
		${1:-Boots HHVM together with Nginx on Heroku and for local development.}
		
		Usage:
		  heroku-hhvm-nginx [options] [<DOCUMENT_ROOT>]
		
		Options:
		  -C <nginx.inc.conf>     The path to the configuration file to include inside
		                          the Nginx server config (see option -c below). Will
		                          be included inside the 'server { ... }' block just
		                          after the 'listen', 'root' etc directives.
		                          Recommended approach when customizing Nginx's config
		                          in most cases, unless you need to set http or
		                          fundamental server level options.
		                          [default: <BPDIR>/conf/nginx/default_include.conf.php,
		                          or a more version-specific file from a subdirectory]
		  -c <nginx.conf>         The path to the full configuration file that is
		                          included after Heroku's main Nginx config has been
		                          loaded. It must contain an 'http { ... }' block with a
		                          'server { ... }' inside that contains 'listen' and
		                          'root' (see option -C above) directives.
		                          [default: <BPDIR>/conf/nginx/heroku.conf.php,
		                          or a more version-specific file from a subdirectory]
		  -h, --help              Display this help screen and exit.
		  -I <php.extra.ini>      The path to an extra php.ini to use in addition to the
		                          default HHVM php.ini (see option -i below).
		  -i <php.ini>            The path to the php.ini file to use. It is highly
		                          recommended to use the -I option (see above) instead.
		                          [default: <BPDIR>/conf/hhvm/php.ini.php,
		                          or a more version-specific file from a subdirectory]
		  -l <tailme.log>         Path to additional log file to tail to STDERR so its
		                          contents appear in 'heroku logs'. If the file does not
		                          exist, it will be created. Wildcards are allowed, but
		                          must be quoted and must match already existing files.
		                          Note: this option can be repeated multiple times.
		  -p <PORT>               Port to listen on for HTTP traffic. If this argument
		                          is not given, then the port number to use is read from
		                          the \$PORT environment variable, or a random port is
		                          chosen if that variable does not exist.
		  -v, --verbose           Be more verbose during startup.
		
		The <BPDIR> placeholder above represents the base directory of this buildpack:
		$bp_dir
		
		All file paths must be relative to '$HEROKU_APP_DIR'.
		
		Any file name that ends in '.php' will be run through the PHP interpreter first.
		You may use this for templating; this is, for instance, necessary for Nginx,
		where environment variables cannot be referenced in configuration files.
		
		If you would like to use the -C and -c options together, make sure you retain
		the appropriate include mechanisms (see default configs for details).
	EOF
}

# we need this in configs
export HEROKU_APP_DIR=$(pwd)
export DOCUMENT_ROOT="$HEROKU_APP_DIR"
# set a default port if none is given
export PORT=${PORT:-$(( $RANDOM+1024 ))}

# init logs array here as empty before parsing options; -l could append to it, but the default list gets added later since we use $PORT in there and that can be set using -p
declare -a logs

optstring=":-:C:c:I:i:l:p:vh"

# process flags first
while getopts "$optstring" opt; do
	case $opt in
		-)
			case "$OPTARG" in
				verbose)
					verbose=1
					;;
				help)
					print_help 2>&1
					exit
					;;
				*)
					echo "Invalid option: --$OPTARG" >&2
					exit 2
					;;
			esac
			;;
		v)
			verbose=1
			;;
		h)
			print_help 2>&1
			exit
			;;
	esac
done

OPTIND=1 # start over with options parsing
while getopts "$optstring" opt; do
	case $opt in
		C)
			nginx_config_include=$(check_exists "$OPTARG" "C")
			;;
		c)
			nginx_config=$(check_exists "$OPTARG" "c")
			;;
		I)
			hhvm_config_extra=$(check_exists "$OPTARG" "I")
			;;
		i)
			php_config=$(check_exists "$OPTARG" "i")
			;;
		l)
			logarg=( $OPTARG ) # must not quote this or wildcards won't get expanded into individual values
			if [[ ${#logarg[@]} -eq 0 ]]; then # we set nullglob to detect if a pattern matched nothing (then the array is empty)
				echo "Pattern '$OPTARG' passed to option -l matched no files" >&2
				exit 1
			fi
			for logfile in "${logarg[@]}"; do
				if [[ -d "$logfile" ]]; then
					echo "-l '$logfile': is a directory" >&2
					exit 1
				fi
				touch_log "$logfile" || { echo "Could not touch '$logfile'; permissions problem?" >&2; exit 1; }
				[[ $verbose ]] && echo "Tailing '$logfile' to stderr" >&2
				logs+=("$logfile") # must quote here in case a wildcard matched a file with a space in the name
			done
			;;
		p)
			PORT="$OPTARG"
			;;
		\?)
			echo "Invalid option: -$OPTARG" >&2
			exit 2
			;;
		:)
			echo "Option -$OPTARG requires an argument" >&2
			exit 2
			;;
	esac
done
# clear processed arguments
shift $((OPTIND-1))

if [[ "$#" -gt "1" ]]; then
	print_help "$(cat <<-EOF
		$(basename $0): too many arguments. If you're using options,
		make sure to list them before any document root argument you're providing.
	EOF
	)"
	exit 2
fi

# our standard logs
logs+=( "/tmp/heroku.nginx_access.$PORT.log" )

hhvm --php -r 'exit((int)version_compare(HHVM_VERSION, "3.0.1", "<"));' || { echo "This program requires HHVM 3.0.1 or newer" >&2; exit 1; }

hhvm_version="$(hhvm --php -r 'echo HHVM_VERSION;')"
nginx_version="$(nginx -v 2>&1 | hhvm --php -r 'echo preg_replace("#.*nginx/([\d\.]+)$#sm", "\\1", file_get_contents("php://stdin"));')"

unset -f composer # set on startup as a function that performs "hhvm $(which composer)"
# make sure we try a local composer.phar if no global installation is available
composer_bin=$(command -v composer ./composer.phar) || { echo "This program requires 'composer' on \$PATH or a 'composer.phar' in the CWD." >&2; exit 1; }
composer_bin=$(head -n1 <<< "$composer_bin") # if both were there, we'd have two lines of output, no good for invoking something :)
composer() {
	# check if the composer binary is executable by HHVM
	if file --brief --dereference "$composer_bin" | grep -e "shell" -e "bash" > /dev/null ; then # newer versions of file return "data" for .phar
		# run it directly; it's probably a bash script or similar (homebrew-php does this)
		"$composer_bin" "$@"
	else
		hhvm "$composer_bin" "$@"
	fi
}
# these exports are used in default web server configs to lock down access to composer directories
COMPOSER_VENDOR_DIR=$(composer config vendor-dir 2> /dev/null | tail -n 1) && export COMPOSER_VENDOR_DIR || { echo "Unable to determine Composer vendor-dir setting; is 'composer' executable on path or 'composer.phar' in current working directory?" >&2; exit 1; } # tail, as composer echos outdated version warnings to STDOUT; export after the assignment or exit status will that be of 'export
COMPOSER_BIN_DIR=$(composer config bin-dir 2> /dev/null | tail -n 1) && export COMPOSER_BIN_DIR || { echo "Unable to determine Composer bin-dir setting; is 'composer' executable on path or 'composer.phar' in current working directory?" >&2; exit 1; } # tail, as composer echos outdated version warnings to STDOUT; export after the assignment or exit status will that be of 'export

if [[ "$#" == "1" ]]; then
	DOCUMENT_ROOT="$HEROKU_APP_DIR/$1"
	if [[ ! -d "$DOCUMENT_ROOT" ]]; then
		echo "DOCUMENT_ROOT '$1' does not exist" >&2
		exit 1
	else
		# strip trailing slashes if present
		DOCUMENT_ROOT=${DOCUMENT_ROOT%%*(/)} # powered by extglob
		if [[ $verbose ]]; then
			echo "DOCUMENT_ROOT changed to '$DOCUMENT_ROOT'" >&2
		else
			echo "DOCUMENT_ROOT changed to '${1%%*(/)}/'" >&2
		fi
	fi
fi

function prepare_hhvm_ini() { # we have to do this twice, as the WEB_CONCURRENCY setting is evaluated using PHP code, not ${...} syntax which HHVM does not support; if a value for $1 is passed then it won't echo messages upon second invocation
if [[ ( -n ${php_config:-} && ! ${1:-} ) || ( ${php_config:=$(findconfig "$hhvm_version" "$bp_dir/conf/hhvm/php.ini.php")} && $verbose && ! ${1:-} ) ]]; then
	echo "Using HHVM configuration (php.ini) file '${php_config#$HEROKU_APP_DIR/}'" >&2
fi
php_configs=( "-c" "$(php_passthrough "$php_config")" )

if [[ -n ${hhvm_config_extra:-} ]]; then
	[[ ${1:-} ]] || echo "Using additional HHVM configuration (php.ini) file '${hhvm_config_extra#$HEROKU_APP_DIR/}'" >&2
	php_configs+=( "-c" "$(php_passthrough "$hhvm_config_extra")" )
fi
}
prepare_hhvm_ini

if [[ -n ${nginx_config_include:-} || ( ${nginx_config_include:=$(findconfig "$nginx_version" "$bp_dir/conf/nginx/default_include.conf.php")} && $verbose ) ]]; then
	echo "Using Nginx server-level configuration include '${nginx_config_include#$HEROKU_APP_DIR/}'" >&2
fi
nginx_config_include=$(php_passthrough "$nginx_config_include")
export HEROKU_PHP_NGINX_CONFIG_INCLUDE="$nginx_config_include"

if [[ -n ${nginx_config:-} || ( ${nginx_config:=$(findconfig "$nginx_version" "$bp_dir/conf/nginx/heroku.conf.php")} && $verbose) ]]; then
	echo "Using Nginx configuration file '${nginx_config#$HEROKU_APP_DIR/}'" >&2
fi
nginx_config=$(php_passthrough "$nginx_config")

nginx_main=$(findconfig "$nginx_version" "$bp_dir/conf/nginx/main.conf")

if [[ -z ${WEB_CONCURRENCY:-} ]]; then
	maxprocs=$(ulimit -u)
	ram="512M"
	if [[ -n ${DYNO:-} && "$maxprocs" == "32768" ]]; then
		echo "Optimizing defaults for PX dyno...." >&2
		ram="6G"
	elif [[ -n ${DYNO:-} && "$maxprocs" == "16384" ]]; then
		echo "Optimizing defaults for IX dyno...." >&2
		ram="2560M"
	elif [[ -n ${DYNO:-} && "$maxprocs" == "512" ]]; then
		echo "Optimizing defaults for 2X dyno..." >&2
		ram="1G"
	elif [[ -n ${DYNO:-} && "$maxprocs" == "256" ]]; then
		echo "Optimizing defaults for 1X dyno..." >&2
	elif [[ -n ${DYNO:-} || $verbose ]]; then
		echo "No dyno detected; using defaults for 1X..." >&2
	fi

	# determine number of HHVM threads to run
	read WEB_CONCURRENCY php_memory_limit <<<$(hhvm "${php_configs[@]}" $bp_dir/bin/util/autotune.php "$ram")
	[[ $WEB_CONCURRENCY -lt 1 ]] && WEB_CONCURRENCY=1
	export WEB_CONCURRENCY

	echo "${WEB_CONCURRENCY} threads at ${php_memory_limit}B memory limit." >&2
else
	echo "Using WEB_CONCURRENCY=${WEB_CONCURRENCY} threads." >&2
fi

# we changed WEB_CONCURRENCY; now we need to re-evaluate the configs as HHVM doesn't support ${...} syntax, so the configs contain PHP getenv() calls and are templated; we pass an argument to it to avoid it echoing "using HHVM config ..." messages again because that already happened
prepare_hhvm_ini no_messages

# make a shared pipe; we'll write the name of the process that exits to it once that happens, and wait for that event below
# this particular call works on Linux and Mac OS (will create a literal ".XXXXXX" on Mac, but that doesn't matter).
wait_pipe=$(mktemp -t "heroku.waitpipe-$PORT.XXXXXX" -u)
rm -f $wait_pipe
mkfifo $wait_pipe
exec 3<> $wait_pipe

pids=()

# trap SIGTERM (for when we get killed) and EXIT (upon failure of any command due to set -e, or because of the exit at the very end), we then
# 1) restore the trap for the signal in question so it doesn't fire again due to the kill at the end of the trap, as well as for EXIT, because that would fire too
# 2) call cleanup() to
# 2a) kill all the subshells we've spawned, in reverse order - they in turn have their own traps to kill their respective subprocesses
# 2b) send STDERR to /dev/null so we don't see "no such process" errors - after all, one of the subshells may be gone
# 2c) || true so that set -e doesn't cause a mess if the kill returns 1 on "no such process" cases
# 2d) 'wait' on that killed subprocess, sending wait's output to /dev/null - this prevents the logs getting cluttered with "vendor/bin/heroku-...: line 309:    96 Terminated" messages (we can't use 'disown' after launching programs for that purpose because that removes the jobs from the shell's jobs table and then we can no longer 'wait' on the program)
# 2e) remove our FIFO from above
# 3) as the signal to subshells, we use SIGUSR1 if we're getting a SIGTERM or SIGINT (for graceful shutdown), and a SIGTERM otherwise
# 3a) this is so subshells can tell whether we sent them the signal and they should in turn stop gracefully (SIGUSR1)...
# 3b) ... or if something crashed, or e.g. Heroku's process manager sent SIGTERM to all processes in the group, in which case a graceful shutdown attempt is futile anyway
# 3c) SIGINT oder SIGQUIT are not an option since backgrounded subshells cannot trap them (http://vaab.blog.kal.fr/2016/07/30/trapping-sigint-sigquit-in-asynchronous-tasks/)
# 4) kill ourselves with the correct signal in case we're handling SIGTERM or, further down, SIGINT (http://www.cons.org/cracauer/sigint.html and http://mywiki.wooledge.org/SignalTrap#Special_Note_On_SIGINT1)
cleanup() {
	signal=${1:-TERM}
	# reverse through list of child processes and kill them
	# we want that order so the web server stops accepting connections and drains properly, then the app is terminated, and then finally the logs redirect, which we want alive up until the very last moment
	for (( i=${#pids[@]}-1 ; i>=0 ; i-- )) ; do
		kill -"$signal" "${pids[$i]}" 2> /dev/null || true # redirect stderr and || true the call because the process might be gone
		wait "${pids[$i]}" 2> /dev/null || true # redirect stderr so that the Bash's confusing "Terminated: …" lines are suppressed and || true the call because wait returns the exit status of the program, and not 0
	done
	rm -f ${wait_pipe} || true;
	echo "Shutdown complete." >&2
}
trap 'trap - TERM EXIT; echo "SIGTERM received, attempting graceful shutdown..." >&2; cleanup USR1; kill -TERM $$' TERM
# on "regular" EXIT (i.e. when we reached the end of the script because a process exited unexpectedly) just clean up and let the exit status bubble through (see last line)
trap 'trap - EXIT; echo "Process exited unexpectedly: $exitproc, shutting down..." >&2; cleanup TERM' EXIT

# if FD 1 is a TTY (that's the -t 1 check), trap SIGINT/Ctrl+C, then same procedure as for TERM above
if [[ -t 1 ]]; then
	trap 'trap - INT EXIT; echo "SIGINT received, initiating graceful shutdown..." >&2; cleanup USR1; kill -INT $$' INT;
# if FD 1 is not a TTY (e.g. when we're run through 'foreman start'), do nothing on SIGINT; the assumption is that the parent will send us a SIGTERM or something when this happens. With the trap above, Ctrl+C-ing out of a 'foreman start' run would trigger the INT trap both in Foreman and here (because Ctrl+C sends SIGINT to the entire process group, but there is no way to tell the two cases apart), and while the trap is still doing its shutdown work triggered by the SIGTERM from the Ctrl+C, Foreman would then send a SIGTERM because that's what it does when it receives a SIGINT itself.
else
	trap '' INT;
fi

# we are now launching a subshell for each of the tasks (log tail, app server, web server)
# 1) each subshell has an echo as an EXIT trap that echos the command name to FD 3 (see the FIFO set up above)
# 1a) a 'read' at the end of the script will block on reading from that FD and, when something is written to the pipe, will unblock, which finishes the script, which triggers the exit trap further above, which does some cleanup
# 2) each subshell also has traps that on TERM or USR1 kill the processes inside the subshell
# 2a) if a SIGTERM or SIGUSR1 is received, this will unblock the first 'wait' and execute the corresponding trap
# 2b) a second 'wait' then ensures that the subshell remains alive until the process inside has shut down
# 2c) both 'wait' calls use || true to prevent 'set -e' failures: 'wait' returns the exit status of the given program, and that won't be zero, because it's getting killed
# 2d) 'wait' on that killed subprocess, sending wait's output to /dev/null - this prevents the logs getting cluttered with "vendor/bin/heroku-...: line 309:    96 Terminated" messages (we can't use 'disown' after launching programs for that purpose because that removes the jobs from the shell's jobs table and then we can no longer 'wait' on the program)
# 3) each subshell has another trap on INT which does nothing (to prevent Ctrl+C interference)
# 4) we execute the command in the background, recording its PID(s) for the subshell's TERM trap
# 5) we also execute the subshell in the background, recording its PID for the main TERM/INT traps

(
	trap 'echo "log redirection" >&3;' EXIT
	trap '' INT;

	[[ $verbose ]] && echo "Starting log redirection..." >&2

	touch "${logs[@]}"

	tail -qF -n 0 "${logs[@]}" 1>&2 & pid=$!

	# after the kill, we are redirecting stderr to /dev/null using exec to suppress "… Terminated …" messages from the shell, which it would print right before the next command is invoked
	trap '[[ $verbose ]] && echo "Stopping log redirection..." >&2; kill -TERM $pid 2> /dev/null || true; exec 2> /dev/null' TERM USR1

	wait $pid 2> /dev/null || true # redirect stderr to prevent possible trap race condition warnings
	trap - TERM USR1
	wait $pid 2> /dev/null || true # redirect stderr to prevent possible trap race condition warnings
) & pids+=($!)

hhvm_pidfile=$(mktemp -t "heroku.hhvm.pid-$PORT.XXXXXX" -u)
(
	trap 'echo "hhvm" >&3;' EXIT
	trap '' INT;

	echo "Starting hhvm..." >&2

	hhvm --mode server -d hhvm.pid_file="$hhvm_pidfile" "${php_configs[@]}" & pid=$!
	# wait for the pidfile in the trap; otherwise, a previous subshell failing may result in us getting a SIGTERM and forwarding it to the child process before that child process is ready to process signals
	trap 'echo "Stopping hhvm..." >&2; wait_pid_and_pidfile $pid "$hhvm_pidfile"; kill -KILL $pid 2> /dev/null || true' TERM
	trap 'echo "Stopping hhvm gracefully..." >&2; wait_pid_and_pidfile $pid "$hhvm_pidfile"; kill -HUP $pid 2> /dev/null || true' USR1

	wait $pid 2> /dev/null || true # redirect stderr to prevent possible trap race condition warnings
	trap - TERM USR1
	wait $pid 2> /dev/null || true # redirect stderr to prevent possible trap race condition warnings
) & pids+=($!)

nginx_pidfile=$(mktemp -t "heroku.nginx.pid-$PORT.XXXXXX" -u)
(
	trap 'echo "nginx" >&3;' EXIT
	trap '' INT;

	# wait for HHVM to write its PID file, which it does after initialization
	# if we didn't wait, the web server would start sending traffic to the FCGI backend before it's ready (or before the socket is even created)
	# if HHVM fails to start, the HHVM subshell will exit, causing the main cleanup procedure to run, which sends this subshell a SIGTERM
	# there is however a possible race condition where this happens exactly the moment we hit the function below, which is why it internally has a timeout of 2500 ms, and we use that to exit 1
	wait_pidfile "$hhvm_pidfile" "Waiting for hhvm to finish initializing..." || { echo "Timed out waiting for hhvm." >&2; exit 1; }

	echo "Starting nginx..." >&2

	nginx -c "$nginx_main" -g "pid $nginx_pidfile; include $nginx_config;" & pid=$!
	# wait for the pidfile in the trap; otherwise, a previous subshell failing may result in us getting a SIGTERM and forwarding it to the child process before that child process is ready to process signals
	trap 'echo "Stopping nginx..." >&2; wait_pid_and_pidfile $pid "$nginx_pidfile"; kill -TERM $pid 2> /dev/null || true' TERM
	trap 'echo "Stopping nginx gracefully..." >&2; wait_pid_and_pidfile $pid "$nginx_pidfile"; kill -QUIT $pid 2> /dev/null || true' USR1

	# first, we wait for the pid file to be written, so that we can emit a ready message
	wait_pid_and_pidfile $pid "$nginx_pidfile"
	# on Heroku, there is a "state changed from starting to up", but for local execution, we want a "ready" message
	[[ -z ${DYNO:-} || $verbose ]] && kill -0 $pid 2> /dev/null && echo "Application ready for connections on port $PORT." >&2

	wait $pid 2> /dev/null || true # redirect stderr to prevent possible trap race condition warnings
	trap - TERM USR1
	wait $pid 2> /dev/null || true # redirect stderr to prevent possible trap race condition warnings
) & pids+=($!)

# wait for something to come from the FIFO attached to FD 3, which means that the given process was killed or has failed
# this will be interrupted by a SIGTERM or SIGINT in the traps further up
# if the pipe unblocks and this executes, then we won't read it again, so if the traps further up kill the remaining subshells above, their writing to FD 3 will have no effect
read exitproc <&3
# we'll only reach this if one of the processes above has terminated
# this will trigger the main EXIT trap further up and kill all remaining children
# code 70 is EX_SOFTWARE from sysexits.h
# Ctrl+C/SIGINT or SIGTERM exits will, through the respective trap's 'kill $$', exit with the correct 128+$signalcode status
exit 70
