Skip to main content

conpty_oxide/blocking/
command.rs

1// SPDX-FileCopyrightText: 2026 conpty-oxide contributors <https://github.com/P4suta/conpty-oxide/graphs/contributors>
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Blocking command construction and root-child process ownership.
6
7use std::ffi::OsStr;
8use std::fmt;
9use std::os::windows::io::{AsHandle, BorrowedHandle};
10use std::path::Path;
11
12use super::pty::Pty;
13use super::session::Session;
14use crate::core::child::ChildCore;
15use crate::core::session;
16use crate::error::Result;
17use crate::status::ExitStatus;
18use crate::SessionOptions;
19
20/// A command to run inside a pseudoconsole.
21///
22/// Mirrors [`std::process::Command`]: the builder methods take `&mut self` and
23/// return `&mut Self`, so a whole invocation can be written as one expression.
24/// The differences from the standard library are the ones a pseudoconsole
25/// forces:
26///
27/// - There is no stdio configuration. The child's console *is* the
28///   pseudoconsole; its standard handles are deliberately set to
29///   `INVALID_HANDLE_VALUE` so a redirected parent cannot leak its own stdio
30///   into the child.
31/// - No handles are inherited (`bInheritHandles` is `FALSE`), because a leaked
32///   copy of the output pipe would keep the session from ever reaching
33///   end-of-file.
34/// - The child and every descendant it creates join a job object, which is
35///   what makes [`Child::kill`] terminate the whole tree.
36///
37/// A command is intentionally not `Clone`: managed spawning must not copy or
38/// mutate its potentially large argument and environment buffers.
39///
40/// ```compile_fail
41/// use conpty_oxide::blocking::Command;
42///
43/// fn requires_clone<T: Clone>() {}
44/// requires_clone::<Command>();
45/// ```
46///
47/// Low-level lifecycle and unvalidated process flags are intentionally absent:
48///
49/// ```compile_fail
50/// let mut command = conpty_oxide::blocking::Command::new("cmd.exe");
51/// command.creation_flags(0);
52/// ```
53///
54/// ```compile_fail
55/// let mut command = conpty_oxide::blocking::Command::new("cmd.exe");
56/// command.kill_on_drop(false);
57/// ```
58///
59/// ```compile_fail
60/// let mut command = conpty_oxide::blocking::Command::new("cmd.exe");
61/// command.spawn_in(());
62/// ```
63#[derive(Debug)]
64pub struct Command {
65    inner: crate::command::Command,
66}
67
68impl Command {
69    /// Creates a builder for launching `program`.
70    ///
71    /// The program is not resolved here; a missing executable surfaces as
72    /// an error with [`crate::ErrorKind::Spawn`] and an
73    /// [`std::io::ErrorKind::NotFound`] source.
74    #[must_use]
75    pub fn new(program: impl AsRef<OsStr>) -> Self {
76        Self {
77            inner: crate::command::Command::new(program),
78        }
79    }
80
81    /// Appends one argument, quoted and escaped as the MSVC C runtime expects.
82    pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
83        self.inner.arg(arg);
84        self
85    }
86
87    /// Appends several arguments; equivalent to calling [`Command::arg`] for
88    /// each one.
89    pub fn args<I, S>(&mut self, args: I) -> &mut Self
90    where
91        I: IntoIterator<Item = S>,
92        S: AsRef<OsStr>,
93    {
94        self.inner.args(args);
95        self
96    }
97
98    /// Appends literal text to the command line, bypassing all quoting.
99    ///
100    /// Same semantics as `std::os::windows::process::CommandExt::raw_arg`:
101    /// intended for callees such as `cmd.exe /c` that parse the raw command
102    /// line themselves.
103    pub fn raw_arg(&mut self, text: impl AsRef<OsStr>) -> &mut Self {
104        self.inner.raw_arg(text);
105        self
106    }
107
108    /// Sets an environment variable for the child (case-insensitively, as
109    /// Windows does).
110    pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
111        self.inner.env(key, value);
112        self
113    }
114
115    /// Sets several environment variables; equivalent to calling
116    /// [`Command::env`] for each pair.
117    pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
118    where
119        I: IntoIterator<Item = (K, V)>,
120        K: AsRef<OsStr>,
121        V: AsRef<OsStr>,
122    {
123        self.inner.envs(vars);
124        self
125    }
126
127    /// Removes an environment variable from the child's environment.
128    pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
129        self.inner.env_remove(key);
130        self
131    }
132
133    /// Clears the child's environment, including modifications recorded so
134    /// far. Variables set afterwards still apply.
135    pub fn env_clear(&mut self) -> &mut Self {
136        self.inner.env_clear();
137        self
138    }
139
140    /// Sets the child's working directory.
141    pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
142        self.inner.current_dir(dir);
143        self
144    }
145
146    /// Terminates the child's whole process tree when its [`Child`] is
147    /// dropped. Defaults to `false`.
148    ///
149    /// This policy applies to [`Command::spawn_in`]. Managed
150    /// [`Command::spawn`] sessions always enable kill-on-drop and
151    /// kill-on-Job-close regardless of this setting.
152    ///
153    /// This also sets `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` on the session's
154    /// job object, so the tree is terminated by the kernel even if this
155    /// process dies without running any destructor.
156    #[cfg(test)]
157    pub(crate) fn kill_on_drop(&mut self, kill: bool) -> &mut Self {
158        self.inner.kill_on_drop(kill);
159        self
160    }
161
162    /// Spawns a managed session with default options.
163    ///
164    /// The returned session owns its pseudoconsole, I/O, child process, and a
165    /// kill-on-close Job. Dropping it before completion terminates the entire
166    /// process tree.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error when the backend or pipes cannot be initialized, or
171    /// when the root process cannot be spawned.
172    pub fn spawn(&mut self) -> Result<Session> {
173        self.spawn_with(SessionOptions::default())
174    }
175
176    /// Spawns a managed session with explicit safe options.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error when the selected backend or pipes cannot be
181    /// initialized, or when the root process cannot be spawned.
182    pub fn spawn_with(&mut self, options: SessionOptions) -> Result<Session> {
183        let (size, backend) = options.into_parts();
184        let mut builder = Pty::builder().size(size);
185        if let Some(backend) = backend {
186            builder = builder.backend(backend);
187        }
188        let pty = builder.build()?;
189
190        // Managed ownership is intentionally stricter than the low-level
191        // command policy. Passing the policy to this spawn leaves the builder
192        // untouched for a later low-level `spawn_in` call.
193        let child = self.spawn_in_with_policy(&pty, true)?;
194        let controller = pty.controller();
195        let (output, input) = pty.into_split();
196        Ok(Session::new(child, output, input, controller))
197    }
198
199    /// Spawns the command as the root child of an existing low-level `pty`.
200    ///
201    /// The session's shutdown strategy is armed as part of this call, in the
202    /// order `ConPTY` requires: the child is created, the pseudoconsole is
203    /// released when possible, and a root watcher is always installed to end
204    /// remaining Job members. Legacy sessions also use it to force output EOF.
205    ///
206    /// A pseudoconsole hosts exactly one root child; spawning into a `Pty`
207    /// that already has one fails with an
208    /// [`std::io::ErrorKind::AlreadyExists`] source. (Descendants are
209    /// unrestricted — the child may create as many as it likes, and they all
210    /// join the same job object.)
211    ///
212    /// # Errors
213    ///
214    /// An error with [`crate::ErrorKind::Spawn`] carrying the program name and the
215    /// underlying failure: [`std::io::ErrorKind::NotFound`] for a missing
216    /// executable, [`std::io::ErrorKind::InvalidInput`] for a command line or
217    /// environment block that cannot be built,
218    /// [`std::io::ErrorKind::AlreadyExists`] for a re-used `Pty`, or the raw OS
219    /// error from `CreateProcessW`.
220    #[cfg(test)]
221    pub(crate) fn spawn_in(&mut self, pty: &Pty) -> Result<Child> {
222        self.spawn_in_with_policy(pty, self.inner.get_kill_on_drop())
223    }
224
225    fn spawn_in_with_policy(&self, pty: &Pty, kill_on_drop: bool) -> Result<Child> {
226        let root = session::spawn_root(&pty.inner, &self.inner, kill_on_drop)?;
227        Ok(Child {
228            core: ChildCore::from_root(root),
229        })
230    }
231}
232
233/// A running (or finished) root child of a pseudoconsole session.
234///
235/// The handle owns the session's job object as well as the process handle, so
236/// [`Child::kill`] terminates the whole process tree rather than just the
237/// process this crate created.
238///
239/// Every publicly obtainable `Child` is managed and kill-on-drop. Dropping it
240/// does not wait; the Job terminates the root process and every descendant
241/// still running.
242///
243/// The process handle is available only through the lifetime-safe
244/// [`AsHandle`] implementation:
245///
246/// ```compile_fail
247/// use std::os::windows::io::AsRawHandle;
248///
249/// fn requires_raw_handle<T: AsRawHandle>() {}
250/// requires_raw_handle::<conpty_oxide::blocking::Child>();
251/// ```
252pub struct Child {
253    core: ChildCore,
254}
255
256/// Shows the child's identity — pid, drop policy, and any cached exit status
257/// — rather than the raw process and job handles, whose values are noise that
258/// varies between runs.
259impl fmt::Debug for Child {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        self.core.fmt(f)
262    }
263}
264
265impl Child {
266    /// Returns the child's process identifier.
267    ///
268    /// The identifier stays valid as long as this `Child` is alive; once the
269    /// process handle is closed, Windows may reuse the number.
270    #[must_use]
271    pub const fn id(&self) -> u32 {
272        self.core.id()
273    }
274
275    /// Waits for the child to exit and returns its status.
276    ///
277    /// Repeated calls return the cached status instead of waiting again.
278    ///
279    /// # Deadlock
280    ///
281    /// The child must be able to make progress while this blocks, which means
282    /// something else has to drain the session's output — see the module docs.
283    ///
284    /// # Errors
285    ///
286    /// An error with [`crate::ErrorKind::Wait`] wrapping the OS error from
287    /// `WaitForSingleObject` or `GetExitCodeProcess`.
288    pub fn wait(&mut self) -> Result<ExitStatus> {
289        self.core.wait_blocking()
290    }
291
292    /// Returns the exit status if the child has already exited, without
293    /// blocking.
294    ///
295    /// # Errors
296    ///
297    /// An error with [`crate::ErrorKind::Wait`] wrapping the OS error from
298    /// `WaitForSingleObject` or `GetExitCodeProcess`.
299    pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
300        self.core.try_wait()
301    }
302
303    /// Terminates the child and every descendant it created.
304    ///
305    /// This terminates the session's job object, so processes the child
306    /// spawned are killed too. Termination is asynchronous: call
307    /// [`Child::wait`] afterwards to observe the resulting status, which is
308    /// exit code `1`.
309    ///
310    /// Killing an already-finished tree succeeds and does nothing.
311    ///
312    /// # Errors
313    ///
314    /// An error with [`crate::ErrorKind::Kill`] wrapping the OS error from
315    /// `TerminateJobObject`.
316    pub fn kill(&mut self) -> Result<()> {
317        self.core.kill()
318    }
319}
320
321/// Borrows the child's process handle, e.g. to duplicate it or to wait on it
322/// together with other objects.
323impl AsHandle for Child {
324    fn as_handle(&self) -> BorrowedHandle<'_> {
325        self.core.as_handle()
326    }
327}