4

I´ve asked Bash trap - exit only at the end of loop and the submitted solution works but while pressing CTRL-C the running command in the script (mp3convert with lame) will be interrupt and than the complete for loop will running to the end. Let me show you the simple script:

#!/bin/bash
mp3convert () { lame -V0 file.wav file.mp3 }

PreTrap() { QUIT=1 }

CleanUp() {
  if [ ! -z $QUIT ]; then
     rm -f $TMPFILE1
     rm -f $TMPFILE2
     echo "... done!" && exit
  fi }

trap PreTrap SIGINT SIGTERM SIGTSTP
trap CleanUp EXIT

case $1 in
     write)
           while [ -n "$line" ]
             do
                mp3convert
                [SOMEMOREMAGIC]
                CleanUp
             done
    ;;
QUIT=1

If I press CTRL-C while function mp3convert is running the lame command will be interrupt and then [SOMEMOREMAGIC] will execute before CleanUp is running. I don´t understand why the lame command will be interrupt and how I could avoid them.

Community
  • 1
  • 1
UsersUser
  • 175
  • 2
  • 14

3 Answers3

2

When you hit Ctrl-C in a terminal, SIGINT gets sent to all processes in the foreground process group of that terminal, as described in this Stack Exchange "Unix & Linux" answer: How Ctrl C works. (The other answers in that thread are well worth reading, too). And that's why your mp3convert function gets interrupted even though you have set a SIGINT trap.

But you can get around that by running the mp3convert function in the background, as mattias mentioned. Here's a variation of your script that demonstrates the technique.

#!/usr/bin/env bash

myfunc() 
{
    echo -n "Starting $1 :"
    for i in {1..7}
    do
        echo -n " $i"
        sleep 1
    done
    echo ". Finished $1"
}

PreTrap() { QUIT=1; echo -n " in trap "; }

CleanUp() {
    #Don't start cleanup until current run of myfunc is completed.
    wait $pid
    [[ -n $QUIT ]] &&
    {
        QUIT=''
        echo "Cleaning up"
        sleep 1
        echo "... done!" && exit
    } 
}

trap PreTrap SIGINT SIGTERM SIGTSTP
trap CleanUp EXIT

for i in {a..e}
do
    #Run myfunc in background but wait until it completes.
    myfunc "$i" &
    pid=$!
    wait $pid
    CleanUp
done

QUIT=1

When you hit Ctrl-C while myfunc is in the middle of a run, PreTrap prints its message and sets the QUIT flag, but myfunc continues running and CleanUp doesn't commence until the current myfunc run has finished.

Note that my version of CleanUp resets the QUIT flag. This prevents CleanUp from running twice.


This version removes the CleanUp call from the main loop and puts it inside the PreTrap function. It uses wait with no ID argument in PreTrap, which means we don't need to bother saving the PID of each child process. This should be ok since if we're in the trap we do want to wait for all child processes to complete before proceeding.

#!/bin/bash

# Yet another Trap demo...

myfunc() 
{
    echo -n "Starting $1 :"
    for i in {1..5}
    do
        echo -n " $i"
        sleep 1
    done
    echo ". Finished $1"
}

PreTrap() { echo -n " in trap "; wait; CleanUp; }

CleanUp() {
    [[ -n $CLEAN ]] && { echo bye; exit; }

    echo "Cleaning up"
    sleep 1
    echo "... done!" 
    CLEAN=1

    exit
}

trap PreTrap SIGINT SIGTERM SIGTSTP
trap "echo exittrap; CleanUp" EXIT

for i in {a..c}
do
    #Run myfunc in background but wait until it completes.
    myfunc "$i" &  wait $!
done

We don't really need to do myfunc "$i" & wait $! in this script, it could be simplified even further to myfunc "$i" & wait. But generally it's better to wait for a specific PID just in case there's some other process running in the background that we don't want to wait for.

Note that pressing Ctrl-C while CleanUp itself is running will interrupt the current foreground process (probably sleep in this demo).

Community
  • 1
  • 1
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • Thanks a lot. Are there a solution for a script with many commands that do not shall interrupt? Do I have to use the 3 lines for every command? And why do you think it is necessary to unset the QUIT variable? – UsersUser Nov 09 '14 at 21:31
  • @UsersUser: Those 3 lines can be written as one line: `myfunc "$i" & pid=$!; wait $pid`. But I'll add a simpler version of my script to my answer. As for your last question, if I don't reset QUIT then cleanup runs twice; at least, it does on my system. – PM 2Ring Nov 10 '14 at 06:10
  • I´ve tried your solution but it doesn´t work like I expect. If I execute your script and press CTRL-C into one loop I get: `./testloop.sh` `Starting a : 1 2 3^C in trap Cleaning up` `... done!` It seams so that by pressing SIGINT it will go to PreTrap but also interrupt the running command. – UsersUser Nov 12 '14 at 08:13
  • An another point: If I delete your `QUIT=1` at the end it doesn´t execute CleanUp function, but it should execute CleanUp in line 39. ./testloop.sh Starting a : 1 2 3 4 5 6 7. Finished a Starting b : 1 2 3 4 5 6 7. Finished b Starting c : 1 2 3 4 5 6 7. Finished c Starting d : 1 2 3 4 5 6 7. Finished d Starting e : 1 2 3 4 5 6 7. Finished e My Bash version is `4.3.30(1)-release` – UsersUser Nov 12 '14 at 08:16
  • For your second example: I can´t find the declaration of `$CLEAN` which do you use in `CleanUp`. – UsersUser Nov 12 '14 at 08:17
  • @UsersUser: It's weird that these scripts don't work properly on your system. I'm using `GNU bash, version 4.1.5(1)-release (i486-pc-linux-gnu)`. In both of my scripts `myfunc` should finish even if the script receives CTRL-C. Do they work properly if you send SIGTERM? – PM 2Ring Nov 12 '14 at 10:25
  • If you delete `QUIT=1` at the end of the 1st script then `CleanUp` will be run by the EXIT trap, but it will do nothing because of the `[[ -n $QUIT ]]` test. The 2nd script doesn't bother to initialize `CLEAN` because `[[ -n $CLEAN ]]` will evaluate to false if `CLEAN` is an empty string _or_ uninitialized. Some people don't like this style, but it is a fairly common practice. – PM 2Ring Nov 12 '14 at 10:33
2

Try to simplify the discussion above, I wrap up an easier understandable version of show-case script below. This script also HANDLES the "double control-C problem": (Double control-C problem: If you hit control C twice, or three times, depending on how many wait $PID you used, those clean up can not be done properly.)

#!/bin/bash

mp3convert () {
  echo "mp3convert..."; sleep 5; echo "mp3convert done..."
}

PreTrap() {
  echo "in trap"
  QUIT=1
  echo "exiting trap..."
}

CleanUp() {
  ### Since 'wait $PID' can be interrupted by ^C, we need to protected it
  ### by the 'kill' loop  ==> double/triple control-C problem.
  while kill -0 $PID >& /dev/null; do wait $PID; echo "check again"; done

  ### This won't work (A simple wait $PID is vulnerable to double control C)
  # wait $PID

  if [ ! -z $QUIT ]; then
     echo "clean up..."
     exit
 fi
}

trap PreTrap SIGINT SIGTERM SIGTSTP
#trap CleanUp EXIT

for loop in 1 2 3; do
    (
      echo "loop #$loop"
      mp3convert
      echo magic 1
      echo magic 2
      echo magic 3
    ) &
    PID=$!
    CleanUp
    echo "done loop #$loop"
done

The kill -0 trick can be found in a comment of this link

Community
  • 1
  • 1
Robin Hsu
  • 4,164
  • 3
  • 20
  • 37
  • 1
    You may just run it and test it. (See what message it prints, Hit many control-C's in between, etc...) – Robin Hsu Nov 12 '14 at 10:26
  • 1
    Yes - thats the solution. The trick are that all commands that could not interrupt must be grouped (via round brackets) and execute as a background job. – UsersUser Nov 13 '14 at 09:13
  • Parenthesis (round brackets) actually forks a new sub-shell. – Robin Hsu Nov 14 '14 at 02:13
  • 1
    The double(triple or more) control-C problem should be generalized by "double signal/trap problem", since it is not limited to control-C (SIGINT), but also other signals. – Robin Hsu Nov 14 '14 at 07:46
1

One way of doing this would be to simply disable the interrupt until your program is done. Some pseudo code follows:

#!/bin/bash

# First, store your stty settings and disable the interrupt
STTY=$(stty -g) 
stty intr undef

#run your program here
runMp3Convert()

#restore stty settings
stty ${STTY}

# eof

Another idea would be to run your bash script in the background (if possible).

mp3convert.sh &

or even,

nohup mp3convert.sh &
mattias
  • 2,079
  • 3
  • 20
  • 27
  • No, I need the chance to interrupt the script but I need a complete loop run. Unfortunaly the lame command are the longest running part of the loop and if I disable the interrupt I´ve got a minimized chance to interrupt the script because of a very little time slot. – UsersUser Nov 07 '14 at 19:50
  • Ok, now I understand :) - I could use this to disable the interrupt for the whole function but not for the script. But I don´t know how robust this code are - I never used `stty` before and I don´t know how compatible it is with other *nix/*bsd os. – UsersUser Nov 09 '14 at 21:26
  • @UsersUser: Well play around with it and try to verify its usability. I just know that it works for the example I gave. And, please consider accepting the answer if you find it useful. – mattias Nov 09 '14 at 21:29
  • Well, it helped me, but the answer from @pm-2ring explains a lot more and it helped me to understood your answer :). If I had more reputation points I would vote your answer up. – UsersUser Nov 09 '14 at 21:34
  • 1
    After think again: I don´t think that your solution will help me. I could not send a SIGINT to the script while running `runMp3Convert()` and the chance to interrupt the script in the other time are short. – UsersUser Nov 12 '14 at 08:21