tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! Un `stderr` roto no mata a la app.
2//!
3//! La regresión es del 2026-09-06 y costó una instancia de `nahual`: al
4//! teselar la ventana contra otra, `view()` pasó el umbral del vigía, el vigía
5//! lo denunció con `eprintln!`, el `stderr` de esa instancia era una pipe sin
6//! lector, `eprintln!` paniqueó por `EPIPE`, y el unwind arrastró la `Surface`
7//! con el `SurfaceTexture` del cuadro en vuelo — `panic in a destructor during
8//! cleanup`, **abort**. Tres trazas encadenadas en `panic.log` para lo que
9//! empezó como una línea de diagnóstico.
10//!
11//! El test vive en su propio binario a propósito: enchufa y desenchufa el
12//! descriptor 2 del proceso, que es estado global y no se comparte con otros
13//! tests.
14#![cfg(unix)]
15
16/// Con el descriptor 2 puesto en una pipe sin lector: `avisar!` no paniquea, y
17/// después de él **nadie** en el proceso puede morir escribiendo ahí.
18///
19/// Las dos mitades importan. La primera es la que faltaba; la segunda es la
20/// que cubre lo que este crate no escribe (el `log` de wgpu, un `eprintln!` de
21/// la app), que si no seguiría siendo un gatillo cada uno.
22#[test]
23fn un_stderr_roto_no_mata_a_la_app() {
24 let mensaje = b"un aviso cualquiera, de quien sea\n";
25 unsafe {
26 // El `stderr` de verdad, para devolverlo al final.
27 let original = libc::dup(2);
28 assert!(original >= 0, "no pude guardar el stderr real");
29
30 // El `stdout` de verdad, ídem: los DOS se rompen juntos y los dos tienen
31 // que quedar tapados. `println!` paniquea por `EPIPE` exactamente igual
32 // que `eprintln!`, y cuando lo que muere es una terminal —un `ssh`, un
33 // `waypipe`, un emulador que se cierra— caen a la vez: tapar sólo el 2
34 // deja media puerta abierta y gana el que escriba primero.
35 let original_1 = libc::dup(1);
36 assert!(original_1 >= 0, "no pude guardar el stdout real");
37
38 // Descriptores 1 y 2 = las puntas que escriben de pipes sin lector.
39 let mut extremos = [0i32; 2];
40 assert_eq!(libc::pipe(extremos.as_mut_ptr()), 0, "no pude armar la pipe");
41 assert!(libc::dup2(extremos[1], 2) >= 0);
42 assert!(libc::dup2(extremos[1], 1) >= 0);
43 libc::close(extremos[1]);
44 libc::close(extremos[0]);
45
46 // Antes: escribir ahí falla con EPIPE — que es exactamente lo que
47 // `eprintln!` y `println!` tratan como fatal. Sin esta mitad, el test
48 // pasaría aunque no hubiera nada roto que arreglar.
49 for fd in [1, 2] {
50 let n = libc::write(fd, mensaje.as_ptr() as *const libc::c_void, mensaje.len());
51 let err = std::io::Error::last_os_error();
52 assert_eq!(n, -1, "la pipe del {fd} tendría que estar rota");
53 assert_eq!(
54 err.kind(),
55 std::io::ErrorKind::BrokenPipe,
56 "en el {fd} esperaba EPIPE, vino {err}"
57 );
58 }
59
60 // **`proteger()` lo ve sin escribir**, que es lo que hace falta para el
61 // aviso que una app da ANTES de que llimphi diga una palabra: en metal,
62 // el 2026-09-06, nahual murió en la línea que anuncia dónde queda su
63 // bitácora de panic, y ahí todavía no había corrido ningún `avisar!`.
64 // ⚠️ **Acá no se puede afirmar nada todavía.** Un `assert!` que falla
65 // imprime por el descriptor 2 — que en este preciso momento es la pipe
66 // rota—, y ese `println!` paniquea mientras ya estábamos paniqueando:
67 // el proceso **aborta sin decir una palabra**. Así se vio la primera
68 // vez que este test falló de verdad (en FreeBSD, 2026-09-16): «test
69 // failed» y ni una línea de por qué. Se juntan los hechos, se
70 // restauran los descriptores, y las afirmaciones van al final.
71 llimphi_hal::aviso::proteger();
72 let vio_el_poll = llimphi_hal::aviso::enmudecido();
73
74 // Y el camino de siempre tampoco paniquea (con `eprintln!` moría acá).
75 llimphi_hal::avisar!("prueba: del otro lado no hay nadie");
76 let enmudecio = llimphi_hal::aviso::enmudecido();
77
78 // Después: los dos aceptan lo que les escriba cualquiera.
79 //
80 // La mitad del descriptor 1 es del 2026-09-16 y salió de FreeBSD:
81 // `khipu` corriendo por `waypipe`, `waypipe` muerto de golpe, y la app
82 // abortando con un core de **640 MB** en el `$HOME` sin un mensaje que
83 // dijera por qué — el mensaje se iba justamente por la salida que se
84 // acababa de morir. La pila, sacada del core:
85 // `panic_in_cleanup → drop_in_place<WinitState> → EventLoop::run`.
86 let mut siguen_matando = Vec::new();
87 for fd in [1, 2] {
88 let n = libc::write(fd, mensaje.as_ptr() as *const libc::c_void, mensaje.len());
89 if n != mensaje.len() as isize {
90 siguen_matando.push((fd, std::io::Error::last_os_error().to_string()));
91 }
92 }
93
94 // Los descriptores vuelven a ser los de verdad ANTES de afirmar, para
95 // que un fallo se pueda leer.
96 libc::dup2(original, 2);
97 libc::close(original);
98 libc::dup2(original_1, 1);
99 libc::close(original_1);
100
101 assert!(
102 vio_el_poll,
103 "poll() no vio la pipe rota: el aviso más temprano sigue siendo mortal \
104 (¿este sistema contesta POLLHUP y no POLLERR?)"
105 );
106 assert!(enmudecio, "el aviso vio la pipe rota y no enmudeció el proceso");
107 assert!(
108 siguen_matando.is_empty(),
109 "estos descriptores siguen matando (un print de cualquiera aborta la app): {siguen_matando:?}"
110 );
111 }
112}