Get exit code of a background process

184,940

Solution 1

1: In bash, $! holds the PID of the last background process that was executed. That will tell you what process to monitor, anyway.

4: wait <n> waits until the process with PID <n> is complete (it will block until the process completes, so you might not want to call this until you are sure the process is done), and then returns the exit code of the completed process.

2, 3: ps or ps | grep " $! " can tell you whether the process is still running. It is up to you how to understand the output and decide how close it is to finishing. (ps | grep isn't idiot-proof. If you have time you can come up with a more robust way to tell whether the process is still running).

Here's a skeleton script:

# simulate a long process that will have an identifiable exit code
(sleep 15 ; /bin/false) &
my_pid=$!

while   ps | grep " $my_pid "     # might also need  | grep -v grep  here
do
    echo $my_pid is still in the ps output. Must still be running.
    sleep 3
done

echo Oh, it looks like the process is done.
wait $my_pid
# The variable $? always holds the exit code of the last command to finish.
# Here it holds the exit code of $my_pid, since wait exits with that code. 
my_status=$?
echo The exit status of the process was $my_status

Solution 2

This is how I solved it when I had a similar need:

# Some function that takes a long time to process
longprocess() {
        # Sleep up to 14 seconds
        sleep $((RANDOM % 15))
        # Randomly exit with 0 or 1
        exit $((RANDOM % 2))
}

pids=""
# Run five concurrent processes
for i in {1..5}; do
        ( longprocess ) &
        # store PID of process
        pids+=" $!"
done

# Wait for all processes to finish, will take max 14s
# as it waits in order of launch, not order of finishing
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done

Solution 3

The pid of a backgrounded child process is stored in $!. You can store all child processes' pids into an array, e.g. PIDS[].

wait [-n] [jobspec or pid …]

Wait until the child process specified by each process ID pid or job specification jobspec exits and return the exit status of the last command waited for. If a job spec is given, all processes in the job are waited for. If no arguments are given, all currently active child processes are waited for, and the return status is zero. If the -n option is supplied, wait waits for any job to terminate and returns its exit status. If neither jobspec nor pid specifies an active child process of the shell, the return status is 127.

Use wait command you can wait for all child processes finish, meanwhile you can get exit status of each child processes via $? and store status into STATUS[]. Then you can do something depending by status.

I have tried the following 2 solutions and they run well. solution01 is more concise, while solution02 is a little complicated.

solution01

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  PIDS+=($!)
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS+=($?)
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

solution02

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
i=0
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  pid=$!
  PIDS[$i]=${pid}
  ((i+=1))
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
i=0
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS[$i]=$?
  ((i+=1))
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

Solution 4

#/bin/bash

#pgm to monitor
tail -f /var/log/messages >> /tmp/log&
# background cmd pid
pid=$!
# loop to monitor running background cmd
while :
do
    ps ax | grep $pid | grep -v grep
    ret=$?
    if test "$ret" != "0"
    then
        echo "Monitored pid ended"
        break
    fi
    sleep 5

done

wait $pid
echo $?

Solution 5

As I see almost all answers use external utilities (mostly ps) to poll the state of the background process. There is a more unixesh solution, catching the SIGCHLD signal. In the signal handler it has to be checked which child process was stopped. It can be done by kill -0 <PID> built-in (universal) or checking the existence of /proc/<PID> directory (Linux specific) or using the jobs built-in ( specific. jobs -l also reports the pid. In this case the 3rd field of the output can be Stopped|Running|Done|Exit . ).

Here is my example.

The launched process is called loop.sh. It accepts -x or a number as an argument. For -x is exits with exit code 1. For a number it waits num*5 seconds. In every 5 seconds it prints its PID.

The launcher process is called launch.sh:

#!/bin/bash

handle_chld() {
    local tmp=()
    for((i=0;i<${#pids[@]};++i)); do
        if [ ! -d /proc/${pids[i]} ]; then
            wait ${pids[i]}
            echo "Stopped ${pids[i]}; exit code: $?"
        else tmp+=(${pids[i]})
        fi
    done
    pids=(${tmp[@]})
}

set -o monitor
trap "handle_chld" CHLD

# Start background processes
./loop.sh 3 &
pids+=($!)
./loop.sh 2 &
pids+=($!)
./loop.sh -x &
pids+=($!)

# Wait until all background processes are stopped
while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
echo STOPPED

For more explanation see: Starting a process from bash script failed

Share:
184,940

Related videos on Youtube

bob
Author by

bob

Updated on April 20, 2021

Comments

  • bob
    bob about 3 years

    I have a command CMD called from my main bourne shell script that takes forever.

    I want to modify the script as follows:

    1. Run the command CMD in parallel as a background process (CMD &).
    2. In the main script, have a loop to monitor the spawned command every few seconds. The loop also echoes some messages to stdout indicating progress of the script.
    3. Exit the loop when the spawned command terminates.
    4. Capture and report the exit code of the spawned process.

    Can someone give me pointers to accomplish this?

  • SourceSeeker
    SourceSeeker over 14 years
    Here's a trick to avoid the grep -v. You can limit the search to the beginning of the line: grep '^'$pid Plus, you can do ps p $pid -o pid=, anyway. Also, tail -f isn't going to end until you kill it so I don't think it's a very good way to demo this (at least without pointing that out). You might want to redirect the output of your ps command to /dev/null or it'll go to the screen at every iteration. Your exit causes the wait to be skipped - it should probably be a break. But aren't the while/ps and the wait redundant?
  • SourceSeeker
    SourceSeeker over 14 years
    ps -p $my_pid -o pid= neither grep is needed.
  • mob
    mob over 14 years
    @Dennis Williamson ps has many flavors. Your call doesn't work for me but ps -p$my_pid does. Your larger point that grep isn't necessary is correct.
  • mob
    mob over 14 years
    Hmmm .. actually I can't figure out a good way to avoid grep on Cygwin. ps -p$pid always has exit status of 0 whether $pid exists or not. I could say something like while [ 'ps -p$pid | wc -l' \> 1 ] but that's hardly an improvement ...
  • ephemient
    ephemient over 14 years
    Why does everybody forget about kill -0 $pid? It doesn't actually send any signal, only checks that the process is alive, using a shell built-in instead of external processes.
  • ephemient
    ephemient over 14 years
    kill -0 $! is a better way of telling whether a process is still running. It doesn't actually send any signal, only checks that the process is alive, using a shell built-in instead of external processes. As man 2 kill says, "If sig is 0, then no signal is sent, but error checking is still performed; this can be used to check for the existence of a process ID or process group ID."
  • errant.info
    errant.info almost 11 years
    Because you can only kill a process you own: bash: kill: (1) - Operation not permitted
  • Craig Ringer
    Craig Ringer almost 11 years
    @ephemient kill -0 will return non-zero if you don't have permission to send signals to a process that is running. Unfortunately it returns 1 in both this case and the case where the process doesn't exist. This makes it useful unless you don't own the process - which can be the case even for processes you created if a tool like sudo is involved or if they're setuid (and possibly drop privs).
  • nmax
    nmax over 9 years
    if you only need to monitor a process and not a specific PID, pgrep is also useful: pgrep -x [process]
  • DifferentPseudonym
    DifferentPseudonym almost 9 years
    wait doesn't return the exit code in the variable $?. It just returns the exit code and $? is the exit code of the latest foreground program.
  • Trevor Boyd Smith
    Trevor Boyd Smith almost 9 years
    For the many people voting up the kill -0. Here is a peer reviewed reference from SO that shows CraigRinger's comment is legit regarding: kill -0 will return non-zero for running processes... but ps -p will always return 0 for any running process.
  • Brais Gabin
    Brais Gabin over 8 years
    The loop is redundant. Just wait. Less code => less edge cases.
  • jarno
    jarno about 8 years
    @mob The current while condition does not work here (in Ubuntu linux). Can't you use -o pid= for ps (to disable the header)? Then while [ -n "$(ps -o pid= -p$my_pid)" ] would work; here it works also without -p. while [ $(ps -p$pid | wc -l) \> 1 ] works, too, but apparently you did not like it.
  • mob
    mob about 8 years
    There are many different flavors of ps. You may have to experiment with options that work for your system and your configuration.
  • benjaoming
    benjaoming about 8 years
    Doesn't work in bash? Syntax error: "else" unexpected (expecting "done")
  • Terry
    Terry over 6 years
    I have tried and proved it runs well. You can read my explaination in code.
  • Noel Widmer
    Noel Widmer over 6 years
    Please read "How do I write a good answer?" where you'll find the following info: ... try to mention any limitations, assumptions or simplifications in your answer. Brevity is acceptable, but fuller explanations are better. You answer is therefore acceptable but you have much better chances of getting upvotes if you can elaborate on the problem and your solution. :-)
  • Luke Davis
    Luke Davis over 6 years
    Thanks! This one seems to me the simplest approach.
  • Dima Korobskiy
    Dima Korobskiy over 6 years
    This solution doesn't satisfy requirement #2: a monitoring loop per background process. waits cause script to wait until the very end of (each) process.
  • Dima Korobskiy
    Dima Korobskiy over 6 years
    @Brais Gabin The monitoring loop is requirement #2 of the question
  • Santosh Kumar Arjunan
    Santosh Kumar Arjunan about 6 years
    Simple and nice approach.. have been searching for this solution quite sometime..
  • PlasmaBinturong
    PlasmaBinturong about 6 years
    Since we are talking about Bash, the for loop could have been written as: for i in ${!pids[@]}; using parameter expansion.
  • codeforester
    codeforester almost 6 years
    pid=$!; PIDS[$i]=${pid}; ((i+=1)) can be written more simply as PIDS+=($!) which simply appends to the array without having to use a separate variable for indexing or the pid itself. The same thing applies to the STATUS array.
  • Terry
    Terry almost 6 years
    @codeforester, thank you for your sugesstion, I have modifed my inital code into solution01, it looks more concise.
  • codeforester
    codeforester almost 6 years
    The same thing applies to other places where you are adding things into an array.
  • Terry
    Terry over 5 years
    @Wernfried Domscheit 1. If b.sh or c.sh have been finished before a.sh, wait [pid of a.sh] still running. until wait [pid of a.sh] finishs, the program will run next step. 2. In my linux, it doesn't happen that wait [pid of b.sh] will return "-bash: wait: pid xxx is not a child of this shell", could you give me some detail on how to reproduce this case ? thank you
  • Wernfried Domscheit
    Wernfried Domscheit over 5 years
    It was my mistake, I tried wait [random number] instead of an existing PID
  • conny
    conny over 5 years
    This doesn't work .. or doesn't do what you want: it doesn't check backgrounded processes exit statuses?
  • Bjorn
    Bjorn over 5 years
    @conny, yes it does check the exit status of the backgrounded processes. The "wait" command returns the exit status of the processes. In the example shown here it is demonstrated by the "Process $p success/fail".
  • punkbit
    punkbit almost 5 years
    great template, thanks for sharing! I believe that instead of trap, we can also do while kill -0 $pid 2> /dev/null; do X; done, hope it's useful for someone else in the future who reads this message ;)
  • Troels Ynddal
    Troels Ynddal almost 2 years
    I think ${#PID} should be ${#PID[@]}