tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-hal/tests/stderr_roto.rs Rust · 112 líneas · 5.6 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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}