rust_pty/
traits.rs

1//! Core traits for PTY abstraction.
2//!
3//! This module defines the primary traits used by the rust-pty crate:
4//!
5//! - [`PtyMaster`]: The master side of a PTY (for reading/writing to the terminal).
6//! - [`PtyChild`]: Handle for the spawned child process.
7//! - [`PtySystem`]: Factory for creating PTY sessions.
8
9use std::future::Future;
10use std::pin::Pin;
11
12use tokio::io::{AsyncRead, AsyncWrite};
13
14use crate::config::{PtyConfig, PtySignal, WindowSize};
15use crate::error::Result;
16
17/// The master side of a pseudo-terminal.
18///
19/// This trait represents the controller end of a PTY pair. It provides
20/// async read/write access to the terminal and methods for controlling
21/// the PTY (resizing, closing, etc.).
22///
23/// # Platform Behavior
24///
25/// - **Unix**: Wraps a file descriptor for the master PTY.
26/// - **Windows**: Wraps `ConPTY` input/output pipes.
27pub trait PtyMaster: AsyncRead + AsyncWrite + Send + Sync + Unpin {
28    /// Resize the PTY to the given window size.
29    ///
30    /// This sends a window size change notification to the child process
31    /// (SIGWINCH on Unix, `ConPTY` resize on Windows).
32    fn resize(&self, size: WindowSize) -> Result<()>;
33
34    /// Get the current window size.
35    fn window_size(&self) -> Result<WindowSize>;
36
37    /// Close the master side of the PTY.
38    ///
39    /// This signals EOF to the child process. After calling this method,
40    /// reads will return EOF and writes will fail.
41    fn close(&mut self) -> Result<()>;
42
43    /// Check if the PTY is still open.
44    fn is_open(&self) -> bool;
45
46    /// Get the raw file descriptor (Unix) or handle (Windows).
47    ///
48    /// # Safety
49    ///
50    /// The returned value is platform-specific and should only be used
51    /// for low-level operations that understand the platform semantics.
52    #[cfg(unix)]
53    fn as_raw_fd(&self) -> std::os::unix::io::RawFd;
54
55    /// Get the raw handle (Windows only).
56    #[cfg(windows)]
57    fn as_raw_handle(&self) -> std::os::windows::io::RawHandle;
58}
59
60/// Handle for a child process spawned in a PTY.
61///
62/// This trait provides methods for monitoring and controlling the child
63/// process. It's separate from [`PtyMaster`] to allow independent lifetime
64/// management of the PTY and the process.
65pub trait PtyChild: Send + Sync {
66    /// Get the process ID of the child.
67    fn pid(&self) -> u32;
68
69    /// Check if the child process is still running.
70    fn is_running(&self) -> bool;
71
72    /// Wait for the child process to exit.
73    ///
74    /// Returns the exit status when the process terminates.
75    fn wait(&mut self) -> Pin<Box<dyn Future<Output = Result<ExitStatus>> + Send + '_>>;
76
77    /// Try to get the exit status without blocking.
78    ///
79    /// Returns `None` if the process is still running.
80    fn try_wait(&mut self) -> Result<Option<ExitStatus>>;
81
82    /// Send a signal to the child process.
83    fn signal(&self, signal: PtySignal) -> Result<()>;
84
85    /// Kill the child process.
86    ///
87    /// This sends SIGKILL on Unix or calls `TerminateProcess` on Windows.
88    fn kill(&mut self) -> Result<()>;
89}
90
91/// Exit status of a child process.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum ExitStatus {
94    /// The process exited normally with the given exit code.
95    Exited(i32),
96
97    /// The process was terminated by a signal (Unix only).
98    #[cfg(unix)]
99    Signaled(i32),
100
101    /// The process was terminated (Windows).
102    /// The exit code may not be meaningful.
103    #[cfg(windows)]
104    Terminated(u32),
105}
106
107impl ExitStatus {
108    /// Check if the process exited successfully (exit code 0).
109    #[must_use]
110    pub const fn success(&self) -> bool {
111        matches!(self, Self::Exited(0))
112    }
113
114    /// Get the exit code, if available.
115    #[must_use]
116    pub const fn code(&self) -> Option<i32> {
117        match self {
118            Self::Exited(code) => Some(*code),
119            #[cfg(unix)]
120            Self::Signaled(_) => None,
121            #[cfg(windows)]
122            Self::Terminated(code) => Some(*code as i32),
123        }
124    }
125
126    /// Get the signal number that terminated the process (Unix only).
127    #[cfg(unix)]
128    #[must_use]
129    pub const fn signal(&self) -> Option<i32> {
130        match self {
131            Self::Signaled(sig) => Some(*sig),
132            Self::Exited(_) => None,
133        }
134    }
135}
136
137impl std::fmt::Display for ExitStatus {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        match self {
140            Self::Exited(code) => write!(f, "exited with code {code}"),
141            #[cfg(unix)]
142            Self::Signaled(sig) => write!(f, "terminated by signal {sig}"),
143            #[cfg(windows)]
144            Self::Terminated(code) => write!(f, "terminated with code {code}"),
145        }
146    }
147}
148
149/// Factory trait for creating PTY sessions.
150///
151/// This trait provides the main entry point for spawning processes in a PTY.
152/// Platform-specific implementations handle the details of PTY creation.
153pub trait PtySystem: Send + Sync {
154    /// The master PTY type for this platform.
155    type Master: PtyMaster;
156    /// The child process type for this platform.
157    type Child: PtyChild;
158
159    /// Spawn a new process in a PTY.
160    ///
161    /// # Arguments
162    ///
163    /// * `program` - The program to execute.
164    /// * `args` - Command-line arguments (not including the program name).
165    /// * `config` - PTY configuration.
166    ///
167    /// # Returns
168    ///
169    /// A tuple of the master PTY and child process handle.
170    fn spawn<S, I>(
171        program: S,
172        args: I,
173        config: &PtyConfig,
174    ) -> impl Future<Output = Result<(Self::Master, Self::Child)>> + Send
175    where
176        S: AsRef<std::ffi::OsStr> + Send,
177        I: IntoIterator + Send,
178        I::Item: AsRef<std::ffi::OsStr>;
179
180    /// Spawn a shell in a PTY using the default configuration.
181    ///
182    /// On Unix, this uses the user's shell from the SHELL environment variable
183    /// or falls back to `/bin/sh`. On Windows, this uses `cmd.exe`.
184    #[must_use]
185    fn spawn_shell(
186        config: &PtyConfig,
187    ) -> impl Future<Output = Result<(Self::Master, Self::Child)>> + Send {
188        async move {
189            #[cfg(unix)]
190            let shell =
191                std::env::var_os("SHELL").unwrap_or_else(|| std::ffi::OsString::from("/bin/sh"));
192            #[cfg(windows)]
193            let shell = std::ffi::OsString::from("cmd.exe");
194
195            Self::spawn(&shell, std::iter::empty::<&str>(), config).await
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn exit_status_success() {
206        let status = ExitStatus::Exited(0);
207        assert!(status.success());
208        assert_eq!(status.code(), Some(0));
209    }
210
211    #[test]
212    fn exit_status_failure() {
213        let status = ExitStatus::Exited(1);
214        assert!(!status.success());
215        assert_eq!(status.code(), Some(1));
216    }
217
218    #[cfg(unix)]
219    #[test]
220    fn exit_status_signaled() {
221        let status = ExitStatus::Signaled(9);
222        assert!(!status.success());
223        assert_eq!(status.code(), None);
224        assert_eq!(status.signal(), Some(9));
225    }
226}