Picture

Hi, I'm Boopathi Rajaa.

Hacker, Dancer, Biker, Adventurist

A fancy timeout in bash

Timing-out-and-moving-on is a programming paradigm generally followed in many multi-process or multi-threaded programs. In a scripting environment like bash, the solution is to do this in a hacky way. Of course coreutils now provides a tool called "timeout" which does exactly what we need - Run the command and timer concurrently. Whenever something exits, the other one gets killed.

Bash is all about finding amazing ways to do things. Here is one of them.

sh -ic "{ sleep $T1; echo one >&3; \
  kill 0; } | { sleep $T2; echo two >&3; \
  kill 0; }" 3>&1 2>/dev/null

 Internals:

Going from inside out.

  • In bash { some command; }  represents a block of code or inline group ( http://tldp.org/LDP/abs/html/special-chars.html#CODEBLOCKREF ). This construct creates an anonymous function and all the processes invoked come under the same process group.
  • The structure of the invoked script is { } | { }.
  • Each inline group has a kill 0;  . The man page of kill says that kill 0 signals all processes in the current process group.
  • When you do kill 0;  all that happens is the shell will start a new job to execute that kill command, so kill  will only kill itself.
  • All the processes invoked inside { }  will be under the same process group, so kill 0 would work there.
  • Note the output redirection >&3 within each of the inline groups. Here, we are writing the output to a different file descriptor identified by 3. 1 and 2 are already defined as STDOUT and STDERR.
  • This file descriptor 3 is used to communicate with the outside world. Since there is a pipe between the two inline groups, the output of first group cannot be captured - It goes as input to the second inline group. You have to either write it to a file or a stream represented by a file descriptor.
  • Just for clarity, I've used >&3  in both the inline groups. Capturing the output in fd3 outside this setup.
  • sh -ic ""  creates a sub-shell and runs the command that's passed as argument. The -i  flag is to run interactively.
  • Within this sub-shell, we have our two inline groups sending necessary outputs to fd3. So outside this sub-shell (running  interactively ) we can capture fd3 and redirect it however required
  • 3>&1  fd3 is redirected to STDOUT of the session from which the sub-shell is invoked.

Putting everything together: (the short version)

Create a new sub-shell to run interactively. Within this sub-shell, create an inline group that outputs to fd3. Pipe this inline group to another inline group which also outputs to fd3. In the session which invoked the sub-shell, redirect fd3 to STDOUT.

Why does this work ?

All that we are doing is pipe two inline groups. One of the inline groups contain our main program which shouldn't run longer than $TIMEOUT . The other inline group would sleep for $TIMEOUT - both followed by a kill 0; statement. Whichever exits first is going to call the kill command and all the processes in that process group will be killed. This includes the sub-shell and all the processes within the sub-shell.

Example:

Capture bandwidth usage per process using nethogs for 1 hour.

sh -ic "{ /usr/sbin/nethogs -t >&3; \
  kill 0; } | { sleep 3600; \
  kill 0; }" 3>&1 2>/dev/null

More about this is discussed here  -  http://boopathi.in/blog/capturing-per-process-bandwidth-usage-using-nethogs.