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;
65use tokio::task::JoinHandle;
66
67use crate::signal::{
68 send_signal_to_process, signal_exit_code, Delivery, DEFAULT_KILL_GRACE_MS, DEFAULT_KILL_SIGNAL,
69};
70use crate::trace::trace_lazy;
71use crate::{CommandResult, Result};
72
73const DEFAULT_EXIT_PUMP_GRACE_MS: u64 = 100;
77
78#[derive(Debug, Clone)]
80pub enum OutputChunk {
81 Stdout(Vec<u8>),
83 Stderr(Vec<u8>),
85 Exit(i32),
87}
88
89pub struct StreamingRunner {
91 command: StreamingCommand,
92 cwd: Option<PathBuf>,
93 env: Option<HashMap<String, String>>,
94 stdin_content: Option<String>,
95 kill_signal: String,
96 kill_grace_ms: u64,
97 exit_pump_grace_ms: u64,
98}
99
100#[derive(Clone)]
101enum StreamingCommand {
102 Shell(String),
103 Argv {
104 program: OsString,
105 args: Vec<OsString>,
106 },
107}
108
109impl StreamingRunner {
110 pub fn new(command: impl Into<String>) -> Self {
113 Self::with_command(StreamingCommand::Shell(command.into()))
114 }
115
116 pub fn from_argv<P, I, S>(program: P, args: I) -> Self
122 where
123 P: Into<OsString>,
124 I: IntoIterator<Item = S>,
125 S: Into<OsString>,
126 {
127 Self::with_command(StreamingCommand::Argv {
128 program: program.into(),
129 args: args.into_iter().map(Into::into).collect(),
130 })
131 }
132
133 fn with_command(command: StreamingCommand) -> Self {
134 StreamingRunner {
135 command,
136 cwd: None,
137 env: None,
138 stdin_content: None,
139 kill_signal: DEFAULT_KILL_SIGNAL.to_string(),
140 kill_grace_ms: DEFAULT_KILL_GRACE_MS,
141 exit_pump_grace_ms: DEFAULT_EXIT_PUMP_GRACE_MS,
142 }
143 }
144
145 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
147 self.cwd = Some(path.into());
148 self
149 }
150
151 pub fn env(mut self, env: HashMap<String, String>) -> Self {
153 self.env = Some(env);
154 self
155 }
156
157 pub fn stdin(mut self, content: impl Into<String>) -> Self {
159 self.stdin_content = Some(content.into());
160 self
161 }
162
163 pub fn kill_signal(mut self, signal: impl Into<String>) -> Self {
170 self.kill_signal = signal.into();
171 self
172 }
173
174 pub fn kill_grace_ms(mut self, ms: u64) -> Self {
181 self.kill_grace_ms = ms;
182 self
183 }
184
185 pub fn exit_pump_grace_ms(mut self, ms: u64) -> Self {
189 self.exit_pump_grace_ms = ms;
190 self
191 }
192
193 fn spawn(mut self) -> (OutputStream, JoinHandle<Result<()>>) {
194 let (tx, rx) = mpsc::channel(1024);
195 let (kill_tx, kill_rx) = mpsc::unbounded_channel::<String>();
197
198 let command = self.command.clone();
200 let cwd = self.cwd.take();
201 let env = self.env.take();
202 let stdin_content = self.stdin_content.take();
203 let grace = GraceWindows {
204 exit_pump_ms: self.exit_pump_grace_ms,
205 kill_ms: self.kill_grace_ms,
206 };
207 let kill_signal = self.kill_signal.clone();
208
209 let task = tokio::spawn(async move {
210 let result =
211 run_streaming_process(command, cwd, env, stdin_content, grace, tx, kill_rx).await;
212 if let Err(error) = &result {
213 trace_lazy("StreamingRunner", || format!("Error: {error}"));
214 }
215 result
216 });
217
218 (
219 OutputStream {
220 rx,
221 kill_tx,
222 kill_signal,
223 killed: false,
224 },
225 task,
226 )
227 }
228
229 pub fn stream(self) -> OutputStream {
231 self.spawn().0
232 }
233
234 pub async fn collect(self) -> Result<CommandResult> {
236 let mut stdout = Vec::new();
237 let mut stderr = Vec::new();
238 let mut exit_code = 0;
239
240 let (mut stream, task) = self.spawn();
241 while let Some(chunk) = stream.rx.recv().await {
242 match chunk {
243 OutputChunk::Stdout(data) => stdout.extend(data),
244 OutputChunk::Stderr(data) => stderr.extend(data),
245 OutputChunk::Exit(code) => exit_code = code,
246 }
247 }
248
249 task.await.map_err(|error| {
250 std::io::Error::other(format!("streaming process task failed: {error}"))
251 })??;
252
253 Ok(CommandResult {
254 stdout: String::from_utf8_lossy(&stdout).to_string(),
255 stderr: String::from_utf8_lossy(&stderr).to_string(),
256 code: exit_code,
257 })
258 }
259}
260
261pub struct OutputStream {
263 rx: mpsc::Receiver<OutputChunk>,
264 kill_tx: mpsc::UnboundedSender<String>,
265 kill_signal: String,
266 killed: bool,
267}
268
269impl OutputStream {
270 pub async fn next(&mut self) -> Option<OutputChunk> {
272 self.rx.recv().await
273 }
274
275 pub fn kill(&mut self) {
281 let signal = self.kill_signal.clone();
282 self.kill_with(&signal);
283 }
284
285 pub fn kill_with(&mut self, signal: &str) {
288 if self.killed {
289 return;
290 }
291 self.killed = true;
292 trace_lazy("OutputStream", || format!("kill | signal={}", signal));
293 let _ = self.kill_tx.send(signal.to_string());
296 }
297
298 pub async fn collect(mut self) -> (Vec<u8>, Vec<u8>, i32) {
300 let mut stdout = Vec::new();
301 let mut stderr = Vec::new();
302 let mut exit_code = 0;
303
304 while let Some(chunk) = self.rx.recv().await {
305 match chunk {
306 OutputChunk::Stdout(data) => stdout.extend(data),
307 OutputChunk::Stderr(data) => stderr.extend(data),
308 OutputChunk::Exit(code) => exit_code = code,
309 }
310 }
311
312 (stdout, stderr, exit_code)
313 }
314
315 pub async fn collect_stdout(mut self) -> Vec<u8> {
317 let mut stdout = Vec::new();
318
319 while let Some(chunk) = self.rx.recv().await {
320 if let OutputChunk::Stdout(data) = chunk {
321 stdout.extend(data);
322 }
323 }
324
325 stdout
326 }
327}
328
329impl Drop for OutputStream {
330 fn drop(&mut self) {
331 if !self.killed {
335 let _ = self.kill_tx.send(self.kill_signal.clone());
336 }
337 }
338}
339
340#[derive(Debug, Clone, Copy)]
343struct GraceWindows {
344 exit_pump_ms: u64,
347 kill_ms: u64,
350}
351
352async fn run_streaming_process(
354 command: StreamingCommand,
355 cwd: Option<PathBuf>,
356 env: Option<HashMap<String, String>>,
357 stdin_content: Option<String>,
358 grace: GraceWindows,
359 tx: mpsc::Sender<OutputChunk>,
360 mut kill_rx: mpsc::UnboundedReceiver<String>,
361) -> Result<()> {
362 trace_lazy("StreamingRunner", || match &command {
363 StreamingCommand::Shell(command) => format!("Starting: {command}"),
364 StreamingCommand::Argv { program, args } => {
365 format!("Starting argv command: {program:?} {args:?}")
366 }
367 });
368
369 let mut cmd = match command {
370 StreamingCommand::Shell(command) => {
371 let shell = find_available_shell();
372 let mut cmd = Command::new(&shell.cmd);
373 cmd.args(&shell.args);
374 crate::utils::append_shell_command(&mut cmd, &command, env.as_ref());
375 cmd
376 }
377 StreamingCommand::Argv { program, args } => {
378 let mut cmd = Command::new(program);
379 cmd.args(args);
380 cmd
381 }
382 };
383
384 if stdin_content.is_some() {
386 cmd.stdin(Stdio::piped());
387 } else {
388 cmd.stdin(Stdio::null());
389 }
390 cmd.stdout(Stdio::piped());
391 cmd.stderr(Stdio::piped());
392
393 #[cfg(unix)]
396 cmd.process_group(0);
397
398 if let Some(ref cwd) = cwd {
400 cmd.current_dir(cwd);
401 }
402
403 if let Some(ref env_vars) = env {
405 for (key, value) in env_vars {
406 cmd.env(key, value);
407 }
408 }
409
410 let mut child = cmd.spawn()?;
411
412 if let Some(content) = stdin_content {
414 if let Some(mut stdin) = child.stdin.take() {
415 use tokio::io::AsyncWriteExt;
416 let _ = stdin.write_all(content.as_bytes()).await;
417 let _ = stdin.shutdown().await;
418 }
419 }
420
421 let stdout = child.stdout.take();
423 let tx_stdout = tx.clone();
424 let stdout_handle = stdout.map(|stdout| {
425 tokio::spawn(async move {
426 let mut reader = BufReader::new(stdout);
427 let mut buf = vec![0u8; 8192];
428 loop {
429 use tokio::io::AsyncReadExt;
430 match reader.read(&mut buf).await {
431 Ok(0) => break,
432 Ok(n) => {
433 if tx_stdout
434 .send(OutputChunk::Stdout(buf[..n].to_vec()))
435 .await
436 .is_err()
437 {
438 break;
439 }
440 }
441 Err(_) => break,
442 }
443 }
444 })
445 });
446
447 let stderr = child.stderr.take();
449 let tx_stderr = tx.clone();
450 let stderr_handle = stderr.map(|stderr| {
451 tokio::spawn(async move {
452 let mut reader = BufReader::new(stderr);
453 let mut buf = vec![0u8; 8192];
454 loop {
455 use tokio::io::AsyncReadExt;
456 match reader.read(&mut buf).await {
457 Ok(0) => break,
458 Ok(n) => {
459 if tx_stderr
460 .send(OutputChunk::Stderr(buf[..n].to_vec()))
461 .await
462 .is_err()
463 {
464 break;
465 }
466 }
467 Err(_) => break,
468 }
469 }
470 })
471 });
472
473 let pid = child.id();
478 let code;
479 tokio::select! {
480 status = child.wait() => {
481 code = status_to_code(status?);
482 }
483 maybe_signal = kill_rx.recv() => {
484 let signal = maybe_signal.unwrap_or_else(|| DEFAULT_KILL_SIGNAL.to_string());
487 trace_lazy("StreamingRunner", || format!("Kill requested | signal={}", signal));
488 let survived_grace = if grace.kill_ms == 0 {
499 true
500 } else {
501 if let Some(pid) = pid {
502 send_signal_to_process(pid, &signal, Delivery::ProcessAndGroup);
505 }
506 tokio::time::timeout(Duration::from_millis(grace.kill_ms), child.wait())
507 .await
508 .is_err()
509 };
510 if survived_grace {
511 if let Some(pid) = pid {
512 send_signal_to_process(pid, "SIGKILL", Delivery::ProcessAndGroup);
513 }
514 let _ = child.start_kill();
515 let _ = child.wait().await;
516 }
517 code = signal_exit_code(&signal);
520 }
521 }
522
523 let stdout_abort = stdout_handle.as_ref().map(|h| h.abort_handle());
527 let stderr_abort = stderr_handle.as_ref().map(|h| h.abort_handle());
528 let drain = async {
529 if let Some(handle) = stdout_handle {
530 let _ = handle.await;
531 }
532 if let Some(handle) = stderr_handle {
533 let _ = handle.await;
534 }
535 };
536 if tokio::time::timeout(Duration::from_millis(grace.exit_pump_ms), drain)
537 .await
538 .is_err()
539 {
540 if let Some(abort) = stdout_abort {
543 abort.abort();
544 }
545 if let Some(abort) = stderr_abort {
546 abort.abort();
547 }
548 }
549
550 let _ = tx.send(OutputChunk::Exit(code)).await;
552
553 trace_lazy("StreamingRunner", || format!("Exited with code: {}", code));
554
555 Ok(())
556}
557
558fn status_to_code(status: std::process::ExitStatus) -> i32 {
561 if let Some(code) = status.code() {
562 return code;
563 }
564 #[cfg(unix)]
565 {
566 use std::os::unix::process::ExitStatusExt;
567 if let Some(sig) = status.signal() {
568 return 128 + sig;
569 }
570 }
571 -1
572}
573
574#[derive(Debug, Clone)]
576struct ShellConfig {
577 cmd: String,
578 args: Vec<String>,
579}
580
581fn find_available_shell() -> ShellConfig {
583 let is_windows = cfg!(windows);
584
585 if is_windows {
586 ShellConfig {
587 cmd: "cmd.exe".to_string(),
588 args: vec!["/c".to_string()],
589 }
590 } else {
591 let shells = [
592 ("/bin/sh", "-c"),
593 ("/usr/bin/sh", "-c"),
594 ("/bin/bash", "-c"),
595 ];
596
597 for (cmd, arg) in shells {
598 if std::path::Path::new(cmd).exists() {
599 return ShellConfig {
600 cmd: cmd.to_string(),
601 args: vec![arg.to_string()],
602 };
603 }
604 }
605
606 ShellConfig {
607 cmd: "/bin/sh".to_string(),
608 args: vec!["-c".to_string()],
609 }
610 }
611}
612
613#[async_trait::async_trait]
615pub trait AsyncIterator {
616 type Item;
617
618 async fn next(&mut self) -> Option<Self::Item>;
620}
621
622#[async_trait::async_trait]
623impl AsyncIterator for OutputStream {
624 type Item = OutputChunk;
625
626 async fn next(&mut self) -> Option<Self::Item> {
627 self.rx.recv().await
628 }
629}
630
631pub trait IntoStream {
633 fn into_stream(self) -> OutputStream;
635}
636
637impl IntoStream for crate::ProcessRunner {
638 fn into_stream(self) -> OutputStream {
639 let streaming = StreamingRunner::new(self.command().to_string());
640 streaming.stream()
641 }
642}