Bash: wait with timeout
I.e., launch two applications in the background, and give them 60 seconds to complete their work. Then, if they don’t finish within that interval, kill them. Unfortunately, the above does not work, since timeout is an executable, while wait is a shell command. I tried changing it to:
timeout 60 bash -c wait $pidApp1 $pidApp2
But this still does not work, since wait can only be called on a PID launched within the same shell. Any ideas?
«60» has to be a maximum upper execution time. The actual runtime of the applications might be a lot lower. So no, it would be to inefficient for me.
If these programs really require you to use kill -9 , they are broken. See also iki.fi/era/unix/award.html#kill
The bash wait doesn’t support timeout because it’s implemented with syscall wait() or waitpid() depending if you pass a PID to it or not. Neither of those supports timeout natively. It might be possible to use signal handlers and alarm to get rid of the wait without actually waiting for the children, but I haven’t tested if it would actually work.
10 Answers 10
Both your example and the accepted answer are overly complicated, why do you not only use timeout since that is exactly its use case? The timeout command even has an inbuilt option ( -k ) to send SIGKILL after sending the initial signal to terminate the command ( SIGTERM by default) if the command is still running after sending the initial signal (see man timeout ).
If the script doesn’t necessarily require to wait and resume control flow after waiting it’s simply a matter of
timeout -k 60s 60s app1 & timeout -k 60s 60s app2 & # [. ]
If it does, however, that’s just as easy by saving the timeout PIDs instead:
pids=() timeout -k 60s 60s app1 & pids+=($!) timeout -k 60s 60s app2 & pids+=($!) wait "$" # [. ]
$ cat t.sh #!/bin/bash echo "$(date +%H:%M:%S): start" pids=() timeout 10 bash -c 'sleep 5; echo "$(date +%H:%M:%S): job 1 terminated successfully"' & pids+=($!) timeout 2 bash -c 'sleep 5; echo "$(date +%H:%M:%S): job 2 terminated successfully"' & pids+=($!) wait "$" echo "$(date +%H:%M:%S): done waiting. both jobs terminated on their own or via timeout; resuming script"
$ ./t.sh 08:59:42: start 08:59:47: job 1 terminated successfully 08:59:47: done waiting. both jobs terminated on their own or via timeout; resuming script
According to gnu.org/software/coreutils/manual/html_node/…, the -k should go before 60s and in addition, you must specify a timeout for -k . So, for example, the first code example should be timeout -k 60s 60s app1 & .
@AnneTheAgile This was a Linux-specific question so that point is moot, especially since the question is built around timeout to begin with. However, it’s easy to install GNU timeout on OSX/MacOS via e.g. Homebrew.
«why do you not only use timeout since that is exactly its use case?» because they don’t know how long a forked job should take, only that it should exit within N seconds after a certain point
Write the PIDs to files and start the apps like this:
pidFile=. ( app ; rm $pidFile ; ) & pid=$! echo $pid > $pidFile ( sleep 60 ; if [[ -e $pidFile ]]; then killChildrenOf $pid ; fi ; ) & killerPid=$! wait $pid kill $killerPid
That would create another process that sleeps for the timeout and kills the process if it hasn’t completed so far.
If the process completes faster, the PID file is deleted and the killer process is terminated.
killChildrenOf is a script that fetches all processes and kills all children of a certain PID. See the answers of this question for different ways to implement this functionality: Best way to kill all child processes
If you want to step outside of BASH, you could write PIDs and timeouts into a directory and watch that directory. Every minute or so, read the entries and check which processes are still around and whether they have timed out.
EDIT If you want to know whether the process has died successfully, you can use kill -0 $pid
EDIT2 Or you can try process groups. kevinarpe said: To get PGID for a PID(146322):
ps -fjww -p 146322 | tail -n 1 | awk '< print $4 >'
In my case: 145974. Then PGID can be used with a special option of kill to terminate all processes in a group: kill — -145974
This does not work. wait requires a pid to be a child of the current shell. I get the following error: «wait.sh: line 2: wait: pid 22603 is not a child of this shell».
I just noticed one problem with my approach. : Did you consider a technique to use process group? Here is one message method to get PGID for a PID(146322): ps -fjww -p 146322 | tail -n 1 | awk ‘< print $4 >‘ . (In my case: Outputs 145974 ) Then PGID can be used with a special mode of kill to terminate all processes in a group: kill — -145974
A similar solution is suggested right in the question stackoverflow.com/q/50975596/94687 . (And I find your idea quite elegant and clever; I couldn’t come up with this myself.)
Here’s a simplified version of Aaron Digulla’s answer, which uses the kill -0 trick that Aaron Digulla leaves in a comment:
app & pidApp=$! ( sleep 60 ; echo 'timeout'; kill $pidApp ) & killerPid=$! wait $pidApp kill -0 $killerPid && kill $killerPid
In my case, I wanted to be both set -e -x safe and return the status code, so I used:
set -e -x app & pidApp=$! ( sleep 45 ; echo 'timeout'; kill $pidApp ) & killerPid=$! wait $pidApp status=$? (kill -0 $killerPid && kill $killerPid) || true exit $status
An exit status of 143 indicates SIGTERM, almost certainly from our timeout.
A similar solution is suggested right in the question stackoverflow.com/q/50975596/94687 . (And I find this kind of solutions to the problem quite elegant and clever; I couldn’t come up with this myself.)
I wrote a bash function that will wait until PIDs finished or until timeout, that return non zero if timeout exceeded and print all the PIDs not finisheds.
function wait_timeout < local limit=$local pids=$ local count=0 while true do local have_to_wait=false for pid in $; do if kill -0 $ &>/dev/null; then have_to_wait=true else pids=`echo $ | sed -e "s/$//g"` fi done if $ && (( $count < $limit )); then count=$(( count + 1 )) sleep 1 else echo $return 1 fi done return 0 >
To use this is just wait_timeout $timeout $PID1 $PID2 .
Note that the sed will remove wrong PID numbers if you’re unlucky enough to have e.g. $pid value 123 and the list of all pids contains PID 1231. You end up with modified list that is going to wait for PID 1 which is obviously not going to go away.
To put in my 2c, we can boild down Teixeira’s solution to:
Bash’s sleep accepts fractional seconds, and 0.001s = 1 ms = 1 KHz = plenty of time. However, UNIX has no loopholes when it comes to files and processes. try_wait accomplishes very little.
$ cat & [1] 16574 $ try_wait %1 && echo 'exited' || echo 'timeout' timeout $ kill %1 $ try_wait %1 && echo 'exited' || echo 'timeout' exited
We have to answer some hard questions to get further.
Why has wait no timeout parameter? Maybe because the timeout , kill -0 , wait and wait -n commands can tell the machine more precisely what we want.
Why is wait builtin to Bash in the first place, so that timeout wait PID is not working? Maybe only so Bash can implement proper signal handling.
$ timeout 30s cat & [1] 6680 $ jobs [1]+ Running timeout 30s cat & $ kill -0 %1 && echo 'running' running $ # now meditate a bit and then. $ kill -0 %1 && echo 'running' || echo 'vanished' bash: kill: (NNN) - No such process vanished
Whether in the material world or in machines, as we require some ground on which to run, we require some ground on which to wait too.
- When kill fails you hardly know why. Unless you wrote the process, or its manual names the circumstances, there is no way to determine a reasonable timeout value.
- When you have written the process, you can implement a proper TERM handler or even respond to «Auf Wiedersehen!» send to it through a named pipe. Then you have some ground even for a spell like try_wait 🙂
You could use the timeout of the ‘read’ internal command.
The following will kill unterminated jobs and display the names of the completed jobs after at most 60 seconds:
( (job1; echo -n "job1 ")& (job2; echo -n "job2 ")&) | (read -t 60 -a jobarr; echo $ $ )
It works by making a sub shell containing all the background jobs. The output of this sub shell is read into a bash array variable, which can be used as desired (in this example by printing the array + element count).
Be sure to reference the $ in the same sub shell as the read command (hence the parenthesis), otherwise $ will be empty.
All sub shells will automatically be muted (not killed) after the read command terminates. You have to kill them you self.
app1 & app2 & sleep 60 & wait -n
Yet another timeout bash’s script
Running many subprocess with an overall timeout. Using recent bash features, I wrote this:
#!/bin/bash maxTime=5.0 jobs=() pids=() cnt=1 Started=$ if [[ $1 == -m ]] ;then maxTime=$2; shift 2; fi for cmd ;do # $cmd is unquoted in order to use strings as command + args $cmd & jobs[$!]=$cnt pids[cnt++]=$! done printf -v endTime %.6f $maxTime endTime=$(( Started + 10#$ )) exec <> )) && (( $ < endTime ));do for cnt in $;do if ! jobs $cnt &>/dev/null;then Elap=00000$(( $ - Started )) printf 'Job %d (%d) ended after %.4f secs.\n' \ $cnt $ $.$ unset jobs[$] pids[cnt] fi done read -ru $pio -t .02 _ done if (($)) ;then Elap=00000$(( $ - Started )) for cnt in $;do printf 'Job %d (%d) killed after %.4f secs.\n' \ $cnt $ $.$ done kill $ fi
- Commands with argument could be submited as strings
- -m switch let you choose a float as max time in seconds.
$ ./execTimeout.sh -m 2.3 "sleep 1" 'sleep 2' sleep\ 'cat /dev/tty' Job 1 (460668) ended after 1.0223 secs. Job 2 (460669) ended after 2.0424 secs. Job 3 (460670) killed after 2.3100 secs. Job 4 (460671) killed after 2.3100 secs. Job 5 (460672) killed after 2.3100 secs.
For testing this, I wrote this script that
- choose random duratiopn between 1.0000 and 9.9999 seconds
- for output random number of line between 0 and 8 . (they could not ouptut anything).
- lines output contain process id ( $$ ), number of line left to print and total duration in seconds.
#!/bin/bash tslp=$RANDOM lnes=$ printf -v tslp %.6f $.$ slp=00$(($/($lnes?$lnes:1))) printf -v slp %.6f $.$ # echo >&2 Slp $lnes x $slp == $tslp exec <> <(: -O) while read -rt $slp -u $dummy; ((--lnes>0)); do echo $$ $lnes $tslp done
Running this script 5 times in once, with a timeout of 5.0 seconds:
$ ./execTimeout.sh -m 5.0 ./tstscript.sh 2869814 6 2.416700 2869815 5 3.645000 2869814 5 2.416700 2869814 4 2.416700 2869815 4 3.645000 2869814 3 2.416700 2869813 5 8.414000 2869812 1 3.408000 2869814 2 2.416700 2869815 3 3.645000 2869814 1 2.416700 2869815 2 3.645000 Job 3 (2869814) ended after 2.4511 secs. 2869813 4 8.414000 2869815 1 3.645000 Job 1 (2869812) ended after 3.4518 secs. Job 4 (2869815) ended after 3.6757 secs. 2869813 3 8.414000 Job 2 (2869813) killed after 5.0159 secs. Job 5 (2869816) killed after 5.0159 secs.
Команда sleep в bash: делаем задержки в скриптах
При написании shell-скрипта может возникнуть необходимость создать в нем паузу в несколько секунд перед выполнением очередного шага. Например, чтобы скрипт «подождал», пока завершится какой-то процесс, или сделал паузу перед повторной попыткой выполнить неудавшуюся команду.
Для этого существует очень простая команда sleep .
Как используется команда sleep в bash
Sleep — универсальная команда с простым синтаксисом. Все, что нужно сделать, это набрать sleep N . Это поставит ваш скрипт на паузу на N секунд. Секунды можно указывать в целых положительных числах или в числах с плавающей запятой.
Рассмотрим базовый пример:
echo "Hello there!" sleep 2 echo "Oops! I fell asleep for a couple seconds!"
Результат работы этого скрипта выглядит так:
Аналогично можно использовать число с плавающей запятой: это позволит указать доли секунды. Например, sleep .8 приостановит работу скрипта на 0,8 с.
Вот и все, что можно сказать о работе команды sleep на базовом уровне!
Что нужно иметь в виду, используя команду sleep
По умолчанию время для sleep указывается в секундах, поэтому в примере мы не указывали единицы измерения времени.
На некоторых типах машин (конкретно — BSD и MacOS) время вообще указывается исключительно в секундах. В других Unix-подобных операционных системах скорее всего будут поддерживаться и другие единицы времени:
С командой sleep также можно использовать больше одного аргумента. Если вы укажете два или больше чисел, задержка будет соответствовать их сумме.
Например, указав sleep 2m 30s , вы создадите паузу на 2,5 минуты. Имейте в виду, что в MacOS или BSD для такого результата нужно написать sleep 150 , поскольку в этих ОС время указывается только в секундах, а 2,5 мин = 150 с.
Итоги
Команда sleep — удобный способ добавить паузу в ваш bash-скрипт. В сочетании с другими командами sleep может помочь запускать операции в правильном порядке, делать паузы между попытками соединения с сайтами и т. п. В общем, этот инструмент точно стоит добавить в свой набор!