Tempo limite em um script de shell

54

Eu tenho um script de shell que é lido a partir da entrada padrão . Em raras circunstâncias, não haverá ninguém pronto para fornecer informações e o script deve expirar o tempo limite . Em caso de tempo limite, o script deve executar algum código de limpeza. Qual é a melhor maneira de fazer isso?

Este script deve ser muito portátil , incluindo sistemas unix do século 20 sem um compilador C e para dispositivos embarcados executando o busybox, assim, Perl, bash, qualquer linguagem compilada e até o POSIX2 completo. não pode ser invocado. Em particular, $PPID , read -t e perfeitamente as armadilhas compatíveis com POSIX não estão disponíveis. Escrever em um arquivo temporário também é excluído; o script pode ser executado mesmo se todos os sistemas de arquivos forem montados somente leitura.

Só para tornar as coisas mais difíceis, também quero que o script seja razoavelmente rápido quando não expirar. Em particular, eu também uso o script no Windows (principalmente no Cygwin), onde fork e exec são particularmente baixos, então eu quero manter seu uso ao mínimo.

Em suma, eu tenho

trap cleanup 1 2 3 15
foo='cat'

e quero adicionar um tempo limite. Não consigo substituir cat pelo read integrado. Em caso de tempo limite, quero executar a função cleanup .

Segundo plano: este script adivinha a codificação do terminal, imprimindo alguns caracteres de 8 bits e comparando a posição do cursor antes e depois. O início do script testa que stdout está conectado a um terminal suportado, mas às vezes o ambiente está mentindo (por exemplo, plink define TERM=xterm , mesmo que seja chamado com TERM=dumb . A parte relevante do script é assim:

text='Éé'  # UTF-8; shows up as Ãé on a latin1 terminal
csi='␛['; dsr_cpr="${csi}6n"; dsr_ok="${csi}5n"  # ␛ is an escape character
stty_save='stty -g'
cleanup () { stty "$stty_save"; }
trap 'cleanup; exit 120' 0 1 2 3 15     # cleanup code
stty eol 0 eof n -echo                # Input will end with '0n'
# echo-n is a function that outputs its argument without a newline
echo-n "$dsr_cpr$dsr_ok"              # Ask the terminal to report the cursor position
initial_report='tr -dc \;0123456789'  # Expect ␛[42;10R␛[0n for y=42,x=10
echo-n "$text$dsr_cpr$dsr_ok"
final_report='tr -dc \;0123456789'
cleanup
# Compute and return initial_x - final_x

Como posso modificar o script para que, se tr não ler nenhuma entrada depois de 2 segundos, ele seja eliminado e o script execute a função cleanup ?

    
por Gilles 06.04.2011 / 00:43

7 respostas

34

E sobre isso:

foo='{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1'

Ou seja: execute o comando de produção de saída e sleep no mesmo grupo de processos, um grupo de processos apenas para eles. Qualquer comando que retorne primeiro mata todo o grupo de processos.

Alguém se perguntaria: Sim, o tubo não é usado; é ignorado usando os redirecionamentos. O único propósito é fazer com que o shell execute os dois processos no mesmo grupo de processos.

Como Gilles apontou em seu comentário, isso não funcionará em um shell script porque o processo do script seria morto junto com os dois subprocessos.

Uma forma de forçar um comando a ser executado em um grupo de processos separado é iniciar um novo shell interativo:

#!/bin/sh
foo='sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null'
[ -n "$foo" ] && echo got: "$foo" || echo timeouted

Mas pode haver ressalvas com isso (por exemplo, quando stdin não é um tty?). O redirecionamento stderr está lá para se livrar da mensagem "Terminado" quando o shell interativo é eliminado.

Testado com zsh , bash e dash . Mas e os velhos?

B98 sugere a seguinte mudança, trabalhando no Mac OS X, com o GNU bash 3.2.57, ou Linux com traço:

foo='sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }''

-
1. diferente de setsid , que parece não ser padrão.

    
por 14.08.2011 / 05:17
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Você já está trapaceando 15 (a versão numérica de SIGTERM , que é o que kill envia, a menos que seja dito o contrário), então você já deve estar pronto. Dito isto, se você está olhando para o pré-POSIX, esteja ciente de que as funções do shell podem não existir (elas vieram do shell do System V).

    
por 06.04.2011 / 00:54
3

Embora os coretuils da versão 7.0 incluam um comando de timeout, você mencionou alguns ambientes que não o possuem. Felizmente, o pixelbeat.org tem um script de tempo limite escrito sh .

Eu usei isso antes em várias ocasiões e funciona muito bem.

link ( Observação: o script abaixo foi ligeiramente modificado em relação ao pixelbeat .org, veja os comentários abaixo desta resposta.)

#!/bin/sh

# Execute a command with a timeout

# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <[email protected]>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <[email protected]>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <[email protected]>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   'basename $0' timeout_in_seconds command" >&2
    echo "Example: 'basename $0' 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value
    
por 14.08.2011 / 19:50
3

Que tal (ab) usar NC para isso

Como;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

Ou enrolado em uma única linha de comando;

   $ foo='nc -l 0 2222 | nc -w 5 0 2222'

A última versão, apesar de parecer stange, realmente funciona quando eu a testo em um sistema linux - meu melhor palpite é que ela funcione em qualquer sistema, e se não houver uma variação do redirecionamento de saída pode resolver a portabilidade. O benefício aqui é que não há processos em segundo plano.

    
por 16.08.2011 / 21:35
0

Outra maneira de executar o pipeline em seu próprio grupo de processos é executar sh -c '....' em um pseudo-terminal usando o comando script (que implicitamente aplica a função setsid ).

#!/bin/sh
stty -echo -onlcr
# GNU script
foo='script -q -c 'sh -c "{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }" 3>&1 2>/dev/null' /dev/null'
# FreeBSD script
#foo='script -q /dev/null sh -c '{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }' 3>&1 2>/dev/null'
stty echo onlcr
echo "foo: $foo"


# alternative without: stty -echo -onlcr
# cr='printf '\r''
# foo='script -q -c 'sh -c "{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null"' /dev/null | sed -e "s/${cr}$//" -ne 'p;N''  # GNU
# foo='script -q /dev/null sh -c '{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null' | sed -e "s/${cr}$//" -ne 'p;N''  # FreeBSD
# echo "foo: $foo"
    
por 18.02.2014 / 18:47
0

A resposta no link é muito boa.

Eu queria alcançar um resultado semelhante, mas sem ter que invocar explicitamente o shell novamente porque queria invocar as funções de shell existentes.

Usando o bash, é possível fazer o seguinte:

eval 'set -m ; ( ... ) ; '"$(set +o)"

Então, suponha que eu já tenha uma função de shell f :

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

Executando isso, vejo:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
    
por 15.03.2016 / 01:27
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
    
por 14.12.2016 / 20:35