James Lemieux | 2a2559e | 2018-01-28 02:56:27 -0800 | [diff] [blame] | 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 | |
| 14 | set -euo pipefail |
| 15 | |
| 16 | # Prints a message and terminates the process. |
| 17 | function 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. |
| 26 | function 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. |
| 43 | function 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). |
Colin Cross | 44058c1 | 2020-01-10 15:54:18 -0800 | [diff] [blame] | 53 | setsid -w java "$@" & pid="$!"; wait "$pid" |
James Lemieux | 2a2559e | 2018-01-28 02:56:27 -0800 | [diff] [blame] | 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. |
| 59 | function 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 |
Elliott Hughes | 060e46f | 2018-10-26 15:43:42 -0700 | [diff] [blame] | 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 |
James Lemieux | 2a2559e | 2018-01-28 02:56:27 -0800 | [diff] [blame] | 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 | |
| 94 | main "$@" |