Bash script catch signal but wait afterwards for processes to terminate
Something rewritted
In order to avoid some useless forks.
clock(){ local prefix=C interval=2
trap : RTMIN{,+{{,1}{1,2,3,4,5},6,7,8,9,10}}
while :;do
printf "%s: %(%d.%m %H:%M:%S)T\n" $prefix -1
sleep $interval
done
}
volume(){ local prefix=V vol=() field playback val foo
while IFS=':[]' read field playback val foo;do
[ "$playback" ] && [ -z "${playback//*Playback*}" ] && [ "$val" ] &&
vol+=(${val%\%})
done < <(amixer get Master)
suffix='%%'
if [ "$vol" = "off" ] ;then
icon="&" #alternative: deaf: mute:
suffix=''
elif (( vol > 50 )) ;then icon="("
elif (( vol > 30 )) ;then icon="("
else icon="'"
fi
printf -v values "%3s$suffix " ${vol[@]}
printf "%s%s %s\n" $prefix "$icon" "$values"
}
clock & volume &
trap volume RTMIN+2
trap : RTMIN{,+{{,1}{1,3,4,5},6,7,8,9,10,12}}
echo -e "To get status, run:\n kill -RTMIN+2 $$"
while :;do wait ;done
Regarding my last comment about stereo bug, there is a volume function working for stereo, mono or even quadra:
volume(){
local prefix=V vol=() field playback val foo
local -i overallvol=0
while IFS=':[]' read field playback val foo ;do
[ "$playback" ] && [ -z "${playback//*Playback*}" ] && [ "$val" ] && {
vol+=($val)
val=${val%\%}
overallvol+=${val//off/0}
}
done < <(
amixer get Master
)
overallvol=$overallvol/${#vol[@]}
if (( overallvol == 0 )) ;then
icon="&"
elif (( overallvol > 50 )) ;then
icon="("
elif (( overallvol > 30 )) ;then
icon="("
else
icon="'"
fi
printf "%s%s %s\n" $prefix "$icon" "${vol[*]}"
}
or even:
volume(){
local prefix=V vol=() field playback val foo icons=(⏻ ¼ ¼ ¼ ½ ½ ¾ ¾ ¾ ¾ ¾)
local -i overallvol=0
while IFS=':[]' read field playback val foo ;do
[ "$playback" ] && [ -z "${playback//*Playback*}" ] && [ "$val" ] && {
vol+=($val)
val=${val%\%}
overallvol+=${val//off/0}
}
done < <(
amixer get Master
)
overallvol=$overallvol/${#vol[@]}
printf "%s%s %s\n" $prefix "${icons[(9+overall)/10]}" "${vol[*]}"
Some explanations
Regarding useless forks in volume()
function
I've posted there some ideas to improve the job, reducing resource eating and doing same job of choosing an icon as function of current volume set.
About while :;do wait;done
loop
As requested sample stand for an infinite loop in backgrounded sub function, the main script use same infinite loop.
But as question title stand for wait afterwards for processes to terminate, I have to agree with oguz-ismail's comment.
In fact, last line would better be written:
until wait;do :;done
For more information on how wait
command work and good practice, please have a look on good oguz-ismail's answer
How can I send a signal without the shell waiting for the currently running program to finish?
As noted in the referenced answer https://unix.stackexchange.com/questions/282525/why-did-my-trap-not-trigger/282631#282631 the shell will normally wait for a utility to complete before running a trap. Some alternatives are:
- Start the long running process in the background, then wait for it using the
wait
builtin. When a trapped signal is received during such a wait, the wait is interrupted and the trap is taken. Unfortunately, the exit status ofwait
does not distinguish between the child process exiting on a signal and a trap occurring. For example
sleep 1000 &
p=$!
wait "$p"
- Send a signal to the whole process group via
kill -s INT 0
. The effect is much like if the user had pressed Ctrl+C, but may be more extreme than you want if your script is run from another script. - Use a shell such as zsh or FreeBSD sh that supports
set -o trapsasync
which allows running traps while waiting for a foreground job.
How do I stop a signal from killing my Bash script?
I believe you're looking for SIGTERM
:
Example:
#! /bin/bash
trap -- '' SIGINT SIGTERM
while true; do
date +%F_%T
sleep 1
done
Running this example cTRL+C
won't kill it nor kill <pid>
you can however kill it with kill -9 <pid>
.
If you don't want CTRL+Z
to interrupt use: trap -- '' SIGINT SIGTERM SIGTSTP
stop currently running bash script lazily/gracefully
In the man page, under SIGNALS section it reads:
If bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes.
This is exactly what you're asking for. You need to set an exit trap for SIGINT, then run exec-program
in a subshell where SIGINT is ignored; so that it'll inherit the SIG_IGN handler and Ctrl+C won't kill it. Below is an implementation of this concept.
#!/bin/bash -
trap exit INT
foo() (
trap '' INT
exec "$@"
)
foo sleep 5
echo alive
If you hit Ctrl+C while sleep 5
is running, bash will wait for it to complete and then exit; you will not see alive
on the terminal.
exec
is for avoiding another fork() btw.
How to wait in bash for several subprocesses to finish, and return exit code !=0 when any subprocess ends with code !=0?
wait
also (optionally) takes the PID
of the process to wait for, and with $!
you get the PID
of the last command launched in the background.
Modify the loop to store the PID
of each spawned sub-process into an array, and then loop again waiting on each PID
.
# run processes and store pids in array
for i in $n_procs; do
./procs[${i}] &
pids[${i}]=$!
done
# wait for all pids
for pid in ${pids[*]}; do
wait $pid
done
Prevent SIGINT from interrupting current task while still passing information about SIGINT (and preserve the exit code)
Both answers from @PSkocik and @WilliamPursell have helped me to get on the right track.
I have a fully working solution. It ain't pretty because it needs to use an external file to indicate that the signal didn't occurred but beside that it should work reliably.
#!/bin/sh
touch ./continue
trap 'rm -f ./continue' 2
( # the whole main body of the script is in a separate background process
trap '' 2 # ignore SIGINT
while true ; do
result="$(sleep 2 ; echo success)" # run some program
echo "result: '$result'"
echo "Cleaning up..." # clean up temporary files
if [ ! -e ./continue ] ; then # exit the loop if file "./continue" is deleted
echo 'OK, time to stop this.'
break
fi
done
) & # end of the main body of the script
while ! wait ; do : ; done # wait for the background process to end (ignore signals)
wait $! # wait again to get the exit code
result=$? # exit code of the background process
rm -f ./continue # clean up if the background process ended without a signal
exit $result
EDIT: There are some problems with this code in Cygwin.
The main functionality regarding signals work.
However, it seems like the finished background process doesn't stay in the system as a zombie. This makes the wait $!
to not work. The exit code of the script has incorrect value of 127
.
Solution to that would be removing lines wait $!
, result=$?
and result=$?
so the script always returns 0.
It should be also possible to keep the proper error code by using another layer of subshell and temporarily store the exit code in a file.
Why does my bash script take so long to respond to kill when it runs in the background?
Possible reason: signals issued while a process is sleeping are not delivered until wake-up of the process. When started via the command line, the process doesn't sleep, so the signal gets delivered immediately.
Terminate running commands when shell script is killed
I would do something like this:
#!/bin/bash
trap : SIGTERM SIGINT
echo $$
find / >/dev/null 2>&1 &
FIND_PID=$!
wait $FIND_PID
if [[ $? -gt 128 ]]
then
kill $FIND_PID
fi
Some explanation is in order, I guess. Out the gate, we need to change some of the default signal handling. :
is a no-op command, since passing an empty string causes the shell to ignore the signal instead of doing something about it (the opposite of what we want to do).
Then, the find
command is run in the background (from the script's perspective) and we call the wait
builtin for it to finish. Since we gave a real command to trap
above, when a signal is handled, wait
will exit with a status greater than 128. If the process wait
ed for completes, wait
will return the exit status of that process.
Last, if the wait
returns that error status, we want to kill
the child process. Luckily we saved its PID. The advantage of this approach is that you can log some error message or otherwise identify that a signal caused the script to exit.
As others have mentioned, putting kill -- -$$
as your argument to trap
is another option if you don't care about leaving any information around post-exit.
For trap
to work the way you want, you do need to pair it up with wait
- the bash
man page says "If bash
is waiting for a command to complete and receives a signal for which a trap
has been set, the trap
will not be executed until the command completes." wait
is the way around this hiccup.
You can extend it to more child processes if you want, as well. I didn't really exhaustively test this one out, but it seems to work here.
$ ./test-k.sh &
[1] 12810
12810
$ kill 12810
$ ps -ef | grep find
$
Related Topics
Profiling Arbitrary Cuda Applications
Hbase Does Not Run After ./Start-Hbase.Sh - Permission Denied
Can't Use Gpg-Agent as an Ssh Agent
Synchronize Shell Script Execution
How to Execute a Command in a Bash Script and Then Focus The Appearing Window
Linux Process Context and Svc Call in Arm
Compile Errors Using Bfd.H on Linux
Python3 No Such File or Directory
Detect When Reader Closes Named Pipe (Fifo)
Disable CPU Caches (L1/L2) on Armv8-A Linux
Combine Two Audio Files with a Command Line Tool
How to Highlight The Differences Between Subsequent Lines in a File
Creating an Alias in Ubuntu, in .Profile
Problem of Understanding Clock_Gettime
How Event Packet Header Is Getiing in Hci_Send_Req API Implementation
Sendmail/Procmail - Get Mail Sender and Mail Subject, Utf8 Encoding Issues