1use std::io;
7use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
8use std::pin::Pin;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::task::{Context, Poll};
12
13use rustix::fs::{OFlags, fcntl_setfl};
14use rustix::pty::{OpenptFlags, grantpt, openpt, ptsname, unlockpt};
15#[cfg(not(target_os = "macos"))]
16use rustix::termios::{Winsize, tcsetwinsize};
17use tokio::io::unix::AsyncFd;
18use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
19
20use crate::config::WindowSize;
21use crate::error::{PtyError, Result};
22use crate::traits::PtyMaster;
23
24pub struct UnixPtyMaster {
29 async_fd: AsyncFd<OwnedFd>,
31 open: Arc<AtomicBool>,
33}
34
35impl std::fmt::Debug for UnixPtyMaster {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("UnixPtyMaster")
38 .field("fd", &self.async_fd.as_raw_fd())
39 .field("open", &self.open.load(Ordering::SeqCst))
40 .finish()
41 }
42}
43
44impl UnixPtyMaster {
45 pub fn open() -> Result<(Self, String)> {
53 let master_fd = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY)
55 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
56
57 grantpt(&master_fd)
59 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
60
61 unlockpt(&master_fd)
63 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
64
65 let slave_name = ptsname(&master_fd, Vec::new())
67 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
68 let slave_path = slave_name
69 .to_str()
70 .map_err(|_| {
71 PtyError::Create(io::Error::new(
72 io::ErrorKind::InvalidData,
73 "invalid slave path encoding",
74 ))
75 })?
76 .to_string();
77
78 fcntl_setfl(&master_fd, OFlags::NONBLOCK)
80 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
81
82 let async_fd = AsyncFd::new(master_fd).map_err(PtyError::Create)?;
84
85 Ok((
86 Self {
87 async_fd,
88 open: Arc::new(AtomicBool::new(true)),
89 },
90 slave_path,
91 ))
92 }
93
94 pub fn slave_name(&self) -> Result<String> {
98 let name = ptsname(self.async_fd.get_ref(), Vec::new())
99 .map_err(|e| PtyError::Io(io::Error::from_raw_os_error(e.raw_os_error())))?;
100 name.to_str()
101 .map(std::string::ToString::to_string)
102 .map_err(|_| {
103 PtyError::Io(io::Error::new(
104 io::ErrorKind::InvalidData,
105 "invalid slave path encoding",
106 ))
107 })
108 }
109
110 #[must_use]
112 pub fn is_open(&self) -> bool {
113 self.open.load(Ordering::SeqCst)
114 }
115
116 pub fn set_window_size(&self, size: WindowSize) -> Result<()> {
118 if !self.is_open() {
119 return Err(PtyError::Closed);
120 }
121
122 #[cfg(target_os = "macos")]
124 {
125 #[allow(clippy::struct_field_names)]
126 #[repr(C)]
127 struct LibcWinsize {
128 ws_row: libc::c_ushort,
129 ws_col: libc::c_ushort,
130 ws_xpixel: libc::c_ushort,
131 ws_ypixel: libc::c_ushort,
132 }
133
134 let winsize = LibcWinsize {
135 ws_row: size.rows,
136 ws_col: size.cols,
137 ws_xpixel: size.xpixel,
138 ws_ypixel: size.ypixel,
139 };
140
141 #[allow(unsafe_code)]
144 let result = unsafe {
145 libc::ioctl(
146 self.async_fd.as_raw_fd(),
147 libc::TIOCSWINSZ,
148 &raw const winsize,
149 )
150 };
151
152 if result == -1 {
153 return Err(PtyError::Resize(io::Error::last_os_error()));
154 }
155 Ok(())
156 }
157
158 #[cfg(not(target_os = "macos"))]
160 {
161 let winsize = Winsize {
162 ws_col: size.cols,
163 ws_row: size.rows,
164 ws_xpixel: size.xpixel,
165 ws_ypixel: size.ypixel,
166 };
167
168 tcsetwinsize(self.async_fd.get_ref(), winsize)
169 .map_err(|e| PtyError::Resize(io::Error::from_raw_os_error(e.raw_os_error())))
170 }
171 }
172
173 pub fn get_window_size(&self) -> Result<WindowSize> {
175 if !self.is_open() {
176 return Err(PtyError::Closed);
177 }
178
179 let winsize = rustix::termios::tcgetwinsize(self.async_fd.get_ref())
180 .map_err(|e| PtyError::GetAttributes(io::Error::from_raw_os_error(e.raw_os_error())))?;
181
182 Ok(WindowSize {
183 cols: winsize.ws_col,
184 rows: winsize.ws_row,
185 xpixel: winsize.ws_xpixel,
186 ypixel: winsize.ws_ypixel,
187 })
188 }
189
190 pub fn close(&mut self) -> Result<()> {
192 self.open.store(false, Ordering::SeqCst);
193 Ok(())
194 }
195}
196
197impl AsRawFd for UnixPtyMaster {
198 fn as_raw_fd(&self) -> RawFd {
199 self.async_fd.as_raw_fd()
200 }
201}
202
203impl AsyncRead for UnixPtyMaster {
204 fn poll_read(
205 self: Pin<&mut Self>,
206 cx: &mut Context<'_>,
207 buf: &mut ReadBuf<'_>,
208 ) -> Poll<io::Result<()>> {
209 if !self.open.load(Ordering::SeqCst) {
210 return Poll::Ready(Ok(())); }
212
213 loop {
214 let mut guard = match self.async_fd.poll_read_ready(cx) {
215 Poll::Ready(Ok(guard)) => guard,
216 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
217 Poll::Pending => return Poll::Pending,
218 };
219
220 let unfilled = buf.initialize_unfilled();
221 match rustix::io::read(self.async_fd.get_ref(), unfilled) {
222 Ok(0) => {
223 return Poll::Ready(Ok(()));
225 }
226 Ok(n) => {
227 buf.advance(n);
228 return Poll::Ready(Ok(()));
229 }
230 Err(rustix::io::Errno::AGAIN) => {
231 guard.clear_ready();
232 }
233 Err(e) => {
234 return Poll::Ready(Err(io::Error::from_raw_os_error(e.raw_os_error())));
235 }
236 }
237 }
238 }
239}
240
241impl AsyncWrite for UnixPtyMaster {
242 fn poll_write(
243 self: Pin<&mut Self>,
244 cx: &mut Context<'_>,
245 buf: &[u8],
246 ) -> Poll<io::Result<usize>> {
247 if !self.open.load(Ordering::SeqCst) {
248 return Poll::Ready(Err(io::Error::new(io::ErrorKind::BrokenPipe, "PTY closed")));
249 }
250
251 loop {
252 let mut guard = match self.async_fd.poll_write_ready(cx) {
253 Poll::Ready(Ok(guard)) => guard,
254 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
255 Poll::Pending => return Poll::Pending,
256 };
257
258 match rustix::io::write(self.async_fd.get_ref(), buf) {
259 Ok(n) => return Poll::Ready(Ok(n)),
260 Err(rustix::io::Errno::AGAIN) => {
261 guard.clear_ready();
262 }
263 Err(e) => {
264 return Poll::Ready(Err(io::Error::from_raw_os_error(e.raw_os_error())));
265 }
266 }
267 }
268 }
269
270 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
271 Poll::Ready(Ok(()))
272 }
273
274 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
275 self.open.store(false, Ordering::SeqCst);
276 Poll::Ready(Ok(()))
277 }
278}
279
280impl PtyMaster for UnixPtyMaster {
281 fn resize(&self, size: WindowSize) -> Result<()> {
282 self.set_window_size(size)
283 }
284
285 fn window_size(&self) -> Result<WindowSize> {
286 self.get_window_size()
287 }
288
289 fn close(&mut self) -> Result<()> {
290 Self::close(self)
291 }
292
293 fn is_open(&self) -> bool {
294 Self::is_open(self)
295 }
296
297 fn as_raw_fd(&self) -> RawFd {
298 AsRawFd::as_raw_fd(self)
299 }
300}
301
302pub fn open_slave(path: &str) -> Result<OwnedFd> {
308 use std::path::Path;
309
310 use rustix::fs::{Mode, OFlags, open};
311
312 let fd = open(
313 Path::new(path),
314 OFlags::RDWR | OFlags::NOCTTY,
315 Mode::empty(),
316 )
317 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
318
319 Ok(fd)
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[tokio::test]
327 async fn open_pty() {
328 let result = UnixPtyMaster::open();
329 assert!(result.is_ok());
330
331 let (master, slave_path) = result.unwrap();
332 assert!(master.is_open());
333 assert!(
335 slave_path.starts_with("/dev/pts/")
336 || slave_path.starts_with("/dev/ttys")
337 || slave_path.starts_with("/dev/ttyp")
338 || slave_path.starts_with("/dev/pty")
339 );
340 }
341
342 #[tokio::test]
343 async fn window_size_operations() {
344 let (master, _slave_path) = UnixPtyMaster::open().unwrap();
345
346 #[cfg(target_os = "macos")]
348 let _slave_fd = open_slave(&_slave_path).unwrap();
349
350 let size = WindowSize::new(120, 40);
352 assert!(master.set_window_size(size).is_ok());
353
354 let retrieved = master.get_window_size().unwrap();
356 assert_eq!(retrieved.cols, 120);
357 assert_eq!(retrieved.rows, 40);
358 }
359
360 #[tokio::test]
361 async fn close_pty() {
362 let (mut master, _) = UnixPtyMaster::open().unwrap();
363 assert!(master.is_open());
364
365 master.close().unwrap();
366 assert!(!master.is_open());
367 }
368}