web-dev-qa-db-fra.com

Obtenir le code de sortie d'un processus en arrière-plan

J'ai une commande appelée CMD à partir de mon script principal Bourne Shell qui prend une éternité.

Je veux modifier le script comme suit:

  1. Exécutez la commande CMD en parallèle en tant que processus en arrière-plan ($ CMD &).
  2. Dans le script principal, prévoyez une boucle pour surveiller la commande générée toutes les quelques secondes. La boucle renvoie également certains messages à stdout indiquant la progression du script.
  3. Quittez la boucle lorsque la commande spawned se termine.
  4. Capturez et signalez le code de sortie du processus généré.

Quelqu'un peut-il me donner des indications pour accomplir cela?

102
bob

1: dans bash, $! contient le PID du dernier processus d'arrière-plan exécuté. Cela vous indiquera quel processus surveiller, de toute façon.

4: wait <n> attend que le processus avec ID soit terminé (il sera bloqué jusqu'à ce que le processus soit terminé, de sorte que vous ne souhaitez peut-être pas l'appeler tant que vous n'êtes pas sûr que le processus est terminé). Après le retour de wait, le code de sortie du processus est renvoyé dans la variable $?

2, 3: ps ou ps | grep " $! " peut vous dire si le processus est toujours en cours d'exécution. Il vous appartient de comprendre le résultat et de décider s'il est proche de la fin. (ps | grep n'est pas idiot. Si vous avez le temps, vous pouvez trouver un moyen plus robuste de dire si le processus est toujours en cours d'exécution).

Voici un script squelette:

# 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
my_status=$?
echo The exit status of the process was $my_status
104
mob

Voici comment je l'ai résolu quand j'avais un besoin similaire:

# 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 finnish, will take max 14s
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done
40
Bjorn
#/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 $?
7
Abu Aqil

Comme je le vois presque toutes les réponses utilisent des utilitaires externes (principalement ps) pour interroger l’état du processus en arrière-plan. Il existe une solution plus unixesh, capturant le signal SIGCHLD. Dans le gestionnaire de signaux, il faut vérifier quel processus enfant a été arrêté. Cela peut être fait par kill -0 <PID> intégré (universel) ou en vérifiant l'existence du répertoire /proc/<PID> (spécifique à Linux) ou en utilisant le jobs intégré ( bash spécifique. jobs -l indique également le pid. Dans ce cas, le 3ème champ de la sortie peut être Stopped | Running | Done | Exit.).

Voici mon exemple.

Le processus lancé s'appelle loop.sh. Il accepte -x ou un nombre en tant qu'argument. Pour -x, quitte avec le code de sortie 1. Pour un numéro, attend num * 5 secondes. Toutes les 5 secondes, il affiche son PID.

Le processus de lancement s'appelle 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

Pour plus d'explications, voir: Le démarrage d'un processus à partir d'un script bash a échoué

7
TrueY

Le pid d'un processus enfant avec arrière-plan est stocké dans $! . Vous pouvez stocker les pids de tous les processus enfants dans un tableau, par exemple. PIDS [] .

wait [-n] [jobspec or pid …]

Attendez que le processus enfant spécifié par chaque ID de processus pid ou jobpec de spécification de travail se ferme et renvoie l'état de sortie de la dernière commande attendue. Si une spécification de travail est donnée, tous les processus du travail sont attendus. Si aucun argument n'est fourni, tous les processus enfants actuellement actifs sont attendus et le statut de retour est zéro. Si l'option -n est fournie, wait attend que tout travail se termine et renvoie son statut de sortie. Si ni jobspec ni pid ne spécifient un processus enfant actif du Shell, le statut de retour est 127.

Utilisez wait command vous permet d’attendre la fin de tous les processus enfants. Vous pouvez également obtenir le statut de sortie de chaque processus enfant via $? et enregistre le statut dans STATUS [] . Ensuite, vous pouvez faire quelque chose en fonction de votre statut.

J'ai essayé les 2 solutions suivantes et elles fonctionnent bien. solution01 est plus concis, alors que solution02 est un peu compliqué.

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
5
Terry wang

Je changerais légèrement votre approche. Plutôt que de vérifier toutes les quelques secondes si la commande est toujours active et de signaler un message, utilisez un autre processus qui indique toutes les quelques secondes que la commande est toujours en cours d'exécution, puis supprimez ce processus à la fin de la commande. Par exemple:

 #!/bin/sh 

 cmd () {sommeil 5; sortie 24; } 

 cmd & # Exécuter le processus de longue durée 
 pid = $! # Enregistrez le pid 

 # Créez un processus qui indique de manière continue que la commande est toujours en cours d'exécution 
 While echo "$ (date): $ pid est toujours en cours d'exécution"; dormez 1; done & 
 echoer = $! 

 # Définir un piège pour tuer le rapporteur à la fin du processus 
 trap 'kill $ echoer' 0 

 # Attendre la fin du processus 
 if wait $ pid; puis
 echo "cmd successed" 
 else 
 echo "cmd FAILED !! (return $?)" 
 fi 
4
William Pursell

Notre équipe avait le même besoin avec un script exécuté à distance par SSH qui expirait après 25 minutes d’inactivité. Voici une solution avec la boucle de surveillance vérifiant le processus d'arrière-plan toutes les secondes, mais n'imprimant que toutes les 10 minutes pour supprimer un délai d'inactivité.

long_running.sh & 
pid=$!

# Wait on a background job completion. Query status every 10 minutes.
declare -i elapsed=0
# `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
while ps -p ${pid} >/dev/null; do
  sleep 1
  if ((++elapsed % 600 == 0)); then
    echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
  fi
done

# Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
# "If neither jobspec nor pid specifies an active child process of the Shell, the return status is 127."
wait ${pid}
3
DKroot

Un exemple simple, semblable aux solutions ci-dessus. Cela ne nécessite aucune surveillance de la sortie du processus. L'exemple suivant utilise tail pour suivre la sortie.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'sleep 30; exit 5' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh &
[1] 7454
$ pid=$!
$ wait $pid
[1]+  Exit 5                  ./tmp.sh
$ echo $?
5

Utilisez tail pour suivre la sortie du processus et quitter à la fin du processus.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh
0
1
2
^C
$ ./tmp.sh > /tmp/tmp.log 2>&1 &
[1] 7673
$ pid=$!
$ tail -f --pid $pid /tmp/tmp.log
0
1
2
3
4
5
6
7
8
9
[1]+  Exit 5                  ./tmp.sh > /tmp/tmp.log 2>&1
$ wait $pid
$ echo $?
5
2
Darren Weber

Une autre solution consiste à surveiller les processus via le système de fichiers proc (plus sûr que le combo ps/grep); lorsque vous démarrez un processus, il a un dossier correspondant dans/proc/$ pid, la solution peut donc être

#!/bin/bash
....
doSomething &
local pid=$!
while [ -d /proc/$pid ]; do # While directory exists, the process is running
    doSomethingElse
    ....
else # when directory is removed from /proc, process has ended
    wait $pid
    local exit_status=$?
done
....

Vous pouvez maintenant utiliser la variable $ exit_status comme bon vous semble.

Avec cette méthode, votre script n’a pas à attendre le processus en arrière-plan, il vous suffira de surveiller un fichier temporaire pour connaître le statut de sortie.

FUNCmyCmd() { sleep 3;return 6; };

export retFile=$(mktemp); 
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
FUNCexecAndWait&

maintenant, votre script peut faire autre chose pendant que vous devez simplement surveiller le contenu de retFile (il peut également contenir toute autre information que vous souhaitez, comme l'heure de sortie).

PS: au fait, j'ai codé en pensant à bash

0
Aquarius Power

Cela peut aller au-delà de votre question, mais si vous êtes préoccupé par la durée d'exécution des processus, vous pouvez être intéressé par la vérification du statut des processus en arrière-plan en cours d'exécution après un intervalle de temps. Il est assez facile de vérifier quels PID enfants sont toujours en cours d'utilisation à l'aide de pgrep -P $$. Cependant, j'ai proposé la solution suivante pour vérifier le statut de sortie de ces PID qui ont déjà expiré:

cmd1() { sleep 5; exit 24; }
cmd2() { sleep 10; exit 0; }

pids=()
cmd1 & pids+=("$!")
cmd2 & pids+=("$!")

lasttimeout=0
for timeout in 2 7 11; do
  echo -n "interval-$timeout: "
  sleep $((timeout-lasttimeout))

  # you can only wait on a pid once
  remainingpids=()
  for pid in ${pids[*]}; do
     if ! ps -p $pid >/dev/null ; then
        wait $pid
        echo -n "pid-$pid:exited($?); "
     else
        echo -n "pid-$pid:running; "
        remainingpids+=("$pid")
     fi
  done
  pids=( ${remainingpids[*]} )

  lasttimeout=$timeout
  echo
done

qui produit:

interval-2: pid-28083:running; pid-28084:running; 
interval-7: pid-28083:exited(24); pid-28084:running; 
interval-11: pid-28084:exited(0); 

Remarque: Vous pouvez remplacer $pids par une variable chaîne plutôt que par un tableau afin de simplifier les choses si vous le souhaitez. 

0
errant.info