1use std::collections::HashMap;
58use std::ffi::OsString;
59use std::path::PathBuf;
60use std::process::Stdio;
61use std::time::Duration;
62use tokio::io::BufReader;
63use tokio::process::Command;
64use tokio::sync::mpsc;
65
66use crate::trace::trace_lazy;
67use crate::{CommandResult, Result};
68
69const DEFAULT_EXIT_PUMP_GRACE_MS: u64 = 100;
73
74const DEFAULT_KILL_SIGNAL: &str = "SIGTERM";
76
77#[derive(Debug, Clone)]
79pub enum OutputChunk {
80 Stdout(Vec<u8>),
82 Stderr(Vec<u8>),
84 Exit(i32),
86}
87
88pub struct StreamingRunner {
90 command: StreamingCommand,
91 cwd: Option<PathBuf>,
92 env: Option<HashMap<String, String>>,
93 stdin_content: Option<String>,
94 kill_signal: String,
95 exit_pump_grace_ms: u64,
96}
97
98#[derive(Clone)]
99enum StreamingCommand {
100 Shell(String),
101 Argv {
102 program: OsString,
103 args: Vec<OsString>,
104 },
105}
106
107impl StreamingRunner {
108 pub fn new(command: impl Into<String>) -> Self {
111 Self::with_command(StreamingCommand::Shell(command.into()))
112 }
113
114 pub fn from_argv<P, I, S>(program: P, args: I) -> Self
120 where
121 P: Into<OsString>,
122 I: IntoIterator<Item = S>,
123 S: Into<OsString>,
124 {
125 Self::with_command(StreamingCommand::Argv {
126 program: program.into(),
127 args: args.into_iter().map(Into::into).collect(),
128 })
129 }
130
131 fn with_command(command: StreamingCommand) -> Self {
132 StreamingRunner {
133 command,
134 cwd: None,
135 env: None,
136 stdin_content: None,
137 kill_signal: DEFAULT_KILL_SIGNAL.to_string(),
138 exit_pump_grace_ms: DEFAULT_EXIT_PUMP_GRACE_MS,
139 }
140 }
141
142 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
144 self.cwd = Some(path.into());
145 self
146 }
147
148 pub fn env(mut self, env: HashMap<String, String>) -> Self {
150 self.env = Some(env);
151 self
152 }
153
154 pub fn stdin(mut self, content: impl Into<String>) -> Self {
156 self.stdin_content = Some(content.into());
157 self
158 }
159
160 pub fn kill_signal(mut self, signal: impl Into<String>) -> Self {
167 self.kill_signal = signal.into();
168 self
169 }
170
171 pub fn exit_pump_grace_ms(mut self, ms: u64) -> Self {
175 self.exit_pump_grace_ms = ms;
176 self
177 }
178
179 pub fn stream(mut self) -> OutputStream {
181 let (tx, rx) = mpsc::channel(1024);
182 let (kill_tx, kill_rx) = mpsc::unbounded_channel::<String>();
184
185 let command = self.command.clone();
187 let cwd = self.cwd.take();
188 let env = self.env.take();
189 let stdin_content = self.stdin_content.take();
190 let grace = self.exit_pump_grace_ms;
191 let kill_signal = self.kill_signal.clone();
192
193 tokio::spawn(async move {
194 if let Err(e) =
195 run_streaming_process(command, cwd, env, stdin_content, grace, tx.clone(), kill_rx)
196 .await
197 {
198 trace_lazy("StreamingRunner", || format!("Error: {}", e));
199 }
200 });
201
202 OutputStream {
203 rx,
204 kill_tx,
205 kill_signal,
206 killed: false,
207 }
208 }
209
210 pub async fn collect(self) -> Result<CommandResult> {
212 let mut stdout = Vec::new();
213 let mut stderr = Vec::new();
214 let mut exit_code = 0;
215
216 let mut stream = self.stream();
217 while let Some(chunk) = stream.rx.recv().await {
218 match chunk {
219 OutputChunk::Stdout(data) => stdout.extend(data),
220 OutputChunk::Stderr(data) => stderr.extend(data),
221 OutputChunk::Exit(code) => exit_code = code,
222 }
223 }
224
225 Ok(CommandResult {
226 stdout: String::from_utf8_lossy(&stdout).to_string(),
227 stderr: String::from_utf8_lossy(&stderr).to_string(),
228 code: exit_code,
229 })
230 }
231}
232
233pub struct OutputStream {
235 rx: mpsc::Receiver<OutputChunk>,
236 kill_tx: mpsc::UnboundedSender<String>,
237 kill_signal: String,
238 killed: bool,
239}
240
241impl OutputStream {
242 pub async fn next(&mut self) -> Option<OutputChunk> {
244 self.rx.recv().await
245 }
246
247 pub fn kill(&mut self) {
253 let signal = self.kill_signal.clone();
254 self.kill_with(&signal);
255 }
256
257 pub fn kill_with(&mut self, signal: &str) {
260 if self.killed {
261 return;
262 }
263 self.killed = true;
264 trace_lazy("OutputStream", || format!("kill | signal={}", signal));
265 let _ = self.kill_tx.send(signal.to_string());
268 }
269
270 pub async fn collect(mut self) -> (Vec<u8>, Vec<u8>, i32) {
272 let mut stdout = Vec::new();
273 let mut stderr = Vec::new();
274 let mut exit_code = 0;
275
276 while let Some(chunk) = self.rx.recv().await {
277 match chunk {
278 OutputChunk::Stdout(data) => stdout.extend(data),
279 OutputChunk::Stderr(data) => stderr.extend(data),
280 OutputChunk::Exit(code) => exit_code = code,
281 }
282 }
283
284 (stdout, stderr, exit_code)
285 }
286
287 pub async fn collect_stdout(mut self) -> Vec<u8> {
289 let mut stdout = Vec::new();
290
291 while let Some(chunk) = self.rx.recv().await {
292 if let OutputChunk::Stdout(data) = chunk {
293 stdout.extend(data);
294 }
295 }
296
297 stdout
298 }
299}
300
301impl Drop for OutputStream {
302 fn drop(&mut self) {
303 if !self.killed {
307 let _ = self.kill_tx.send(self.kill_signal.clone());
308 }
309 }
310}
311
312async fn run_streaming_process(
314 command: StreamingCommand,
315 cwd: Option<PathBuf>,
316 env: Option<HashMap<String, String>>,
317 stdin_content: Option<String>,
318 exit_pump_grace_ms: u64,
319 tx: mpsc::Sender<OutputChunk>,
320 mut kill_rx: mpsc::UnboundedReceiver<String>,
321) -> Result<()> {
322 trace_lazy("StreamingRunner", || match &command {
323 StreamingCommand::Shell(command) => format!("Starting: {command}"),
324 StreamingCommand::Argv { program, args } => {
325 format!("Starting argv command: {program:?} {args:?}")
326 }
327 });
328
329 let mut cmd = match command {
330 StreamingCommand::Shell(command) => {
331 let shell = find_available_shell();
332 let mut cmd = Command::new(&shell.cmd);
333 cmd.args(&shell.args)
334 .arg(crate::utils::with_exported_process_context(
335 &command,
336 env.as_ref(),
337 ));
338 cmd
339 }
340 StreamingCommand::Argv { program, args } => {
341 let mut cmd = Command::new(program);
342 cmd.args(args);
343 cmd
344 }
345 };
346
347 if stdin_content.is_some() {
349 cmd.stdin(Stdio::piped());
350 } else {
351 cmd.stdin(Stdio::null());
352 }
353 cmd.stdout(Stdio::piped());
354 cmd.stderr(Stdio::piped());
355
356 #[cfg(unix)]
359 cmd.process_group(0);
360
361 if let Some(ref cwd) = cwd {
363 cmd.current_dir(cwd);
364 }
365
366 if let Some(ref env_vars) = env {
368 for (key, value) in env_vars {
369 cmd.env(key, value);
370 }
371 }
372
373 let mut child = cmd.spawn()?;
374
375 if let Some(content) = stdin_content {
377 if let Some(mut stdin) = child.stdin.take() {
378 use tokio::io::AsyncWriteExt;
379 let _ = stdin.write_all(content.as_bytes()).await;
380 let _ = stdin.shutdown().await;
381 }
382 }
383
384 let stdout = child.stdout.take();
386 let tx_stdout = tx.clone();
387 let stdout_handle = stdout.map(|stdout| {
388 tokio::spawn(async move {
389 let mut reader = BufReader::new(stdout);
390 let mut buf = vec![0u8; 8192];
391 loop {
392 use tokio::io::AsyncReadExt;
393 match reader.read(&mut buf).await {
394 Ok(0) => break,
395 Ok(n) => {
396 if tx_stdout
397 .send(OutputChunk::Stdout(buf[..n].to_vec()))
398 .await
399 .is_err()
400 {
401 break;
402 }
403 }
404 Err(_) => break,
405 }
406 }
407 })
408 });
409
410 let stderr = child.stderr.take();
412 let tx_stderr = tx.clone();
413 let stderr_handle = stderr.map(|stderr| {
414 tokio::spawn(async move {
415 let mut reader = BufReader::new(stderr);
416 let mut buf = vec![0u8; 8192];
417 loop {
418 use tokio::io::AsyncReadExt;
419 match reader.read(&mut buf).await {
420 Ok(0) => break,
421 Ok(n) => {
422 if tx_stderr
423 .send(OutputChunk::Stderr(buf[..n].to_vec()))
424 .await
425 .is_err()
426 {
427 break;
428 }
429 }
430 Err(_) => break,
431 }
432 }
433 })
434 });
435
436 let pid = child.id();
441 let code;
442 tokio::select! {
443 status = child.wait() => {
444 code = status_to_code(status?);
445 }
446 maybe_signal = kill_rx.recv() => {
447 let signal = maybe_signal.unwrap_or_else(|| DEFAULT_KILL_SIGNAL.to_string());
450 trace_lazy("StreamingRunner", || format!("Kill requested | signal={}", signal));
451 if let Some(pid) = pid {
452 send_signal_to_process(pid, &signal);
453 }
454 if tokio::time::timeout(Duration::from_millis(exit_pump_grace_ms), child.wait())
457 .await
458 .is_err()
459 {
460 let _ = child.start_kill();
461 let _ = child.wait().await;
462 }
463 code = 128 + signal_number(&signal);
466 }
467 }
468
469 let stdout_abort = stdout_handle.as_ref().map(|h| h.abort_handle());
473 let stderr_abort = stderr_handle.as_ref().map(|h| h.abort_handle());
474 let drain = async {
475 if let Some(handle) = stdout_handle {
476 let _ = handle.await;
477 }
478 if let Some(handle) = stderr_handle {
479 let _ = handle.await;
480 }
481 };
482 if tokio::time::timeout(Duration::from_millis(exit_pump_grace_ms), drain)
483 .await
484 .is_err()
485 {
486 if let Some(abort) = stdout_abort {
489 abort.abort();
490 }
491 if let Some(abort) = stderr_abort {
492 abort.abort();
493 }
494 }
495
496 let _ = tx.send(OutputChunk::Exit(code)).await;
498
499 trace_lazy("StreamingRunner", || format!("Exited with code: {}", code));
500
501 Ok(())
502}
503
504fn status_to_code(status: std::process::ExitStatus) -> i32 {
507 if let Some(code) = status.code() {
508 return code;
509 }
510 #[cfg(unix)]
511 {
512 use std::os::unix::process::ExitStatusExt;
513 if let Some(sig) = status.signal() {
514 return 128 + sig;
515 }
516 }
517 -1
518}
519
520fn signal_number(signal: &str) -> i32 {
523 match signal {
524 "SIGHUP" => 1,
525 "SIGINT" => 2,
526 "SIGQUIT" => 3,
527 "SIGKILL" => 9,
528 "SIGUSR1" => 10,
529 "SIGUSR2" => 12,
530 "SIGTERM" => 15,
531 _ => 15,
532 }
533}
534
535#[cfg(unix)]
537fn send_signal_to_process(pid: u32, signal: &str) {
538 use nix::sys::signal::{kill, Signal};
539 use nix::unistd::Pid;
540
541 let sig = match signal {
542 "SIGHUP" => Signal::SIGHUP,
543 "SIGINT" => Signal::SIGINT,
544 "SIGQUIT" => Signal::SIGQUIT,
545 "SIGKILL" => Signal::SIGKILL,
546 "SIGUSR1" => Signal::SIGUSR1,
547 "SIGUSR2" => Signal::SIGUSR2,
548 "SIGTERM" => Signal::SIGTERM,
549 _ => Signal::SIGTERM,
550 };
551
552 let _ = kill(Pid::from_raw(pid as i32), sig);
554 let _ = kill(Pid::from_raw(-(pid as i32)), sig);
556}
557
558#[cfg(not(unix))]
561fn send_signal_to_process(_pid: u32, _signal: &str) {}
562
563#[derive(Debug, Clone)]
565struct ShellConfig {
566 cmd: String,
567 args: Vec<String>,
568}
569
570fn find_available_shell() -> ShellConfig {
572 let is_windows = cfg!(windows);
573
574 if is_windows {
575 ShellConfig {
576 cmd: "cmd.exe".to_string(),
577 args: vec!["/c".to_string()],
578 }
579 } else {
580 let shells = [
581 ("/bin/sh", "-c"),
582 ("/usr/bin/sh", "-c"),
583 ("/bin/bash", "-c"),
584 ];
585
586 for (cmd, arg) in shells {
587 if std::path::Path::new(cmd).exists() {
588 return ShellConfig {
589 cmd: cmd.to_string(),
590 args: vec![arg.to_string()],
591 };
592 }
593 }
594
595 ShellConfig {
596 cmd: "/bin/sh".to_string(),
597 args: vec!["-c".to_string()],
598 }
599 }
600}
601
602#[async_trait::async_trait]
604pub trait AsyncIterator {
605 type Item;
606
607 async fn next(&mut self) -> Option<Self::Item>;
609}
610
611#[async_trait::async_trait]
612impl AsyncIterator for OutputStream {
613 type Item = OutputChunk;
614
615 async fn next(&mut self) -> Option<Self::Item> {
616 self.rx.recv().await
617 }
618}
619
620pub trait IntoStream {
622 fn into_stream(self) -> OutputStream;
624}
625
626impl IntoStream for crate::ProcessRunner {
627 fn into_stream(self) -> OutputStream {
628 let streaming = StreamingRunner::new(self.command().to_string());
629 streaming.stream()
630 }
631}