teaching machines

Limiting Execution Time in a Shell

Every fall my school participates in the ACM’s International Collegiate Programming Contest. Thankfully, we don’t have far to travel. In even years, we host the competition locally. In odd years, we go to a town only a half-hour away. This year was odd.

When we arrived, I learned that there was zero infrastructure for judging the teams’ submissions. That meant I had to roll my sleeves up and starting writing a shell script to pull a team’s code off a USB drive, compile it, run it with the test cases, and diff the actual output with the expected output. I wrote the script, it worked great, and I felt like a true hero.

However, sometimes the teams’ code would take longer than the allotted time to compute an answer. I really wanted my script to run their code for no more than N seconds. It didn’t happen that day. Several months later, I now have a little utility that does just that.

#!/usr/bin/env zsh

# ---------------------------------------------------------------------------- 
# FILE:   forn                                                              
# AUTHOR: Chris Johnson                                                        
# DATE:   Dec 29 2013                                                          
#                                                                              
# Runs a shell command for at most a specified number of seconds. If the job
# takes longer than the allotted time, it's killed and the script exits with
# status 143. Otherwise, the job finishes normally or fails before the timer
# expires. In either case, this script exits with the job's exit status.
#
# The long-running job does not accept input interactively.
#
# Usage:
#   forn nseconds command [arg1 [arg2 ...]]
#
# Examples:
#   forn 3 sleep 5
#   forn 90 java MyClass -g -a -l case006.txt
#   forn 10 sort < names.txt
# ---------------------------------------------------------------------------- 

if [[ $# -lt 2 ]]; then
  echo "Usage: $0 nseconds command [arg1 [arg2 ...]]" >&2
  exit 1
fi

nseconds=$1
shift

# cmd is going to be embedded in a string, so we'll need to do some quoting of
# its elements for correct interpolation.
cmd=(${(q-)@})

# Kills the specified process after nseconds have expired.
sleepkill() {
  sleep $1
  kill $2
}

# Start up both the long-running process and a sleep timer. The long-running
# process is made up of ARGV[2..]
eval "($cmd) &"
longpid=$!

sleepkill $nseconds $longpid &
sleeppid=$!

# By default, Control-C will kill the wait that happens below -- and not
# longpid. I want it to kill longpid and the sleep timer.
TRAPINT() {
  kill $longpid
  kill -HUP -$$
}

# Wait for longpid to finish. If longpid has already finished, wait will flash
# a message saying it doesn't know about the process. I don't want to see that
# message.
wait $longpid 2>/dev/null

# If longpid's exit status is > 128, then a signal was sent to it to kill it.
# If that signal was SIGTERM/15, then we'll see a status of 143 (128 + 15).
ok=$?
if [[ $ok -eq 143 ]]; then
  print "Command $cmd timed out."
fi

# If longpid finishes before the sleep timer, let's kill the sleep timer.
kill $sleeppid 2>/dev/null

exit $ok

2015, here we come.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *