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