Write a bash script to leveraging timeout when waiting for command completion

Sometimes, waiting for a command to finish execution or ignoring commands until completion might not be considered a solid practice in scripting, though it does have applications:

  • Where commands take variable lengths of time to complete (for example, pinging a network host)
  • Where tasks or commands can be executed in such a way that the master script waits for the success or failure of several multiple operations

However, the important thing to note is that timeout/wait requires a process, or even a subshell so that it can be monitored (by the Process ID or PID). In this recipe, we will demonstrate the use of waiting for a subshell with the timeout command (which was added into the coreutils package 7.0) and how to do so using trap and kill (for alarms/timers).

Prerequisites

In earlier script, we introduced the use of trap to catch signals, and the use of kill to send signals to processes. These will be explained further in this script, but here are three new native Bash variables:

  • $$: Which returns the PID of the current script
  • $?: Which returns the PID of the last job that was sent to the background
  • $@ :Which returns the array of input variables (for example, $!$2)

Write Script:

We begin this script knowing that there is a command called timeout available to the Bash shell. However, it falls short of being able to provide the functionality of timeouts in functions within a script itself. Using trapkill, and signals, we can set timers or alarms (ALRM) to perform clean exits from runaway functions or commands. Let’s begin:

Open a new terminal and create a new script by the name of mytimeout.sh with the following contents:

mytimeout.sh

#!/bin/bash 
SUBPID=0 
function func_timer() { 
	trap "clean_up" SIGALRM 
	sleep $1& wait 
	kill -s SIGALRM $$ 
} 
function clean_up() { 
	trap - ALRM 
	kill -s SIGALRM $SUBPID 
	kill $! 2>/dev/null 
} 
# Call function for timer & notice we record the job's PID 
func_timer $1& SUBPID=$! 
# Shift the parameters to ignore $0 and $1 
shift 
# Setup the trap for signals SIGALRM and SIGINT 
trap "clean_up" ALRM INT 
# Execute the command passed and then wait for a signal 
"$@" & wait $! 
# kill the running subpid and wait before exit 
kill -ALRM $SUBPID 
wait $SUBPID 
exit 0

Using a command with variable times (ping), we can test mytimeout.sh using the first parameter to mytimeout.sh as the timeout variable!

$ bash mytimeout.sh 1 ping -c 10 google.ca 
$ bash mytimeout.sh 10 ping -c 10 google.ca

How script works:

You might be asking yourself, can I put functions to the background? Absolutely—and you could even use a command called export with the -f flag (although it may not be supported in all environments). If you were to use the timeout command instead, you would have to either run ONLY the command you wish to monitor or put the function inside of a second script to be called by timeout. Clearly, this is less than optimal in some situations. In this recipe, we use signals or rather, the alarm signal, to act as a timer. When we set the alarm with a specific variable, it will raise SIGALARM once the timer expires! If the process is still alive, we merely kill it and exit the script if we haven’t already exited:

In step 1, we create the mytimeout.sh script. It uses a few of our new primitives such as $! to monitor the PID of the function we sent to execute in the background as a job (or subshell, in this case). We arm the timer and then carry on with the execution of the script. Then, we use shift to literally shift the parameters passed to our script to ignore $1 (or the timeout variable). Finally, we watch for SIGALRM and perform a cleanup if necessary.

In step 2, mytimeout.sh is executed twice using the ping command, which is targeting google.ca. In the first instance, we use a timeout of 1 second, and in the second instance, we use a timeout of 10 seconds. Ping, in both cases, will perform 10 pings (for example, one ping there and back to whatever host is answering ICMP requests for the DNS entry for google.ca). The first instance will execute early, and the second allows 10 pings to execute cleanly and exit:

$ bash mytimeout.sh 1 ping -c 10 google.com
PING google.com (172.217.13.99) 56(84) bytes of data. 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=1 ttl=57 time=10.0 ms 
$ bash mytimeout.sh 10 ping -c 10 google.com
PING google.com (172.217.13.99) 56(84) bytes of data. 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=1 ttl=57 time=11.8 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=2 ttl=57 time=14.5 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=3 ttl=57 time=10.8 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=4 ttl=57 time=13.1 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=5 ttl=57 time=12.7 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=6 ttl=57 time=13.4 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=7 ttl=57 time=9.15 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=8 ttl=57 time=14.0 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=9 ttl=57 time=12.0 ms 
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=10 ttl=57 time=11.2 ms 
--- google.ca ping statistics --- 
10 packets transmitted, 10 received, 0% packet loss, time 9015ms 
rtt min/avg/max/mdev = 9.155/12.307/14.520/1.545 ms

0 Comments

Submit a Comment

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

Related Articles