1#!/bin/bash 2# 3# Script for running java with a timeout. 4# 5# The timeout in seconds must be the first argument. The rest of the arguments 6# are passed to the java binary itself. 7# 8# For example: 9# java-timeout 120 -cp classes.jar org.junit.runner.JUnitCore 10# runs: 11# java -cp classes.jar org.junit.runner.JUnitCore 12# with a timeout of 2 minutes. 13 14set -euo pipefail 15 16# Prints a message and terminates the process. 17function fatal() { 18 echo "FATAL: $*" 19 exit 113 20} 21 22# Function that is invoked if java is terminated due to timeout. 23# It take the process ID of the java command as an argument if it has already 24# been started, or the empty string if not. It should very rarely receive the 25# empty string as the pid, but it is possible. 26function on_timeout() { 27 echo 'FATAL: command timed out' 28 29 local pid="${1-}" 30 shift || fatal '[on_timeout] missing argument: pid' 31 test $# = 0 || fatal '[on_timeout] too many arguments' 32 33 if [ "$pid" != '' ]; then 34 # It is possible that the process already terminated, but there is not much 35 # we can do about that. 36 kill -TERM -- "-$pid" # Kill the entire process group. 37 fi 38} 39 40# Executes java with the given argument, waiting for a termination signal from 41# runalarm which this script is running under. The arguments are passed to the 42# java binary itself. 43function execute() { 44 # Trap SIGTERM, which is what we will receive if runalarm interrupts us. 45 local pid # Set below after we run the process. 46 trap 'on_timeout $pid' SIGTERM 47 # Starts java within a new process group and saves it process ID before 48 # blocking waiting for it to complete. 'setsid' starts the process within a 49 # new process group, which means that it will not be killed when this shell 50 # command is killed. This is needed so that the signal handler in the trap 51 # command above to be invoked before the java command is terminated (and will 52 # in fact have to terminate it itself). 53 setsid java "$@" & pid="$!"; wait "$pid" 54} 55 56# Runs java with a timeout. The first argument is either the timeout in seconds 57# or the string 'execute', which is used internally to execute the command under 58# runalarm. 59function main() { 60 local timeout_secs="${1-}" 61 shift || fatal '[main]: missing argument: timeout_secs' 62 # The reset of the arguments are meant for the java binary itself. 63 64 if [[ $timeout_secs = '0' ]]; then 65 # Run without any timeout. 66 java "$@" 67 elif [[ $timeout_secs = 'execute' ]]; then 68 # This means we actually have to execute the command. 69 execute "$@" 70 elif (( timeout_secs < 30 )); then 71 # We want to have a timeout of at least 30 seconds, so that we are 72 # guaranteed to be able to start the java command in the subshell. This also 73 # catches non-numeric arguments. 74 fatal 'Must specify a timeout of at least 30 seconds.' 75 else 76 # Wrap the command with the standard timeout(1) if available. 77 # "runalarm" is a Google timeout clone, and Mac users who've installed 78 # GNU coreutils have timeout available as "gtimeout". 79 if type timeout > /dev/null 2>&1 ; then 80 timeout "${timeout_secs}" "$0" 'execute' "$@" 81 elif type runalarm > /dev/null 2>&1 ; then 82 runalarm -t "$timeout_secs" "$0" 'execute' "$@" 83 elif type gtimeout > /dev/null 2>&1 ; then 84 gtimeout "${timeout_secs}s" "$0" 'execute' "$@" 85 else 86 # No way to set a timeout available, just execute directly. 87 echo "Warning: unable to enforce timeout." 1>&2 88 java "$@" 89 fi 90 fi 91} 92 93 94main "$@" 95