Skip to content

Commit e246031

Browse files
committed
feat: agregar módulo para ejecución de comandos con soporte de streaming y manejo de procesos
feat: implementar árbol de proyecto y mapa de símbolos para facilitar la navegación y análisis de archivos
1 parent 2efe02e commit e246031

3 files changed

Lines changed: 566 additions & 541 deletions

File tree

src/fs/exec.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//! Ejecución de comandos del proyecto: streaming línea a línea, timeout,
2+
//! cancelación por Ctrl-C, kill del árbol de procesos y captura del código de
3+
//! salida REAL. Extraído de `fs`.
4+
5+
use std::path::Path;
6+
7+
use super::cap_tail;
8+
9+
/// Tiempo máximo de ejecución de un comando (`dpx:run` y búsquedas). Un proceso
10+
/// que no termina (servidor, watch) se corta y se le explica al modelo.
11+
pub const RUN_TIMEOUT_SECS: u64 = 180;
12+
13+
/// Resultado de ejecutar un comando.
14+
pub struct RunResult {
15+
pub output: String,
16+
/// True si el usuario lo interrumpió (Ctrl-C): el turno debería abortarse.
17+
pub cancelled: bool,
18+
/// Código de salida REAL del proceso (`Some(0)` = éxito determinista). `None`
19+
/// si ni siquiera arrancó. Es la fuente de verdad para el green-gate: el texto
20+
/// `exit code: N` puede perderse si `cap_tail` recorta una salida muy larga.
21+
pub exit_code: Option<i32>,
22+
}
23+
24+
enum StreamLine {
25+
Out(String),
26+
Err(String),
27+
}
28+
29+
/// Bombea un pipe del hijo al canal, línea a línea y tolerante a no-UTF-8
30+
/// (la salida de Maven/Gradle en Windows trae acentos en la codepage local).
31+
fn pump_lines<R: std::io::Read + Send + 'static>(
32+
reader: R,
33+
tx: std::sync::mpsc::Sender<StreamLine>,
34+
wrap: fn(String) -> StreamLine,
35+
) {
36+
use std::io::BufRead;
37+
std::thread::spawn(move || {
38+
let mut reader = std::io::BufReader::new(reader);
39+
let mut buf = Vec::new();
40+
loop {
41+
buf.clear();
42+
match reader.read_until(b'\n', &mut buf) {
43+
Ok(0) | Err(_) => break,
44+
Ok(_) => {
45+
let line = String::from_utf8_lossy(&buf)
46+
.trim_end_matches(['\r', '\n'])
47+
.to_string();
48+
if tx.send(wrap(line)).is_err() {
49+
break;
50+
}
51+
}
52+
}
53+
}
54+
});
55+
}
56+
57+
/// Mata el proceso y todo su árbol. En Windows hace falta `taskkill /T`: matar
58+
/// solo el `cmd` dejaría vivo al build/servidor que lanzó.
59+
fn kill_tree(child: &mut std::process::Child) {
60+
if cfg!(windows) {
61+
let _ = std::process::Command::new("taskkill")
62+
.args(["/PID", &child.id().to_string(), "/T", "/F"])
63+
.output();
64+
}
65+
let _ = child.kill();
66+
let _ = child.wait();
67+
}
68+
69+
/// Ejecuta un comando en la raíz del proyecto entregando cada línea de salida
70+
/// por `on_line` según llega. Se corta por `timeout_secs` o cuando
71+
/// `should_cancel` devuelve true (Ctrl-C), matando el árbol de procesos.
72+
/// El stdin va a null: un comando que espere entrada no congela el REPL.
73+
pub fn run_command_streaming(
74+
cwd: &Path,
75+
cmd: &str,
76+
timeout_secs: u64,
77+
on_line: &mut dyn FnMut(&str),
78+
should_cancel: &dyn Fn() -> bool,
79+
) -> RunResult {
80+
use std::process::{Command, Stdio};
81+
use std::sync::mpsc;
82+
use std::time::{Duration, Instant};
83+
84+
let (shell, flag) = if cfg!(windows) { ("cmd", "/C") } else { ("sh", "-c") };
85+
let spawned = Command::new(shell)
86+
.args([flag, cmd])
87+
.current_dir(cwd)
88+
.stdin(Stdio::null())
89+
.stdout(Stdio::piped())
90+
.stderr(Stdio::piped())
91+
.spawn();
92+
let mut child = match spawned {
93+
Ok(c) => c,
94+
Err(e) => {
95+
return RunResult {
96+
output: format!("error al ejecutar el comando: {e}"),
97+
cancelled: false,
98+
exit_code: None,
99+
};
100+
}
101+
};
102+
103+
let (tx, rx) = mpsc::channel();
104+
if let Some(out) = child.stdout.take() {
105+
pump_lines(out, tx.clone(), StreamLine::Out);
106+
}
107+
if let Some(err) = child.stderr.take() {
108+
pump_lines(err, tx.clone(), StreamLine::Err);
109+
}
110+
drop(tx);
111+
112+
let start = Instant::now();
113+
let mut stdout_buf = String::new();
114+
let mut stderr_buf = String::new();
115+
let (mut cancelled, mut timed_out) = (false, false);
116+
117+
loop {
118+
match rx.recv_timeout(Duration::from_millis(100)) {
119+
Ok(line) => {
120+
let (text, buf) = match line {
121+
StreamLine::Out(t) => (t, &mut stdout_buf),
122+
StreamLine::Err(t) => (t, &mut stderr_buf),
123+
};
124+
on_line(&text);
125+
buf.push_str(&text);
126+
buf.push('\n');
127+
}
128+
Err(mpsc::RecvTimeoutError::Timeout) => {}
129+
// Ambos pipes cerrados: el proceso terminó.
130+
Err(mpsc::RecvTimeoutError::Disconnected) => break,
131+
}
132+
if should_cancel() {
133+
kill_tree(&mut child);
134+
cancelled = true;
135+
break;
136+
}
137+
if start.elapsed().as_secs() >= timeout_secs {
138+
kill_tree(&mut child);
139+
timed_out = true;
140+
break;
141+
}
142+
}
143+
144+
// Lo que quedara encolado tras cortar.
145+
while let Ok(line) = rx.try_recv() {
146+
match line {
147+
StreamLine::Out(t) => {
148+
stdout_buf.push_str(&t);
149+
stdout_buf.push('\n');
150+
}
151+
StreamLine::Err(t) => {
152+
stderr_buf.push_str(&t);
153+
stderr_buf.push('\n');
154+
}
155+
}
156+
}
157+
158+
let code = child.wait().ok().and_then(|s| s.code()).unwrap_or(-1);
159+
let mut s = format!("exit code: {code}\n");
160+
if !stdout_buf.trim().is_empty() {
161+
s.push_str(&format!("--- stdout ---\n{}\n", stdout_buf.trim_end()));
162+
}
163+
if !stderr_buf.trim().is_empty() {
164+
s.push_str(&format!("--- stderr ---\n{}\n", stderr_buf.trim_end()));
165+
}
166+
if timed_out {
167+
s.push_str(&format!(
168+
"[TIMEOUT: el comando superó {timeout_secs}s y fue terminado. Si es un proceso de larga \
169+
duración (servidor, watch), NO lo ejecutes con dpx:run: pídele al usuario que lo corra \
170+
en su propia terminal.]\n"
171+
));
172+
}
173+
if cancelled {
174+
s.push_str("[interrumpido por el usuario con Ctrl-C]\n");
175+
}
176+
RunResult { output: cap_tail(&s, 200), cancelled, exit_code: Some(code) }
177+
}
178+
179+
/// Ejecuta un comando y devuelve su salida acotada (sin streaming ni cancelación;
180+
/// para tareas internas rápidas como las búsquedas).
181+
pub fn run_command(cwd: &Path, cmd: &str) -> String {
182+
run_command_streaming(cwd, cmd, RUN_TIMEOUT_SECS, &mut |_| {}, &|| false).output
183+
}

0 commit comments

Comments
 (0)