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