Как дождаться завершения в bash нескольких подпроцессов и возврата кода завершения! = 0, когда любой подпроцесс заканчивается кодом! = 0? - PullRequest
475 голосов
/ 10 декабря 2008

Как ожидать в bash-скрипте несколько подпроцессов, порожденных из этого скрипта, чтобы завершить и вернуть код завершения! = 0, когда любой из подпроцессов заканчивается кодом! = 0?

Простой скрипт:

#!/bin/bash
for i in `seq 0 9`; do
  doCalculations $i &
done
wait

Приведенный выше скрипт будет ожидать всех 10 порожденных подпроцессов, но он всегда будет иметь статус выхода 0 (см. help wait). Как я могу изменить этот скрипт, чтобы он обнаруживал состояния выхода порожденных подпроцессов и возвращал код выхода 1, когда любой из подпроцессов заканчивался кодом! = 0?

Есть ли лучшее решение для этого, чем сбор PID подпроцессов, ожидание их в порядке и суммирование состояний выхода?

Ответы [ 28 ]

5 голосов
/ 04 августа 2016

Вот моя версия, которая работает для нескольких pids, регистрирует предупреждения, если выполнение занимает слишком много времени, и останавливает подпроцессы, если выполнение занимает больше заданного значения.

function WaitForTaskCompletion {
    local pids="${1}" # pids to wait for, separated by semi-colon
    local soft_max_time="${2}" # If execution takes longer than $soft_max_time seconds, will log a warning, unless $soft_max_time equals 0.
    local hard_max_time="${3}" # If execution takes longer than $hard_max_time seconds, will stop execution, unless $hard_max_time equals 0.
    local caller_name="${4}" # Who called this function
    local exit_on_error="${5:-false}" # Should the function exit program on subprocess errors       

    Logger "${FUNCNAME[0]} called by [$caller_name]."

    local soft_alert=0 # Does a soft alert need to be triggered, if yes, send an alert once 
    local log_ttime=0 # local time instance for comparaison

    local seconds_begin=$SECONDS # Seconds since the beginning of the script
    local exec_time=0 # Seconds since the beginning of this function

    local retval=0 # return value of monitored pid process
    local errorcount=0 # Number of pids that finished with errors

    local pidCount # number of given pids

    IFS=';' read -a pidsArray <<< "$pids"
    pidCount=${#pidsArray[@]}

    while [ ${#pidsArray[@]} -gt 0 ]; do
        newPidsArray=()
        for pid in "${pidsArray[@]}"; do
            if kill -0 $pid > /dev/null 2>&1; then
                newPidsArray+=($pid)
            else
                wait $pid
                result=$?
                if [ $result -ne 0 ]; then
                    errorcount=$((errorcount+1))
                    Logger "${FUNCNAME[0]} called by [$caller_name] finished monitoring [$pid] with exitcode [$result]."
                fi
            fi
        done

        ## Log a standby message every hour
        exec_time=$(($SECONDS - $seconds_begin))
        if [ $((($exec_time + 1) % 3600)) -eq 0 ]; then
            if [ $log_ttime -ne $exec_time ]; then
                log_ttime=$exec_time
                Logger "Current tasks still running with pids [${pidsArray[@]}]."
            fi
        fi

        if [ $exec_time -gt $soft_max_time ]; then
            if [ $soft_alert -eq 0 ] && [ $soft_max_time -ne 0 ]; then
                Logger "Max soft execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]."
                soft_alert=1
                SendAlert

            fi
            if [ $exec_time -gt $hard_max_time ] && [ $hard_max_time -ne 0 ]; then
                Logger "Max hard execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]. Stopping task execution."
                kill -SIGTERM $pid
                if [ $? == 0 ]; then
                    Logger "Task stopped successfully"
                else
                    errrorcount=$((errorcount+1))
                fi
            fi
        fi

        pidsArray=("${newPidsArray[@]}")
        sleep 1
    done

    Logger "${FUNCNAME[0]} ended for [$caller_name] using [$pidCount] subprocesses with [$errorcount] errors."
    if [ $exit_on_error == true ] && [ $errorcount -gt 0 ]; then
        Logger "Stopping execution."
        exit 1337
    else
        return $errorcount
    fi
}

# Just a plain stupid logging function to replace with yours
function Logger {
    local value="${1}"

    echo $value
}

Пример: дождитесь завершения всех трех процессов, зарегистрируйте предупреждение, если выполнение занимает 5 секунд, остановите все процессы, если выполнение займет более 120 секунд. Не выходить из программы при сбоях.

function something {

    sleep 10 &
    pids="$!"
    sleep 12 &
    pids="$pids;$!"
    sleep 9 &
    pids="$pids;$!"

    WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false
}
# Launch the function
someting
5 голосов
/ 03 октября 2011

Следующий код будет ожидать завершения всех вычислений и возвращать статус выхода 1 в случае сбоя любого из doCalculations .

#!/bin/bash
for i in $(seq 0 9); do
   (doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1
4 голосов
/ 07 сентября 2013

Если у вас есть bash 4.2 или более поздняя версия, вам может быть полезно следующее. Он использует ассоциативные массивы для хранения имен задач и их «кода», а также имен задач и их pids. Я также встроил простой метод ограничения скорости, который может пригодиться, если ваши задачи потребляют много ресурсов ЦП или ввода-вывода и вы хотите ограничить количество одновременных задач.

Скрипт запускает все задачи в первом цикле и использует результаты во втором.

Это немного излишне для простых случаев, но допускает довольно аккуратные вещи. Например, можно хранить сообщения об ошибках для каждой задачи в другом ассоциативном массиве и распечатывать их после того, как все установится.

#! /bin/bash

main () {
    local -A pids=()
    local -A tasks=([task1]="echo 1"
                    [task2]="echo 2"
                    [task3]="echo 3"
                    [task4]="false"
                    [task5]="echo 5"
                    [task6]="false")
    local max_concurrent_tasks=2

    for key in "${!tasks[@]}"; do
        while [ $(jobs 2>&1 | grep -c Running) -ge "$max_concurrent_tasks" ]; do
            sleep 1 # gnu sleep allows floating point here...
        done
        ${tasks[$key]} &
        pids+=(["$key"]="$!")
    done

    errors=0
    for key in "${!tasks[@]}"; do
        pid=${pids[$key]}
        local cur_ret=0
        if [ -z "$pid" ]; then
            echo "No Job ID known for the $key process" # should never happen
            cur_ret=1
        else
            wait $pid
            cur_ret=$?
        fi
        if [ "$cur_ret" -ne 0 ]; then
            errors=$(($errors + 1))
            echo "$key (${tasks[$key]}) failed."
        fi
    done

    return $errors
}

main
4 голосов
/ 03 декабря 2014

Я только что изменил сценарий для создания фона и распараллеливания процесса.

Я провел некоторые эксперименты (в Solaris с bash и ksh) и обнаружил, что «wait» выводит состояние выхода, если оно не равно нулю, или список заданий, которые возвращают ненулевой выход, если не указан аргумент PID. Э.Г.

Bash:

$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]-  Exit 2                  sleep 20 && exit 2
[2]+  Exit 1                  sleep 10 && exit 1

КШ:

$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]+  Done(2)                  sleep 20 && exit 2
[2]+  Done(1)                  sleep 10 && exit 1

Этот вывод записывается в stderr, поэтому простое решение для примера OP может быть:

#!/bin/bash

trap "rm -f /tmp/x.$$" EXIT

for i in `seq 0 9`; do
  doCalculations $i &
done

wait 2> /tmp/x.$$
if [ `wc -l /tmp/x.$$` -gt 0 ] ; then
  exit 1
fi

Пока это:

wait 2> >(wc -l)

также возвращает счетчик, но без файла tmp. Это также может быть использовано следующим образом, например:

wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi)

Но это не намного полезнее, чем IMO-файл tmp. Я не смог найти полезного способа избежать файла tmp, а также избежать запуска «wait» в подоболочке, которая вообще не будет работать.

3 голосов
/ 13 февраля 2014

Я попробовал это и объединил все лучшие части из других примеров здесь. Этот сценарий будет выполнять функцию checkpids при выходе из любого фонового процесса и выводить состояние выхода без обращения к опросу.

#!/bin/bash

set -o monitor

sleep 2 &
sleep 4 && exit 1 &
sleep 6 &

pids=`jobs -p`

checkpids() {
    for pid in $pids; do
        if kill -0 $pid 2>/dev/null; then
            echo $pid is still alive.
        elif wait $pid; then
            echo $pid exited with zero exit status.
        else
            echo $pid exited with non-zero exit status.
        fi
    done
    echo
}

trap checkpids CHLD

wait
3 голосов
/ 13 февраля 2017

Это работает, должно быть так же хорошо, если не лучше, чем ответ @ HoverHell!

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function foo() {
     echo "CHLD exit code is $1"
     echo "CHLD pid is $2"
     echo $(jobs -l)

     for job in `jobs -p`; do
         echo "PID => ${job}"
         wait ${job} ||  echo "At least one test failed with exit code => $?" ; EXIT_CODE=1
     done
}

trap 'foo $? $$' CHLD

DIRN=$(dirname "$0");

commands=(
    "{ echo "foo" && exit 4; }"
    "{ echo "bar" && exit 3; }"
    "{ echo "baz" && exit 5; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

# wait for all to finish
wait;

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"

# end

и, конечно, я увековечил этот скрипт в проекте NPM, который позволяет вам параллельно запускать команды bash, полезные для тестирования:

https://github.com/ORESoftware/generic-subshell

3 голосов
/ 10 января 2014
#!/bin/bash
set -m
for i in `seq 0 9`; do
  doCalculations $i &
done
while fg; do true; done
  • set -m позволяет использовать fg & bg в скрипте
  • fg, в дополнение к выводу последнего процесса на передний план, имеет тот же статус выхода, что и процесс, на котором он передний план
  • while fg прекратит зацикливание, когда любой fg выйдет с ненулевым статусом выхода

к сожалению, это не поможет в случае, когда процесс в фоновом режиме завершает работу с ненулевым состоянием выхода. (цикл не завершится немедленно. Он будет ждать завершения предыдущих процессов.)

2 голосов
/ 24 июня 2016
set -e
fail () {
    touch .failure
}
expect () {
    wait
    if [ -f .failure ]; then
        rm -f .failure
        exit 1
    fi
}

sleep 2 || fail &
sleep 2 && false || fail &
sleep 2 || fail
expect

set -e вверху заставляет ваш скрипт останавливаться при ошибке.

expect вернет 1, если какое-либо задание не выполнено.

2 голосов
/ 27 августа 2015

ловушка твой друг. Вы можете поймать ERR во многих системах. Вы можете перехватить EXIT или DEBUG для выполнения фрагмента кода после каждой команды.

Это в дополнение ко всем стандартным сигналам.

2 голосов
/ 28 октября 2016

Здесь уже много ответов, но я удивлен, что никто, похоже, не предложил использовать массивы ... Итак, вот что я сделал - это может пригодиться некоторым в будущем.

n=10 # run 10 jobs
c=0
PIDS=()

while true

    my_function_or_command &
    PID=$!
    echo "Launched job as PID=$PID"
    PIDS+=($PID)

    (( c+=1 ))

    # required to prevent any exit due to error
    # caused by additional commands run which you
    # may add when modifying this example
    true

do

    if (( c < n ))
    then
        continue
    else
        break
    fi
done 


# collect launched jobs

for pid in "${PIDS[@]}"
do
    wait $pid || echo "failed job PID=$pid"
done
...