1use std::cfg_select;
9use std::ffi::{OsStr, OsString};
10use std::io;
11use std::path::PathBuf;
12use std::process::{ExitStatus, Output, Stdio};
13
14use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
15use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
16
17pub mod platform;
22
23cfg_select! {
26 target_os = "windows" => {
27 mod platform_win;
28 pub(crate) use platform_win as platform_imp;
29 }
30 target_os = "linux" => {
31 mod platform_linux;
32 pub(crate) use platform_linux as platform_imp;
33 }
34 target_os = "macos" => {
35 mod platform_macos;
36 pub(crate) use platform_macos as platform_imp;
37 }
38}
39
40pub use platform_imp::{
44 assign_child_to_windows_job, cancel_capture_reader, canonical_environment_pairs,
45 capture_reader_done, compat_shell_command, configure_exact_trace, configure_process_command,
46 configure_sync_contained_command, configure_sync_daemon_command, configure_trampoline_command,
47 current_executable_build_id, exact_trace_capability, exit_code, kill_tree,
48 monitor_console_windows, parent_has_console, prepare_capture_reader, process_snapshot,
49 process_snapshot_for_pid, set_process_name, shell_command, soft_terminate_process_group,
50 spawn_sync, spawn_sync_daemon, start_descendant_monitor, start_exact_trace,
51 sync_child_native_handle, trampoline_exit_code, unix_mark_extra_fds_close_on_exec,
52 unix_set_priority, unix_signal_process, unix_signal_process_group, unix_signal_raw,
53 CaptureCancellation, TracedChild, WindowsJobHandle,
54};
55
56#[cfg(feature = "session-relay")]
57pub use platform_imp::relay_local_socket_session;
58
59pub fn configure_compat_tokio_command(
64 command: &mut Command,
65 show_console: bool,
66 kill_when_owner_dies: bool,
67) -> io::Result<()> {
68 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
69}
70
71pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) {
73 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum StreamMode {
79 Inherit,
81 Piped,
83 Null,
85}
86
87impl StreamMode {
88 fn apply(self) -> Stdio {
89 match self {
90 Self::Inherit => Stdio::inherit(),
91 Self::Piped => Stdio::piped(),
92 Self::Null => Stdio::null(),
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct SpawnSpec {
100 program: OsString,
101 args: Vec<OsString>,
102 current_dir: Option<PathBuf>,
103 env: Vec<(OsString, OsString)>,
104 clear_env: bool,
105 stdin: StreamMode,
106 stdout: StreamMode,
107 stderr: StreamMode,
108 create_process_group: bool,
109 kill_when_owner_dies: bool,
110}
111
112impl SpawnSpec {
113 pub fn new(program: impl Into<OsString>) -> Self {
115 Self {
116 program: program.into(),
117 args: Vec::new(),
118 current_dir: None,
119 env: Vec::new(),
120 clear_env: false,
121 stdin: StreamMode::Inherit,
122 stdout: StreamMode::Inherit,
123 stderr: StreamMode::Inherit,
124 create_process_group: false,
125 kill_when_owner_dies: false,
126 }
127 }
128
129 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
131 self.args.push(arg.into());
132 self
133 }
134
135 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
137 self.current_dir = Some(path.into());
138 self
139 }
140
141 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
143 self.env.push((key.into(), value.into()));
144 self
145 }
146
147 pub fn clear_env(mut self, clear: bool) -> Self {
149 self.clear_env = clear;
150 self
151 }
152
153 pub fn stdin(mut self, mode: StreamMode) -> Self {
155 self.stdin = mode;
156 self
157 }
158
159 pub fn stdout(mut self, mode: StreamMode) -> Self {
161 self.stdout = mode;
162 self
163 }
164
165 pub fn stderr(mut self, mode: StreamMode) -> Self {
167 self.stderr = mode;
168 self
169 }
170
171 pub fn create_process_group(mut self, create: bool) -> Self {
180 self.create_process_group = create;
181 self
182 }
183
184 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
191 self.kill_when_owner_dies = kill;
192 self
193 }
194
195 pub async fn spawn(self) -> io::Result<PlatformChild> {
197 let mut command = Command::new(&self.program);
198 command.args(&self.args);
199 if let Some(current_dir) = self.current_dir.as_deref() {
200 command.current_dir(current_dir);
201 }
202 if self.clear_env {
203 command.env_clear();
204 }
205 for (key, value) in &self.env {
206 command.env(key, value);
207 }
208 command
209 .stdin(self.stdin.apply())
210 .stdout(self.stdout.apply())
211 .stderr(self.stderr.apply());
212 platform_imp::configure_command(
213 &mut command,
214 self.create_process_group,
215 self.kill_when_owner_dies,
216 )?;
217
218 let child = command.spawn()?;
219 platform_imp::after_spawn(&child, self.kill_when_owner_dies);
220 Ok(PlatformChild::new(child, self.create_process_group))
221 }
222}
223
224pub struct PlatformChild {
226 child: Child,
227 stdin: Option<ChildStdin>,
228 stdout: Option<ChildStdout>,
229 stderr: Option<ChildStderr>,
230 signal: PlatformEmergencySignal,
231}
232
233impl PlatformChild {
234 fn new(mut child: Child, own_process_group: bool) -> Self {
235 let signal = PlatformEmergencySignal {
236 pid: child.id(),
237 own_process_group,
238 };
239 Self {
240 stdin: child.stdin.take(),
241 stdout: child.stdout.take(),
242 stderr: child.stderr.take(),
243 child,
244 signal,
245 }
246 }
247
248 pub fn id(&self) -> Option<u32> {
250 self.child.id()
251 }
252
253 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
255 self.child.wait().await
256 }
257
258 pub async fn kill(&mut self) -> io::Result<()> {
260 self.child.kill().await
261 }
262
263 pub async fn wait_with_output(self) -> io::Result<Output> {
265 let Self {
266 mut child,
267 stdin,
268 stdout,
269 stderr,
270 ..
271 } = self;
272 drop(stdin);
275 let (status, stdout, stderr) = tokio::try_join!(
276 child.wait(),
277 read_owned_to_end(stdout),
278 read_owned_to_end(stderr),
279 )?;
280 Ok(Output {
281 status,
282 stdout,
283 stderr,
284 })
285 }
286
287 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
289 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
290 stdin.write_all(bytes).await?;
291 stdin.flush().await
292 }
293
294 pub fn close_stdin(&mut self) {
299 drop(self.stdin.take());
300 }
301
302 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
304 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
305 let mut bytes = Vec::new();
306 stdout.read_to_end(&mut bytes).await?;
307 Ok(bytes)
308 }
309
310 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
312 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
313 let mut bytes = Vec::new();
314 stderr.read_to_end(&mut bytes).await?;
315 Ok(bytes)
316 }
317
318 pub fn into_actor_parts(
324 self,
325 ) -> (
326 PlatformLifecycle,
327 PlatformEmergencySignal,
328 Option<PlatformStdin>,
329 Option<PlatformOutput>,
330 Option<PlatformOutput>,
331 ) {
332 (
333 PlatformLifecycle { child: self.child },
334 self.signal,
335 self.stdin.map(|stdin| PlatformStdin { stdin }),
336 self.stdout.map(PlatformOutput::stdout),
337 self.stderr.map(PlatformOutput::stderr),
338 )
339 }
340}
341
342pub struct PlatformLifecycle {
344 child: Child,
345}
346
347impl PlatformLifecycle {
348 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
350 self.child.wait().await
351 }
352}
353
354pub struct PlatformEmergencySignal {
359 pid: Option<u32>,
360 own_process_group: bool,
361}
362
363impl PlatformEmergencySignal {
364 pub fn kill(&self) -> io::Result<()> {
366 platform_imp::signal_process(self.target()?)
367 }
368
369 pub fn terminate_group_soft(&self) -> io::Result<bool> {
378 if !self.own_process_group {
379 return Ok(false);
380 }
381 platform_imp::signal_process_group(self.target()?).map(|()| true)
382 }
383
384 fn target(&self) -> io::Result<u32> {
385 self.pid.ok_or_else(|| {
386 io::Error::new(
387 io::ErrorKind::BrokenPipe,
388 "child process no longer has an emergency signal target",
389 )
390 })
391 }
392}
393
394pub struct PlatformStdin {
396 stdin: ChildStdin,
397}
398
399impl PlatformStdin {
400 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
402 self.stdin.write_all(bytes).await?;
403 self.stdin.flush().await
404 }
405}
406
407pub struct PlatformOutput {
409 reader: OutputReader,
410}
411
412enum OutputReader {
413 Stdout(ChildStdout),
414 Stderr(ChildStderr),
415}
416
417impl PlatformOutput {
418 fn stdout(stdout: ChildStdout) -> Self {
419 Self {
420 reader: OutputReader::Stdout(stdout),
421 }
422 }
423
424 fn stderr(stderr: ChildStderr) -> Self {
425 Self {
426 reader: OutputReader::Stderr(stderr),
427 }
428 }
429
430 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
432 match self.reader {
433 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
434 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
435 }
436 }
437
438 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
443 match &mut self.reader {
444 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
445 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
446 }
447 }
448}
449
450fn stdin_not_piped() -> io::Error {
451 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
452}
453
454fn stdout_not_piped() -> io::Error {
455 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
456}
457
458fn stderr_not_piped() -> io::Error {
459 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
460}
461
462async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
463where
464 R: AsyncRead + Unpin,
465{
466 let Some(mut reader) = reader else {
467 return Ok(Vec::new());
468 };
469 let mut bytes = Vec::new();
470 reader.read_to_end(&mut bytes).await?;
471 Ok(bytes)
472}
473
474pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
476 platform_imp::shell_spec(command.as_ref())
477}
478
479#[cfg(test)]
480mod tests {
481 use super::{shell_spec, SpawnSpec, StreamMode};
482
483 fn fixture_command() -> SpawnSpec {
484 #[cfg(windows)]
485 {
486 shell_spec("echo async-platform-internal")
487 }
488 #[cfg(not(windows))]
489 {
490 shell_spec("printf async-platform-internal")
491 }
492 }
493
494 #[tokio::test]
495 async fn blessed_spawn_captures_output_without_sync_wait() {
496 let output = fixture_command()
497 .stdout(StreamMode::Piped)
498 .stderr(StreamMode::Piped)
499 .spawn()
500 .await
501 .expect("spawn")
502 .wait_with_output()
503 .await
504 .expect("wait with output");
505
506 assert!(output.status.success());
507 let expected = if cfg!(windows) {
508 b"async-platform-internal\r\n".as_slice()
509 } else {
510 b"async-platform-internal".as_slice()
511 };
512 assert_eq!(output.stdout, expected);
513 assert!(output.stderr.is_empty());
514 }
515
516 #[tokio::test]
517 async fn blessed_spawn_reports_missing_program() {
518 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
519 .spawn()
520 .await;
521 assert!(result.is_err());
522 }
523
524 #[tokio::test]
525 async fn one_shot_output_closes_owned_stdin() {
526 #[cfg(windows)]
527 let spec = shell_spec("more > nul & echo done");
528 #[cfg(not(windows))]
529 let spec = shell_spec("cat > /dev/null; printf done");
530
531 let output = tokio::time::timeout(
532 std::time::Duration::from_secs(2),
533 spec.stdin(StreamMode::Piped)
534 .stdout(StreamMode::Piped)
535 .stderr(StreamMode::Piped)
536 .spawn()
537 .await
538 .expect("spawn")
539 .wait_with_output(),
540 )
541 .await
542 .expect("stdin is closed for one-shot output")
543 .expect("output succeeds");
544
545 let expected = if cfg!(windows) {
546 b"done\r\n".as_slice()
547 } else {
548 b"done".as_slice()
549 };
550 assert_eq!(output.stdout, expected);
551 }
552}