Skip to main content

running_process_platform_internal/
lib.rs

1//! Blessed asynchronous process operations.
2//!
3//! This crate is intentionally published as an implementation detail. It is
4//! the only production owner of the Tokio process primitives used by the
5//! async process API. Higher layers receive typed operations and never name
6//! `tokio::process::Command` directly.
7
8use std::cfg_select;
9#[cfg(feature = "async-process")]
10use std::ffi::{OsStr, OsString};
11#[cfg(any(feature = "async-process", feature = "ipc"))]
12use std::io;
13#[cfg(feature = "async-process")]
14use std::path::PathBuf;
15#[cfg(feature = "async-process")]
16use std::process::{ExitStatus, Output, Stdio};
17
18#[cfg(feature = "async-process")]
19use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
20#[cfg(feature = "async-process")]
21use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
22
23/// Neutral capability indexes for the eventual workspace-wide host boundary.
24///
25/// The indexes intentionally expose no operations yet: phase 2 establishes
26/// ownership names before later phases move a capability behind them.
27pub mod platform;
28
29/// Temporary source-compatibility re-export for the pre-boundary PTY API.
30///
31/// New code must use [`platform::terminal`] facade-owned types. This root-only
32/// alias deliberately stays outside the neutral facade and can be removed in
33/// the next major release after downstream users have migrated.
34#[cfg(feature = "pty")]
35#[doc(hidden)]
36pub use portable_pty as portable_pty_compat;
37
38// This is deliberately the crate's only host selector.  Facade modules are
39// neutral; native details live behind the selected private root.
40cfg_select! {
41    target_os = "windows" => {
42        mod platform_win;
43        pub(crate) use platform_win as platform_imp;
44    }
45    target_os = "linux" => {
46        mod platform_linux;
47        pub(crate) use platform_linux as platform_imp;
48    }
49    target_os = "macos" => {
50        mod platform_macos;
51        pub(crate) use platform_macos as platform_imp;
52    }
53}
54
55// Re-export the selected implementation once from this allowed host-selector
56// root. Neutral capability facades re-export only crate-root names and never
57// name the private `platform_imp` alias themselves.
58pub use platform_imp::{
59    active_graphics_probe, assign_child_to_windows_job, cancel_capture_reader,
60    canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace,
61    configure_process_command, configure_sync_contained_command, configure_sync_daemon_command,
62    configure_sync_daemon_command_with_inheritance, configure_trampoline_command,
63    current_executable_build_id, exact_trace_capability, exit_code, monitor_console_windows,
64    parent_has_console, prepare_capture_reader, set_process_name, set_window_icon_impl,
65    shell_command, soft_terminate_process_group, spawn_sync, spawn_sync_daemon,
66    spawn_sync_daemon_with_inheritance, start_descendant_monitor, start_exact_trace,
67    sync_child_native_handle, trampoline_exit_code, unix_mark_extra_fds_close_on_exec,
68    unix_set_priority, unix_signal_process, unix_signal_process_group, unix_signal_raw,
69    window_icon_support_impl, CaptureCancellation, TracedChild, WindowsJobHandle,
70};
71
72#[cfg(feature = "process-inspection")]
73pub use platform_imp::{kill_tree, process_snapshot, process_snapshot_for_pid};
74
75pub use platform_imp::{autostart_register, autostart_render_registration, autostart_unregister};
76
77pub use platform_imp::{process_install_owner_death_cleanup, process_owner_death_cleanup_target};
78
79pub use platform_imp::process_install_shutdown_request_handler;
80
81pub use platform_imp::fs_write_all_to_descriptor;
82
83pub use platform_imp::{process_can_replace_current_image, process_replace_current_image};
84
85pub use platform_imp::{
86    process_executable_path, process_force_kill, process_same_executable_path,
87    process_signal_terminate, ProcessLiveness,
88};
89
90pub use platform_imp::{
91    resources_fd_exhaustion_error, resources_inode_capacity, resources_signals_fd_exhaustion,
92    resources_signals_storage_exhaustion, resources_storage_exhaustion_error,
93};
94
95pub use platform_imp::{
96    executable_file_name, executable_sibling_of_current_image, EXECUTABLE_EXTENSION,
97};
98
99#[cfg(feature = "fs")]
100pub use platform_imp::{
101    fs_create_private_file, fs_decode_path_bytes, fs_encode_path_bytes, fs_file_identity,
102    fs_is_lock_conflict, fs_open_lock_file, fs_path_identity, fs_replace_file, fs_sync_directory,
103    fs_try_lock_exclusive, fs_unlock, fs_user_config_dir, fs_user_data_dir, fs_user_run_data_root,
104    fs_user_runtime_dir, fs_user_state_dir, FsFileIdentity,
105};
106
107pub use platform_imp::{
108    host_boot_id, host_current_process_privilege, host_environment_keys_are_case_insensitive,
109    host_filesystem_device_id, host_hostname, host_login_environment, host_machine_id,
110    host_namespace_id, host_user_machine_identity, HostPrivilegedIdentity,
111};
112
113pub use platform_imp::host_login_environment_block;
114
115pub use platform_imp::terminal_input;
116
117#[cfg(feature = "ipc")]
118pub use platform_imp::{
119    ipc_broker_endpoint_name as IpcBrokerEndpointName, ipc_broker_v1_endpoint_path,
120    ipc_broker_v2_runtime_dir, ipc_current_user_id, ipc_endpoint_is_filesystem_backed,
121    ipc_endpoint_name_limit, ipc_endpoint_scope_bytes, ipc_ensure_owner_private_directory,
122    ipc_nonblocking_zero_read_is_pending, ipc_owner_private_directory, ipc_select_endpoint_address,
123    IpcEndpoint, IpcInheritedListener, IpcListener, IpcListenerNonblockingMode, IpcPeerIdentity,
124    IpcPeerIdentitySource, IpcStream,
125};
126
127/// Failure details for the deprecated 4.x raw descriptor/handle handoff API.
128///
129/// This type exists only at the crate-root compatibility boundary. New product
130/// mechanics use opaque [`platform::ipc::Stream`] operations instead.
131#[cfg(feature = "ipc")]
132#[doc(hidden)]
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct LegacyHandoffError {
135    kind: platform::ipc::HandoffTransferErrorKind,
136    raw_os_error: Option<i32>,
137    transferred_bytes: Option<usize>,
138    expected_bytes: Option<usize>,
139    detail: Option<String>,
140}
141
142#[cfg(feature = "ipc")]
143impl LegacyHandoffError {
144    pub(crate) fn new(
145        kind: platform::ipc::HandoffTransferErrorKind,
146        raw_os_error: Option<i32>,
147    ) -> Self {
148        Self {
149            kind,
150            raw_os_error,
151            transferred_bytes: None,
152            expected_bytes: None,
153            detail: None,
154        }
155    }
156
157    pub(crate) fn with_detail(
158        kind: platform::ipc::HandoffTransferErrorKind,
159        raw_os_error: Option<i32>,
160        detail: impl Into<String>,
161    ) -> Self {
162        Self {
163            kind,
164            raw_os_error,
165            transferred_bytes: None,
166            expected_bytes: None,
167            detail: Some(detail.into()),
168        }
169    }
170
171    #[doc(hidden)]
172    pub fn partial(transferred_bytes: usize, expected_bytes: usize) -> Self {
173        Self {
174            kind: platform::ipc::HandoffTransferErrorKind::Failed,
175            raw_os_error: None,
176            transferred_bytes: Some(transferred_bytes),
177            expected_bytes: Some(expected_bytes),
178            detail: Some(format!(
179                "SCM_RIGHTS connection transfer was partial ({transferred_bytes}/{expected_bytes} bytes)"
180            )),
181        }
182    }
183
184    /// Return the policy-neutral failure category.
185    pub fn kind(&self) -> platform::ipc::HandoffTransferErrorKind {
186        self.kind
187    }
188
189    /// Return the native error code retained for legacy public diagnostics.
190    pub fn raw_os_error(&self) -> Option<i32> {
191        self.raw_os_error
192    }
193
194    /// Return a partial payload count when the descriptor may have transferred.
195    pub fn partial_counts(&self) -> Option<(usize, usize)> {
196        self.transferred_bytes.zip(self.expected_bytes)
197    }
198
199    pub(crate) fn detail(&self) -> Option<&str> {
200        self.detail.as_deref()
201    }
202}
203
204/// Whether the deprecated 4.x SCM_RIGHTS compatibility transport is available.
205#[cfg(feature = "ipc")]
206#[doc(hidden)]
207pub const LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED: bool =
208    platform_imp::LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED;
209
210/// Whether the deprecated 4.x DuplicateHandle compatibility transport is available.
211#[cfg(feature = "ipc")]
212#[doc(hidden)]
213pub const LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED: bool =
214    platform_imp::LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED;
215
216/// Root-only adapter for the deprecated raw-descriptor handoff API.
217#[cfg(feature = "ipc")]
218#[doc(hidden)]
219pub fn legacy_send_fd_to(
220    socket: &std::path::Path,
221    sent_fd: i32,
222    payload: &[u8],
223) -> Result<(), LegacyHandoffError> {
224    platform_imp::legacy_send_fd_to(socket, sent_fd, payload)
225}
226
227/// Root-only adapter for the deprecated connected raw-descriptor handoff API.
228#[cfg(feature = "ipc")]
229#[doc(hidden)]
230pub fn legacy_send_fd_over(
231    socket_fd: i32,
232    sent_fd: i32,
233    payload: &[u8],
234) -> Result<(), LegacyHandoffError> {
235    platform_imp::legacy_send_fd_over(socket_fd, sent_fd, payload)
236}
237
238/// Root-only adapter for the deprecated raw-handle duplication API.
239#[cfg(feature = "ipc")]
240#[doc(hidden)]
241pub fn legacy_duplicate_handle(
242    source_handle: usize,
243    backend_pid: u32,
244) -> Result<usize, LegacyHandoffError> {
245    platform_imp::legacy_duplicate_handle(source_handle, backend_pid)
246}
247
248/// Temporary source-compatibility conversion for public APIs that predate the
249/// opaque IPC facade.
250///
251/// New code must keep [`IpcStream`] opaque. This root-only adapter exists so
252/// `running-process` can preserve its established raw-stream callback contract
253/// until the next major release without exposing the transport through
254/// [`platform::ipc`].
255#[cfg(feature = "ipc")]
256#[doc(hidden)]
257pub fn into_legacy_ipc_stream(stream: IpcStream) -> interprocess::local_socket::Stream {
258    platform_imp::into_legacy_ipc_stream(stream)
259}
260
261/// Temporary source-compatibility conversion for legacy 4.x callback inputs.
262#[cfg(feature = "ipc")]
263#[doc(hidden)]
264pub fn from_legacy_ipc_stream(stream: interprocess::local_socket::Stream) -> IpcStream {
265    platform_imp::from_legacy_ipc_stream(stream)
266}
267
268/// Temporary source-compatibility conversion for public APIs that return an
269/// `interprocess` endpoint name.
270#[cfg(feature = "ipc")]
271#[doc(hidden)]
272pub fn legacy_ipc_name(path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
273    platform_imp::legacy_ipc_name(path)
274}
275
276#[cfg(feature = "ipc-async")]
277pub use platform_imp::{
278    IpcAsyncListener, IpcAsyncStream, IpcIntoAsyncListener, IpcIntoAsyncStream,
279};
280
281#[cfg(feature = "pty")]
282pub use platform_imp::terminal::{
283    before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
284    input_payload, is_ignorable_process_control_error, prepare_unmanaged_pty_child,
285    query_responses, resize_pty, shell_argv, signal_pty_tree, terminate_pty_child,
286    wait_before_pty_close_supported, Backend, ChildProcessInfo, ConPtyBackendKind,
287    OrphanConhostInfo, PtyProcessGuard, PtySpawnContext, TerminalInputSession,
288};
289
290#[cfg(feature = "session-relay")]
291pub use platform_imp::relay_local_socket_session;
292
293/// Apply host-owned setup for the legacy Tokio-command compatibility surface.
294///
295/// The public wrapper retains its policy type, while console suppression and
296/// owner-death primitives stay inside the selected platform root.
297#[cfg(feature = "async-process")]
298pub fn configure_compat_tokio_command(
299    command: &mut Command,
300    show_console: bool,
301    kill_when_owner_dies: bool,
302) -> io::Result<()> {
303    platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
304}
305
306/// Complete host-owned setup after a legacy Tokio child has been spawned.
307#[cfg(feature = "async-process")]
308pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) -> io::Result<()> {
309    platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
310}
311
312/// Stdio policy for one child stream.
313#[cfg(feature = "async-process")]
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum StreamMode {
316    /// Leave the stream connected to the parent process.
317    Inherit,
318    /// Create an asynchronous pipe owned by the child handle.
319    Piped,
320    /// Connect the stream to the platform null device.
321    Null,
322}
323
324#[cfg(feature = "async-process")]
325impl StreamMode {
326    fn apply(self) -> Stdio {
327        match self {
328            Self::Inherit => Stdio::inherit(),
329            Self::Piped => Stdio::piped(),
330            Self::Null => Stdio::null(),
331        }
332    }
333}
334
335/// Typed spawn description accepted by the blessed process boundary.
336#[cfg(feature = "async-process")]
337#[derive(Debug, Clone)]
338pub struct SpawnSpec {
339    program: OsString,
340    args: Vec<OsString>,
341    current_dir: Option<PathBuf>,
342    env: Vec<(OsString, OsString)>,
343    clear_env: bool,
344    stdin: StreamMode,
345    stdout: StreamMode,
346    stderr: StreamMode,
347    create_process_group: bool,
348    kill_when_owner_dies: bool,
349}
350
351#[cfg(feature = "async-process")]
352impl SpawnSpec {
353    /// Create a direct (non-shell) command description.
354    pub fn new(program: impl Into<OsString>) -> Self {
355        Self {
356            program: program.into(),
357            args: Vec::new(),
358            current_dir: None,
359            env: Vec::new(),
360            clear_env: false,
361            stdin: StreamMode::Inherit,
362            stdout: StreamMode::Inherit,
363            stderr: StreamMode::Inherit,
364            create_process_group: false,
365            kill_when_owner_dies: false,
366        }
367    }
368
369    /// Append one argument without requiring UTF-8.
370    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
371        self.args.push(arg.into());
372        self
373    }
374
375    /// Set the child working directory.
376    pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
377        self.current_dir = Some(path.into());
378        self
379    }
380
381    /// Add an environment override.
382    pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
383        self.env.push((key.into(), value.into()));
384        self
385    }
386
387    /// Start with an empty inherited environment before applying overrides.
388    pub fn clear_env(mut self, clear: bool) -> Self {
389        self.clear_env = clear;
390        self
391    }
392
393    /// Configure child stdin.
394    pub fn stdin(mut self, mode: StreamMode) -> Self {
395        self.stdin = mode;
396        self
397    }
398
399    /// Configure child stdout.
400    pub fn stdout(mut self, mode: StreamMode) -> Self {
401        self.stdout = mode;
402        self
403    }
404
405    /// Configure child stderr.
406    pub fn stderr(mut self, mode: StreamMode) -> Self {
407        self.stderr = mode;
408        self
409    }
410
411    /// Put the child in its own process group.
412    ///
413    /// This is what makes a group-wide soft signal addressable at all:
414    /// [`PlatformEmergencySignal::terminate_group_soft`] is a no-op without
415    /// it, because on POSIX the negative-PID signal would otherwise reach the
416    /// caller's own group, and on Windows `GenerateConsoleCtrlEvent` only
417    /// routes to children spawned with `CREATE_NEW_PROCESS_GROUP`. It also
418    /// detaches the child from the parent's console Ctrl+C, so it is opt-in.
419    pub fn create_process_group(mut self, create: bool) -> Self {
420        self.create_process_group = create;
421        self
422    }
423
424    /// Kill this child when the spawning process exits unexpectedly.
425    ///
426    /// Linux uses `PR_SET_PDEATHSIG(SIGTERM)`. Windows assigns the child to a
427    /// process-wide kill-on-close Job Object. macOS forks a kqueue supervisor
428    /// before exec and reports spawn success only after its owner and child
429    /// watches are registered.
430    pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
431        self.kill_when_owner_dies = kill;
432        self
433    }
434
435    /// Spawn using the canonical asynchronous platform operation.
436    pub async fn spawn(self) -> io::Result<PlatformChild> {
437        let mut command = Command::new(&self.program);
438        command.args(&self.args);
439        if let Some(current_dir) = self.current_dir.as_deref() {
440            command.current_dir(current_dir);
441        }
442        if self.clear_env {
443            command.env_clear();
444        }
445        for (key, value) in &self.env {
446            command.env(key, value);
447        }
448        command
449            .stdin(self.stdin.apply())
450            .stdout(self.stdout.apply())
451            .stderr(self.stderr.apply());
452        platform_imp::configure_command(
453            &mut command,
454            self.create_process_group,
455            self.kill_when_owner_dies,
456        )?;
457
458        let child = command.spawn()?;
459        platform_imp::after_spawn(&child, self.kill_when_owner_dies)?;
460        Ok(PlatformChild::new(child, self.create_process_group))
461    }
462}
463
464/// Owned child handle returned by [`SpawnSpec::spawn`].
465#[cfg(feature = "async-process")]
466pub struct PlatformChild {
467    child: Child,
468    stdin: Option<ChildStdin>,
469    stdout: Option<ChildStdout>,
470    stderr: Option<ChildStderr>,
471    signal: PlatformEmergencySignal,
472}
473
474#[cfg(feature = "async-process")]
475impl PlatformChild {
476    fn new(mut child: Child, own_process_group: bool) -> Self {
477        let signal = PlatformEmergencySignal {
478            pid: child.id(),
479            own_process_group,
480        };
481        Self {
482            stdin: child.stdin.take(),
483            stdout: child.stdout.take(),
484            stderr: child.stderr.take(),
485            child,
486            signal,
487        }
488    }
489
490    /// Return the operating-system process identifier, if available.
491    pub fn id(&self) -> Option<u32> {
492        self.child.id()
493    }
494
495    /// Wait for completion without capturing output.
496    pub async fn wait(&mut self) -> io::Result<ExitStatus> {
497        self.child.wait().await
498    }
499
500    /// Terminate the child and wait for its exit.
501    pub async fn kill(&mut self) -> io::Result<()> {
502        self.child.kill().await
503    }
504
505    /// Capture piped stdout and stderr while waiting for the child.
506    pub async fn wait_with_output(self) -> io::Result<Output> {
507        let Self {
508            mut child,
509            stdin,
510            stdout,
511            stderr,
512            ..
513        } = self;
514        // Match Tokio's `Child::wait_with_output` contract: one-shot output
515        // closes an owned stdin pipe so a child waiting for EOF can finish.
516        drop(stdin);
517        let (status, stdout, stderr) = tokio::try_join!(
518            child.wait(),
519            read_owned_to_end(stdout),
520            read_owned_to_end(stderr),
521        )?;
522        Ok(Output {
523            status,
524            stdout,
525            stderr,
526        })
527    }
528
529    /// Write bytes to piped stdin and flush them.
530    pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
531        let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
532        stdin.write_all(bytes).await?;
533        stdin.flush().await
534    }
535
536    /// Close the piped stdin handle, delivering EOF to the child.
537    ///
538    /// This operation is idempotent. Closing an inherited or null stdin is
539    /// also a no-op because there is no owned pipe to close.
540    pub fn close_stdin(&mut self) {
541        drop(self.stdin.take());
542    }
543
544    /// Read all bytes from piped stdout without waiting for process exit.
545    pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
546        let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
547        let mut bytes = Vec::new();
548        stdout.read_to_end(&mut bytes).await?;
549        Ok(bytes)
550    }
551
552    /// Read all bytes from piped stderr without waiting for process exit.
553    pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
554        let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
555        let mut bytes = Vec::new();
556        stderr.read_to_end(&mut bytes).await?;
557        Ok(bytes)
558    }
559
560    /// Split this child into sealed actor capabilities.
561    ///
562    /// The lifecycle wait handle, emergency termination handle, input pipe,
563    /// and output readers are deliberately separate so the actor can keep
564    /// accepting control commands while an asynchronous exit wait is pending.
565    pub fn into_actor_parts(
566        self,
567    ) -> (
568        PlatformLifecycle,
569        PlatformEmergencySignal,
570        Option<PlatformStdin>,
571        Option<PlatformOutput>,
572        Option<PlatformOutput>,
573    ) {
574        (
575            PlatformLifecycle { child: self.child },
576            self.signal,
577            self.stdin.map(|stdin| PlatformStdin { stdin }),
578            self.stdout.map(PlatformOutput::stdout),
579            self.stderr.map(PlatformOutput::stderr),
580        )
581    }
582}
583
584/// Opaque exit-wait capability owned by a process actor.
585#[cfg(feature = "async-process")]
586pub struct PlatformLifecycle {
587    child: Child,
588}
589
590#[cfg(feature = "async-process")]
591impl PlatformLifecycle {
592    /// Wait asynchronously for the child to exit.
593    pub async fn wait(&mut self) -> io::Result<ExitStatus> {
594        self.child.wait().await
595    }
596}
597
598/// Opaque, non-reap-capable emergency termination capability.
599///
600/// It can be used while the actor has a pending wait on
601/// [`PlatformLifecycle`], but it cannot observe or consume the exit result.
602#[cfg(feature = "async-process")]
603pub struct PlatformEmergencySignal {
604    pid: Option<u32>,
605    own_process_group: bool,
606}
607
608#[cfg(feature = "async-process")]
609impl PlatformEmergencySignal {
610    /// Request immediate termination without waiting for process reaping.
611    pub fn kill(&self) -> io::Result<()> {
612        platform_imp::signal_process(self.target()?)
613    }
614
615    /// Ask the child's whole process group to shut down gracefully.
616    ///
617    /// Returns `Ok(false)` when the child was not spawned with
618    /// [`SpawnSpec::create_process_group`]: there is no group to address, and
619    /// signalling anyway would hit the caller's own group on POSIX or the
620    /// caller's console on Windows. A child that has already exited is also
621    /// `Ok` -- the soft step's only job is to give a live child a chance to
622    /// clean up before a hard kill, so a dead target is a success.
623    pub fn terminate_group_soft(&self) -> io::Result<bool> {
624        if !self.own_process_group {
625            return Ok(false);
626        }
627        platform_imp::signal_process_group(self.target()?).map(|()| true)
628    }
629
630    fn target(&self) -> io::Result<u32> {
631        self.pid.ok_or_else(|| {
632            io::Error::new(
633                io::ErrorKind::BrokenPipe,
634                "child process no longer has an emergency signal target",
635            )
636        })
637    }
638}
639
640/// Opaque piped stdin capability owned by a process actor.
641#[cfg(feature = "async-process")]
642pub struct PlatformStdin {
643    stdin: ChildStdin,
644}
645
646#[cfg(feature = "async-process")]
647impl PlatformStdin {
648    /// Write and flush bytes to the child stdin pipe.
649    pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
650        self.stdin.write_all(bytes).await?;
651        self.stdin.flush().await
652    }
653}
654
655/// Opaque stdout or stderr reader owned by a process actor.
656#[cfg(feature = "async-process")]
657pub struct PlatformOutput {
658    reader: OutputReader,
659}
660
661#[cfg(feature = "async-process")]
662enum OutputReader {
663    Stdout(ChildStdout),
664    Stderr(ChildStderr),
665}
666
667#[cfg(feature = "async-process")]
668impl PlatformOutput {
669    fn stdout(stdout: ChildStdout) -> Self {
670        Self {
671            reader: OutputReader::Stdout(stdout),
672        }
673    }
674
675    fn stderr(stderr: ChildStderr) -> Self {
676        Self {
677            reader: OutputReader::Stderr(stderr),
678        }
679    }
680
681    /// Drain this output endpoint to EOF without blocking a runtime worker.
682    pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
683        match self.reader {
684            OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
685            OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
686        }
687    }
688
689    /// Read the next asynchronous chunk from this output endpoint.
690    ///
691    /// The caller owns the buffer and therefore controls the amount of data
692    /// retained at each read. EOF is reported as `Ok(0)`.
693    pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
694        match &mut self.reader {
695            OutputReader::Stdout(stdout) => stdout.read(buffer).await,
696            OutputReader::Stderr(stderr) => stderr.read(buffer).await,
697        }
698    }
699}
700
701#[cfg(feature = "async-process")]
702fn stdin_not_piped() -> io::Error {
703    io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
704}
705
706#[cfg(feature = "async-process")]
707fn stdout_not_piped() -> io::Error {
708    io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
709}
710
711#[cfg(feature = "async-process")]
712fn stderr_not_piped() -> io::Error {
713    io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
714}
715
716#[cfg(feature = "async-process")]
717async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
718where
719    R: AsyncRead + Unpin,
720{
721    let Some(mut reader) = reader else {
722        return Ok(Vec::new());
723    };
724    let mut bytes = Vec::new();
725    reader.read_to_end(&mut bytes).await?;
726    Ok(bytes)
727}
728
729/// Build a shell command using the host platform's supported shell.
730#[cfg(feature = "async-process")]
731pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
732    platform_imp::shell_spec(command.as_ref())
733}
734
735#[cfg(all(test, feature = "async-process"))]
736mod tests {
737    use super::{shell_spec, SpawnSpec, StreamMode};
738
739    fn fixture_command() -> SpawnSpec {
740        #[cfg(windows)]
741        {
742            shell_spec("echo async-platform-internal")
743        }
744        #[cfg(not(windows))]
745        {
746            shell_spec("printf async-platform-internal")
747        }
748    }
749
750    #[tokio::test]
751    async fn blessed_spawn_captures_output_without_sync_wait() {
752        let output = fixture_command()
753            .stdout(StreamMode::Piped)
754            .stderr(StreamMode::Piped)
755            .spawn()
756            .await
757            .expect("spawn")
758            .wait_with_output()
759            .await
760            .expect("wait with output");
761
762        assert!(output.status.success());
763        let expected = if cfg!(windows) {
764            b"async-platform-internal\r\n".as_slice()
765        } else {
766            b"async-platform-internal".as_slice()
767        };
768        assert_eq!(output.stdout, expected);
769        assert!(output.stderr.is_empty());
770    }
771
772    #[tokio::test]
773    async fn blessed_spawn_reports_missing_program() {
774        let result = SpawnSpec::new("running-process-program-that-does-not-exist")
775            .spawn()
776            .await;
777        assert!(result.is_err());
778    }
779
780    #[tokio::test]
781    async fn one_shot_output_closes_owned_stdin() {
782        #[cfg(windows)]
783        let spec = shell_spec("more > nul & echo done");
784        #[cfg(not(windows))]
785        let spec = shell_spec("cat > /dev/null; printf done");
786
787        let output = tokio::time::timeout(
788            std::time::Duration::from_secs(2),
789            spec.stdin(StreamMode::Piped)
790                .stdout(StreamMode::Piped)
791                .stderr(StreamMode::Piped)
792                .spawn()
793                .await
794                .expect("spawn")
795                .wait_with_output(),
796        )
797        .await
798        .expect("stdin is closed for one-shot output")
799        .expect("output succeeds");
800
801        let expected = if cfg!(windows) {
802            b"done\r\n".as_slice()
803        } else {
804            b"done".as_slice()
805        };
806        assert_eq!(output.stdout, expected);
807    }
808}