conpty-oxide 0.1.2

Correctness-first Windows ConPTY (pseudoconsole) library with sync and async (tokio) APIs
Documentation
// SPDX-FileCopyrightText: 2026 conpty-oxide contributors <https://github.com/P4suta/conpty-oxide/graphs/contributors>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Blocking command construction and root-child process ownership.

use std::ffi::OsStr;
use std::fmt;
use std::os::windows::io::{AsHandle, BorrowedHandle};
use std::path::Path;

use super::pty::Pty;
use super::session::Session;
use crate::core::child::ChildCore;
use crate::core::session;
use crate::error::Result;
use crate::status::ExitStatus;
use crate::SessionOptions;

/// A command to run inside a pseudoconsole.
///
/// Mirrors [`std::process::Command`]: the builder methods take `&mut self` and
/// return `&mut Self`, so a whole invocation can be written as one expression.
/// The differences from the standard library are the ones a pseudoconsole
/// forces:
///
/// - There is no stdio configuration. The child's console *is* the
///   pseudoconsole; its standard handles are deliberately set to
///   `INVALID_HANDLE_VALUE` so a redirected parent cannot leak its own stdio
///   into the child.
/// - No handles are inherited (`bInheritHandles` is `FALSE`), because a leaked
///   copy of the output pipe would keep the session from ever reaching
///   end-of-file.
/// - The child and every descendant it creates join a job object, which is
///   what makes [`Child::kill`] terminate the whole tree.
///
/// A command is intentionally not `Clone`: managed spawning must not copy or
/// mutate its potentially large argument and environment buffers. Low-level
/// lifecycle and unvalidated process flags are intentionally absent; hidden
/// compile-fail doctests pin both boundaries.
#[derive(Debug)]
pub struct Command {
    inner: crate::command::Command,
}

impl Command {
    /// Creates a builder for launching `program`.
    ///
    /// The program is not resolved here; a missing executable surfaces as
    /// an error with [`crate::ErrorKind::Spawn`] and an
    /// [`std::io::ErrorKind::NotFound`] source.
    #[must_use]
    pub fn new(program: impl AsRef<OsStr>) -> Self {
        Self {
            inner: crate::command::Command::new(program),
        }
    }

    /// Appends one argument, quoted and escaped as the MSVC C runtime expects.
    pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
        self.inner.arg(arg);
        self
    }

    /// Appends several arguments; equivalent to calling [`Command::arg`] for
    /// each one.
    pub fn args<I, S>(&mut self, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        self.inner.args(args);
        self
    }

    /// Appends literal text to the command line, bypassing all quoting.
    ///
    /// Same semantics as `std::os::windows::process::CommandExt::raw_arg`:
    /// intended for callees such as `cmd.exe /c` that parse the raw command
    /// line themselves.
    pub fn raw_arg(&mut self, text: impl AsRef<OsStr>) -> &mut Self {
        self.inner.raw_arg(text);
        self
    }

    /// Sets an environment variable for the child (case-insensitively, as
    /// Windows does).
    pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
        self.inner.env(key, value);
        self
    }

    /// Sets several environment variables; equivalent to calling
    /// [`Command::env`] for each pair.
    pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.inner.envs(vars);
        self
    }

    /// Removes an environment variable from the child's environment.
    pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
        self.inner.env_remove(key);
        self
    }

    /// Clears the child's environment, including modifications recorded so
    /// far. Variables set afterwards still apply.
    pub fn env_clear(&mut self) -> &mut Self {
        self.inner.env_clear();
        self
    }

    /// Sets the child's working directory.
    pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
        self.inner.current_dir(dir);
        self
    }

    /// Terminates the child's whole process tree when its [`Child`] is
    /// dropped. Defaults to `false`.
    ///
    /// This policy applies to [`Command::spawn_in`]. Managed
    /// [`Command::spawn`] sessions always enable kill-on-drop and
    /// kill-on-Job-close regardless of this setting.
    ///
    /// This also sets `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` on the session's
    /// job object, so the tree is terminated by the kernel even if this
    /// process dies without running any destructor.
    #[cfg(test)]
    pub(crate) fn kill_on_drop(&mut self, kill: bool) -> &mut Self {
        self.inner.kill_on_drop(kill);
        self
    }

    /// Spawns a managed session with default options.
    ///
    /// The returned session owns its pseudoconsole, I/O, child process, and a
    /// kill-on-close Job. Dropping it before completion terminates the entire
    /// process tree.
    ///
    /// # Errors
    ///
    /// Returns an error when the backend or pipes cannot be initialized, or
    /// when the root process cannot be spawned.
    pub fn spawn(&mut self) -> Result<Session> {
        self.spawn_with(SessionOptions::default())
    }

    /// Spawns a managed session with explicit safe options.
    ///
    /// # Errors
    ///
    /// Returns an error when the selected backend or pipes cannot be
    /// initialized, or when the root process cannot be spawned.
    pub fn spawn_with(&mut self, options: SessionOptions) -> Result<Session> {
        let (size, backend) = options.into_parts();
        let mut builder = Pty::builder().size(size);
        if let Some(backend) = backend {
            builder = builder.backend(backend);
        }
        let pty = builder.build()?;

        // Managed ownership is intentionally stricter than the low-level
        // command policy. Passing the policy to this spawn leaves the builder
        // untouched for a later low-level `spawn_in` call.
        let child = self.spawn_in_with_policy(&pty, true)?;
        let controller = pty.controller();
        let (output, input) = pty.into_split();
        Ok(Session::new(child, output, input, controller))
    }

    /// Spawns the command as the root child of an existing low-level `pty`.
    ///
    /// The session's shutdown strategy is armed as part of this call, in the
    /// order `ConPTY` requires: the child is created, the pseudoconsole is
    /// released when possible, and a root watcher is always installed to end
    /// remaining Job members. Legacy sessions also use it to force output EOF.
    ///
    /// A pseudoconsole hosts exactly one root child; spawning into a `Pty`
    /// that already has one fails with an
    /// [`std::io::ErrorKind::AlreadyExists`] source. (Descendants are
    /// unrestricted — the child may create as many as it likes, and they all
    /// join the same job object.)
    ///
    /// # Errors
    ///
    /// An error with [`crate::ErrorKind::Spawn`] carrying the program name and the
    /// underlying failure: [`std::io::ErrorKind::NotFound`] for a missing
    /// executable, [`std::io::ErrorKind::InvalidInput`] for a command line or
    /// environment block that cannot be built,
    /// [`std::io::ErrorKind::AlreadyExists`] for a re-used `Pty`, or the raw OS
    /// error from `CreateProcessW`.
    #[cfg(test)]
    pub(crate) fn spawn_in(&mut self, pty: &Pty) -> Result<Child> {
        self.spawn_in_with_policy(pty, self.inner.get_kill_on_drop())
    }

    fn spawn_in_with_policy(&mut self, pty: &Pty, kill_on_drop: bool) -> Result<Child> {
        let root = session::spawn_root(&pty.inner, &mut self.inner, kill_on_drop)?;
        Ok(Child {
            core: ChildCore::from_root(root),
        })
    }
}

/// A running (or finished) root child of a pseudoconsole session.
///
/// The handle owns the session's job object as well as the process handle, so
/// [`Child::kill`] terminates the whole process tree rather than just the
/// process this crate created.
///
/// Every publicly obtainable `Child` is managed and kill-on-drop. Dropping it
/// does not wait; the Job terminates the root process and every descendant
/// still running.
///
/// The process handle is available only through the lifetime-safe
/// [`AsHandle`] implementation; a hidden compile-fail doctest pins the
/// missing raw-handle escape hatch.
pub struct Child {
    core: ChildCore,
}

/// Shows the child's identity — pid, drop policy, and any cached exit status
/// — rather than the raw process and job handles, whose values are noise that
/// varies between runs.
impl fmt::Debug for Child {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.core.fmt(f)
    }
}

impl Child {
    /// Returns the child's process identifier.
    ///
    /// The identifier stays valid as long as this `Child` is alive; once the
    /// process handle is closed, Windows may reuse the number.
    #[must_use]
    pub const fn id(&self) -> u32 {
        self.core.id()
    }

    /// Waits for the child to exit and returns its status.
    ///
    /// Repeated calls return the cached status instead of waiting again.
    ///
    /// # Deadlock
    ///
    /// The child must be able to make progress while this blocks, which means
    /// something else has to drain the session's output — see the module docs.
    ///
    /// # Errors
    ///
    /// An error with [`crate::ErrorKind::Wait`] wrapping the OS error from
    /// `WaitForSingleObject` or `GetExitCodeProcess`.
    pub fn wait(&mut self) -> Result<ExitStatus> {
        self.core.wait_blocking()
    }

    /// Returns the exit status if the child has already exited, without
    /// blocking.
    ///
    /// # Errors
    ///
    /// An error with [`crate::ErrorKind::Wait`] wrapping the OS error from
    /// `WaitForSingleObject` or `GetExitCodeProcess`.
    pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
        self.core.try_wait()
    }

    /// Terminates the child and every descendant it created.
    ///
    /// This terminates the session's job object, so processes the child
    /// spawned are killed too. Termination is asynchronous: call
    /// [`Child::wait`] afterwards to observe the resulting status, which is
    /// exit code `1`.
    ///
    /// Killing an already-finished tree succeeds and does nothing.
    ///
    /// # Errors
    ///
    /// An error with [`crate::ErrorKind::Kill`] wrapping the OS error from
    /// `TerminateJobObject`.
    pub fn kill(&mut self) -> Result<()> {
        self.core.kill()
    }
}

/// Borrows the child's process handle, e.g. to duplicate it or to wait on it
/// together with other objects.
impl AsHandle for Child {
    fn as_handle(&self) -> BorrowedHandle<'_> {
        self.core.as_handle()
    }
}

/// The API boundaries stated on [`Command`] and [`Child`], pinned as
/// compile-fail doctests without rendering as examples.
///
/// A command is never `Clone`:
///
/// ```compile_fail
/// use conpty_oxide::blocking::Command;
///
/// fn requires_clone<T: Clone>() {}
/// requires_clone::<Command>();
/// ```
///
/// Low-level lifecycle and unvalidated process flags stay absent:
///
/// ```compile_fail
/// let mut command = conpty_oxide::blocking::Command::new("cmd.exe");
/// command.creation_flags(0);
/// ```
///
/// ```compile_fail
/// let mut command = conpty_oxide::blocking::Command::new("cmd.exe");
/// command.kill_on_drop(false);
/// ```
///
/// ```compile_fail
/// let mut command = conpty_oxide::blocking::Command::new("cmd.exe");
/// command.spawn_in(());
/// ```
///
/// The child exposes no raw process handle:
///
/// ```compile_fail
/// use std::os::windows::io::AsRawHandle;
///
/// fn requires_raw_handle<T: AsRawHandle>() {}
/// requires_raw_handle::<conpty_oxide::blocking::Child>();
/// ```
#[cfg(doctest)]
mod api_boundary {}