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