1. Introduction
Even the best computers have limited processing power. This means processes can’t run forever. To avoid situations when they try to, we can use timeouts.
In this tutorial, we start by discussing what timeouts are. Next, we talk about some timeout conditions. Finally, we go into different ways of using timeouts in Bash. In particular, we describe one standard and one custom solution.
We tested the code on Debian 11 Bullseye with GNU Bash 5.1.4. It complies with POSIX and should work in any such environment.
2. Timeout
Timeouts exist on many levels of computing. In all cases, timeouts serve the same purpose: limit the run time of a process. While exactly how we force a timeout varies, the basic idea is the same.
First, we decide on a timeout value. Next, we note the start time of a process. We then wait for the timeout value and check whether the process is running. If it is, we send a termination signal to stop it. If the process completes on its own, we just stop monitoring.
Broadly speaking, any action can be constrained in terms of time, but it makes more sense for some than others. The choice often depends on the probability of timeout conditions.
3. Timeout Conditions
There can be many reasons for timeouts. Generally, many fall into one of several categories. Let’s go over them.
3.1. Hardware Failure
Any process may not be completed due to problems with hardware components. For example, memory errors can produce unreadable data and files. Consequently, processes might not be completed because of these files or data. Another example is a damaged network cable stopping replies.
3.2. Hardware Specifications
Even without errors, hardware might just not be powerful enough to complete a process in a reasonable amount of time. For instance, low memory can be the reason for slowdowns or even thrashing. Furthermore, slow network connections may delay a process unreasonably.
3.3. Development
This is often the broadest category of timeout conditions. Indeed, outside of the environment, how we implement a process has the most significant effect on its running time and limits.
Importantly, getting the runtime wrong can lead to unexpected timeouts. Partially, this means we should consider differing hardware.
Another common problem is deadlocks. They occur when two processes wait for each other to complete. Consequently, none of them do complete, and they wait indefinitely.
Of course, issues can exist even in the simplest of programs. Indeed, a simple infinite loop in a single thread can prevent process completion.
There are multiple other possibilities, but these are the general ones. Next, we’ll discuss how to deal with them in Bash.
4. Bash Timeout
Different ways exist to use timeouts in Bash. Let’s check out solutions in two broad categories – standard and custom.
4.1. Timeout Command
Clearly, we can just use a specific tool for our needs. For instance, there is the timeout command, part of the GNU coreutils package.
The timeout command stops an executed process after the timeout period:
$ timeout 1s bash -c 'for((;;)); do :; done'
$ echo $?
124
Here, we run an endless loop. We set a timeout of one second before timeout should kill the process.
Importantly, note the exit code. It means that the process was stopped with the default SIGTERM signal. We can choose another signal with the –signal option.
Additionally, the –preserve-status switch can force timeout to return the status code of the original process:
$ timeout --preserve-status 1s bash -c 'exit 66'
$ echo $?
66
Using this option, we can check for the code that caused the issue.
The timeout command is part of an external package. This means that it might not be available on every system.
4.2. Custom Timeouts
Sometimes we don’t have access to custom commands or just don’t want to use them. In such situations, we can create our own version of the timeout command:
function xtimeout {
timeout=$1
shift
command=("$@")
(
"${command[@]}" &
runnerpid=$!
trap -- '' SIGTERM
( # killer job
sleep $timeout
if ps -p $runnerpid > /dev/null; then
kill -SIGKILL $runnerpid 2>/dev/null
fi
) &
killerpid=$!
wait $runnerpid
kill -SIGKILL $killerpid
)
}
This function expects the timeout in seconds as its first argument. It then removes that first argument and uses the rest as the command line to execute.
First, we execute the command. Actually, it runs in the background of a subshell. This is to mute any background job messages. Next, our function uses sleep to wait for the specified amount of seconds. Finally, we check for the PID of the started command. If it’s running, the killer’s’s job stops it. In case the command completes before the timeout runs out, we stop the killer job.
Of course, this function isn’t as flexible or robust as the timeout command. However, all of its parts are available on any standard Linux machine.
5. Conclusion
In this tutorial, we discussed timeouts in Bash. First, we defined timeouts and some typical timeout conditions. After that, we checked two timeout solutions:
- the GNU coreutils timeout command
- custom timeout function in Bash
In conclusion, we can easily write our timeouts in Bash, but this isn’t necessary due to the timeout command.