#!/bin/bash # # Script for running java with a timeout. # # The timeout in seconds must be the first argument. The rest of the arguments # are passed to the java binary itself. # # For example: # java-timeout 120 -cp classes.jar org.junit.runner.JUnitCore # runs: # java -cp classes.jar org.junit.runner.JUnitCore # with a timeout of 2 minutes. set -euo pipefail # Prints a message and terminates the process. function fatal() { echo "FATAL: $*" exit 113 } # Function that is invoked if java is terminated due to timeout. # It take the process ID of the java command as an argument if it has already # been started, or the empty string if not. It should very rarely receive the # empty string as the pid, but it is possible. function on_timeout() { echo 'FATAL: command timed out' local pid="${1-}" shift || fatal '[on_timeout] missing argument: pid' test $# = 0 || fatal '[on_timeout] too many arguments' if [ "$pid" != '' ]; then # It is possible that the process already terminated, but there is not much # we can do about that. kill -TERM -- "-$pid" # Kill the entire process group. fi } # Executes java with the given argument, waiting for a termination signal from # runalarm which this script is running under. The arguments are passed to the # java binary itself. function execute() { # Trap SIGTERM, which is what we will receive if runalarm interrupts us. local pid # Set below after we run the process. trap 'on_timeout $pid' SIGTERM # Starts java within a new process group and saves it process ID before # blocking waiting for it to complete. 'setsid' starts the process within a # new process group, which means that it will not be killed when this shell # command is killed. This is needed so that the signal handler in the trap # command above to be invoked before the java command is terminated (and will # in fact have to terminate it itself). setsid -w java "$@" & pid="$!"; wait "$pid" } # Runs java with a timeout. The first argument is either the timeout in seconds # or the string 'execute', which is used internally to execute the command under # runalarm. function main() { local timeout_secs="${1-}" shift || fatal '[main]: missing argument: timeout_secs' # The reset of the arguments are meant for the java binary itself. if [[ $timeout_secs = '0' ]]; then # Run without any timeout. java "$@" elif [[ $timeout_secs = 'execute' ]]; then # This means we actually have to execute the command. execute "$@" elif (( timeout_secs < 30 )); then # We want to have a timeout of at least 30 seconds, so that we are # guaranteed to be able to start the java command in the subshell. This also # catches non-numeric arguments. fatal 'Must specify a timeout of at least 30 seconds.' else # Wrap the command with the standard timeout(1) if available. # "runalarm" is a Google timeout clone, and Mac users who've installed # GNU coreutils have timeout available as "gtimeout". if type timeout > /dev/null 2>&1 ; then timeout "${timeout_secs}" "$0" 'execute' "$@" elif type runalarm > /dev/null 2>&1 ; then runalarm -t "$timeout_secs" "$0" 'execute' "$@" elif type gtimeout > /dev/null 2>&1 ; then gtimeout "${timeout_secs}s" "$0" 'execute' "$@" else # No way to set a timeout available, just execute directly. echo "Warning: unable to enforce timeout." 1>&2 java "$@" fi fi } main "$@"