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 active_graphics_probe, assign_child_to_windows_job, cancel_capture_reader,
45 canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace,
46 configure_process_command, configure_sync_contained_command, configure_sync_daemon_command,
47 configure_trampoline_command, current_executable_build_id, exact_trace_capability, exit_code,
48 kill_tree, monitor_console_windows, parent_has_console, prepare_capture_reader,
49 process_snapshot, process_snapshot_for_pid, set_process_name, set_window_icon_impl,
50 shell_command, soft_terminate_process_group, spawn_sync, spawn_sync_daemon,
51 start_descendant_monitor, start_exact_trace, sync_child_native_handle, trampoline_exit_code,
52 unix_mark_extra_fds_close_on_exec, unix_set_priority, unix_signal_process,
53 unix_signal_process_group, unix_signal_raw, window_icon_support_impl, CaptureCancellation,
54 TracedChild, WindowsJobHandle,
55};
56
57pub use platform_imp::terminal_input;
58
59#[cfg(feature = "pty")]
60pub use platform_imp::terminal::{
61 before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
62 input_payload, is_ignorable_process_control_error, kill_pty_process_group, preferred_pty_pid,
63 prepare_pty_child, query_responses, resize_pty, send_pty_interrupt, shell_argv,
64 signal_pty_tree, terminate_pty_child, wait_before_pty_close_supported, Backend,
65 ChildProcessInfo, ConPtyBackendKind, OrphanConhostInfo, PtyProcessGuard, PtySpawnContext,
66 TerminalInputSession,
67};
68
69#[cfg(feature = "session-relay")]
70pub use platform_imp::relay_local_socket_session;
71
72pub fn configure_compat_tokio_command(
77 command: &mut Command,
78 show_console: bool,
79 kill_when_owner_dies: bool,
80) -> io::Result<()> {
81 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
82}
83
84pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) {
86 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum StreamMode {
92 Inherit,
94 Piped,
96 Null,
98}
99
100impl StreamMode {
101 fn apply(self) -> Stdio {
102 match self {
103 Self::Inherit => Stdio::inherit(),
104 Self::Piped => Stdio::piped(),
105 Self::Null => Stdio::null(),
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct SpawnSpec {
113 program: OsString,
114 args: Vec<OsString>,
115 current_dir: Option<PathBuf>,
116 env: Vec<(OsString, OsString)>,
117 clear_env: bool,
118 stdin: StreamMode,
119 stdout: StreamMode,
120 stderr: StreamMode,
121 create_process_group: bool,
122 kill_when_owner_dies: bool,
123}
124
125impl SpawnSpec {
126 pub fn new(program: impl Into<OsString>) -> Self {
128 Self {
129 program: program.into(),
130 args: Vec::new(),
131 current_dir: None,
132 env: Vec::new(),
133 clear_env: false,
134 stdin: StreamMode::Inherit,
135 stdout: StreamMode::Inherit,
136 stderr: StreamMode::Inherit,
137 create_process_group: false,
138 kill_when_owner_dies: false,
139 }
140 }
141
142 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
144 self.args.push(arg.into());
145 self
146 }
147
148 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
150 self.current_dir = Some(path.into());
151 self
152 }
153
154 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
156 self.env.push((key.into(), value.into()));
157 self
158 }
159
160 pub fn clear_env(mut self, clear: bool) -> Self {
162 self.clear_env = clear;
163 self
164 }
165
166 pub fn stdin(mut self, mode: StreamMode) -> Self {
168 self.stdin = mode;
169 self
170 }
171
172 pub fn stdout(mut self, mode: StreamMode) -> Self {
174 self.stdout = mode;
175 self
176 }
177
178 pub fn stderr(mut self, mode: StreamMode) -> Self {
180 self.stderr = mode;
181 self
182 }
183
184 pub fn create_process_group(mut self, create: bool) -> Self {
193 self.create_process_group = create;
194 self
195 }
196
197 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
204 self.kill_when_owner_dies = kill;
205 self
206 }
207
208 pub async fn spawn(self) -> io::Result<PlatformChild> {
210 let mut command = Command::new(&self.program);
211 command.args(&self.args);
212 if let Some(current_dir) = self.current_dir.as_deref() {
213 command.current_dir(current_dir);
214 }
215 if self.clear_env {
216 command.env_clear();
217 }
218 for (key, value) in &self.env {
219 command.env(key, value);
220 }
221 command
222 .stdin(self.stdin.apply())
223 .stdout(self.stdout.apply())
224 .stderr(self.stderr.apply());
225 platform_imp::configure_command(
226 &mut command,
227 self.create_process_group,
228 self.kill_when_owner_dies,
229 )?;
230
231 let child = command.spawn()?;
232 platform_imp::after_spawn(&child, self.kill_when_owner_dies);
233 Ok(PlatformChild::new(child, self.create_process_group))
234 }
235}
236
237pub struct PlatformChild {
239 child: Child,
240 stdin: Option<ChildStdin>,
241 stdout: Option<ChildStdout>,
242 stderr: Option<ChildStderr>,
243 signal: PlatformEmergencySignal,
244}
245
246impl PlatformChild {
247 fn new(mut child: Child, own_process_group: bool) -> Self {
248 let signal = PlatformEmergencySignal {
249 pid: child.id(),
250 own_process_group,
251 };
252 Self {
253 stdin: child.stdin.take(),
254 stdout: child.stdout.take(),
255 stderr: child.stderr.take(),
256 child,
257 signal,
258 }
259 }
260
261 pub fn id(&self) -> Option<u32> {
263 self.child.id()
264 }
265
266 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
268 self.child.wait().await
269 }
270
271 pub async fn kill(&mut self) -> io::Result<()> {
273 self.child.kill().await
274 }
275
276 pub async fn wait_with_output(self) -> io::Result<Output> {
278 let Self {
279 mut child,
280 stdin,
281 stdout,
282 stderr,
283 ..
284 } = self;
285 drop(stdin);
288 let (status, stdout, stderr) = tokio::try_join!(
289 child.wait(),
290 read_owned_to_end(stdout),
291 read_owned_to_end(stderr),
292 )?;
293 Ok(Output {
294 status,
295 stdout,
296 stderr,
297 })
298 }
299
300 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
302 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
303 stdin.write_all(bytes).await?;
304 stdin.flush().await
305 }
306
307 pub fn close_stdin(&mut self) {
312 drop(self.stdin.take());
313 }
314
315 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
317 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
318 let mut bytes = Vec::new();
319 stdout.read_to_end(&mut bytes).await?;
320 Ok(bytes)
321 }
322
323 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
325 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
326 let mut bytes = Vec::new();
327 stderr.read_to_end(&mut bytes).await?;
328 Ok(bytes)
329 }
330
331 pub fn into_actor_parts(
337 self,
338 ) -> (
339 PlatformLifecycle,
340 PlatformEmergencySignal,
341 Option<PlatformStdin>,
342 Option<PlatformOutput>,
343 Option<PlatformOutput>,
344 ) {
345 (
346 PlatformLifecycle { child: self.child },
347 self.signal,
348 self.stdin.map(|stdin| PlatformStdin { stdin }),
349 self.stdout.map(PlatformOutput::stdout),
350 self.stderr.map(PlatformOutput::stderr),
351 )
352 }
353}
354
355pub struct PlatformLifecycle {
357 child: Child,
358}
359
360impl PlatformLifecycle {
361 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
363 self.child.wait().await
364 }
365}
366
367pub struct PlatformEmergencySignal {
372 pid: Option<u32>,
373 own_process_group: bool,
374}
375
376impl PlatformEmergencySignal {
377 pub fn kill(&self) -> io::Result<()> {
379 platform_imp::signal_process(self.target()?)
380 }
381
382 pub fn terminate_group_soft(&self) -> io::Result<bool> {
391 if !self.own_process_group {
392 return Ok(false);
393 }
394 platform_imp::signal_process_group(self.target()?).map(|()| true)
395 }
396
397 fn target(&self) -> io::Result<u32> {
398 self.pid.ok_or_else(|| {
399 io::Error::new(
400 io::ErrorKind::BrokenPipe,
401 "child process no longer has an emergency signal target",
402 )
403 })
404 }
405}
406
407pub struct PlatformStdin {
409 stdin: ChildStdin,
410}
411
412impl PlatformStdin {
413 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
415 self.stdin.write_all(bytes).await?;
416 self.stdin.flush().await
417 }
418}
419
420pub struct PlatformOutput {
422 reader: OutputReader,
423}
424
425enum OutputReader {
426 Stdout(ChildStdout),
427 Stderr(ChildStderr),
428}
429
430impl PlatformOutput {
431 fn stdout(stdout: ChildStdout) -> Self {
432 Self {
433 reader: OutputReader::Stdout(stdout),
434 }
435 }
436
437 fn stderr(stderr: ChildStderr) -> Self {
438 Self {
439 reader: OutputReader::Stderr(stderr),
440 }
441 }
442
443 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
445 match self.reader {
446 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
447 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
448 }
449 }
450
451 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
456 match &mut self.reader {
457 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
458 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
459 }
460 }
461}
462
463fn stdin_not_piped() -> io::Error {
464 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
465}
466
467fn stdout_not_piped() -> io::Error {
468 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
469}
470
471fn stderr_not_piped() -> io::Error {
472 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
473}
474
475async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
476where
477 R: AsyncRead + Unpin,
478{
479 let Some(mut reader) = reader else {
480 return Ok(Vec::new());
481 };
482 let mut bytes = Vec::new();
483 reader.read_to_end(&mut bytes).await?;
484 Ok(bytes)
485}
486
487pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
489 platform_imp::shell_spec(command.as_ref())
490}
491
492#[cfg(test)]
493mod tests {
494 use super::{shell_spec, SpawnSpec, StreamMode};
495
496 fn fixture_command() -> SpawnSpec {
497 #[cfg(windows)]
498 {
499 shell_spec("echo async-platform-internal")
500 }
501 #[cfg(not(windows))]
502 {
503 shell_spec("printf async-platform-internal")
504 }
505 }
506
507 #[tokio::test]
508 async fn blessed_spawn_captures_output_without_sync_wait() {
509 let output = fixture_command()
510 .stdout(StreamMode::Piped)
511 .stderr(StreamMode::Piped)
512 .spawn()
513 .await
514 .expect("spawn")
515 .wait_with_output()
516 .await
517 .expect("wait with output");
518
519 assert!(output.status.success());
520 let expected = if cfg!(windows) {
521 b"async-platform-internal\r\n".as_slice()
522 } else {
523 b"async-platform-internal".as_slice()
524 };
525 assert_eq!(output.stdout, expected);
526 assert!(output.stderr.is_empty());
527 }
528
529 #[tokio::test]
530 async fn blessed_spawn_reports_missing_program() {
531 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
532 .spawn()
533 .await;
534 assert!(result.is_err());
535 }
536
537 #[tokio::test]
538 async fn one_shot_output_closes_owned_stdin() {
539 #[cfg(windows)]
540 let spec = shell_spec("more > nul & echo done");
541 #[cfg(not(windows))]
542 let spec = shell_spec("cat > /dev/null; printf done");
543
544 let output = tokio::time::timeout(
545 std::time::Duration::from_secs(2),
546 spec.stdin(StreamMode::Piped)
547 .stdout(StreamMode::Piped)
548 .stderr(StreamMode::Piped)
549 .spawn()
550 .await
551 .expect("spawn")
552 .wait_with_output(),
553 )
554 .await
555 .expect("stdin is closed for one-shot output")
556 .expect("output succeeds");
557
558 let expected = if cfg!(windows) {
559 b"done\r\n".as_slice()
560 } else {
561 b"done".as_slice()
562 };
563 assert_eq!(output.stdout, expected);
564 }
565}