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