[comp.unix.questions] Timeout on shell command.

brister@decwrl.dec.com (James Brister) (08/11/90)

I'd like to have a shell script run a command, but if that command doesn't
finish in X seconds, then the script should kill it, if the command
finishes sooner then the script should immediately continue. Any ideas on
how one could achieve this?

Thanks

James
--
James Brister                                           brister@decwrl.dec.com
DEC Western Software Lab.                                 .....!decwrl!brister
"Politics is comedy with pretense" -- Mark Alan Stamaty

tchrist@convex.COM (Tom Christiansen) (08/12/90)

In article <BRISTER.90Aug10222433@westworld.decwrl.dec.com> brister@decwrl.dec.com (James Brister) writes:
>I'd like to have a shell script run a command, but if that command doesn't
>finish in X seconds, then the script should kill it, if the command
>finishes sooner then the script should immediately continue. Any ideas on
>how one could achieve this?

Here's timeout.c; syntax is 'timeout seconds command'.  

--tom

#include <stdio.h>
#include <signal.h>
#include <sysexits.h>
#include <sys/wait.h>

int pid,count;
union wait status;
int bang();
char **commands;

main(ac,av) 
    char **av;
{
    if (ac < 3) {
usage:  fprintf (stderr, "usage: %s seconds command\n",*av);
	exit (EX_USAGE);
    } 
    if ((count=atoi(av[1])) < 1) {
	fprintf (stderr, "seconds (%s) malformed or nonpositive\n",av[1]);
	goto usage;
    } 

    commands = &av[2];
    switch (pid=fork()) {
	default: parent(); 
		 /* NOTREACHED */
		 break;
	case 0: child();  
		 /* NOTREACHED */
	case -1: perror("fork"); 
		 exit(EX_OSERR); 
		 /* NOTREACHED */
    } 
} 

parent() {
    (void) signal(SIGALRM,bang);
    alarm(count);
    while(wait(&status) != pid) 
	  /* VOID */; 
    if (WIFSIGNALED(status)) 
	exit(-status.w_termsig);
    exit(status.w_retcode);

} 


bang() {
    fprintf(stderr,"Timeout!\n");
    (void) signal(SIGALRM,SIG_DFL);
    (void) kill(pid,SIGTERM);
    if (kill(pid,0)) {
	sleep(1);
	(void) kill(pid,SIGKILL);
    }
    exit(EX_TEMPFAIL);
} 

child() {
    execvp(*commands,commands);
    perror(*commands);
    _exit(EX_DATAERR);
    /* NOTREACHED */
} 

/* lint output:
 *	timeout.c:
 */
--

    Tom Christiansen                       {uunet,uiucdcs,sun}!convex!tchrist 
    Convex Computer Corporation                            tchrist@convex.COM
		 "EMACS belongs in <sys/errno.h>: Editor too big!"

donlash@uncle.uucp (Donald Lashomb) (08/13/90)

In article <BRISTER.90Aug10222433@westworld.decwrl.dec.com> brister@decwrl.dec.com (James Brister) writes:
>I'd like to have a shell script run a command, but if that command doesn't
>finish in X seconds, then the script should kill it, if the command
>finishes sooner then the script should immediately continue. Any ideas on
>how one could achieve this?

I'd approach it like this-

#!/bin/sh
#
#  run the command in the background
#  remember its process ID
#  sleep for X seconds
#  kill the background command --- note: if
#  the background command is finished, then
#  the kill will fail benignly.
#
command &
cmdpid=$!
sleep $X
kill $cmdpid

hope this helps
-Don

lee@sq.sq.com (Liam R. E. Quin) (08/15/90)

brister@decwrl.dec.com (James Brister) writes:
>I'd like to have a shell script run a command, but if that command doesn't
>finish in X seconds, then the script should kill it, if the command
>finishes sooner then the script should immediately continue.

tchrist@convex.COM (Tom Christiansen) writes:
>Here's timeout.c; syntax is 'timeout seconds command'.  

That is probably the neatest solution.
Another way (if you need a shell-only solution) is

Process One:
    # write my PID ($$) to a tmp file, tmp1
	echo $$ > $tmp1
    # run command
	$command
    # mark ourselves as done:
	/bin/rm -f $tmp1
    # if tmp2 exists, kill that process
	test -f $tmp2 && kill `cat $tmp2`

Process Two:
    # write my PID to a file, tmp2
	echo $$ > $tmp2
    # wait a while
	sleep 27
    # mark ourselves as done so the other half does not kill us now
	/bin/rm -f $tmp2
    # kill the other half if it is still running
	test -f $tmp1 && kill `cat $tmp1`

Now do
    ProcessOne &
    ProcessTwo &
    wait

If you have other background tasks, write these three "lines" out to
a temporary file and execute that -- then the wait will only wait for
ProcessOne and ProcessTwo.

If you don't want the PIDs to appear when you do the Process1 &, you
can play tricks with
	/bin/sh -c "command &"
which won't print the PID of the background process on most systems, but
only works with simple commands (no |, &&, etc) without care.

The variant
	( /bin/sh -c "exec 2>&3; command" & ) 3>&2 2>/dev/null
works better.  It says, 
					     ^^^^
Run the thing in (  ) with file descriptor 3 open in addition to the
standard 0, 1 and 2  (2 is standard error, used for messages).  Make
file descriptor 3 be the same as descriptor 2.

Having done that, send everything written         ^^^^^^^^^^^
on file descriptor 2 (standard error) to /dev/null -- in other words,
throw away all error messages.  This will throw away the PID, which is
what we want, but will also throw away any error messages generated
by "command". 

The
	exec 2>&3;
tells the /bin/sh _inside_ the ( )  to make file descriptor 2 be the
same as file descriptor 3, which we previously opened as the original
standard error.  So this restores standard error messages for the
command inside the quotes.
Which is what we need.

Well, you might say that it would be less arcane to include the C
program `timeout' given by Tom Christiansen, and to compile it on the
fly, but in practice the commands are likely (I hope) to be simple, and
the first form with /bin/sh -c "command" will probably work fine.

Lee
-- 
Liam R. E. Quin,  lee@sq.com, {utai,utzoo}!sq!lee,  SoftQuad Inc., Toronto

jak@sactoh0.UUCP (Jay A. Konigsberg) (08/16/90)

In article <1990Aug12.221658.27776@uncle.uucp> donlash@uncle.UUCP (Donald Lashomb) writes:
>In article <BRISTER.90Aug10222433@westworld.decwrl.dec.com> brister@decwrl.dec.com (James Brister) writes:
>>I'd like to have a shell script run a command, but if that command doesn't
>>finish in X seconds, then the script should kill it, if the command
>>finishes sooner then the script should immediately continue. Any ideas on
=============================================================
>>how one could achieve this?
>
>I'd approach it like this-
>
>#!/bin/sh
>#
>#  run the command in the background
>#  remember its process ID
>#  sleep for X seconds
>#  kill the background command --- note: if
>#  the background command is finished, then
>#  the kill will fail benignly.
>#
>command &
>cmdpid=$!
>sleep $X
>kill $cmdpid
>
When I saw the original post, I almost answered with this type of solution.
However, it lacks an important part of the request. If the command finishes
early, execution will still wait for the sleep to complete.

However, there just may be a way if the sleep is placed in its own file.

# main program file
SEC={number here}        # set the number of seconds to wait
command &                # execute in background
cmdpid = $!              # save the PID
sleepit $SEC $cmdpid &   # sleep in background
wait $cmdpid             # wait for the process to complete or be killed

# sleepit - sleep $1 sec and then kill -9 $2
sleep $1
kill -9 $2 2>/dev/null #don't want those nasty error messages.

My thanks to Donald Lashomb's post. Without it, I wouldn't have come up
with this solution.

-- 
-------------------------------------------------------------
Jay @ SAC-UNIX, Sacramento, Ca.   UUCP=...pacbell!sactoh0!jak
If something is worth doing, its worth doing correctly.

maart@cs.vu.nl (Maarten Litmaath) (08/16/90)

In article <3716@sactoh0.UUCP>,
	jak@sactoh0.UUCP (Jay A. Konigsberg) writes:
)...
)command &                # execute in background

What if the command is supposed to run in the _foreground_?
The following timeout shell script can be easily converted to a C program
if desired.
--------------------cut here--------------------
#!/bin/sh
# @(#)timeout 6.2 90/03/01 Maarten Litmaath

prog=`basename $0`
usage="Usage: $prog [-signal] [timeout] [:interval] [+delay] [--] <command>"

SIG=-KILL	# default signal sent to the process when the timer expires,
		# unless a delay option has been given: then it is -TERM
sigopt=0
timeout=60	# default timeout
interval=15	# default interval between checks if the process is still alive
delay=		# (if specified) the delay between posting the given signal and
		# destroying the process (kill -KILL)

while :
do
	case $1 in
	--)
		shift
		break
		;;
	-*)
		SIG=$1
		sigopt=1
		;;
	[0-9]*)
		timeout=$1
		;;
	:*)
		EXPR='..\(.*\)'
		interval=`expr x"$1" : "$EXPR"`
		;;
	+*)
		EXPR='..\(.*\)'
		delay=`expr x"$1" : "$EXPR"`
		case $sigopt in
		0)
			SIG=-TERM
		esac
		;;
	*)
		break
	esac
	shift
done

case $# in
0)
	echo "$usage" >&2
	exit 2
esac

(
	for t in $timeout $delay
	do
		while test $t -gt $interval
		do
			sleep $interval
			kill -0 $$ || exit
			t=`expr $t - $interval`
		done
		sleep $t
		kill $SIG $$ && kill -0 $$ || exit
		SIG=-KILL
	done
) 2> /dev/null &

exec "$@"
--
   "UNIX was never designed to keep people from doing stupid things, because
    that policy would also keep them from doing clever things."  (Doug Gwyn)