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