Tempo limite excedido em um script de shell

53

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

Esse script deve ser muito portátil , incluindo sistemas unix do século XX sem um compilador C e dispositivos incorporados executando o busybox, para que Perl, bash, qualquer linguagem compilada e até mesmo o POSIX.2 completo não sejam confiáveis. Em particular, $PPID, read -tarmadilhas e perfeitamente compatíveis com POSIX não estão disponíveis. A gravação em um arquivo temporário também é excluída; o script pode ser executado mesmo que todos os sistemas de arquivos sejam montados somente leitura.

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

Em poucas palavras, eu tenho

trap cleanup 1 2 3 15
foo=`cat`

e quero adicionar um tempo limite. Não posso substituir catpelo readincorporado. Em caso de tempo limite, desejo executar a cleanupfunção.


Antecedentes: esse script está adivinhando 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, plinkdefine TERM=xtermmesmo que seja chamado comTERM=dumb ). A parte relevante do script fica 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 trnão tiver lido nenhuma entrada após 2 segundos, ele seja morto e o script execute a cleanupfunção?

Gilles 'SO- parar de ser mau'
fonte
2
Consulte também Como introduzir o tempo limite para scripts de shell? para soluções menos portáteis
Gilles 'SO- stop be evil'

Respostas:

33

Que tal 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 sleepno 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 cachimbo não é usado; é ignorado usando os redirecionamentos. O único objetivo disso é 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 script de shell, porque o processo de script seria eliminado junto com os dois subprocessos.

Uma maneira de forçar a execução de um comando 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 existe para se livrar da mensagem "Terminated" quando o shell interativo é morto.

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

O B98 sugere a seguinte alteração, trabalhando no Mac OS X, com GNU bash 3.2.57 ou Linux com dash:

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

-
1. diferente do setsidque parece não ser padrão.

Stéphane Gimenez
fonte
11
Eu gosto disso, não tinha pensado em usar um grupo de processos dessa maneira. É bem simples e não há condição de corrida. Preciso testar mais a portabilidade, mas isso parece um vencedor.
Gilles 'SO- stop be evil'
Infelizmente, isso falha espetacularmente assim que eu coloco isso em um script: o pipeline na substituição de comando não é executado em seu próprio grupo de processos e kill 0acaba matando também o chamador do script. Existe uma maneira portátil de forçar o pipeline para seu próprio grupo de processos?
Gilles 'SO- stop be evil' (
@Gilles: Eek! não poderia encontrar uma maneira de setprgp()sem setsidpor agora :-(
Stéphane Gimenez
11
Eu realmente gosto do truque, então estou premiando a recompensa. Parece funcionar no Linux, ainda não tive tempo de testá-lo em outros sistemas.
Gilles 'SO- stop be evil'
2
@ Zac: Não é possível reverter a ordem no caso original, porque apenas o primeiro processo obtém acesso ao stdin.
Stéphane Gimenez
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Você já está capturando 15 (a versão numérica de SIGTERM, que é o que killenvia, a menos que seja dito o contrário), então você já deve estar pronto. Dito isto, se você estiver visualizando o pré-POSIX, saiba que as funções do shell também podem não existir (elas vieram do shell do System V).

geekosaur
fonte
Não é tão simples assim. Se você prender o sinal, muitos shells (dash, bash, pdksh, zsh ... provavelmente todos, exceto ATT ksh) o ignoram enquanto aguardam cata saída. Eu já experimentei um pouco, mas não encontrei nada com o qual estou satisfeito até agora.
Gilles 'SO- stop be evil'
Sobre portabilidade: felizmente, tenho funções em todos os lugares. Não tenho certeza $!, acho que algumas dessas máquinas que uso raramente não têm controle de trabalho, está $!universalmente disponível?
Gilles 'SO- stop be evil'
@Gilles: Hum, sim. Lembro-me de coisas realmente hediondas e bashespecíficas das quais fiz uma vez envolvendo abuso -o monitor. Eu estava pensando em termos de conchas verdadeiramente antigas quando escrevi isso (funcionou na v7). Dito isso, acho que você pode fazer uma de duas outras coisas: (1) o histórico "seja qual for" e wait $!, ou (2) também envia SIGCLD/ SIGCHLD... mas em máquinas suficientemente antigas, a última é inexistente ou não é portável (a primeira é System III / V, o último BSD e V7 não tinham).
Geekosaur
@Gilles: $!volta à V7, pelo menos, e certamente é anterior a shalgo que sabia alguma coisa sobre controle de tarefas (na verdade, por um longo tempo /bin/shno BSD não fazia controle de tarefas; você tinha que correr cshpara obtê-la - mas $!estava lá )
Geekosaur
Não posso ter um plano de fundo "seja qual for", preciso da sua saída. Obrigado pela informação sobre $!.
Gilles 'SO- stop be evil'
4

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

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

http://www.pixelbeat.org/scripts/timeout ( 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
bahamat
fonte
Parece que isso não permite recuperar a saída do comando. Veja "Não posso comentar o que quer que seja", comentado por Gilles na outra resposta.
Stéphane Gimenez
Ah, interessante. Modifiquei o script (para que ele não corresponda mais ao do pixelbeat) para redirecionar / dev / stdin para o comando Parece funcionar nos meus testes.
bahamat
Isso não funciona para que o comando seja lido da entrada padrão, exceto (estranhamente) no bash. </dev/stdiné um não-op. </dev/ttypermitiria a leitura do terminal, o que é bom o suficiente para o meu caso de uso.
Gilles 'SO- stop be evil'
@ Giles: legal, eu vou fazer essa atualização.
bahamat
Isso não pode funcionar sem muito mais esforço: preciso recuperar a saída do comando e não posso fazer isso se o comando estiver em segundo plano.
Gilles 'SO- stop be evil' (
3

Que tal (ab) usar NC para este

Gostar;

   $ 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 agrupados em uma única linha de comando;

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

A última versão, embora pareça estranha, na verdade funciona quando eu a testo em um sistema Linux - o meu melhor palpite é que ele funcione em qualquer sistema e, caso contrário, uma variação do redirecionamento de saída pode resolver a portabilidade. O benefício aqui é que nenhum processo em segundo plano está envolvido.

Soren
fonte
Não é portátil o suficiente. Eu não tenho ncem algumas antigas caixas Unix, nem em muitos Linux embarcados.
Gilles 'SO- stop be evil'
0

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

#!/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"
pete
fonte
Não é portátil o suficiente. Eu não tenho scriptem algumas antigas caixas Unix, nem em muitos Linux embarcados.
Gilles 'SO- stop be evil'
0

A resposta em https://unix.stackexchange.com/a/18711 é muito boa.

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

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

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

Então, suponha que eu já tenha uma função 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
Conde
fonte
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
Cory Schwartz
fonte
Isso não funciona pelo mesmo motivo que a resposta de um nerd . Preciso obter a saída de um comando externo, e o sinal é ignorado enquanto este comando está sendo executado.
Gilles 'SO- stop be evil'