diff options
Diffstat (limited to 'crates/asist-core/src/proc.rs')
| -rw-r--r-- | crates/asist-core/src/proc.rs | 80 |
1 files changed, 41 insertions, 39 deletions
diff --git a/crates/asist-core/src/proc.rs b/crates/asist-core/src/proc.rs index 8e38108..976a2a8 100644 --- a/crates/asist-core/src/proc.rs +++ b/crates/asist-core/src/proc.rs @@ -1,12 +1,15 @@ -//! Ejecutar un programa con un plazo máximo. +//! Running a program with a deadline. //! -//! Tres sitios lo necesitaban con los mismos cuidados —no dejar nunca un -//! proceso colgado, no pasar por una shell, distinguir «falló» de «tardó -//! demasiado»— y cada uno lo tenía escrito a su manera. Aquí está una sola vez. +//! Three places needed it with the same care (never leave a process hanging, +//! never go through a shell, tell «failed» from «took too long») and each had +//! its own version. Here it is, once. //! -//! Nada de esto pasa por `sh`: los argumentos van al `execve` tal cual. Importa -//! más de lo que parece, porque lo que acaba en ellos viene, en última -//! instancia, de lo que se ha oído por el micrófono. +//! None of this goes through `sh`: the arguments go to `execve` as they are. +//! It matters more than it seems, because what ends up in them comes, +//! ultimately, from what the microphone heard. +//! +//! The error messages are in Spanish on purpose: they end up in tool results +//! that the model reads and speaks. use std::ffi::OsStr; use std::io::Read; @@ -14,7 +17,7 @@ use std::path::Path; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; -/// Cómo terminó. +/// How it ended. #[derive(Debug)] pub struct Output { pub stdout: Vec<u8>, @@ -28,8 +31,8 @@ impl Output { self.status == Some(0) } - /// Última línea del error, que es donde los programas suelen poner el - /// motivo de verdad. + /// Last line of stderr, which is where programs usually put the real + /// reason. pub fn last_error_line(&self) -> &str { self.stderr .trim() @@ -57,11 +60,11 @@ pub enum ProcError { }, } -/// Lanza `program`, espera hasta `timeout` y devuelve lo que haya escrito. +/// Launches `program`, waits up to `timeout` and returns whatever it wrote. /// -/// Si vence el plazo, mata el proceso antes de rendirse: un ffmpeg contra una -/// cámara ocupada, o un buscador que no contesta, se quedarían ahí para -/// siempre. +/// If the deadline passes, it kills the process before giving up: an ffmpeg +/// against a busy camera, or a search engine that does not answer, would stay +/// there forever. pub fn run( program: impl AsRef<Path>, args: impl IntoIterator<Item = impl AsRef<OsStr>>, @@ -87,14 +90,13 @@ pub fn run( source, })?; - // Las tuberías se vacían en hilos aparte, y no después de esperar. + // The pipes are drained on separate threads, not after waiting. // - // Esto no es una optimización: un hijo que escribe más de lo que cabe en el - // búfer de la tubería (64 KB en Linux) se queda bloqueado escribiendo, no - // termina nunca, y el bucle de espera acaba matándolo por plazo vencido - // aunque estuviera haciendo su trabajo. Costó descubrirlo porque los tres - // primeros usos —una fecha, un JSON de búsqueda, un fotograma de 9 KB— - // cabían de sobra; la primera captura de pantalla, de 180 KB, no. + // This is not an optimization: a child that writes more than fits in the + // pipe buffer (64 KB on Linux) blocks writing, never finishes, and the wait + // loop ends up killing it for missing the deadline even though it was doing + // its job. It was hard to find because the first three uses (a date, a + // search JSON, a 9 KB frame) fit easily; the first 180 KB screenshot did not. let mut stdout = child.stdout.take(); let mut stderr = child.stderr.take(); let stdout_reader = std::thread::spawn(move || { @@ -117,8 +119,8 @@ pub fn run( match child.try_wait() { Ok(Some(status)) => break status, Ok(None) if Instant::now() >= deadline => { - // Matar cierra las tuberías, así que los lectores terminan y - // se pueden recoger sin quedarse colgados. + // Killing closes the pipes, so the readers finish and can be + // joined without hanging. let _ = child.kill(); let _ = child.wait(); let _ = stdout_reader.join(); @@ -154,75 +156,75 @@ mod tests { use super::*; #[test] - fn se_recoge_la_salida_estandar() { + fn stdout_is_collected() { let out = run("echo", ["hola"], Duration::from_secs(5), None).unwrap(); assert!(out.success()); assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hola"); } #[test] - fn un_proceso_colgado_se_mata_al_vencer_el_plazo() { + fn hung_process_is_killed_at_the_deadline() { let started = Instant::now(); let err = run("sleep", ["30"], Duration::from_millis(300), None).unwrap_err(); assert!(matches!(err, ProcError::Timeout { .. }), "{err}"); assert!( started.elapsed() < Duration::from_secs(3), - "debió cortar enseguida, tardó {:?}", + "should have stopped right away, took {:?}", started.elapsed() ); } #[test] - fn una_salida_grande_no_bloquea_al_hijo() { - // La regresión que dejó colgada la captura de pantalla: sin vaciar la - // tubería mientras se espera, un hijo que escribe más de 64 KB se - // bloquea y acaba muriendo por plazo vencido. + fn large_output_does_not_block_the_child() { + // The regression that hung the screenshot: without draining the pipe + // while waiting, a child that writes more than 64 KB blocks and ends up + // dying for missing the deadline. let out = run( "dd", ["if=/dev/zero", "bs=1024", "count=512", "status=none"], Duration::from_secs(10), None, ) - .expect("no debió vencer el plazo"); + .expect("the deadline should not have passed"); assert!(out.success()); assert_eq!(out.stdout.len(), 512 * 1024); } #[test] - fn el_error_grande_tampoco_bloquea() { - // Mismo problema por la otra tubería: mucho ruido en stderr y poca - // salida es justo lo que hace ffmpeg cuando algo va mal. + fn large_stderr_does_not_block_either() { + // Same problem on the other pipe: lots of noise on stderr and little + // output is exactly what ffmpeg does when something goes wrong. let out = run( "sh", ["-c", "yes error | head -c 200000 >&2; echo ok"], Duration::from_secs(10), None, ) - .expect("no debió vencer el plazo"); + .expect("the deadline should not have passed"); assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "ok"); assert!(out.stderr.len() > 100_000); } #[test] - fn un_programa_inexistente_da_un_error_con_su_nombre() { + fn missing_program_error_includes_its_name() { let err = run("/no/existe/nada", ["x"], Duration::from_secs(1), None).unwrap_err(); assert!(err.to_string().contains("/no/existe/nada"), "{err}"); } #[test] - fn un_fallo_conserva_el_codigo_y_el_mensaje() { + fn failure_keeps_the_code_and_message() { let out = run("ls", ["/no/existe"], Duration::from_secs(5), None).unwrap(); assert!(!out.success()); assert!(!out.last_error_line().is_empty()); } #[test] - fn los_metacaracteres_no_los_interpreta_ninguna_shell() { + fn no_shell_interprets_metacharacters() { let out = run("echo", ["a; echo b"], Duration::from_secs(5), None).unwrap(); assert_eq!( String::from_utf8_lossy(&out.stdout).trim(), "a; echo b", - "el punto y coma debe llegar como texto, no como separador de órdenes" + "the semicolon must arrive as text, not as a command separator" ); } } |