Ottieni il codice di uscita di un processo in background

Ho un comando chiamato CMD dal mio script principale della shell di Bourne che richiede sempre.

Voglio modificare lo script come segue:

  1. Esegui il comando CMD in parallelo come processo in background ($ CMD e).
  2. Nello script principale, avere un ciclo per monitorare il comando generato ogni pochi secondi. Il ciclo fa anche eco ad alcuni messaggi allo stdout che indicano lo stato di avanzamento dello script.
  3. Esci dal ciclo quando termina il comando spawn.
  4. Cattura e segnala il codice di uscita del processo generato.

Qualcuno può darmi dei consigli per farlo?

1: in bash, $! contiene il PID dell’ultimo processo in background che è stato eseguito. Questo ti dirà quale processo monitorare, comunque.

4: wait attende fino a quando il processo con ID è completo (bloccherà fino al completamento del processo, quindi potresti non voler chiamare questo finché non sei sicuro che il processo sia completato). Dopo l’ wait ritorna, il codice di uscita del processo viene restituito nella variabile $?

2, 3: ps o ps | grep " $! " ps | grep " $! " può dirti se il processo è ancora in esecuzione. Spetta a te capire l’output e decidere quanto è vicino alla finitura. ( ps | grep non è a prova di idiota. Se hai tempo puoi trovare un modo più efficace per capire se il processo è ancora in esecuzione).

Ecco uno script scheletro:

 # 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 

Ecco come l’ho risolto quando ho avuto una necessità simile:

 # 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 

Come vedo quasi tutte le risposte utilizzano utilità esterne (principalmente ps ) per interrogare lo stato del processo in background. C’è una soluzione più unixesh, che cattura il segnale SIGCHLD. Nel gestore del segnale deve essere controllato quale processo figlio è stato fermato. Può essere fatto da kill -0 built-in (universale) o controllando l’esistenza della /proc/ (specifica per Linux) o usando i jobs integrati ( bash specifico. pid. In questo caso il terzo campo dell’output può essere Stopped | Running | Done | Exit.).

Ecco il mio esempio

Il processo avviato si chiama loop.sh Accetta -x o un numero come argomento. Per -x è chiuso con il codice di uscita 1. Per un numero attende num * 5 secondi. In ogni 5 secondi stampa il suo PID.

Il processo di avvio si chiama 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 

Per ulteriori spiegazioni, consultare: Avvio di un processo dallo script di bash non riuscito

 #/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 $? 

Vorrei cambiare leggermente il tuo approccio. Anziché controllare ogni pochi secondi se il comando è ancora attivo e segnalare un messaggio, avere un altro processo che riporta ogni secondo che il comando è ancora in esecuzione e quindi uccidere quel processo al termine del comando. Per esempio:

 #! / Bin / sh

 cmd () {sleep 5;  uscita 24;  }

 cmd & # Esegui il processo di lunga durata
 pid = $!  # Registra il pid

 # Crea un processo che riporta in modo inequivocabile che il comando è ancora in esecuzione
 while echo "$ (date): $ pid è ancora in esecuzione";  dormi 1;  fatto &
 echoer = $!

 # Imposta una trappola per uccidere il reporter al termine del processo
 trap 'kill $ echoer' 0

 # Aspetta che il processo finisca
 se aspetti $ pid;  poi
     echo "cmd riuscito"
 altro
     echo "cmd NON RIUSCITO !! (restituito $?)"
 fi

Un semplice esempio, simile alle soluzioni sopra. Questo non richiede il monitoraggio di alcun output di processo. Il prossimo esempio usa tail per seguire l’output.

 $ 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 

Utilizzare tail per seguire l’output del processo e uscire quando il processo è completo.

 $ 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 

Il nostro team ha avuto la stessa necessità con uno script eseguito da SSH remoto che era scaduto dopo 25 minuti di inattività. Ecco una soluzione con il ciclo di monitoraggio che controlla il processo in background ogni secondo, ma stampa ogni 10 minuti per sopprimere un timeout di inattività.

 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} 

Il pid di un processo figlio in background è memorizzato in $! . È ansible memorizzare tutti i pid dei processi figli in una matrice, ad esempio PIDS [] .

 wait [-n] [jobspec or pid …] 

Attendere fino a quando il processo figlio specificato da ciascun ID di processo ID o le exit di jobspec della specifica del lavoro e restituire lo stato di uscita dell’ultimo comando atteso. Se viene fornita una specifica del lavoro, vengono attesi tutti i processi nel lavoro. Se non vengono forniti argomenti, vengono attesi tutti i processi figli attualmente attivi e lo stato di ritorno è zero. Se viene fornita l’opzione -n, attendere attende che qualsiasi lavoro venga terminato e restituisce lo stato di uscita. Se né jobspec né pid specificano un processo figlio attivo della shell, lo stato di ritorno è 127.

Usa il comando wait che puoi attendere che tutti i processi figli finiscano, nel frattempo puoi ottenere lo stato di uscita di ogni processo figlio tramite $? e memorizza lo stato in STATUS [] . Quindi puoi fare qualcosa in base allo stato.

Ho provato le seguenti 2 soluzioni e funzionano bene. solution01 è più conciso, mentre solution02 è un po ‘complicato.

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 

Un’altra soluzione è monitorare i processi tramite il filesystem proc (più sicuro della combo ps / grep); quando si avvia un processo ha una cartella corrispondente in / proc / $ pid, quindi la soluzione potrebbe essere

 #!/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 .... 

Ora puoi usare la variabile $ exit_status come preferisci.

Questo potrebbe estendersi oltre la tua domanda, tuttavia se sei preoccupato per il periodo di tempo in cui i processi sono in esecuzione, potresti essere interessato a controllare lo stato dei processi in background in esecuzione dopo un intervallo di tempo. È abbastanza facile controllare quali PID figli sono ancora in esecuzione usando pgrep -P $$ , tuttavia ho trovato la seguente soluzione per verificare lo stato di uscita di quei PID che sono già scaduti:

 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 

quali uscite:

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

Nota: è ansible modificare $pids in una variabile stringa anziché in array per semplificare le cose, se lo si desidera.

Con questo metodo, il tuo script non deve attendere il processo in background, dovrai solo monitorare un file temporaneo per lo stato di uscita.

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

ora, il tuo script può fare qualsiasi altra cosa mentre devi solo continuare a monitorare il contenuto di retFile (può anche contenere qualsiasi altra informazione tu voglia come il tempo di uscita).

PS .: btw, ho pensato in codice in bash