How to kill a program if it did not produce any output in a given timeout?

Solution 1:

The code

Save this as tkill (make it executable and adjust your PATH if needed):

#!/bin/bash

_terminate_children() {
        trap "exit 143" SIGTERM && kill -- "-$$"
}

trap _terminate_children SIGINT SIGTERM

tout="$1"
shift
eval "$@" | tee >(while :; do
   read -t "$tout"
   case $? in
      0) : ;;
      1) break ;;
      *) _terminate_children ;;
   esac
done)
exit "${PIPESTATUS[0]}"

Basic usage

tkill 30 some_command

The first argument (30 here) is the timeout in seconds.


Notes

  • tkill expects some_command to generate text (not binary) output.
  • tkill probes stdout of the given command. To include stderr redirect it like in the last advanced example below.

Advanced usage

These are valid examples:

tkill 9 foo -option value
tkill 9 "foo -option value"  # equivalent to the above
tkill 5 "foo | bar"
tkill 5 'foo | bar'
tkill 5 'foo | bar | baz'    # tkill monitors baz
tkill 5 'foo | bar' | baz    # baz reads from tkill
tkill 3 "foo; bar"
tkill 6 "foo && bar || baz"
tkill 7 "some_command 2>&1"

Use Bash syntax in these quotes.


Exit status

  • If some_command exits by itself then its exit status will be reused as the exit status of tkill; tkill 5 true returns 0; tkill 5 false returns 1; tkill 5 "true; false" returns 1.
  • If the given timeout expires or tkill gets interrupted by SIGINT or SIGTERM then the exit status will be 143.

Fragments of code explained

  • eval makes the advanced examples possible.
  • tee allows us to analyze stdin while still passing a copy of it to stdout.
  • read -t is responsible for applying the timeout, its exit status is used to determine what to do next.
  • Command(s) being monitored are killed when needed with this solution.
  • Exit status of monitored command(s) is retrieved with this solution.

Quirks

  • eval makes the advanced examples possible but you need to remember it does this by evaluating its arguments. Example (somewhat artificial): if you had a file literally named |, then tkill 9 ls * would expand * in the current shell, | would appear as an argument to eval and it would be interpreted as a pipe operator. In this case tkill 9 'ls *' is better (but note it expands nothing in the current shell). It's similar with watch (I mean e.g. watch ls * vs watch 'ls *').
  • The evaluated command gets piped to tee, it does not write directly to the terminal. Some commands alter their behavior depending on whether their stdout is a terminal or not. They may colorize and/or columnize their output to a terminal, but not their output to a regular file or a pipe. E.g. ls --color=auto and tkill 9 'ls --color=auto' give different output.