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

130

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

Je souhaite modifier le script comme suit:

  1. Exécutez la commande CMD en parallèle en tant que processus d'arrière-plan ( CMD &).
  2. Dans le script principal, créez 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 générée se termine.
  4. Capturez et signalez le code de sortie du processus généré.

Quelqu'un peut-il me donner des conseils pour y parvenir?

bob
la source
1
...et le gagnant est?
TrueY
1
@TrueY .. bob ne s'est pas connecté depuis le jour où il a posé la question. Il est peu probable que nous le sachions jamais!
ghoti

Réponses:

128

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

4: wait <n>attend que le processus avec PID<n> soit terminé (il se bloquera jusqu'à ce que le processus se termine, vous ne voudrez peut-être pas l'appeler tant que vous n'êtes pas sûr que le processus est terminé), puis renvoie le code de sortie du processus terminé.

2, 3: psou ps | grep " $! "peut vous dire si le processus est toujours en cours d'exécution. C'est à vous de comprendre comment comprendre la sortie et de décider à quel point elle est proche de la finition. ( ps | grepn'est pas à l'épreuve des idiots. Si vous avez le temps, vous pouvez trouver un moyen plus robuste de savoir 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
# 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
foule
la source
15
ps -p $my_pid -o pid=ni l'un ni l'autre grepn'est nécessaire.
Suspendu jusqu'à nouvel ordre.
54
kill -0 $!est une meilleure façon de savoir si un processus est toujours en cours d'exécution. Il n'envoie en fait aucun signal, vérifie seulement que le processus est actif, en utilisant un shell intégré au lieu de processus externes. Comme man 2 killindiqué, "Si sig vaut 0, aucun signal n'est envoyé, mais la vérification des erreurs est toujours effectuée; cela peut être utilisé pour vérifier l'existence d'un ID de processus ou d'un ID de groupe de processus."
éphémère
14
@ephemient kill -0renverra une valeur non nulle si vous n'êtes pas autorisé à envoyer des signaux à un processus en cours d'exécution. Malheureusement, il revient 1à la fois dans ce cas et dans le cas où le processus n'existe pas. Cela le rend utile sauf si vous ne possédez pas le processus - ce qui peut être le cas même pour les processus que vous avez créés si un outil comme sudoest impliqué ou s'ils sont setuid (et éventuellement abandonner les privs).
Craig Ringer
13
waitne renvoie pas le code de sortie dans la variable $?. Il renvoie simplement le code de sortie et $?est le code de sortie du dernier programme de premier plan.
MindlessRanger
7
Pour les nombreuses personnes votant pour le kill -0. Voici une référence révisée par les pairs du SO qui montre que le commentaire de CraigRinger est légitime concernant: kill -0retournera non nul pour les processus en cours d'exécution ... mais ps -pretournera toujours 0 pour tout processus en cours .
Trevor Boyd Smith
58

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 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
Bjorn
la source
J'aime cette approche.
Kemin Zhou
Merci! Celle-ci me semble la plus simple.
Luke Davis
4
Cette solution ne répond pas à l'exigence n ° 2: une boucle de surveillance par processus d'arrière-plan. waits fait attendre le script jusqu'à la toute fin de (chaque) processus.
DKroot
Approche simple et agréable .. ont été à la recherche de cette solution un certain temps ..
Santosh Kumar Arjunan
Cela ne fonctionne pas ... ou ne fait pas ce que vous voulez: cela ne vérifie pas les états de sortie des processus en arrière-plan?
conny
10

Le pid d'un processus enfant en arrière-plan est stocké dans $! . Vous pouvez stocker tous les pids des 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 spécification de tâche jobspec se termine et renvoie l'état de sortie de la dernière commande attendue. Si une spécification de tâche est fournie, tous les processus de la tâche sont attendus. Si aucun argument n'est donné, tous les processus enfants actuellement actifs sont attendus et l'état de retour est zéro. Si l'option -n est fournie, wait attend que tout travail se termine et renvoie son état de sortie. Si ni jobspec ni pid ne spécifient un processus enfant actif du shell, l'état de retour est 127.

Utilisez la commande wait, vous pouvez attendre la fin de tous les processus enfants, en attendant, vous pouvez obtenir le statut de sortie de chaque processus enfant via $? et stocker le statut dans STATUS [] . Ensuite, vous pouvez faire quelque chose en fonction du statut.

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

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
Terry
la source
J'ai essayé et prouvé qu'il fonctionne bien. Vous pouvez lire mon explication dans le code.
Terry le
Veuillez lire « Comment rédiger une bonne réponse? » Où vous trouverez les informations suivantes: ... essayez de mentionner les limites, les hypothèses ou les simplifications dans votre réponse. La brièveté est acceptable, mais des explications plus complètes sont meilleures. Votre réponse est donc acceptable, mais vous avez de bien meilleures chances d'obtenir des votes positifs si vous pouvez élaborer sur le problème et votre solution. :-)
Noel Widmer
1
pid=$!; PIDS[$i]=${pid}; ((i+=1))peut être écrit plus simplement comme PIDS+=($!)qui ajoute simplement au tableau sans avoir à utiliser une variable distincte pour l'indexation ou le pid lui-même. La même chose s'applique au STATUStableau.
codeforester
1
@codeforester, merci pour votre suggestion, j'ai modifié mon code initial en solution01, il semble plus concis.
Terry
La même chose s'applique aux autres endroits où vous ajoutez des éléments dans un tableau.
codeforester
8

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

Voici mon exemple.

Le processus lancé est appelé loop.sh. Il accepte -xou un nombre comme argument. Pour-x est sort avec le code de sortie 1. Pour un nombre, il attend num * 5 secondes. Toutes les 5 secondes, il imprime 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é

VraiY
la source
1
Puisque nous parlons de Bash, la boucle for aurait pu être écrite comme: en for i in ${!pids[@]};utilisant l'expansion des paramètres.
PlasmaBinturong
8
#/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 $?
Abu Aqil
la source
2
Voici une astuce pour éviter les fichiers grep -v. Vous pouvez limiter la recherche au début de la ligne: grep '^'$pidDe plus, vous pouvez le faire de ps p $pid -o pid=toute façon. De plus, cela tail -fne se terminera pas tant que vous ne l'aurez pas tué, donc je ne pense pas que ce soit un très bon moyen de faire une démonstration (du moins sans le souligner). Vous voudrez peut-être rediriger la sortie de votre pscommande vers /dev/nullou elle ira à l'écran à chaque itération. Votre exitfait waitsauter le - il devrait probablement s'agir d'un fichier break. Mais le while/ pset le ne sont-ils pas waitredondants?
Suspendu jusqu'à nouvel ordre.
5
Pourquoi tout le monde oublie- kill -0 $pidt- il ? Il n'envoie en fait aucun signal, vérifie seulement que le processus est actif, en utilisant un shell intégré au lieu de processus externes.
éphémère
3
Parce que vous ne pouvez tuer qu'un processus que vous possédez:bash: kill: (1) - Operation not permitted
errant.info
2
La boucle est redondante. Attends. Moins de code => moins de cas de bord.
Brais Gabin
@Brais Gabin La boucle de surveillance est l'exigence n ° 2 de la question
DKroot
5

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, ayez un autre processus qui signale toutes les quelques secondes que la commande est toujours en cours d'exécution, puis arrêtez ce processus lorsque la commande se termine. Par exemple:

#! / bin / sh

cmd () {sommeil 5; sortie 24; }

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

# Génère un processus qui signale en permanence que la commande est toujours en cours d'exécution
while echo "$ (date): $ pid est toujours en cours d'exécution"; dormir 1; terminé &
echoer = $!

# Définir un piège pour tuer le journaliste lorsque le processus se termine
piège 'kill $ echoer' 0

# Attendez que le processus se termine
si attendez $ pid; puis
    echo "cmd a réussi"
autre
    echo "cmd FAILED !! (a retourné $?)"
Fi
William Pursell
la source
excellent modèle, merci pour le partage! Je crois qu'au lieu de piège, nous pouvons aussi faire while kill -0 $pid 2> /dev/null; do X; done, j'espère que c'est utile pour quelqu'un d'autre dans le futur qui lira ce message;)
punkbit
3

Notre équipe avait le même besoin avec un script exécuté par SSH à distance 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}
DKroot
la source
2

Un exemple simple, similaire aux solutions ci-dessus. Cela ne nécessite de surveiller aucune sortie de 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 quittez lorsque le processus est terminé.

$ 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
Darren Weber
la source
1

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, donc la solution pourrait ê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 vous le souhaitez.

Искрен Хаджинедев
la source
Ça ne marche pas en bash? Syntax error: "else" unexpected (expecting "done")
benjaoming
1

Avec cette méthode, votre script n'a pas à attendre le processus d'arrière-plan, vous n'aurez qu'à surveiller un fichier temporaire pour l'état de sortie.

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

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

maintenant, votre script peut faire autre chose alors que vous n'avez plus qu'à continuer à surveiller le contenu de retFile (il peut aussi contenir toute autre information que vous voulez comme l'heure de sortie).

PS: btw, j'ai codé la pensée en bash

Puissance du Verseau
la source
0

Cela peut aller au-delà de votre question, mais si vous êtes préoccupé par la durée d'exécution des processus, vous voudrez peut-être vérifier l'état des processus en arrière-plan en cours d'exécution après un certain temps. Il est assez facile de vérifier quels PID enfants sont toujours en cours d'exécution pgrep -P $$, mais j'ai proposé la solution suivante pour vérifier l'état 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 sort:

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 passer $pidsà une variable chaîne plutôt qu'à un tableau pour simplifier les choses si vous le souhaitez.

errant.info
la source
0

Ma solution était d'utiliser un tube anonyme pour transmettre l'état à une boucle de surveillance. Il n'y a pas de fichiers temporaires utilisés pour échanger le statut, donc rien à nettoyer. Si vous n'êtes pas sûr du nombre de travaux en arrière-plan, la condition de rupture pourrait être [ -z "$(jobs -p)" ].

#!/bin/bash

exec 3<> <(:)

{ sleep 15 ; echo "sleep/exit $?" >&3 ; } &

while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
    echo "stat: ${STAT}; code: ${CODE}"
    if [ "${STAT}" = "sleep/exit" ] ; then
        break
    fi
done
James Dingwall
la source