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).arg(command);
334 cmd
335 }
336 StreamingCommand::Argv { program, args } => {
337 let mut cmd = Command::new(program);
338 cmd.args(args);
339 cmd
340 }
341 };
342
343 if stdin_content.is_some() {
345 cmd.stdin(Stdio::piped());
346 } else {
347 cmd.stdin(Stdio::null());
348 }
349 cmd.stdout(Stdio::piped());
350 cmd.stderr(Stdio::piped());
351
352 #[cfg(unix)]
355 cmd.process_group(0);
356
357 if let Some(ref cwd) = cwd {
359 cmd.current_dir(cwd);
360 }
361
362 if let Some(ref env_vars) = env {
364 for (key, value) in env_vars {
365 cmd.env(key, value);
366 }
367 }
368
369 let mut child = cmd.spawn()?;
371
372 if let Some(content) = stdin_content {
374 if let Some(mut stdin) = child.stdin.take() {
375 use tokio::io::AsyncWriteExt;
376 let _ = stdin.write_all(content.as_bytes()).await;
377 let _ = stdin.shutdown().await;
378 }
379 }
380
381 let stdout = child.stdout.take();
383 let tx_stdout = tx.clone();
384 let stdout_handle = stdout.map(|stdout| {
385 tokio::spawn(async move {
386 let mut reader = BufReader::new(stdout);
387 let mut buf = vec![0u8; 8192];
388 loop {
389 use tokio::io::AsyncReadExt;
390 match reader.read(&mut buf).await {
391 Ok(0) => break,
392 Ok(n) => {
393 if tx_stdout
394 .send(OutputChunk::Stdout(buf[..n].to_vec()))
395 .await
396 .is_err()
397 {
398 break;
399 }
400 }
401 Err(_) => break,
402 }
403 }
404 })
405 });
406
407 let stderr = child.stderr.take();
409 let tx_stderr = tx.clone();
410 let stderr_handle = stderr.map(|stderr| {
411 tokio::spawn(async move {
412 let mut reader = BufReader::new(stderr);
413 let mut buf = vec![0u8; 8192];
414 loop {
415 use tokio::io::AsyncReadExt;
416 match reader.read(&mut buf).await {
417 Ok(0) => break,
418 Ok(n) => {
419 if tx_stderr
420 .send(OutputChunk::Stderr(buf[..n].to_vec()))
421 .await
422 .is_err()
423 {
424 break;
425 }
426 }
427 Err(_) => break,
428 }
429 }
430 })
431 });
432
433 let pid = child.id();
438 let code;
439 tokio::select! {
440 status = child.wait() => {
441 code = status_to_code(status?);
442 }
443 maybe_signal = kill_rx.recv() => {
444 let signal = maybe_signal.unwrap_or_else(|| DEFAULT_KILL_SIGNAL.to_string());
447 trace_lazy("StreamingRunner", || format!("Kill requested | signal={}", signal));
448 if let Some(pid) = pid {
449 send_signal_to_process(pid, &signal);
450 }
451 if tokio::time::timeout(Duration::from_millis(exit_pump_grace_ms), child.wait())
454 .await
455 .is_err()
456 {
457 let _ = child.start_kill();
458 let _ = child.wait().await;
459 }
460 code = 128 + signal_number(&signal);
463 }
464 }
465
466 let stdout_abort = stdout_handle.as_ref().map(|h| h.abort_handle());
470 let stderr_abort = stderr_handle.as_ref().map(|h| h.abort_handle());
471 let drain = async {
472 if let Some(handle) = stdout_handle {
473 let _ = handle.await;
474 }
475 if let Some(handle) = stderr_handle {
476 let _ = handle.await;
477 }
478 };
479 if tokio::time::timeout(Duration::from_millis(exit_pump_grace_ms), drain)
480 .await
481 .is_err()
482 {
483 if let Some(abort) = stdout_abort {
486 abort.abort();
487 }
488 if let Some(abort) = stderr_abort {
489 abort.abort();
490 }
491 }
492
493 let _ = tx.send(OutputChunk::Exit(code)).await;
495
496 trace_lazy("StreamingRunner", || format!("Exited with code: {}", code));
497
498 Ok(())
499}
500
501fn status_to_code(status: std::process::ExitStatus) -> i32 {
504 if let Some(code) = status.code() {
505 return code;
506 }
507 #[cfg(unix)]
508 {
509 use std::os::unix::process::ExitStatusExt;
510 if let Some(sig) = status.signal() {
511 return 128 + sig;
512 }
513 }
514 -1
515}
516
517fn signal_number(signal: &str) -> i32 {
520 match signal {
521 "SIGHUP" => 1,
522 "SIGINT" => 2,
523 "SIGQUIT" => 3,
524 "SIGKILL" => 9,
525 "SIGUSR1" => 10,
526 "SIGUSR2" => 12,
527 "SIGTERM" => 15,
528 _ => 15,
529 }
530}
531
532#[cfg(unix)]
534fn send_signal_to_process(pid: u32, signal: &str) {
535 use nix::sys::signal::{kill, Signal};
536 use nix::unistd::Pid;
537
538 let sig = match signal {
539 "SIGHUP" => Signal::SIGHUP,
540 "SIGINT" => Signal::SIGINT,
541 "SIGQUIT" => Signal::SIGQUIT,
542 "SIGKILL" => Signal::SIGKILL,
543 "SIGUSR1" => Signal::SIGUSR1,
544 "SIGUSR2" => Signal::SIGUSR2,
545 "SIGTERM" => Signal::SIGTERM,
546 _ => Signal::SIGTERM,
547 };
548
549 let _ = kill(Pid::from_raw(pid as i32), sig);
551 let _ = kill(Pid::from_raw(-(pid as i32)), sig);
553}
554
555#[cfg(not(unix))]
558fn send_signal_to_process(_pid: u32, _signal: &str) {}
559
560#[derive(Debug, Clone)]
562struct ShellConfig {
563 cmd: String,
564 args: Vec<String>,
565}
566
567fn find_available_shell() -> ShellConfig {
569 let is_windows = cfg!(windows);
570
571 if is_windows {
572 ShellConfig {
573 cmd: "cmd.exe".to_string(),
574 args: vec!["/c".to_string()],
575 }
576 } else {
577 let shells = [
578 ("/bin/sh", "-c"),
579 ("/usr/bin/sh", "-c"),
580 ("/bin/bash", "-c"),
581 ];
582
583 for (cmd, arg) in shells {
584 if std::path::Path::new(cmd).exists() {
585 return ShellConfig {
586 cmd: cmd.to_string(),
587 args: vec![arg.to_string()],
588 };
589 }
590 }
591
592 ShellConfig {
593 cmd: "/bin/sh".to_string(),
594 args: vec!["-c".to_string()],
595 }
596 }
597}
598
599#[async_trait::async_trait]
601pub trait AsyncIterator {
602 type Item;
603
604 async fn next(&mut self) -> Option<Self::Item>;
606}
607
608#[async_trait::async_trait]
609impl AsyncIterator for OutputStream {
610 type Item = OutputChunk;
611
612 async fn next(&mut self) -> Option<Self::Item> {
613 self.rx.recv().await
614 }
615}
616
617pub trait IntoStream {
619 fn into_stream(self) -> OutputStream;
621}
622
623impl IntoStream for crate::ProcessRunner {
624 fn into_stream(self) -> OutputStream {
625 let streaming = StreamingRunner::new(self.command().to_string());
626 streaming.stream()
627 }
628}