How do I limit the running time of a BASH script
Asked Answered
B

5

20

I have a long running BASH script that I am running under CYGWIN on Windows.

I would like to limit the script to run for 30 seconds, and automatically terminate if it exceeds this limit. Ideally, I'd like to be able to do this to any command.

For example:

$ limittime -t 30 'myscript.sh'

or

$ limittime -t 30 'grep func *.c'

Under cygwin the ulimit command doesn't seem to work.

Bodega answered 9/2, 2009 at 0:40 Comment(4)
@jm, see my updated answer for how to stop waiting at the earliest of timeout and child-exit-normally.Dancette
Similar question, some different answers: stackoverflow.com/questions/687948Irreverence
Any reason to not use the gnu timeout utility?Jillene
timeout is great! you can even use with multiple commands (multi-line script): https://mcmap.net/q/88021/-how-to-timeout-a-group-of-commands-in-bashAtop
D
19

See the http://www.pixelbeat.org/scripts/timeout script the functionality of which has been integrated into newer coreutils:

#!/bin/sh

# Execute a command with a timeout

# License: LGPLv2
# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <[email protected]>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <[email protected]>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <[email protected]>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@"& wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value
Depoliti answered 2/2, 2010 at 12:41 Comment(2)
That's a nice script. Worked fine under CYGWIN, too.Bodega
this script won't work if you are using stdin redirection - something like: ./time_limit.sh cat < my_file.txt won't print anything. You can fix that by replacing "$@" with "$@" < /dev/stdin. Maybe somebody will find this helpful.Anglicanism
D
12

The following script shows how to do this using background tasks. The first section kills a 60-second process after the 10-second limit. The second attempts to kill a process that's already exited. Keep in mind that, if you set your timeout really high, the process IDs may roll over and you'll kill the wrong process but this is more of a theoretical issue - the timeout would have to be very large and you would have to be starting a lot of processes.

#!/usr/bin/bash

sleep 60 &
pid=$!
sleep 10
kill -9 $pid

sleep 3 &
pid=$!
sleep 10
kill -9 $pid

Here's the output on my Cygwin box:

$ ./limit10
./limit10: line 9:  4492 Killed sleep 60
./limit10: line 11: kill: (4560) - No such process

If you want to only wait until the process has finished, you need to enter a loop and check. This is slightly less accurate since sleep 1 and the other commands will actually take more than one second (but not much more). Use this script to replace the second section above (the "echo $proc" and "date" commands are for debugging, I wouldn't expect to have them in the final solution).

#!/usr/bin/bash

date
sleep 3 &
pid=$!
((lim = 10))
while [[ $lim -gt 0 ]] ; do
    sleep 1
    proc=$(ps -ef | awk -v pid=$pid '$2==pid{print}{}')
    echo $proc
    ((lim = lim - 1))
    if [[ -z "$proc" ]] ; then
            ((lim = -9))
    fi
done
date
if [[ $lim -gt -9 ]] ; then
    kill -9 $pid
fi
date

It basically loops, checking if the process is still running every second. If not, it exits the loop with a special value to not try and kill the child. Otherwise it times out and does kill the child.

Here's the output for a sleep 3:

Mon Feb  9 11:10:37 WADT 2009
pax 4268 2476 con 11:10:37 /usr/bin/sleep
pax 4268 2476 con 11:10:37 /usr/bin/sleep
Mon Feb  9 11:10:41 WADT 2009
Mon Feb  9 11:10:41 WADT 2009

and a sleep 60:

Mon Feb  9 11:11:51 WADT 2009
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
Mon Feb  9 11:12:03 WADT 2009
Mon Feb  9 11:12:03 WADT 2009
./limit10: line 20:  4176 Killed sleep 60
Dancette answered 9/2, 2009 at 0:55 Comment(6)
That is good, but the minimum length the process can run is now the timeout. That is correct for the way I phrased the question, though.Bodega
Ack, don’t use kill -9 unless absolutely necessary! SIGKILL can’t be trapped so the killed program can’t run any shutdown routines to e.g. erase temporary files. First try HUP (1), then INT (2), then QUIT (3).Faircloth
The signal was a sample, @www. And it's quite acceptable to use -9 if you understand what the underlying script is doing. Otherwise you have to complicate your code with HUP, wait, INT, wait, QUIT and so on. Not good for an answer but you're right, it may be needed in the real world.Dancette
This method requires the time-limited script to run in the background, which may be undesirable. It also has a race condition between 'ps' and 'kill'. I like <bashcookbook.com/bashinfo/source/bash-4.0/examples/scripts/…> which runs in the foreground, and sends TERM before KILL.Irreverence
@www.blindrut.ca~neitsch, doesn't HUP mean different things to different programs, eg. "re-read your config file"? And I suspect that TERM is a more graceful shutdown than INT. #690915Irreverence
Great answer, found via google and answered my (fairly different) question.Motorbus
A
5

Check out this link. The idea is just that you would run myscript.sh as a subprocess of your script and record its PID, then kill it if it runs too long.

Analyst answered 9/2, 2009 at 0:44 Comment(3)
I couldn't get that sample to run on cygwin. I got: sh: line 46: kill: SIGUSR1: invalid signal specificationBodega
That solution actually seems bizarre. It starts the timed tasks, then a separate subshell to sleep and send USR1 to the running shell. Why not just sleep in the running shell?Dancette
Like the accepted answer by Pax, that script requires your script to run in the background and has a race condition between 'ps' and 'kill'. Of course the race condition is strictly a cosmetic thing.Irreverence
F
5
timeout 30s YOUR_COMMAND COMMAND_ARGUMENTS

Below are all the options for "timeout" under coreutils:

$ timeout --help
Usage: timeout [OPTION] DURATION COMMAND [ARG]...
  or:  timeout [OPTION]
Start COMMAND, and kill it if still running after DURATION.

Mandatory arguments to long options are mandatory for short options too.
      --preserve-status
                 exit with the same status as COMMAND, even when the
                   command times out
      --foreground
                 when not running timeout directly from a shell prompt,
                   allow COMMAND to read from the TTY and get TTY signals;
                   in this mode, children of COMMAND will not be timed out
  -k, --kill-after=DURATION
                 also send a KILL signal if COMMAND is still running
                   this long after the initial signal was sent
  -s, --signal=SIGNAL
                 specify the signal to be sent on timeout;
                   SIGNAL may be a name like 'HUP' or a number;
                   see 'kill -l' for a list of signals
      --help     display this help and exit
      --version  output version information and exit

DURATION is a floating point number with an optional suffix:
's' for seconds (the default), 'm' for minutes, 'h' for hours or 'd' for days.

If the command times out, and --preserve-status is not set, then exit with
status 124.  Otherwise, exit with the status of COMMAND.  If no signal
is specified, send the TERM signal upon timeout.  The TERM signal kills
any process that does not block or catch that signal.  It may be necessary
to use the KILL (9) signal, since this signal cannot be caught, in which
case the exit status is 128+9 rather than 124.

GNU coreutils online help: <http://www.gnu.org/software/coreutils/>
Full documentation at: <http://www.gnu.org/software/coreutils/timeout>
or available locally via: info '(coreutils) timeout invocation'
Fabyola answered 27/1, 2020 at 22:54 Comment(0)
V
2

You could run the command as a background job (i.e. with "&"), use the bash variable for "pid of last command run," sleep for the requisite amount of time, then run kill with that pid.

Verbatim answered 9/2, 2009 at 0:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.