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}