1#![deny(missing_docs)]
2#![deny(unsafe_op_in_unsafe_fn)]
3
4mod backend;
14mod child;
15mod pty;
16mod size;
17
18#[cfg(any(test, all(not(unix), not(windows))))]
19pub(crate) mod unsupported_op {
20 pub(crate) const OPEN_PTY_PAIR: &str = "open pty pair";
27 pub(crate) const SPAWN_PTY_CHILD: &str = "spawn pty child";
28 pub(crate) const WAIT_FOR_PTY_CHILD: &str = "wait for pty child";
29 pub(crate) const TRY_WAIT_FOR_PTY_CHILD: &str = "try wait for pty child";
30 pub(crate) const SIGNAL_PTY_FOREGROUND: &str = "signal pty foreground process group";
31 pub(crate) const SIGNAL_PTY_SESSION_LEADER: &str = "signal pty session leader";
32 pub(crate) const QUERY_PTY_SIZE: &str = "query pty size";
33 pub(crate) const RESIZE_PTY: &str = "resize pty";
34 pub(crate) const CLONE_PTY_IO: &str = "clone pty io";
35
36 pub(crate) const ALL: &[&str] = &[
37 OPEN_PTY_PAIR,
38 SPAWN_PTY_CHILD,
39 WAIT_FOR_PTY_CHILD,
40 TRY_WAIT_FOR_PTY_CHILD,
41 SIGNAL_PTY_FOREGROUND,
42 SIGNAL_PTY_SESSION_LEADER,
43 QUERY_PTY_SIZE,
44 RESIZE_PTY,
45 CLONE_PTY_IO,
46 ];
47}
48
49use std::error::Error as StdError;
50use std::ffi::NulError;
51use std::fmt;
52
53pub use child::{ChildCommand, PtyChild, SpawnedPty};
54#[cfg(unix)]
55pub use pty::PtySlave;
56pub use pty::{PtyIo, PtyMaster, PtyPair};
57pub use size::{TerminalGeometry, TerminalPixels, TerminalSize};
58
59pub type Result<T> = std::result::Result<T, PtyError>;
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
64pub struct ProcessId(u32);
65
66impl ProcessId {
67 pub fn new(raw: u32) -> Result<Self> {
69 if raw == 0 {
70 return Err(PtyError::InvalidPid(raw));
71 }
72 Ok(Self(raw))
73 }
74
75 #[must_use]
77 pub const fn as_u32(self) -> u32 {
78 self.0
79 }
80
81 #[cfg(unix)]
82 pub(crate) fn as_rustix_pid(self) -> Result<rustix::process::Pid> {
83 let raw = i32::try_from(self.0).map_err(|_| PtyError::InvalidPid(self.0))?;
84 rustix::process::Pid::from_raw(raw).ok_or(PtyError::InvalidPid(self.0))
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub enum PtySignal {
91 Interrupt,
93 Terminate,
95 Kill,
97 Hangup,
99 Continue,
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct Signal(PtySignal);
106
107impl Signal {
108 pub const INT: Self = Self(PtySignal::Interrupt);
110 pub const TERM: Self = Self(PtySignal::Terminate);
112 pub const KILL: Self = Self(PtySignal::Kill);
114 pub const HUP: Self = Self(PtySignal::Hangup);
116 pub const CONT: Self = Self(PtySignal::Continue);
118
119 #[cfg(unix)]
120 pub(crate) const fn as_rustix_signal(self) -> rustix::process::Signal {
121 match self.0 {
122 PtySignal::Interrupt => rustix::process::Signal::INT,
123 PtySignal::Terminate => rustix::process::Signal::TERM,
124 PtySignal::Kill => rustix::process::Signal::KILL,
125 PtySignal::Hangup => rustix::process::Signal::HUP,
126 PtySignal::Continue => rustix::process::Signal::CONT,
127 }
128 }
129}
130
131#[derive(Debug)]
133pub enum PtyError {
134 #[cfg(unix)]
136 Os(rustix::io::Errno),
137 Spawn(std::io::Error),
139 Nul(NulError),
141 InvalidPid(u32),
143 Unsupported(&'static str),
145}
146
147impl fmt::Display for PtyError {
148 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149 match self {
150 #[cfg(unix)]
151 Self::Os(errno) => write!(formatter, "pty syscall failed: {errno}"),
152 Self::Spawn(error) => write!(formatter, "child process operation failed: {error}"),
153 Self::Nul(error) => write!(
154 formatter,
155 "interior NUL byte in process configuration: {error}"
156 ),
157 Self::InvalidPid(pid) => {
158 write!(formatter, "child process returned an invalid pid: {pid}")
159 }
160 Self::Unsupported(operation) => {
161 write!(
162 formatter,
163 "pty operation is unsupported on this platform: {operation}"
164 )
165 }
166 }
167 }
168}
169
170impl StdError for PtyError {
171 fn source(&self) -> Option<&(dyn StdError + 'static)> {
172 match self {
173 #[cfg(unix)]
174 Self::Os(errno) => Some(errno),
175 Self::Spawn(error) => Some(error),
176 Self::Nul(error) => Some(error),
177 Self::InvalidPid(_) => None,
178 Self::Unsupported(_) => None,
179 }
180 }
181}
182
183#[cfg(unix)]
184impl From<rustix::io::Errno> for PtyError {
185 fn from(value: rustix::io::Errno) -> Self {
186 Self::Os(value)
187 }
188}
189
190impl From<std::io::Error> for PtyError {
191 fn from(value: std::io::Error) -> Self {
192 Self::Spawn(value)
193 }
194}
195
196impl From<NulError> for PtyError {
197 fn from(value: NulError) -> Self {
198 Self::Nul(value)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::{unsupported_op, PtyError};
205
206 #[test]
207 fn pty_error_unsupported_display_is_stable_for_documented_operations() {
208 for operation in unsupported_op::ALL {
209 let formatted = format!("{}", PtyError::Unsupported(operation));
210 assert_eq!(
211 formatted,
212 format!("pty operation is unsupported on this platform: {operation}")
213 );
214 }
215 }
216
217 #[test]
218 fn pty_error_unsupported_inventory_matches_documented_count() {
219 assert_eq!(unsupported_op::ALL.len(), 9);
220 }
221
222 #[test]
223 fn pty_error_unsupported_inventory_entries_are_unique_and_non_empty() {
224 use std::collections::BTreeSet;
225
226 let unique: BTreeSet<&&str> = unsupported_op::ALL.iter().collect();
227 assert_eq!(unique.len(), unsupported_op::ALL.len());
228 for operation in unsupported_op::ALL {
229 assert!(!operation.is_empty());
230 assert!(!operation.contains(':'));
231 }
232 }
233
234 #[test]
235 fn pty_error_unsupported_carries_no_source() {
236 use std::error::Error as _;
237
238 let err = PtyError::Unsupported(unsupported_op::QUERY_PTY_SIZE);
239 assert!(err.source().is_none());
240 }
241
242 #[cfg(all(not(unix), not(windows)))]
243 #[test]
244 fn unsupported_backend_returns_explicit_errors() {
245 use std::io;
246
247 use super::{ChildCommand, PtyPair};
248
249 let open_pair =
250 PtyPair::open().expect_err("non-Unix non-Windows targets have no PTY backend");
251 assert!(matches!(
252 open_pair,
253 PtyError::Unsupported(op) if op == unsupported_op::OPEN_PTY_PAIR
254 ));
255
256 let spawn = ChildCommand::new("cmd.exe")
257 .spawn()
258 .expect_err("non-Unix non-Windows targets have no PTY backend");
259 assert!(matches!(
260 spawn,
261 PtyError::Unsupported(op) if op == unsupported_op::SPAWN_PTY_CHILD
262 ));
263
264 let _ = io::ErrorKind::Unsupported;
272 }
273}