Tiempo de espera en un script de shell

53

Tengo un script de shell que lee de la entrada estándar . En circunstancias excepcionales, no habrá nadie listo para proporcionar información, y el script debe agotar el tiempo de espera . En caso de tiempo de espera, el script debe ejecutar algún código de limpieza. ¿Cuál es la mejor manera de hacer eso?

Este script debe ser muy portátil , incluidos los sistemas Unix del siglo XX sin un compilador de C y los dispositivos integrados que ejecutan busybox, por lo que no se puede confiar en Perl, bash, cualquier lenguaje compilado e incluso POSIX.2 completo. En particular, $PPID, read -ttrampas y perfectamente compatibles con POSIX no están disponibles. Escribir en un archivo temporal también está excluido; el script podría ejecutarse incluso si todos los sistemas de archivos están montados de solo lectura.

Solo para hacer las cosas más difíciles, también quiero que el script sea razonablemente rápido cuando no se agote el tiempo. En particular, también uso el script en Windows (principalmente en Cygwin), donde fork y exec son particularmente bajos, por lo que quiero mantener su uso al mínimo.

En pocas palabras, tengo

trap cleanup 1 2 3 15
foo=`cat`

y quiero agregar un tiempo de espera No puedo reemplazar catcon el readincorporado. En caso de tiempo de espera, quiero ejecutar la cleanupfunción.


Antecedentes: este script adivina la codificación del terminal imprimiendo algunos caracteres de 8 bits y comparando la posición del cursor antes y después. El comienzo de la secuencia de comandos prueba que stdout está conectado a un terminal compatible, pero a veces el entorno está mintiendo (por ejemplo, se plinkestablece TERM=xtermincluso si se llama conTERM=dumb ). La parte relevante del script se ve así:

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

¿Cómo puedo modificar el script para que si trno ha leído ninguna entrada después de 2 segundos, se elimina y el script ejecuta la cleanupfunción?

Gilles 'SO- deja de ser malvado'
fuente
2
Consulte también ¿Cómo introducir el tiempo de espera para las secuencias de comandos de shell? para soluciones menos portátiles
Gilles 'SO- deja de ser malvado'

Respuestas:

33

¿Qué hay de esto?

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

Es decir: ejecute el comando de producción de salida y sleepen el mismo grupo de procesos, un grupo de procesos solo para ellos. Cualquier comando que regrese primero mata a todo el grupo de procesos.

¿Alguien se preguntaría: Sí, la tubería no se utiliza; se omite usando las redirecciones. El único propósito es que el shell ejecute los dos procesos en el mismo grupo de procesos.


Como Gilles señaló en su comentario, esto no funcionará en un script de shell porque el proceso de script se eliminaría junto con los dos subprocesos.

Una forma de forzar que un comando se ejecute en un grupo de proceso separado es iniciar un nuevo shell interactivo:

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

Pero puede haber advertencias con esto (por ejemplo, ¿cuándo stdin no es un tty?). La redirección stderr está ahí para deshacerse del mensaje "Terminado" cuando se mata el shell interactivo.

Probado con zsh, bashy dash. ¿Pero qué hay de los viejos?

B98 sugiere el siguiente cambio, trabajando en Mac OS X, con GNU bash 3.2.57, o Linux con guión:

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

-
1. Aparte de lo setsidque parece no ser estándar.

Stéphane Gimenez
fuente
1
Esto me gusta bastante, no había pensado en usar un grupo de procesos de esa manera. Es bastante simple y no hay condición de carrera. Necesito probar un poco más la portabilidad, pero parece un ganador.
Gilles 'SO- deja de ser malvado'
Desafortunadamente, esto falla espectacularmente tan pronto como lo pongo en un script: la canalización en la sustitución de comandos no se ejecuta en su propio grupo de procesos, y kill 0termina matando al llamador del script también. ¿Hay alguna forma portátil de forzar la tubería a su propio grupo de procesos?
Gilles 'SO- deja de ser malvado'
@Gilles: ¡Eek! no pudo encontrar una manera de setprgp()sin setsidpor ahora :-(
Stéphane Giménez
1
Realmente me gusta el truco, así que estoy otorgando la recompensa. Parece funcionar en Linux, todavía no he tenido tiempo de probarlo en otros sistemas.
Gilles 'SO- deja de ser malvado'
2
@Zac: No es posible revertir el orden en el caso original, porque solo el primer proceso tiene acceso a stdin.
Stéphane Gimenez
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Ya está atrapando 15 (la versión numérica de SIGTERM, que es lo que killenvía a menos que se le indique lo contrario), por lo que ya debería estar listo. Dicho esto, si está buscando pre-POSIX, tenga en cuenta que las funciones de shell tampoco pueden existir (provienen del shell de System V).

geekosaur
fuente
No es tan simple. Si atrapa la señal, muchos shells (guión, bash, pdksh, zsh ... probablemente todos excepto ATT ksh) la ignoran mientras esperan catsalir. Ya he experimentado un poco, pero no he encontrado nada con lo que esté satisfecho hasta ahora.
Gilles 'SO- deja de ser malvado'
Sobre portabilidad: afortunadamente tengo funciones en todas partes. No estoy seguro $!, creo que algunas de esas máquinas que uso rara vez no tienen control de trabajo, ¿están $!disponibles universalmente?
Gilles 'SO- deja de ser malvado'
@Gilles: Hm, sí. Recuerdo algunas bashcosas realmente horribles y específicas que hice una vez que involucraron abuso -o monitor. Estaba pensando en términos de conchas verdaderamente antiguas cuando escribí eso (funcionó en v7). Dicho esto, creo que puede hacer cualquiera de las otras dos cosas: (1) fondo "lo que sea" y wait $!, o (2) también enviar SIGCLD/ SIGCHLD... pero en máquinas lo suficientemente viejas, esta última es inexistente o no portátil (la primera es Sistema III / V, este último BSD y V7 no tenían ninguno).
geekosaur
@Gilles: $!regresa al menos a V7, y ciertamente es anterior a algo shsimilar a lo que sabía algo sobre el control del trabajo (de hecho, durante mucho tiempo /bin/shen BSD no hizo el control del trabajo; había que correr cshpara obtenerlo, pero $!estaba allí )
geekosaur
No puedo poner en segundo plano "lo que sea", necesito su salida. Gracias por la información sobre $!.
Gilles 'SO- deja de ser malvado'
4

Aunque coretuils a partir de la versión 7.0 incluye un comando de tiempo de espera, ha mencionado algunos entornos que no lo tendrán. Afortunadamente, pixelbeat.org tiene un script de tiempo de espera escrito sh.

Lo he usado antes en varias ocasiones y funciona muy bien.

http://www.pixelbeat.org/scripts/timeout ( Nota: la secuencia de comandos a continuación se ha modificado ligeramente de la de pixelbeat.org, consulte los comentarios debajo de esta respuesta).

#!/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
fuente
Parece que esto no permite recuperar la salida del comando. Vea el comentario "No puedo" "lo que sea" de Gilles en la otra respuesta.
Stéphane Gimenez
Ah interesante. Modifiqué el script (para que no coincida con el de pixelbeat) para redirigir / dev / stdin al comando. Parece funcionar en mis pruebas.
bahamat
Esto no funciona para que el comando se lea desde la entrada estándar, excepto (curiosamente) en bash. </dev/stdines un no-op. </dev/ttyle permitiría leer desde el terminal, lo cual es lo suficientemente bueno para mi caso de uso.
Gilles 'SO- deja de ser malvado'
@Giles: genial, haré esa actualización.
bahamat
Esto no puede funcionar sin un esfuerzo considerablemente mayor: necesito recuperar la salida del comando, y no puedo hacerlo si el comando está en segundo plano.
Gilles 'SO- deja de ser malvado'
3

¿Qué pasa con (ab) usar NC para esto

Me gusta;

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

O enrollado en una sola línea de comando;

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

La última versión, aunque parece extraña realmente funciona cuando la pruebo en un sistema Linux: mi mejor suposición es que debería funcionar en cualquier sistema, y ​​si no, una variación de la redirección de salida puede resolver la portabilidad. El beneficio aquí es que no hay procesos en segundo plano involucrados.

Soren
fuente
No es lo suficientemente portátil. No tengo ncen algunos viejos Unix boxen, ni en muchos Linux embebidos.
Gilles 'SO- deja de ser malvado'
0

Otra forma de ejecutar la canalización en su propio grupo de procesos es ejecutar sh -c '....'en un pseudo-terminal utilizando el scriptcomando (que aplica implícitamente la setsidfunción).

#!/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
fuente
No es lo suficientemente portátil. No tengo scripten algunos viejos Unix boxen, ni en muchos Linux embebidos.
Gilles 'SO- deja de ser malvado'
0

La respuesta en https://unix.stackexchange.com/a/18711 es muy buena.

Quería lograr un resultado similar, pero sin tener que invocar explícitamente el shell nuevamente porque quería invocar las funciones existentes del shell.

Usando bash, es posible hacer lo siguiente:

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

Supongamos que ya tengo una función de shell f:

f() { date ; kill 0 ; }

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

Ejecutando esto veo:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
Conde
fuente
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
Cory Schwartz
fuente
Esto no funciona por la misma razón que la respuesta de geekausor . Necesito obtener la salida de un comando externo, y la señal se ignora mientras se ejecuta este comando.
Gilles 'SO- deja de ser malvado'