Otro Heisenbug más allá del cocodrilo

Otro Heisenbug más allá del cocodrilo

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли

es fortune programa condicional sin exit(rand()).

¿Puedes explicar? ¿Qué pasa aquí??

Digresión lírico-histórica

Conocí a este Heisenbug por primera vez hace un cuarto de siglo. Luego, para la puerta de enlace en FaxNET, fue necesario crear varias utilidades a través de tubería "jugar a las damas" en FreeBSD. Como era de esperar, me consideraba un programador avanzado y con bastante experiencia. Por lo tanto, tenía la intención de hacer todo con el mayor cuidado y cuidado posible, prestando especial atención al manejo de errores...

Mi experiencia previa en el manejo de errores en sendmail y uucp/uupc se sumó a mi diligencia en el “manejo minucioso de errores”. No tiene sentido profundizar en los detalles de esa historia, pero luché con este Heisenbug durante dos semanas durante 10 a 14 horas. Por eso, se recordó, y ayer este viejo conocido pasó a visitarnos nuevamente.

TL;DR Respuesta

Utilidad head lata cerrar el canal de fortune de una vez tan pronto como lea la primera línea. Si fortune genera más de una línea, luego la llamada correspondiente write() devolverá un error o informará que se generan menos bytes de los solicitados. A su vez, escrito con cuidadoso manejo de errores. fortune tiene derecho a reflejar esta situación en su situación de salida. Luego debido a la instalación set -o pipefail trabajará || echo "Вы проиграли".

Sin embargo, head puede que no llegue a tiempo cerrar antes fortune terminará de generar datos. Entonces funcionará && echo "Повезло!".

En uno de mis hoy GNUMakefile hay tal un fragmento:

echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'

Traducido al humano

Es común aquí para Marca GNU и golpear en el modo compilador usando la opción --version pregunta quién es y, si la opción no es compatible, se inserta un código auxiliar "Utilice un compilador compatible con GCC o CLANG".

como repetitivo se puede encontrar en cualquier lugar. Apareció en este lugar hace mucho tiempo y funcionó perfectamente en todas partes (Linux, Solaris, OSX, FreeBSD, WSL etc.). Pero ayer en AltLinux en la plataforma Elbrús 2000 (E2K) Me di cuenta de:

#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"

Francamente, no reconocí de inmediato a mi antiguo "conocido". Además, el proyecto ya ha sido probado muchas veces en Elbrus y en muchas distribuciones diferentes, incluida Alt. Con varios compiladores, versiones de GNU Make y bash. Por lo tanto, no quería ver mi error aquí.

Al intentar reproducir el problema y/o entender lo que estaba pasando, empezaron a suceder cosas más extrañas.
Hechizo de línea de comando:

echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"

De vez en cuando producía texto adicional, luego no... A menudo, una de las opciones se mantenía durante bastante tiempo, pero si presionabas más, ¡siempre obtenías ambas!

Por supuesto, strace nuestro todo! Y después de escribir una diatriba sobre strace, pero al no tener tiempo de presionar Enter, reconocí a mi viejo amigo, el Sr. Heisenbug, y a los desarrolladores. compilador yo hace 25 años, Nostalgia… Y decidí ponerme triste y escribir esta nota 😉

Por cierto, como cualquier persona que se precie. Heisenbugdebajo strace Prefiere no reproducirse.

Entonces, ¿qué está pasando?

  • Utilidad head tiene el derecho (o mejor dicho, está incluso obligado) a cerrar el canal que se está leyendo tan pronto como lee el número de líneas solicitado.
  • El programa-escritor generador de datos (en este caso cc) lata imprimir varias líneas y gratis haz esto a través de múltiples llamadas write().
  • si el lector tendrá tiempo de cerrar el canal de su lado antes de que finalice la grabación del lado del escritor, luego el escritor recibirá un error.
  • programa de escritor tener el derecho ambos ignoran el error de escritura del canal y lo reflejan en su código de finalización.
  • Debido a la instalación set -o pipefail el código de finalización de la tubería será distinto de cero (erróneo) si el resultado es distinto de cero de al menos un elemento, y entonces funcionará || echo "Please use GCC or CLANG compatible compiler".

Puede haber variaciones dependiendo de cómo trabaja el programa escritor con las señales. Por ejemplo, el programa puede terminar de manera anormal (con generación automática de un estado de terminación distinto de cero/error), o write() devolverá el resultado de escribir menos bytes de los solicitados y establecerá errno = EPIPE.

¿Quién tiene la culpa?

En el caso descrito un poco de todo. Manejo de errores en cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) no es redundante. En muchos casos es mejor pecar de cauteloso, aunque esto expone fallas repentinas en un texto estándar diseñado para el comportamiento tradicional.

¿Qué hacer?

Incorrecto:

fortune | head -1 && echo "Повезло, но вы рискуете!" || echo "WTF?"

Correcto:

  1. (fortune && echo "Успешно" || echo "Ошибка") | head -1

    Aquí, el intérprete de comandos se encargará del cierre anticipado de una tubería cuando se dé servicio a la tubería anidada ("dentro" de los paréntesis). En consecuencia, si fortune informará un error al escribir a un canal cerrado en el estado, luego la salida || echo "Ошибка" No llegará a ninguna parte porque el canal ya está cerrado.

  2. fortune | cat - | head -1 && echo "Успешно" || echo "Ошибка"

    Aquí está la utilidad cat Actúa como un amortiguador porque ignora el error. EPIPE al retirarse. Esto es suficiente por ahora conclusión. fortune pequeño (varias líneas) y cabe en el búfer del canal (de 512 bytes a ≈64K, en la mayoría de los sistemas operativos ≥4K). De lo contrario, el problema puede volver.

Cómo manejar EPIPE y otros errores de grabación?

No existe una única solución adecuada, pero sí recomendaciones sencillas:

  • EPIPE necesario debe ser procesado (y reflejado en el estado de salida) al generar datos que requieren integridad. Por ejemplo, durante el funcionamiento de archivadores o utilidades de copia de seguridad.
  • EPIPE mejor ignorar al mostrar información y mensajes auxiliares. Por ejemplo, al mostrar información sobre opciones --help o --version.
  • Si el código que se está desarrollando se puede utilizar en una canalización antes | headentonces EPIPE Es mejor ignorarlo; de lo contrario, es mejor procesarlo y reflejarlo en el estado de salida.

Me gustaría aprovechar esta oportunidad para expresar mi agradecimiento a los equipos. MCST и AltLinux por un gran trabajo productivo. ¡Tu determinación es asombrosa!
Sigan así Camaradas, arriba reuniones en el otoño!

Gracias boina para corregir errores tipográficos y tipográficos.
KDPV de Georgy A.

Fuente: habr.com

Añadir un comentario