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 = "ipc")]
60pub use platform_imp::{
61 ipc_current_user_id, IpcEndpoint, IpcListener, IpcListenerNonblockingMode, IpcPeerIdentity,
62 IpcStream,
63};
64
65#[cfg(feature = "ipc-async")]
66pub use platform_imp::{IpcAsyncListener, IpcAsyncStream};
67
68#[cfg(feature = "pty")]
69pub use platform_imp::terminal::{
70 before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
71 input_payload, is_ignorable_process_control_error, kill_pty_process_group, preferred_pty_pid,
72 prepare_pty_child, query_responses, resize_pty, send_pty_interrupt, shell_argv,
73 signal_pty_tree, terminate_pty_child, wait_before_pty_close_supported, Backend,
74 ChildProcessInfo, ConPtyBackendKind, OrphanConhostInfo, PtyProcessGuard, PtySpawnContext,
75 TerminalInputSession,
76};
77
78#[cfg(feature = "session-relay")]
79pub use platform_imp::relay_local_socket_session;
80
81pub fn configure_compat_tokio_command(
86 command: &mut Command,
87 show_console: bool,
88 kill_when_owner_dies: bool,
89) -> io::Result<()> {
90 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
91}
92
93pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) {
95 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum StreamMode {
101 Inherit,
103 Piped,
105 Null,
107}
108
109impl StreamMode {
110 fn apply(self) -> Stdio {
111 match self {
112 Self::Inherit => Stdio::inherit(),
113 Self::Piped => Stdio::piped(),
114 Self::Null => Stdio::null(),
115 }
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct SpawnSpec {
122 program: OsString,
123 args: Vec<OsString>,
124 current_dir: Option<PathBuf>,
125 env: Vec<(OsString, OsString)>,
126 clear_env: bool,
127 stdin: StreamMode,
128 stdout: StreamMode,
129 stderr: StreamMode,
130 create_process_group: bool,
131 kill_when_owner_dies: bool,
132}
133
134impl SpawnSpec {
135 pub fn new(program: impl Into<OsString>) -> Self {
137 Self {
138 program: program.into(),
139 args: Vec::new(),
140 current_dir: None,
141 env: Vec::new(),
142 clear_env: false,
143 stdin: StreamMode::Inherit,
144 stdout: StreamMode::Inherit,
145 stderr: StreamMode::Inherit,
146 create_process_group: false,
147 kill_when_owner_dies: false,
148 }
149 }
150
151 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
153 self.args.push(arg.into());
154 self
155 }
156
157 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
159 self.current_dir = Some(path.into());
160 self
161 }
162
163 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
165 self.env.push((key.into(), value.into()));
166 self
167 }
168
169 pub fn clear_env(mut self, clear: bool) -> Self {
171 self.clear_env = clear;
172 self
173 }
174
175 pub fn stdin(mut self, mode: StreamMode) -> Self {
177 self.stdin = mode;
178 self
179 }
180
181 pub fn stdout(mut self, mode: StreamMode) -> Self {
183 self.stdout = mode;
184 self
185 }
186
187 pub fn stderr(mut self, mode: StreamMode) -> Self {
189 self.stderr = mode;
190 self
191 }
192
193 pub fn create_process_group(mut self, create: bool) -> Self {
202 self.create_process_group = create;
203 self
204 }
205
206 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
213 self.kill_when_owner_dies = kill;
214 self
215 }
216
217 pub async fn spawn(self) -> io::Result<PlatformChild> {
219 let mut command = Command::new(&self.program);
220 command.args(&self.args);
221 if let Some(current_dir) = self.current_dir.as_deref() {
222 command.current_dir(current_dir);
223 }
224 if self.clear_env {
225 command.env_clear();
226 }
227 for (key, value) in &self.env {
228 command.env(key, value);
229 }
230 command
231 .stdin(self.stdin.apply())
232 .stdout(self.stdout.apply())
233 .stderr(self.stderr.apply());
234 platform_imp::configure_command(
235 &mut command,
236 self.create_process_group,
237 self.kill_when_owner_dies,
238 )?;
239
240 let child = command.spawn()?;
241 platform_imp::after_spawn(&child, self.kill_when_owner_dies);
242 Ok(PlatformChild::new(child, self.create_process_group))
243 }
244}
245
246pub struct PlatformChild {
248 child: Child,
249 stdin: Option<ChildStdin>,
250 stdout: Option<ChildStdout>,
251 stderr: Option<ChildStderr>,
252 signal: PlatformEmergencySignal,
253}
254
255impl PlatformChild {
256 fn new(mut child: Child, own_process_group: bool) -> Self {
257 let signal = PlatformEmergencySignal {
258 pid: child.id(),
259 own_process_group,
260 };
261 Self {
262 stdin: child.stdin.take(),
263 stdout: child.stdout.take(),
264 stderr: child.stderr.take(),
265 child,
266 signal,
267 }
268 }
269
270 pub fn id(&self) -> Option<u32> {
272 self.child.id()
273 }
274
275 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
277 self.child.wait().await
278 }
279
280 pub async fn kill(&mut self) -> io::Result<()> {
282 self.child.kill().await
283 }
284
285 pub async fn wait_with_output(self) -> io::Result<Output> {
287 let Self {
288 mut child,
289 stdin,
290 stdout,
291 stderr,
292 ..
293 } = self;
294 drop(stdin);
297 let (status, stdout, stderr) = tokio::try_join!(
298 child.wait(),
299 read_owned_to_end(stdout),
300 read_owned_to_end(stderr),
301 )?;
302 Ok(Output {
303 status,
304 stdout,
305 stderr,
306 })
307 }
308
309 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
311 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
312 stdin.write_all(bytes).await?;
313 stdin.flush().await
314 }
315
316 pub fn close_stdin(&mut self) {
321 drop(self.stdin.take());
322 }
323
324 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
326 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
327 let mut bytes = Vec::new();
328 stdout.read_to_end(&mut bytes).await?;
329 Ok(bytes)
330 }
331
332 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
334 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
335 let mut bytes = Vec::new();
336 stderr.read_to_end(&mut bytes).await?;
337 Ok(bytes)
338 }
339
340 pub fn into_actor_parts(
346 self,
347 ) -> (
348 PlatformLifecycle,
349 PlatformEmergencySignal,
350 Option<PlatformStdin>,
351 Option<PlatformOutput>,
352 Option<PlatformOutput>,
353 ) {
354 (
355 PlatformLifecycle { child: self.child },
356 self.signal,
357 self.stdin.map(|stdin| PlatformStdin { stdin }),
358 self.stdout.map(PlatformOutput::stdout),
359 self.stderr.map(PlatformOutput::stderr),
360 )
361 }
362}
363
364pub struct PlatformLifecycle {
366 child: Child,
367}
368
369impl PlatformLifecycle {
370 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
372 self.child.wait().await
373 }
374}
375
376pub struct PlatformEmergencySignal {
381 pid: Option<u32>,
382 own_process_group: bool,
383}
384
385impl PlatformEmergencySignal {
386 pub fn kill(&self) -> io::Result<()> {
388 platform_imp::signal_process(self.target()?)
389 }
390
391 pub fn terminate_group_soft(&self) -> io::Result<bool> {
400 if !self.own_process_group {
401 return Ok(false);
402 }
403 platform_imp::signal_process_group(self.target()?).map(|()| true)
404 }
405
406 fn target(&self) -> io::Result<u32> {
407 self.pid.ok_or_else(|| {
408 io::Error::new(
409 io::ErrorKind::BrokenPipe,
410 "child process no longer has an emergency signal target",
411 )
412 })
413 }
414}
415
416pub struct PlatformStdin {
418 stdin: ChildStdin,
419}
420
421impl PlatformStdin {
422 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
424 self.stdin.write_all(bytes).await?;
425 self.stdin.flush().await
426 }
427}
428
429pub struct PlatformOutput {
431 reader: OutputReader,
432}
433
434enum OutputReader {
435 Stdout(ChildStdout),
436 Stderr(ChildStderr),
437}
438
439impl PlatformOutput {
440 fn stdout(stdout: ChildStdout) -> Self {
441 Self {
442 reader: OutputReader::Stdout(stdout),
443 }
444 }
445
446 fn stderr(stderr: ChildStderr) -> Self {
447 Self {
448 reader: OutputReader::Stderr(stderr),
449 }
450 }
451
452 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
454 match self.reader {
455 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
456 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
457 }
458 }
459
460 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
465 match &mut self.reader {
466 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
467 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
468 }
469 }
470}
471
472fn stdin_not_piped() -> io::Error {
473 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
474}
475
476fn stdout_not_piped() -> io::Error {
477 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
478}
479
480fn stderr_not_piped() -> io::Error {
481 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
482}
483
484async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
485where
486 R: AsyncRead + Unpin,
487{
488 let Some(mut reader) = reader else {
489 return Ok(Vec::new());
490 };
491 let mut bytes = Vec::new();
492 reader.read_to_end(&mut bytes).await?;
493 Ok(bytes)
494}
495
496pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
498 platform_imp::shell_spec(command.as_ref())
499}
500
501#[cfg(test)]
502mod tests {
503 use super::{shell_spec, SpawnSpec, StreamMode};
504
505 fn fixture_command() -> SpawnSpec {
506 #[cfg(windows)]
507 {
508 shell_spec("echo async-platform-internal")
509 }
510 #[cfg(not(windows))]
511 {
512 shell_spec("printf async-platform-internal")
513 }
514 }
515
516 #[tokio::test]
517 async fn blessed_spawn_captures_output_without_sync_wait() {
518 let output = fixture_command()
519 .stdout(StreamMode::Piped)
520 .stderr(StreamMode::Piped)
521 .spawn()
522 .await
523 .expect("spawn")
524 .wait_with_output()
525 .await
526 .expect("wait with output");
527
528 assert!(output.status.success());
529 let expected = if cfg!(windows) {
530 b"async-platform-internal\r\n".as_slice()
531 } else {
532 b"async-platform-internal".as_slice()
533 };
534 assert_eq!(output.stdout, expected);
535 assert!(output.stderr.is_empty());
536 }
537
538 #[tokio::test]
539 async fn blessed_spawn_reports_missing_program() {
540 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
541 .spawn()
542 .await;
543 assert!(result.is_err());
544 }
545
546 #[tokio::test]
547 async fn one_shot_output_closes_owned_stdin() {
548 #[cfg(windows)]
549 let spec = shell_spec("more > nul & echo done");
550 #[cfg(not(windows))]
551 let spec = shell_spec("cat > /dev/null; printf done");
552
553 let output = tokio::time::timeout(
554 std::time::Duration::from_secs(2),
555 spec.stdin(StreamMode::Piped)
556 .stdout(StreamMode::Piped)
557 .stderr(StreamMode::Piped)
558 .spawn()
559 .await
560 .expect("spawn")
561 .wait_with_output(),
562 )
563 .await
564 .expect("stdin is closed for one-shot output")
565 .expect("output succeeds");
566
567 let expected = if cfg!(windows) {
568 b"done\r\n".as_slice()
569 } else {
570 b"done".as_slice()
571 };
572 assert_eq!(output.stdout, expected);
573 }
574}