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
23#[cfg(feature = "pty")]
29#[doc(hidden)]
30pub use portable_pty as portable_pty_compat;
31
32cfg_select! {
35 target_os = "windows" => {
36 mod platform_win;
37 pub(crate) use platform_win as platform_imp;
38 }
39 target_os = "linux" => {
40 mod platform_linux;
41 pub(crate) use platform_linux as platform_imp;
42 }
43 target_os = "macos" => {
44 mod platform_macos;
45 pub(crate) use platform_macos as platform_imp;
46 }
47}
48
49pub use platform_imp::{
53 active_graphics_probe, assign_child_to_windows_job, cancel_capture_reader,
54 canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace,
55 configure_process_command, configure_sync_contained_command, configure_sync_daemon_command,
56 configure_trampoline_command, current_executable_build_id, exact_trace_capability, exit_code,
57 kill_tree, monitor_console_windows, parent_has_console, prepare_capture_reader,
58 process_snapshot, process_snapshot_for_pid, set_process_name, set_window_icon_impl,
59 shell_command, soft_terminate_process_group, spawn_sync, spawn_sync_daemon,
60 start_descendant_monitor, start_exact_trace, sync_child_native_handle, trampoline_exit_code,
61 unix_mark_extra_fds_close_on_exec, unix_set_priority, unix_signal_process,
62 unix_signal_process_group, unix_signal_raw, window_icon_support_impl, CaptureCancellation,
63 TracedChild, WindowsJobHandle,
64};
65
66pub use platform_imp::terminal_input;
67
68#[cfg(feature = "ipc")]
69pub use platform_imp::{
70 ipc_current_user_id, IpcEndpoint, IpcListener, IpcListenerNonblockingMode, IpcPeerIdentity,
71 IpcStream,
72};
73
74#[cfg(feature = "ipc-async")]
75pub use platform_imp::{
76 IpcAsyncListener, IpcAsyncStream, IpcIntoAsyncListener, IpcIntoAsyncStream,
77};
78
79#[cfg(feature = "pty")]
80pub use platform_imp::terminal::{
81 before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
82 input_payload, is_ignorable_process_control_error, prepare_unmanaged_pty_child,
83 query_responses, resize_pty, shell_argv, signal_pty_tree, terminate_pty_child,
84 wait_before_pty_close_supported, Backend, ChildProcessInfo, ConPtyBackendKind,
85 OrphanConhostInfo, PtyProcessGuard, PtySpawnContext, TerminalInputSession,
86};
87
88#[cfg(feature = "session-relay")]
89pub use platform_imp::relay_local_socket_session;
90
91pub fn configure_compat_tokio_command(
96 command: &mut Command,
97 show_console: bool,
98 kill_when_owner_dies: bool,
99) -> io::Result<()> {
100 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
101}
102
103pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) {
105 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum StreamMode {
111 Inherit,
113 Piped,
115 Null,
117}
118
119impl StreamMode {
120 fn apply(self) -> Stdio {
121 match self {
122 Self::Inherit => Stdio::inherit(),
123 Self::Piped => Stdio::piped(),
124 Self::Null => Stdio::null(),
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct SpawnSpec {
132 program: OsString,
133 args: Vec<OsString>,
134 current_dir: Option<PathBuf>,
135 env: Vec<(OsString, OsString)>,
136 clear_env: bool,
137 stdin: StreamMode,
138 stdout: StreamMode,
139 stderr: StreamMode,
140 create_process_group: bool,
141 kill_when_owner_dies: bool,
142}
143
144impl SpawnSpec {
145 pub fn new(program: impl Into<OsString>) -> Self {
147 Self {
148 program: program.into(),
149 args: Vec::new(),
150 current_dir: None,
151 env: Vec::new(),
152 clear_env: false,
153 stdin: StreamMode::Inherit,
154 stdout: StreamMode::Inherit,
155 stderr: StreamMode::Inherit,
156 create_process_group: false,
157 kill_when_owner_dies: false,
158 }
159 }
160
161 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
163 self.args.push(arg.into());
164 self
165 }
166
167 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
169 self.current_dir = Some(path.into());
170 self
171 }
172
173 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
175 self.env.push((key.into(), value.into()));
176 self
177 }
178
179 pub fn clear_env(mut self, clear: bool) -> Self {
181 self.clear_env = clear;
182 self
183 }
184
185 pub fn stdin(mut self, mode: StreamMode) -> Self {
187 self.stdin = mode;
188 self
189 }
190
191 pub fn stdout(mut self, mode: StreamMode) -> Self {
193 self.stdout = mode;
194 self
195 }
196
197 pub fn stderr(mut self, mode: StreamMode) -> Self {
199 self.stderr = mode;
200 self
201 }
202
203 pub fn create_process_group(mut self, create: bool) -> Self {
212 self.create_process_group = create;
213 self
214 }
215
216 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
223 self.kill_when_owner_dies = kill;
224 self
225 }
226
227 pub async fn spawn(self) -> io::Result<PlatformChild> {
229 let mut command = Command::new(&self.program);
230 command.args(&self.args);
231 if let Some(current_dir) = self.current_dir.as_deref() {
232 command.current_dir(current_dir);
233 }
234 if self.clear_env {
235 command.env_clear();
236 }
237 for (key, value) in &self.env {
238 command.env(key, value);
239 }
240 command
241 .stdin(self.stdin.apply())
242 .stdout(self.stdout.apply())
243 .stderr(self.stderr.apply());
244 platform_imp::configure_command(
245 &mut command,
246 self.create_process_group,
247 self.kill_when_owner_dies,
248 )?;
249
250 let child = command.spawn()?;
251 platform_imp::after_spawn(&child, self.kill_when_owner_dies);
252 Ok(PlatformChild::new(child, self.create_process_group))
253 }
254}
255
256pub struct PlatformChild {
258 child: Child,
259 stdin: Option<ChildStdin>,
260 stdout: Option<ChildStdout>,
261 stderr: Option<ChildStderr>,
262 signal: PlatformEmergencySignal,
263}
264
265impl PlatformChild {
266 fn new(mut child: Child, own_process_group: bool) -> Self {
267 let signal = PlatformEmergencySignal {
268 pid: child.id(),
269 own_process_group,
270 };
271 Self {
272 stdin: child.stdin.take(),
273 stdout: child.stdout.take(),
274 stderr: child.stderr.take(),
275 child,
276 signal,
277 }
278 }
279
280 pub fn id(&self) -> Option<u32> {
282 self.child.id()
283 }
284
285 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
287 self.child.wait().await
288 }
289
290 pub async fn kill(&mut self) -> io::Result<()> {
292 self.child.kill().await
293 }
294
295 pub async fn wait_with_output(self) -> io::Result<Output> {
297 let Self {
298 mut child,
299 stdin,
300 stdout,
301 stderr,
302 ..
303 } = self;
304 drop(stdin);
307 let (status, stdout, stderr) = tokio::try_join!(
308 child.wait(),
309 read_owned_to_end(stdout),
310 read_owned_to_end(stderr),
311 )?;
312 Ok(Output {
313 status,
314 stdout,
315 stderr,
316 })
317 }
318
319 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
321 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
322 stdin.write_all(bytes).await?;
323 stdin.flush().await
324 }
325
326 pub fn close_stdin(&mut self) {
331 drop(self.stdin.take());
332 }
333
334 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
336 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
337 let mut bytes = Vec::new();
338 stdout.read_to_end(&mut bytes).await?;
339 Ok(bytes)
340 }
341
342 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
344 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
345 let mut bytes = Vec::new();
346 stderr.read_to_end(&mut bytes).await?;
347 Ok(bytes)
348 }
349
350 pub fn into_actor_parts(
356 self,
357 ) -> (
358 PlatformLifecycle,
359 PlatformEmergencySignal,
360 Option<PlatformStdin>,
361 Option<PlatformOutput>,
362 Option<PlatformOutput>,
363 ) {
364 (
365 PlatformLifecycle { child: self.child },
366 self.signal,
367 self.stdin.map(|stdin| PlatformStdin { stdin }),
368 self.stdout.map(PlatformOutput::stdout),
369 self.stderr.map(PlatformOutput::stderr),
370 )
371 }
372}
373
374pub struct PlatformLifecycle {
376 child: Child,
377}
378
379impl PlatformLifecycle {
380 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
382 self.child.wait().await
383 }
384}
385
386pub struct PlatformEmergencySignal {
391 pid: Option<u32>,
392 own_process_group: bool,
393}
394
395impl PlatformEmergencySignal {
396 pub fn kill(&self) -> io::Result<()> {
398 platform_imp::signal_process(self.target()?)
399 }
400
401 pub fn terminate_group_soft(&self) -> io::Result<bool> {
410 if !self.own_process_group {
411 return Ok(false);
412 }
413 platform_imp::signal_process_group(self.target()?).map(|()| true)
414 }
415
416 fn target(&self) -> io::Result<u32> {
417 self.pid.ok_or_else(|| {
418 io::Error::new(
419 io::ErrorKind::BrokenPipe,
420 "child process no longer has an emergency signal target",
421 )
422 })
423 }
424}
425
426pub struct PlatformStdin {
428 stdin: ChildStdin,
429}
430
431impl PlatformStdin {
432 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
434 self.stdin.write_all(bytes).await?;
435 self.stdin.flush().await
436 }
437}
438
439pub struct PlatformOutput {
441 reader: OutputReader,
442}
443
444enum OutputReader {
445 Stdout(ChildStdout),
446 Stderr(ChildStderr),
447}
448
449impl PlatformOutput {
450 fn stdout(stdout: ChildStdout) -> Self {
451 Self {
452 reader: OutputReader::Stdout(stdout),
453 }
454 }
455
456 fn stderr(stderr: ChildStderr) -> Self {
457 Self {
458 reader: OutputReader::Stderr(stderr),
459 }
460 }
461
462 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
464 match self.reader {
465 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
466 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
467 }
468 }
469
470 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
475 match &mut self.reader {
476 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
477 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
478 }
479 }
480}
481
482fn stdin_not_piped() -> io::Error {
483 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
484}
485
486fn stdout_not_piped() -> io::Error {
487 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
488}
489
490fn stderr_not_piped() -> io::Error {
491 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
492}
493
494async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
495where
496 R: AsyncRead + Unpin,
497{
498 let Some(mut reader) = reader else {
499 return Ok(Vec::new());
500 };
501 let mut bytes = Vec::new();
502 reader.read_to_end(&mut bytes).await?;
503 Ok(bytes)
504}
505
506pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
508 platform_imp::shell_spec(command.as_ref())
509}
510
511#[cfg(test)]
512mod tests {
513 use super::{shell_spec, SpawnSpec, StreamMode};
514
515 fn fixture_command() -> SpawnSpec {
516 #[cfg(windows)]
517 {
518 shell_spec("echo async-platform-internal")
519 }
520 #[cfg(not(windows))]
521 {
522 shell_spec("printf async-platform-internal")
523 }
524 }
525
526 #[tokio::test]
527 async fn blessed_spawn_captures_output_without_sync_wait() {
528 let output = fixture_command()
529 .stdout(StreamMode::Piped)
530 .stderr(StreamMode::Piped)
531 .spawn()
532 .await
533 .expect("spawn")
534 .wait_with_output()
535 .await
536 .expect("wait with output");
537
538 assert!(output.status.success());
539 let expected = if cfg!(windows) {
540 b"async-platform-internal\r\n".as_slice()
541 } else {
542 b"async-platform-internal".as_slice()
543 };
544 assert_eq!(output.stdout, expected);
545 assert!(output.stderr.is_empty());
546 }
547
548 #[tokio::test]
549 async fn blessed_spawn_reports_missing_program() {
550 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
551 .spawn()
552 .await;
553 assert!(result.is_err());
554 }
555
556 #[tokio::test]
557 async fn one_shot_output_closes_owned_stdin() {
558 #[cfg(windows)]
559 let spec = shell_spec("more > nul & echo done");
560 #[cfg(not(windows))]
561 let spec = shell_spec("cat > /dev/null; printf done");
562
563 let output = tokio::time::timeout(
564 std::time::Duration::from_secs(2),
565 spec.stdin(StreamMode::Piped)
566 .stdout(StreamMode::Piped)
567 .stderr(StreamMode::Piped)
568 .spawn()
569 .await
570 .expect("spawn")
571 .wait_with_output(),
572 )
573 .await
574 .expect("stdin is closed for one-shot output")
575 .expect("output succeeds");
576
577 let expected = if cfg!(windows) {
578 b"done\r\n".as_slice()
579 } else {
580 b"done".as_slice()
581 };
582 assert_eq!(output.stdout, expected);
583 }
584}