'Trap and exit big shell script with background processes

I have 2 shell scripts that contain ffmpeg commands (command1.sh and command2.sh). command2.sh has like 500 ffmpeg commands that trigger one after another with ';', while command1.sh handles audio ffmpeg commands.

The main issue: It takes too much to kill the whole script and it takes away for 1-2 minutes the CPU power i need for another executing script so I'm losing CPU power for nothing, because i cannot kill it instantly.

Code: I have init.sh that contains :

trap 'print TERM received;exit' 15
chmod +x command1.sh;
chmod +x command2.sh;
./command1.sh & ./command2.sh

so it triggers both in background.

Then i execute pkill init.sh and i catch on the shell trap and exit the shell script but i get exitCode: 1, failed: true and the commands in the background still execute for 1 min until they get killed by another kill ${pid} which i execute after pkill.



Solution 1:[1]

kill parallelized subtasks

Some remarks:

  • chmod +x is useless here. At all you could run sh command1.sh & instead.
  • You have to kill all subtasks independantly
  • As this question is tagged , my answer don't use bashisms. All script here is tested under , and .

Something like:

#!/bin/sh

for cmd in ./command1.sh ./command2.sh;do
    exec $cmd &
    PIDS="$PIDS $!"
done

trap "kill $PIDS;exit" 15
wait

Of coure, between for cmd in and ;do, you could put as many commandXX.sh you want (as long you keep line length into maximum supported by your installed OS)

Test script:

Here is a quick test script that sleep randomly between 2.0 to 12.99 seconds, then print done. before exit:

#!/bin/bash

declare -i toSleep
case $1 in '' | *[!0-9]* ) toSleep='RANDOM%10+2' ;; * ) toSleep=$1 ;; esac

exec {dummy}<> <(:)
read -t $toSleep.$RANDOM -u $dummy _
echo done.

I've saved this into command1.sh, chmod +x .. and linked to command2.sh...

More portable wrapper:

#!/bin/sh

for cmd in "$@";do
    exec $cmd &
    PIDS="$PIDS $!"
done

printf "You have to: kill -TERM %d\nto end %d tasks: %s\n" \
    $$ $(echo $PIDS|wc -w) "$PIDS"

trap "kill $PIDS;echo 'Process $$ killed.';exit" 15
wait

echo "Process $$ running $@ ended normally"

You could save this script into a file named simpleParallel.sh, for sample, then:

chmod +x simpleParallel.sh

./simpleParallel.sh ./command1.sh ./command2.sh 
You have to: kill -TERM 741297
to end 2 tasks:  741298 741299

Then if you kill -TERM 741297 from elsewhere,

Process 741297 killed.

But if you don't, you may read something like:

./simpleParallel.sh ./command1.sh ./command2.sh 
You have to: kill -TERM 741968
to end 2 tasks:  741969 741970
done.
done.
Process 741968 running ./command1.sh ./command2.sh ended normally

Nota: If you send your kill command while one process is already done, you may see error message like:

./simpleParallel.sh: 1: kill: No such process

See version could avoid this bug.

Following this from another terminal console

Before running ./simpleParallel.sh, you could run tty without argument:

tty
/dev/pts/2

Then in a new free window, you could run:

watch ps --tty pts/2

while using another window again to run kill command.

with some bashisms, now:

Under recent bash, there are lot of special feature.

  • You could use Array to store backgrounded tasks's PIDs.
  • From version 5.0, There is an $EPOCHREALTIME variable, which expands to the time in seconds since the Unix epoch with microsecond granularity.
  • From version 5.0, Associative arrays allow subscripts containing whitespace.
  • From version 5.1, wait has a new [-p VARNAME] option, which stores the PID returned by wait -n or wait without arguments.

My script could become:

#!/bin/bash
declare -A PIDS ENDED
started=$EPOCHREALTIME
for cmd; do
    exec "$cmd" &
    PIDS["$cmd"]=$!
    CMDS[$!]="$cmd"
done
printf "You have to: kill -TERM %d\nto end %d tasks: %s\n" \
    $$ ${#PIDS[@]} "${PIDS[*]}"

trapExit(){
    kill "${PIDS[@]}"
    printf 'Process %s + %d task killed: %s\n' $$ ${#PIDS[@]} "${!PIDS[*]}"
    showDone
    exit 1
}
trap trapExit 15
showDone() {
    for cmd in "${!ENDED[@]}";do
        read -r elap < <(bc -l <<<"${ENDED["$cmd"]}-$started")
        printf "Elapsed: %.4f sec for %s\n" "$elap" "$cmd"
    done
}
while ((${#PIDS[@]}));do  if wait -n -p pid ;then
        printf "Process %d done (%s).\n" "$pid" "${CMDS[pid]}"
        ENDED["${CMDS[pid]}"]=$EPOCHREALTIME
        unset PIDS["${CMDS[pid]}"] CMDS[pid]
fi; done
echo "Process $$ running $* ended normally"
showDone

Output could look like:

./simpleParallel.bash ./command{1,2}.sh
You have to: kill -TERM 1309816
to end 2 tasks: 1309817 1309818
done.
Process 1309817 done (./command1.sh).
done.
Process 1309818 done (./command2.sh).
Process 1309816 running ./command1.sh ./command2.sh ended normally
Elapsed: 3.1644 sec for ./command1.sh
Elapsed: 4.2488 sec for ./command2.sh

or

./simpleParallel.bash ./command{1,2}.sh
You have to: kill -TERM 1310031
to end 2 tasks: 1310032 1310033
done.
Process 1310033 done (./command2.sh).
done.
Process 1310032 done (./command1.sh).
Process 1310031 running ./command1.sh ./command2.sh ended normally
Elapsed: 9.1868 sec for ./command1.sh
Elapsed: 3.2310 sec for ./command2.sh

If you kill them early:

./simpleParallel.bash ./command{1,2}.sh
You have to: kill -TERM 1294577
to end 2 tasks: 1294578 1294579
Process 1294577 + 2 task killed: ./command1.sh ./command2.sh

If you kill them later:

./simpleParallel.bash ./command{1,2}.sh
You have to: kill -TERM 1294958
to end 2 tasks: 1294959 1294960
done.
Process 1294959 done (./command1.sh).
Process 1294958 + 1 task killed: ./command2.sh
Elapsed: 3.1779 sec for ./command1.sh

or

./simpleParallel.bash ./command{1,2}.sh
You have to: kill -TERM 1294971
to end 2 tasks: 1294972 1294973
done.
Process 1294973 done (./command2.sh).
Process 1294971 + 1 task killed: ./command1.sh
Elapsed: 6.9344 sec for ./command2.sh

and no error message about trying to kill unexistant pid.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1