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;
9use std::ffi::{OsStr, OsString};
10use std::io;
11use std::path::PathBuf;
12use std::process::{ExitStatus, Output, Stdio};
13
14use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
15use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
16
17/// Neutral capability indexes for the eventual workspace-wide host boundary.
18///
19/// The indexes intentionally expose no operations yet: phase 2 establishes
20/// ownership names before later phases move a capability behind them.
21pub mod platform;
22
23/// Temporary source-compatibility re-export for the pre-boundary PTY API.
24///
25/// New code must use [`platform::terminal`] facade-owned types. This root-only
26/// alias deliberately stays outside the neutral facade and can be removed in
27/// the next major release after downstream users have migrated.
28#[cfg(feature = "pty")]
29#[doc(hidden)]
30pub use portable_pty as portable_pty_compat;
31
32// This is deliberately the crate's only host selector.  Facade modules are
33// neutral; native details live behind the selected private root.
34cfg_select! {
35    target_os = "windows" => {
36        mod platform_win;
37        pub(crate) use platform_win as platform_imp;
38    }
39    target_os = "linux" => {
40        mod platform_linux;
41        pub(crate) use platform_linux as platform_imp;
42    }
43    target_os = "macos" => {
44        mod platform_macos;
45        pub(crate) use platform_macos as platform_imp;
46    }
47}
48
49// Re-export the selected implementation once from this allowed host-selector
50// root. Neutral capability facades re-export only crate-root names and never
51// name the private `platform_imp` alias themselves.
52pub use platform_imp::{
53    active_graphics_probe, assign_child_to_windows_job, cancel_capture_reader,
54    canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace,
55    configure_process_command, configure_sync_contained_command, configure_sync_daemon_command,
56    configure_trampoline_command, current_executable_build_id, exact_trace_capability, exit_code,
57    kill_tree, monitor_console_windows, parent_has_console, prepare_capture_reader,
58    process_snapshot, process_snapshot_for_pid, set_process_name, set_window_icon_impl,
59    shell_command, soft_terminate_process_group, spawn_sync, spawn_sync_daemon,
60    start_descendant_monitor, start_exact_trace, sync_child_native_handle, trampoline_exit_code,
61    unix_mark_extra_fds_close_on_exec, unix_set_priority, unix_signal_process,
62    unix_signal_process_group, unix_signal_raw, window_icon_support_impl, CaptureCancellation,
63    TracedChild, WindowsJobHandle,
64};
65
66pub use platform_imp::terminal_input;
67
68#[cfg(feature = "ipc")]
69pub use platform_imp::{
70    ipc_current_user_id, IpcEndpoint, IpcListener, IpcListenerNonblockingMode, IpcPeerIdentity,
71    IpcStream,
72};
73
74#[cfg(feature = "ipc-async")]
75pub use platform_imp::{
76    IpcAsyncListener, IpcAsyncStream, IpcIntoAsyncListener, IpcIntoAsyncStream,
77};
78
79#[cfg(feature = "pty")]
80pub use platform_imp::terminal::{
81    before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
82    input_payload, is_ignorable_process_control_error, prepare_unmanaged_pty_child,
83    query_responses, resize_pty, shell_argv, signal_pty_tree, terminate_pty_child,
84    wait_before_pty_close_supported, Backend, ChildProcessInfo, ConPtyBackendKind,
85    OrphanConhostInfo, PtyProcessGuard, PtySpawnContext, TerminalInputSession,
86};
87
88#[cfg(feature = "session-relay")]
89pub use platform_imp::relay_local_socket_session;
90
91/// Apply host-owned setup for the legacy Tokio-command compatibility surface.
92///
93/// The public wrapper retains its policy type, while console suppression and
94/// owner-death primitives stay inside the selected platform root.
95pub fn configure_compat_tokio_command(
96    command: &mut Command,
97    show_console: bool,
98    kill_when_owner_dies: bool,
99) -> io::Result<()> {
100    platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
101}
102
103/// Complete host-owned setup after a legacy Tokio child has been spawned.
104pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) {
105    platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
106}
107
108/// Stdio policy for one child stream.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum StreamMode {
111    /// Leave the stream connected to the parent process.
112    Inherit,
113    /// Create an asynchronous pipe owned by the child handle.
114    Piped,
115    /// Connect the stream to the platform null device.
116    Null,
117}
118
119impl StreamMode {
120    fn apply(self) -> Stdio {
121        match self {
122            Self::Inherit => Stdio::inherit(),
123            Self::Piped => Stdio::piped(),
124            Self::Null => Stdio::null(),
125        }
126    }
127}
128
129/// Typed spawn description accepted by the blessed process boundary.
130#[derive(Debug, Clone)]
131pub struct SpawnSpec {
132    program: OsString,
133    args: Vec<OsString>,
134    current_dir: Option<PathBuf>,
135    env: Vec<(OsString, OsString)>,
136    clear_env: bool,
137    stdin: StreamMode,
138    stdout: StreamMode,
139    stderr: StreamMode,
140    create_process_group: bool,
141    kill_when_owner_dies: bool,
142}
143
144impl SpawnSpec {
145    /// Create a direct (non-shell) command description.
146    pub fn new(program: impl Into<OsString>) -> Self {
147        Self {
148            program: program.into(),
149            args: Vec::new(),
150            current_dir: None,
151            env: Vec::new(),
152            clear_env: false,
153            stdin: StreamMode::Inherit,
154            stdout: StreamMode::Inherit,
155            stderr: StreamMode::Inherit,
156            create_process_group: false,
157            kill_when_owner_dies: false,
158        }
159    }
160
161    /// Append one argument without requiring UTF-8.
162    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
163        self.args.push(arg.into());
164        self
165    }
166
167    /// Set the child working directory.
168    pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
169        self.current_dir = Some(path.into());
170        self
171    }
172
173    /// Add an environment override.
174    pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
175        self.env.push((key.into(), value.into()));
176        self
177    }
178
179    /// Start with an empty inherited environment before applying overrides.
180    pub fn clear_env(mut self, clear: bool) -> Self {
181        self.clear_env = clear;
182        self
183    }
184
185    /// Configure child stdin.
186    pub fn stdin(mut self, mode: StreamMode) -> Self {
187        self.stdin = mode;
188        self
189    }
190
191    /// Configure child stdout.
192    pub fn stdout(mut self, mode: StreamMode) -> Self {
193        self.stdout = mode;
194        self
195    }
196
197    /// Configure child stderr.
198    pub fn stderr(mut self, mode: StreamMode) -> Self {
199        self.stderr = mode;
200        self
201    }
202
203    /// Put the child in its own process group.
204    ///
205    /// This is what makes a group-wide soft signal addressable at all:
206    /// [`PlatformEmergencySignal::terminate_group_soft`] is a no-op without
207    /// it, because on POSIX the negative-PID signal would otherwise reach the
208    /// caller's own group, and on Windows `GenerateConsoleCtrlEvent` only
209    /// routes to children spawned with `CREATE_NEW_PROCESS_GROUP`. It also
210    /// detaches the child from the parent's console Ctrl+C, so it is opt-in.
211    pub fn create_process_group(mut self, create: bool) -> Self {
212        self.create_process_group = create;
213        self
214    }
215
216    /// Kill this child when the spawning process exits unexpectedly.
217    ///
218    /// Linux uses `PR_SET_PDEATHSIG(SIGTERM)`. Windows assigns the child to a
219    /// process-wide kill-on-close Job Object. macOS forks a kqueue supervisor
220    /// before exec and reports spawn success only after its owner and child
221    /// watches are registered.
222    pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
223        self.kill_when_owner_dies = kill;
224        self
225    }
226
227    /// Spawn using the canonical asynchronous platform operation.
228    pub async fn spawn(self) -> io::Result<PlatformChild> {
229        let mut command = Command::new(&self.program);
230        command.args(&self.args);
231        if let Some(current_dir) = self.current_dir.as_deref() {
232            command.current_dir(current_dir);
233        }
234        if self.clear_env {
235            command.env_clear();
236        }
237        for (key, value) in &self.env {
238            command.env(key, value);
239        }
240        command
241            .stdin(self.stdin.apply())
242            .stdout(self.stdout.apply())
243            .stderr(self.stderr.apply());
244        platform_imp::configure_command(
245            &mut command,
246            self.create_process_group,
247            self.kill_when_owner_dies,
248        )?;
249
250        let child = command.spawn()?;
251        platform_imp::after_spawn(&child, self.kill_when_owner_dies);
252        Ok(PlatformChild::new(child, self.create_process_group))
253    }
254}
255
256/// Owned child handle returned by [`SpawnSpec::spawn`].
257pub struct PlatformChild {
258    child: Child,
259    stdin: Option<ChildStdin>,
260    stdout: Option<ChildStdout>,
261    stderr: Option<ChildStderr>,
262    signal: PlatformEmergencySignal,
263}
264
265impl PlatformChild {
266    fn new(mut child: Child, own_process_group: bool) -> Self {
267        let signal = PlatformEmergencySignal {
268            pid: child.id(),
269            own_process_group,
270        };
271        Self {
272            stdin: child.stdin.take(),
273            stdout: child.stdout.take(),
274            stderr: child.stderr.take(),
275            child,
276            signal,
277        }
278    }
279
280    /// Return the operating-system process identifier, if available.
281    pub fn id(&self) -> Option<u32> {
282        self.child.id()
283    }
284
285    /// Wait for completion without capturing output.
286    pub async fn wait(&mut self) -> io::Result<ExitStatus> {
287        self.child.wait().await
288    }
289
290    /// Terminate the child and wait for its exit.
291    pub async fn kill(&mut self) -> io::Result<()> {
292        self.child.kill().await
293    }
294
295    /// Capture piped stdout and stderr while waiting for the child.
296    pub async fn wait_with_output(self) -> io::Result<Output> {
297        let Self {
298            mut child,
299            stdin,
300            stdout,
301            stderr,
302            ..
303        } = self;
304        // Match Tokio's `Child::wait_with_output` contract: one-shot output
305        // closes an owned stdin pipe so a child waiting for EOF can finish.
306        drop(stdin);
307        let (status, stdout, stderr) = tokio::try_join!(
308            child.wait(),
309            read_owned_to_end(stdout),
310            read_owned_to_end(stderr),
311        )?;
312        Ok(Output {
313            status,
314            stdout,
315            stderr,
316        })
317    }
318
319    /// Write bytes to piped stdin and flush them.
320    pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
321        let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
322        stdin.write_all(bytes).await?;
323        stdin.flush().await
324    }
325
326    /// Close the piped stdin handle, delivering EOF to the child.
327    ///
328    /// This operation is idempotent. Closing an inherited or null stdin is
329    /// also a no-op because there is no owned pipe to close.
330    pub fn close_stdin(&mut self) {
331        drop(self.stdin.take());
332    }
333
334    /// Read all bytes from piped stdout without waiting for process exit.
335    pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
336        let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
337        let mut bytes = Vec::new();
338        stdout.read_to_end(&mut bytes).await?;
339        Ok(bytes)
340    }
341
342    /// Read all bytes from piped stderr without waiting for process exit.
343    pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
344        let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
345        let mut bytes = Vec::new();
346        stderr.read_to_end(&mut bytes).await?;
347        Ok(bytes)
348    }
349
350    /// Split this child into sealed actor capabilities.
351    ///
352    /// The lifecycle wait handle, emergency termination handle, input pipe,
353    /// and output readers are deliberately separate so the actor can keep
354    /// accepting control commands while an asynchronous exit wait is pending.
355    pub fn into_actor_parts(
356        self,
357    ) -> (
358        PlatformLifecycle,
359        PlatformEmergencySignal,
360        Option<PlatformStdin>,
361        Option<PlatformOutput>,
362        Option<PlatformOutput>,
363    ) {
364        (
365            PlatformLifecycle { child: self.child },
366            self.signal,
367            self.stdin.map(|stdin| PlatformStdin { stdin }),
368            self.stdout.map(PlatformOutput::stdout),
369            self.stderr.map(PlatformOutput::stderr),
370        )
371    }
372}
373
374/// Opaque exit-wait capability owned by a process actor.
375pub struct PlatformLifecycle {
376    child: Child,
377}
378
379impl PlatformLifecycle {
380    /// Wait asynchronously for the child to exit.
381    pub async fn wait(&mut self) -> io::Result<ExitStatus> {
382        self.child.wait().await
383    }
384}
385
386/// Opaque, non-reap-capable emergency termination capability.
387///
388/// It can be used while the actor has a pending wait on
389/// [`PlatformLifecycle`], but it cannot observe or consume the exit result.
390pub struct PlatformEmergencySignal {
391    pid: Option<u32>,
392    own_process_group: bool,
393}
394
395impl PlatformEmergencySignal {
396    /// Request immediate termination without waiting for process reaping.
397    pub fn kill(&self) -> io::Result<()> {
398        platform_imp::signal_process(self.target()?)
399    }
400
401    /// Ask the child's whole process group to shut down gracefully.
402    ///
403    /// Returns `Ok(false)` when the child was not spawned with
404    /// [`SpawnSpec::create_process_group`]: there is no group to address, and
405    /// signalling anyway would hit the caller's own group on POSIX or the
406    /// caller's console on Windows. A child that has already exited is also
407    /// `Ok` -- the soft step's only job is to give a live child a chance to
408    /// clean up before a hard kill, so a dead target is a success.
409    pub fn terminate_group_soft(&self) -> io::Result<bool> {
410        if !self.own_process_group {
411            return Ok(false);
412        }
413        platform_imp::signal_process_group(self.target()?).map(|()| true)
414    }
415
416    fn target(&self) -> io::Result<u32> {
417        self.pid.ok_or_else(|| {
418            io::Error::new(
419                io::ErrorKind::BrokenPipe,
420                "child process no longer has an emergency signal target",
421            )
422        })
423    }
424}
425
426/// Opaque piped stdin capability owned by a process actor.
427pub struct PlatformStdin {
428    stdin: ChildStdin,
429}
430
431impl PlatformStdin {
432    /// Write and flush bytes to the child stdin pipe.
433    pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
434        self.stdin.write_all(bytes).await?;
435        self.stdin.flush().await
436    }
437}
438
439/// Opaque stdout or stderr reader owned by a process actor.
440pub struct PlatformOutput {
441    reader: OutputReader,
442}
443
444enum OutputReader {
445    Stdout(ChildStdout),
446    Stderr(ChildStderr),
447}
448
449impl PlatformOutput {
450    fn stdout(stdout: ChildStdout) -> Self {
451        Self {
452            reader: OutputReader::Stdout(stdout),
453        }
454    }
455
456    fn stderr(stderr: ChildStderr) -> Self {
457        Self {
458            reader: OutputReader::Stderr(stderr),
459        }
460    }
461
462    /// Drain this output endpoint to EOF without blocking a runtime worker.
463    pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
464        match self.reader {
465            OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
466            OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
467        }
468    }
469
470    /// Read the next asynchronous chunk from this output endpoint.
471    ///
472    /// The caller owns the buffer and therefore controls the amount of data
473    /// retained at each read. EOF is reported as `Ok(0)`.
474    pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
475        match &mut self.reader {
476            OutputReader::Stdout(stdout) => stdout.read(buffer).await,
477            OutputReader::Stderr(stderr) => stderr.read(buffer).await,
478        }
479    }
480}
481
482fn stdin_not_piped() -> io::Error {
483    io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
484}
485
486fn stdout_not_piped() -> io::Error {
487    io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
488}
489
490fn stderr_not_piped() -> io::Error {
491    io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
492}
493
494async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
495where
496    R: AsyncRead + Unpin,
497{
498    let Some(mut reader) = reader else {
499        return Ok(Vec::new());
500    };
501    let mut bytes = Vec::new();
502    reader.read_to_end(&mut bytes).await?;
503    Ok(bytes)
504}
505
506/// Build a shell command using the host platform's supported shell.
507pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
508    platform_imp::shell_spec(command.as_ref())
509}
510
511#[cfg(test)]
512mod tests {
513    use super::{shell_spec, SpawnSpec, StreamMode};
514
515    fn fixture_command() -> SpawnSpec {
516        #[cfg(windows)]
517        {
518            shell_spec("echo async-platform-internal")
519        }
520        #[cfg(not(windows))]
521        {
522            shell_spec("printf async-platform-internal")
523        }
524    }
525
526    #[tokio::test]
527    async fn blessed_spawn_captures_output_without_sync_wait() {
528        let output = fixture_command()
529            .stdout(StreamMode::Piped)
530            .stderr(StreamMode::Piped)
531            .spawn()
532            .await
533            .expect("spawn")
534            .wait_with_output()
535            .await
536            .expect("wait with output");
537
538        assert!(output.status.success());
539        let expected = if cfg!(windows) {
540            b"async-platform-internal\r\n".as_slice()
541        } else {
542            b"async-platform-internal".as_slice()
543        };
544        assert_eq!(output.stdout, expected);
545        assert!(output.stderr.is_empty());
546    }
547
548    #[tokio::test]
549    async fn blessed_spawn_reports_missing_program() {
550        let result = SpawnSpec::new("running-process-program-that-does-not-exist")
551            .spawn()
552            .await;
553        assert!(result.is_err());
554    }
555
556    #[tokio::test]
557    async fn one_shot_output_closes_owned_stdin() {
558        #[cfg(windows)]
559        let spec = shell_spec("more > nul & echo done");
560        #[cfg(not(windows))]
561        let spec = shell_spec("cat > /dev/null; printf done");
562
563        let output = tokio::time::timeout(
564            std::time::Duration::from_secs(2),
565            spec.stdin(StreamMode::Piped)
566                .stdout(StreamMode::Piped)
567                .stderr(StreamMode::Piped)
568                .spawn()
569                .await
570                .expect("spawn")
571                .wait_with_output(),
572        )
573        .await
574        .expect("stdin is closed for one-shot output")
575        .expect("output succeeds");
576
577        let expected = if cfg!(windows) {
578            b"done\r\n".as_slice()
579        } else {
580            b"done".as_slice()
581        };
582        assert_eq!(output.stdout, expected);
583    }
584}