Why can't I kill a timeout called from a Bash script with a keystroke?
Solution 1
Signal keys such as Ctrl+C send a signal to all processes in the foreground process group.
In the typical case, a process group is a pipeline. For example, in head <somefile | sort
, the process running head
and the process running sort
are in the same process group, as is the shell, so they all receive the signal. When you run a job in the background (somecommand &
), that job is in its own process group, so pressing Ctrl+C doesn't affect it.
The timeout
program places itself in its own process group. From the source code:
/* Ensure we're in our own group so all subprocesses can be killed.
Note we don't just put the child in a separate group as
then we would need to worry about foreground and background groups
and propagating signals between them. */
setpgid (0, 0);
When a timeout occurs, timeout
goes through the simple expedient of killing the process group of which it is a member. Since it has put itself in a separate process group, its parent process will not be in the group. Using a process group here ensures that if the child application forks into several processes, all its processes will receive the signal.
When you run timeout
directly on the command line and press Ctrl+C, the resulting SIGINT is received both by timeout
and by the child process, but not by interactive shell which is timeout
's parent process. When timeout
is called from a script, only the shell running the script receives the signal: timeout
doesn't get it since it's in a different process group.
You can set a signal handler in a shell script with the trap
builtin. Unfortunately, it's not that simple. Consider this:
#!/bin/sh
trap 'echo Interrupted at $(date)' INT
date
timeout 5 sleep 10
date
If you press Ctrl+C after 2 seconds, this still waits the full 5 seconds, then print the “Interrupted” message. That's because the shell refrains from running the trap code while a foreground job is active.
To remedy this, run the job in the background. In the signal handler, call kill
to relay the signal to the timeout
process group.
#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid
Solution 2
Building on the excellent answer provided by Gilles. The timeout command has a foreground option, if used then a CTRL+C will exit the timeout command.
#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
Solution 3
Basically Ctrl+C sends a SIGINT
signal, while Ctrl+Z send a SIGTSTP
signal.
SIGTSTP
just stops the process, a SIGCONT
will continue it.
This works on the foreground-process that was forked on the command-line.
If your process is a background-process you will have to send that signal to the processes in a different way. kill
will do that. In theory a "-"-operator on that kill should also signal child processes but this seldom works as expected.
For further reading: Unix-Signals
meetar
Art and code; emergent systems of righteousness. #fjordgrown
Updated on September 18, 2022Comments
-
meetar almost 2 years
[Edit: This looks similar to some other questions asking how to kill all spawned processes – the answers all seem to be to use pkill. So the core of my question may be: Is there a way to propagate Ctrl-C/Z to all processes spawned by a script?]
When calling a SoX
rec
with thetimeout
command from coreutils (discussed here), there doesn't seem to be any way to kill it with a keystroke once it's been invoked from within a Bash script.Examples:
timeout 10 rec test.wav
...can be killed with Ctrl+C or Ctrl+Z from bash, but not when it's been called from inside a script.
timeout 10 ping nowhere
...can be killed with Ctrl+C or Ctrl+Z from bash, and with Ctrl+Z when it's run from inside a script.
I can find the process ID and kill it that way, but why can't I use a standard break keystroke? And is there any way to structure my script so that I can?
-
Admin over 11 yearsCtrl+Z does not kill processes, only pauses them. They will keep running if you give the
bg
offg
commands. Anyway, is there a difference between your 1st and 3d examples? -
Admin over 11 yearsI don't have
timeout
on my system but killingsleep
works whether it's typed directly on the command line, sourced, executed, or passed through the interpreter explicitly -
Admin over 11 years@terdon Thanks, I've clarified the examples.
-
-
Gilles 'SO- stop being evil' over 11 yearsThis is true, at least up to “seldom works as expected” (which depends on your expectations — you should read up about process groups), but completely irrelevant. The question does not betray any confusion about SIGINT and SIGTSTP.
-
meetar over 11 yearsVery sneaky - worked beautifully! I am much smarter now, merci!
-
RichVel about 5 yearsThis is a great answer - simpler than the accepted answer and worked fine for me, using
timeout
from GNU coreutils. -
Admin almost 2 yearsMore elegant, but won't kill any child processes
-
Admin almost 2 years@marwan-alsabbagh answer seems more elegant and simpler, but won't kill any child processes. On this alternative we must
test -d /proc/$pid && kill -INT -$pid
to avoidNo such process
while Ctrl+Cing afterwait
.