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::trace::trace_lazy;
68use crate::{CommandResult, Result};
69
70const DEFAULT_EXIT_PUMP_GRACE_MS: u64 = 100;
74
75const DEFAULT_KILL_SIGNAL: &str = "SIGTERM";
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 exit_pump_grace_ms: u64,
97}
98
99#[derive(Clone)]
100enum StreamingCommand {
101 Shell(String),
102 Argv {
103 program: OsString,
104 args: Vec<OsString>,
105 },
106}
107
108impl StreamingRunner {
109 pub fn new(command: impl Into<String>) -> Self {
112 Self::with_command(StreamingCommand::Shell(command.into()))
113 }
114
115 pub fn from_argv<P, I, S>(program: P, args: I) -> Self
121 where
122 P: Into<OsString>,
123 I: IntoIterator<Item = S>,
124 S: Into<OsString>,
125 {
126 Self::with_command(StreamingCommand::Argv {
127 program: program.into(),
128 args: args.into_iter().map(Into::into).collect(),
129 })
130 }
131
132 fn with_command(command: StreamingCommand) -> Self {
133 StreamingRunner {
134 command,
135 cwd: None,
136 env: None,
137 stdin_content: None,
138 kill_signal: DEFAULT_KILL_SIGNAL.to_string(),
139 exit_pump_grace_ms: DEFAULT_EXIT_PUMP_GRACE_MS,
140 }
141 }
142
143 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
145 self.cwd = Some(path.into());
146 self
147 }
148
149 pub fn env(mut self, env: HashMap<String, String>) -> Self {
151 self.env = Some(env);
152 self
153 }
154
155 pub fn stdin(mut self, content: impl Into<String>) -> Self {
157 self.stdin_content = Some(content.into());
158 self
159 }
160
161 pub fn kill_signal(mut self, signal: impl Into<String>) -> Self {
168 self.kill_signal = signal.into();
169 self
170 }
171
172 pub fn exit_pump_grace_ms(mut self, ms: u64) -> Self {
176 self.exit_pump_grace_ms = ms;
177 self
178 }
179
180 fn spawn(mut self) -> (OutputStream, JoinHandle<Result<()>>) {
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 let task = tokio::spawn(async move {
194 let result =
195 run_streaming_process(command, cwd, env, stdin_content, grace, tx, kill_rx).await;
196 if let Err(error) = &result {
197 trace_lazy("StreamingRunner", || format!("Error: {error}"));
198 }
199 result
200 });
201
202 (
203 OutputStream {
204 rx,
205 kill_tx,
206 kill_signal,
207 killed: false,
208 },
209 task,
210 )
211 }
212
213 pub fn stream(self) -> OutputStream {
215 self.spawn().0
216 }
217
218 pub async fn collect(self) -> Result<CommandResult> {
220 let mut stdout = Vec::new();
221 let mut stderr = Vec::new();
222 let mut exit_code = 0;
223
224 let (mut stream, task) = self.spawn();
225 while let Some(chunk) = stream.rx.recv().await {
226 match chunk {
227 OutputChunk::Stdout(data) => stdout.extend(data),
228 OutputChunk::Stderr(data) => stderr.extend(data),
229 OutputChunk::Exit(code) => exit_code = code,
230 }
231 }
232
233 task.await.map_err(|error| {
234 std::io::Error::other(format!("streaming process task failed: {error}"))
235 })??;
236
237 Ok(CommandResult {
238 stdout: String::from_utf8_lossy(&stdout).to_string(),
239 stderr: String::from_utf8_lossy(&stderr).to_string(),
240 code: exit_code,
241 })
242 }
243}
244
245pub struct OutputStream {
247 rx: mpsc::Receiver<OutputChunk>,
248 kill_tx: mpsc::UnboundedSender<String>,
249 kill_signal: String,
250 killed: bool,
251}
252
253impl OutputStream {
254 pub async fn next(&mut self) -> Option<OutputChunk> {
256 self.rx.recv().await
257 }
258
259 pub fn kill(&mut self) {
265 let signal = self.kill_signal.clone();
266 self.kill_with(&signal);
267 }
268
269 pub fn kill_with(&mut self, signal: &str) {
272 if self.killed {
273 return;
274 }
275 self.killed = true;
276 trace_lazy("OutputStream", || format!("kill | signal={}", signal));
277 let _ = self.kill_tx.send(signal.to_string());
280 }
281
282 pub async fn collect(mut self) -> (Vec<u8>, Vec<u8>, i32) {
284 let mut stdout = Vec::new();
285 let mut stderr = Vec::new();
286 let mut exit_code = 0;
287
288 while let Some(chunk) = self.rx.recv().await {
289 match chunk {
290 OutputChunk::Stdout(data) => stdout.extend(data),
291 OutputChunk::Stderr(data) => stderr.extend(data),
292 OutputChunk::Exit(code) => exit_code = code,
293 }
294 }
295
296 (stdout, stderr, exit_code)
297 }
298
299 pub async fn collect_stdout(mut self) -> Vec<u8> {
301 let mut stdout = Vec::new();
302
303 while let Some(chunk) = self.rx.recv().await {
304 if let OutputChunk::Stdout(data) = chunk {
305 stdout.extend(data);
306 }
307 }
308
309 stdout
310 }
311}
312
313impl Drop for OutputStream {
314 fn drop(&mut self) {
315 if !self.killed {
319 let _ = self.kill_tx.send(self.kill_signal.clone());
320 }
321 }
322}
323
324async fn run_streaming_process(
326 command: StreamingCommand,
327 cwd: Option<PathBuf>,
328 env: Option<HashMap<String, String>>,
329 stdin_content: Option<String>,
330 exit_pump_grace_ms: u64,
331 tx: mpsc::Sender<OutputChunk>,
332 mut kill_rx: mpsc::UnboundedReceiver<String>,
333) -> Result<()> {
334 trace_lazy("StreamingRunner", || match &command {
335 StreamingCommand::Shell(command) => format!("Starting: {command}"),
336 StreamingCommand::Argv { program, args } => {
337 format!("Starting argv command: {program:?} {args:?}")
338 }
339 });
340
341 let mut cmd = match command {
342 StreamingCommand::Shell(command) => {
343 let shell = find_available_shell();
344 let mut cmd = Command::new(&shell.cmd);
345 cmd.args(&shell.args);
346 crate::utils::append_shell_command(&mut cmd, &command, env.as_ref());
347 cmd
348 }
349 StreamingCommand::Argv { program, args } => {
350 let mut cmd = Command::new(program);
351 cmd.args(args);
352 cmd
353 }
354 };
355
356 if stdin_content.is_some() {
358 cmd.stdin(Stdio::piped());
359 } else {
360 cmd.stdin(Stdio::null());
361 }
362 cmd.stdout(Stdio::piped());
363 cmd.stderr(Stdio::piped());
364
365 #[cfg(unix)]
368 cmd.process_group(0);
369
370 if let Some(ref cwd) = cwd {
372 cmd.current_dir(cwd);
373 }
374
375 if let Some(ref env_vars) = env {
377 for (key, value) in env_vars {
378 cmd.env(key, value);
379 }
380 }
381
382 let mut child = cmd.spawn()?;
383
384 if let Some(content) = stdin_content {
386 if let Some(mut stdin) = child.stdin.take() {
387 use tokio::io::AsyncWriteExt;
388 let _ = stdin.write_all(content.as_bytes()).await;
389 let _ = stdin.shutdown().await;
390 }
391 }
392
393 let stdout = child.stdout.take();
395 let tx_stdout = tx.clone();
396 let stdout_handle = stdout.map(|stdout| {
397 tokio::spawn(async move {
398 let mut reader = BufReader::new(stdout);
399 let mut buf = vec![0u8; 8192];
400 loop {
401 use tokio::io::AsyncReadExt;
402 match reader.read(&mut buf).await {
403 Ok(0) => break,
404 Ok(n) => {
405 if tx_stdout
406 .send(OutputChunk::Stdout(buf[..n].to_vec()))
407 .await
408 .is_err()
409 {
410 break;
411 }
412 }
413 Err(_) => break,
414 }
415 }
416 })
417 });
418
419 let stderr = child.stderr.take();
421 let tx_stderr = tx.clone();
422 let stderr_handle = stderr.map(|stderr| {
423 tokio::spawn(async move {
424 let mut reader = BufReader::new(stderr);
425 let mut buf = vec![0u8; 8192];
426 loop {
427 use tokio::io::AsyncReadExt;
428 match reader.read(&mut buf).await {
429 Ok(0) => break,
430 Ok(n) => {
431 if tx_stderr
432 .send(OutputChunk::Stderr(buf[..n].to_vec()))
433 .await
434 .is_err()
435 {
436 break;
437 }
438 }
439 Err(_) => break,
440 }
441 }
442 })
443 });
444
445 let pid = child.id();
450 let code;
451 tokio::select! {
452 status = child.wait() => {
453 code = status_to_code(status?);
454 }
455 maybe_signal = kill_rx.recv() => {
456 let signal = maybe_signal.unwrap_or_else(|| DEFAULT_KILL_SIGNAL.to_string());
459 trace_lazy("StreamingRunner", || format!("Kill requested | signal={}", signal));
460 if let Some(pid) = pid {
461 send_signal_to_process(pid, &signal);
462 }
463 if tokio::time::timeout(Duration::from_millis(exit_pump_grace_ms), child.wait())
466 .await
467 .is_err()
468 {
469 let _ = child.start_kill();
470 let _ = child.wait().await;
471 }
472 code = 128 + signal_number(&signal);
475 }
476 }
477
478 let stdout_abort = stdout_handle.as_ref().map(|h| h.abort_handle());
482 let stderr_abort = stderr_handle.as_ref().map(|h| h.abort_handle());
483 let drain = async {
484 if let Some(handle) = stdout_handle {
485 let _ = handle.await;
486 }
487 if let Some(handle) = stderr_handle {
488 let _ = handle.await;
489 }
490 };
491 if tokio::time::timeout(Duration::from_millis(exit_pump_grace_ms), drain)
492 .await
493 .is_err()
494 {
495 if let Some(abort) = stdout_abort {
498 abort.abort();
499 }
500 if let Some(abort) = stderr_abort {
501 abort.abort();
502 }
503 }
504
505 let _ = tx.send(OutputChunk::Exit(code)).await;
507
508 trace_lazy("StreamingRunner", || format!("Exited with code: {}", code));
509
510 Ok(())
511}
512
513fn status_to_code(status: std::process::ExitStatus) -> i32 {
516 if let Some(code) = status.code() {
517 return code;
518 }
519 #[cfg(unix)]
520 {
521 use std::os::unix::process::ExitStatusExt;
522 if let Some(sig) = status.signal() {
523 return 128 + sig;
524 }
525 }
526 -1
527}
528
529fn signal_number(signal: &str) -> i32 {
532 match signal {
533 "SIGHUP" => 1,
534 "SIGINT" => 2,
535 "SIGQUIT" => 3,
536 "SIGKILL" => 9,
537 "SIGUSR1" => 10,
538 "SIGUSR2" => 12,
539 "SIGTERM" => 15,
540 _ => 15,
541 }
542}
543
544#[cfg(unix)]
546fn send_signal_to_process(pid: u32, signal: &str) {
547 use nix::sys::signal::{kill, Signal};
548 use nix::unistd::Pid;
549
550 let sig = match signal {
551 "SIGHUP" => Signal::SIGHUP,
552 "SIGINT" => Signal::SIGINT,
553 "SIGQUIT" => Signal::SIGQUIT,
554 "SIGKILL" => Signal::SIGKILL,
555 "SIGUSR1" => Signal::SIGUSR1,
556 "SIGUSR2" => Signal::SIGUSR2,
557 "SIGTERM" => Signal::SIGTERM,
558 _ => Signal::SIGTERM,
559 };
560
561 let _ = kill(Pid::from_raw(pid as i32), sig);
563 let _ = kill(Pid::from_raw(-(pid as i32)), sig);
565}
566
567#[cfg(not(unix))]
570fn send_signal_to_process(_pid: u32, _signal: &str) {}
571
572#[derive(Debug, Clone)]
574struct ShellConfig {
575 cmd: String,
576 args: Vec<String>,
577}
578
579fn find_available_shell() -> ShellConfig {
581 let is_windows = cfg!(windows);
582
583 if is_windows {
584 ShellConfig {
585 cmd: "cmd.exe".to_string(),
586 args: vec!["/c".to_string()],
587 }
588 } else {
589 let shells = [
590 ("/bin/sh", "-c"),
591 ("/usr/bin/sh", "-c"),
592 ("/bin/bash", "-c"),
593 ];
594
595 for (cmd, arg) in shells {
596 if std::path::Path::new(cmd).exists() {
597 return ShellConfig {
598 cmd: cmd.to_string(),
599 args: vec![arg.to_string()],
600 };
601 }
602 }
603
604 ShellConfig {
605 cmd: "/bin/sh".to_string(),
606 args: vec!["-c".to_string()],
607 }
608 }
609}
610
611#[async_trait::async_trait]
613pub trait AsyncIterator {
614 type Item;
615
616 async fn next(&mut self) -> Option<Self::Item>;
618}
619
620#[async_trait::async_trait]
621impl AsyncIterator for OutputStream {
622 type Item = OutputChunk;
623
624 async fn next(&mut self) -> Option<Self::Item> {
625 self.rx.recv().await
626 }
627}
628
629pub trait IntoStream {
631 fn into_stream(self) -> OutputStream;
633}
634
635impl IntoStream for crate::ProcessRunner {
636 fn into_stream(self) -> OutputStream {
637 let streaming = StreamingRunner::new(self.command().to_string());
638 streaming.stream()
639 }
640}