//! Un `stderr` roto no mata a la app. //! //! La regresión es del 2026-09-06 y costó una instancia de `nahual`: al //! teselar la ventana contra otra, `view()` pasó el umbral del vigía, el vigía //! lo denunció con `eprintln!`, el `stderr` de esa instancia era una pipe sin //! lector, `eprintln!` paniqueó por `EPIPE`, y el unwind arrastró la `Surface` //! con el `SurfaceTexture` del cuadro en vuelo — `panic in a destructor during //! cleanup`, **abort**. Tres trazas encadenadas en `panic.log` para lo que //! empezó como una línea de diagnóstico. //! //! El test vive en su propio binario a propósito: enchufa y desenchufa el //! descriptor 2 del proceso, que es estado global y no se comparte con otros //! tests. #![cfg(unix)] /// Con el descriptor 2 puesto en una pipe sin lector: `avisar!` no paniquea, y /// después de él **nadie** en el proceso puede morir escribiendo ahí. /// /// Las dos mitades importan. La primera es la que faltaba; la segunda es la /// que cubre lo que este crate no escribe (el `log` de wgpu, un `eprintln!` de /// la app), que si no seguiría siendo un gatillo cada uno. #[test] fn un_stderr_roto_no_mata_a_la_app() { let mensaje = b"un aviso cualquiera, de quien sea\n"; unsafe { // El `stderr` de verdad, para devolverlo al final. let original = libc::dup(2); assert!(original >= 0, "no pude guardar el stderr real"); // El `stdout` de verdad, ídem: los DOS se rompen juntos y los dos tienen // que quedar tapados. `println!` paniquea por `EPIPE` exactamente igual // que `eprintln!`, y cuando lo que muere es una terminal —un `ssh`, un // `waypipe`, un emulador que se cierra— caen a la vez: tapar sólo el 2 // deja media puerta abierta y gana el que escriba primero. let original_1 = libc::dup(1); assert!(original_1 >= 0, "no pude guardar el stdout real"); // Descriptores 1 y 2 = las puntas que escriben de pipes sin lector. let mut extremos = [0i32; 2]; assert_eq!(libc::pipe(extremos.as_mut_ptr()), 0, "no pude armar la pipe"); assert!(libc::dup2(extremos[1], 2) >= 0); assert!(libc::dup2(extremos[1], 1) >= 0); libc::close(extremos[1]); libc::close(extremos[0]); // Antes: escribir ahí falla con EPIPE — que es exactamente lo que // `eprintln!` y `println!` tratan como fatal. Sin esta mitad, el test // pasaría aunque no hubiera nada roto que arreglar. for fd in [1, 2] { let n = libc::write(fd, mensaje.as_ptr() as *const libc::c_void, mensaje.len()); let err = std::io::Error::last_os_error(); assert_eq!(n, -1, "la pipe del {fd} tendría que estar rota"); assert_eq!( err.kind(), std::io::ErrorKind::BrokenPipe, "en el {fd} esperaba EPIPE, vino {err}" ); } // **`proteger()` lo ve sin escribir**, que es lo que hace falta para el // aviso que una app da ANTES de que llimphi diga una palabra: en metal, // el 2026-09-06, nahual murió en la línea que anuncia dónde queda su // bitácora de panic, y ahí todavía no había corrido ningún `avisar!`. // ⚠️ **Acá no se puede afirmar nada todavía.** Un `assert!` que falla // imprime por el descriptor 2 — que en este preciso momento es la pipe // rota—, y ese `println!` paniquea mientras ya estábamos paniqueando: // el proceso **aborta sin decir una palabra**. Así se vio la primera // vez que este test falló de verdad (en FreeBSD, 2026-09-16): «test // failed» y ni una línea de por qué. Se juntan los hechos, se // restauran los descriptores, y las afirmaciones van al final. llimphi_hal::aviso::proteger(); let vio_el_poll = llimphi_hal::aviso::enmudecido(); // Y el camino de siempre tampoco paniquea (con `eprintln!` moría acá). llimphi_hal::avisar!("prueba: del otro lado no hay nadie"); let enmudecio = llimphi_hal::aviso::enmudecido(); // Después: los dos aceptan lo que les escriba cualquiera. // // La mitad del descriptor 1 es del 2026-09-16 y salió de FreeBSD: // `khipu` corriendo por `waypipe`, `waypipe` muerto de golpe, y la app // abortando con un core de **640 MB** en el `$HOME` sin un mensaje que // dijera por qué — el mensaje se iba justamente por la salida que se // acababa de morir. La pila, sacada del core: // `panic_in_cleanup → drop_in_place → EventLoop::run`. let mut siguen_matando = Vec::new(); for fd in [1, 2] { let n = libc::write(fd, mensaje.as_ptr() as *const libc::c_void, mensaje.len()); if n != mensaje.len() as isize { siguen_matando.push((fd, std::io::Error::last_os_error().to_string())); } } // Los descriptores vuelven a ser los de verdad ANTES de afirmar, para // que un fallo se pueda leer. libc::dup2(original, 2); libc::close(original); libc::dup2(original_1, 1); libc::close(original_1); assert!( vio_el_poll, "poll() no vio la pipe rota: el aviso más temprano sigue siendo mortal \ (¿este sistema contesta POLLHUP y no POLLERR?)" ); assert!(enmudecio, "el aviso vio la pipe rota y no enmudeció el proceso"); assert!( siguen_matando.is_empty(), "estos descriptores siguen matando (un print de cualquiera aborta la app): {siguen_matando:?}" ); } }