rust-pty 0.1.0

Cross-platform async PTY (pseudo-terminal) library for Rust
Documentation
//! Windows ConPTY (Console Pseudo Terminal) management.
//!
//! This module provides the core ConPTY functionality, wrapping the Windows
//! Pseudo Console API introduced in Windows 10 1809.

use std::os::windows::io::{AsRawHandle, OwnedHandle};

use windows_sys::Win32::Foundation::{HANDLE, S_OK};
use windows_sys::Win32::System::Console::{
    COORD, ClosePseudoConsole, CreatePseudoConsole, HPCON, ResizePseudoConsole,
};

use crate::config::WindowSize;
use crate::error::{PtyError, Result};

/// A wrapper around a Windows Pseudo Console (ConPTY).
#[derive(Debug)]
pub struct ConPty {
    /// The pseudo console handle.
    handle: HPCON,
    /// The input pipe (for writing to the PTY).
    input_write: OwnedHandle,
    /// The output pipe (for reading from the PTY).
    output_read: OwnedHandle,
    /// Pipe handles passed to CreatePseudoConsole that must be closed after child spawn.
    /// These are kept alive until close_pty_pipes() is called.
    pty_pipes: Option<(OwnedHandle, OwnedHandle)>,
}

// SAFETY: ConPTY handles can be safely sent between threads
unsafe impl Send for ConPty {}
unsafe impl Sync for ConPty {}

impl ConPty {
    /// Create a new ConPTY with the specified window size.
    ///
    /// # Arguments
    ///
    /// * `size` - The initial window size.
    /// * `input_read` - The read end of the input pipe (passed to ConPTY).
    /// * `output_write` - The write end of the output pipe (passed to ConPTY).
    /// * `input_write` - The write end of the input pipe (kept for writing).
    /// * `output_read` - The read end of the output pipe (kept for reading).
    ///
    /// # Errors
    ///
    /// Returns an error if ConPTY creation fails or is not available.
    pub fn new(
        size: WindowSize,
        input_read: OwnedHandle,
        output_write: OwnedHandle,
        input_write: OwnedHandle,
        output_read: OwnedHandle,
    ) -> Result<Self> {
        let coord = COORD {
            X: size.cols as i16,
            Y: size.rows as i16,
        };

        let mut hpc: HPCON = 0;

        // SAFETY: All handles are valid and the pointer is valid
        let result = unsafe {
            CreatePseudoConsole(
                coord,
                input_read.as_raw_handle() as HANDLE,
                output_write.as_raw_handle() as HANDLE,
                0, // dwFlags
                &mut hpc,
            )
        };

        if result != S_OK {
            return Err(PtyError::Windows {
                message: "failed to create pseudo console".into(),
                code: result as u32,
            });
        }

        if hpc == 0 {
            return Err(PtyError::ConPtyNotAvailable);
        }

        // Store the pipe handles - they must be closed AFTER CreateProcess
        // per Microsoft documentation to enable proper channel detection
        Ok(Self {
            handle: hpc,
            input_write,
            output_read,
            pty_pipes: Some((input_read, output_write)),
        })
    }

    /// Close the PTY pipe handles after child process is spawned.
    ///
    /// This must be called after CreateProcess to enable proper channel detection.
    /// Closing these handles signals to ConPTY that no more handles exist on
    /// the "other side" of the pipes.
    pub fn close_pty_pipes(&mut self) {
        // Drop the handles, which closes them
        self.pty_pipes = None;
    }

    /// Get the ConPTY handle.
    #[must_use]
    pub fn handle(&self) -> HPCON {
        self.handle
    }

    /// Get a reference to the input write handle.
    #[must_use]
    pub fn input(&self) -> &OwnedHandle {
        &self.input_write
    }

    /// Get a reference to the output read handle.
    #[must_use]
    pub fn output(&self) -> &OwnedHandle {
        &self.output_read
    }

    /// Resize the ConPTY window.
    ///
    /// # Errors
    ///
    /// Returns an error if the resize operation fails.
    pub fn resize(&self, size: WindowSize) -> Result<()> {
        let coord = COORD {
            X: size.cols as i16,
            Y: size.rows as i16,
        };

        // SAFETY: handle is valid
        let result = unsafe { ResizePseudoConsole(self.handle, coord) };

        if result != S_OK {
            return Err(PtyError::Windows {
                message: "failed to resize pseudo console".into(),
                code: result as u32,
            });
        }

        Ok(())
    }
}

impl Drop for ConPty {
    fn drop(&mut self) {
        // SAFETY: handle was obtained from CreatePseudoConsole
        unsafe {
            ClosePseudoConsole(self.handle);
        }
    }
}

/// Check if ConPTY is available on this Windows version.
///
/// ConPTY was introduced in Windows 10 version 1809 (build 17763).
#[must_use]
pub fn is_conpty_available() -> bool {
    use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};

    // Check if CreatePseudoConsole exists in kernel32
    // SAFETY: kernel32.dll is always loaded in Windows processes
    let kernel32 = unsafe { GetModuleHandleW(windows_sys::w!("kernel32.dll")) };
    if kernel32.is_null() {
        return false;
    }

    // SAFETY: kernel32 handle is valid
    let proc = unsafe { GetProcAddress(kernel32, windows_sys::s!("CreatePseudoConsole")) };

    proc.is_some()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn check_conpty_availability() {
        // This just tests that the function runs without panicking
        let _ = is_conpty_available();
    }
}