tab_pty_process/
lib.rs

1// Copyright 2018-2019 Peter Williams <peter@newton.cx>
2// Licensed under both the MIT License and the Apache-2.0 license.
3
4#![deny(missing_docs)]
5#![doc(html_root_url = "https://docs.rs/tokio-pty-process/0.4.0")]
6
7//! Spawn a child process under a pseudo-TTY, interacting with it
8//! asynchronously using Tokio.
9//!
10//! A [pseudo-terminal](https://en.wikipedia.org/wiki/Pseudoterminal) (or
11//! “pseudo-TTY” or “PTY”) is a special Unix file handle that models the kind
12//! of text terminal through which users used to interact with computers. A
13//! PTY enables a specialized form of bidirectional interprocess communication
14//! that a variety of user-facing Unix programs take advantage of.
15//!
16//! The basic way to use this crate is:
17//!
18//! 1. Create a Tokio [Reactor](https://docs.rs/tokio/*/tokio/reactor/struct.Reactor.html)
19//!    that will handle all of your asynchronous I/O.
20//! 2. Create an `AsyncPtyMaster` that represents your ownership of
21//!    an OS pseudo-terminal.
22//! 3. Use your master and the `spawn_pty_async` or `spawn_pty_async_raw`
23//!    functions of the `CommandExt` extension trait, which extends
24//!    `std::process::Command`, to launch a child process that is connected to
25//!    your master.
26//! 4. Optionally control the child process (e.g. send it signals) through the
27//!    `Child` value returned by that function.
28//!
29//! This crate only works on Unix since pseudo-terminals are a Unix-specific
30//! concept.
31//!
32//! The `Child` type is largely copied from Alex Crichton’s
33//! [tokio-process](https://github.com/alexcrichton/tokio-process) crate.
34
35use async_trait::async_trait;
36
37use futures::Future;
38use io::{Read, Write};
39use libc::{c_int, c_ushort};
40use mio::event::Evented;
41use mio::unix::{EventedFd, UnixReady};
42use mio::{PollOpt, Ready, Token};
43use std::ffi::{CStr, OsStr, OsString};
44use std::fmt;
45use std::fs::{File, OpenOptions};
46use std::io::{self};
47use std::mem;
48use std::os::unix::prelude::*;
49use std::os::unix::process::CommandExt as StdUnixCommandExt;
50use std::{
51    pin::Pin,
52    process::{self, ExitStatus},
53    task::{Context, Poll},
54};
55use tokio::io::PollEvented;
56use tokio::io::{AsyncRead, AsyncWrite};
57use tokio::signal::unix::{signal, Signal, SignalKind};
58mod split;
59pub use split::{AsyncPtyMasterReadHalf, AsyncPtyMasterWriteHalf};
60
61// First set of hoops to jump through: a read-write pseudo-terminal master
62// with full async support. As far as I can tell, we need to create an inner
63// wrapper type to implement Evented on a type that we can then wrap in a
64// PollEvented. Lame.
65
66#[derive(Debug)]
67struct AsyncPtyFile(File);
68
69impl AsyncPtyFile {
70    pub fn new(inner: File) -> Self {
71        AsyncPtyFile(inner)
72    }
73}
74
75impl Read for AsyncPtyFile {
76    fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
77        self.0.read(bytes)
78    }
79}
80
81impl Write for AsyncPtyFile {
82    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
83        self.0.write(bytes)
84    }
85
86    fn flush(&mut self) -> io::Result<()> {
87        self.0.flush()
88    }
89}
90
91impl Evented for AsyncPtyFile {
92    fn register(
93        &self,
94        poll: &mio::Poll,
95        token: Token,
96        interest: Ready,
97        opts: PollOpt,
98    ) -> io::Result<()> {
99        EventedFd(&self.0.as_raw_fd()).register(poll, token, interest | UnixReady::hup(), opts)
100    }
101
102    fn reregister(
103        &self,
104        poll: &mio::Poll,
105        token: Token,
106        interest: Ready,
107        opts: PollOpt,
108    ) -> io::Result<()> {
109        EventedFd(&self.0.as_raw_fd()).reregister(poll, token, interest | UnixReady::hup(), opts)
110    }
111
112    fn deregister(&self, poll: &mio::Poll) -> io::Result<()> {
113        EventedFd(&self.0.as_raw_fd()).deregister(poll)
114    }
115}
116
117/// A handle to a pseudo-TTY master that can be interacted with
118/// asynchronously.
119///
120/// This type implements both `AsyncRead` and `AsyncWrite`.
121pub struct AsyncPtyMaster(PollEvented<AsyncPtyFile>);
122
123impl AsyncPtyMaster {
124    /// Open a pseudo-TTY master.
125    ///
126    /// This function performs the C library calls `posix_openpt()`,
127    /// `grantpt()`, and `unlockpt()`. It also sets the resulting pseudo-TTY
128    /// master handle to nonblocking mode.
129    pub fn open() -> Result<Self, io::Error> {
130        let inner = unsafe {
131            // On MacOS, O_NONBLOCK is not documented as an allowed option to
132            // posix_openpt(), but it is in fact allowed and functional, and
133            // trying to add it later with fcntl() is forbidden. Meanwhile, on
134            // FreeBSD, O_NONBLOCK is *not* an allowed option to
135            // posix_openpt(), and the only way to get a nonblocking PTY
136            // master is to add the nonblocking flag with fcntl() later. So,
137            // we have to jump through some #[cfg()] hoops.
138
139            const APPLY_NONBLOCK_AFTER_OPEN: bool = cfg!(target_os = "freebsd");
140
141            let fd = if APPLY_NONBLOCK_AFTER_OPEN {
142                libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY)
143            } else {
144                libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY | libc::O_NONBLOCK)
145            };
146
147            if fd < 0 {
148                return Err(io::Error::last_os_error());
149            }
150
151            if libc::grantpt(fd) != 0 {
152                return Err(io::Error::last_os_error());
153            }
154
155            if libc::unlockpt(fd) != 0 {
156                return Err(io::Error::last_os_error());
157            }
158
159            if APPLY_NONBLOCK_AFTER_OPEN {
160                let flags = libc::fcntl(fd, libc::F_GETFL, 0);
161                if flags < 0 {
162                    return Err(io::Error::last_os_error());
163                }
164
165                if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) == -1 {
166                    return Err(io::Error::last_os_error());
167                }
168            }
169
170            File::from_raw_fd(fd)
171        };
172
173        let evented = PollEvented::new(AsyncPtyFile::new(inner))?;
174        Ok(AsyncPtyMaster(evented))
175    }
176
177    /// Split the AsyncPtyMaster into an AsyncPtyReadHalf implementing `Read` and
178    /// and `AsyncRead` as well as an `AsyncPtyWriteHalf` implementing
179    /// `AsyncPtyWrite`.
180    pub fn split(self) -> (AsyncPtyMasterReadHalf, AsyncPtyMasterWriteHalf) {
181        split::split(self)
182    }
183
184    /// Open a pseudo-TTY slave that is connected to this master.
185    ///
186    /// The resulting file handle is *not* set to non-blocking mode.
187    fn open_sync_pty_slave(&self) -> Result<File, io::Error> {
188        let mut buf: [libc::c_char; 512] = [0; 512];
189        let fd = self.as_raw_fd();
190
191        #[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
192        {
193            if unsafe { libc::ptsname_r(fd, buf.as_mut_ptr(), buf.len()) } != 0 {
194                return Err(io::Error::last_os_error());
195            }
196        }
197        #[cfg(any(target_os = "macos", target_os = "freebsd"))]
198        unsafe {
199            let st = libc::ptsname(fd);
200            if st.is_null() {
201                return Err(io::Error::last_os_error());
202            }
203            libc::strncpy(buf.as_mut_ptr(), st, buf.len());
204        }
205
206        let ptsname = OsStr::from_bytes(unsafe { CStr::from_ptr(&buf as _) }.to_bytes());
207        OpenOptions::new().read(true).write(true).open(ptsname)
208    }
209}
210
211impl AsRawFd for AsyncPtyMaster {
212    fn as_raw_fd(&self) -> RawFd {
213        self.0.get_ref().0.as_raw_fd()
214    }
215}
216
217impl AsyncRead for AsyncPtyMaster {
218    fn poll_read(
219        mut self: std::pin::Pin<&mut Self>,
220        cx: &mut std::task::Context<'_>,
221        buf: &mut [u8],
222    ) -> std::task::Poll<io::Result<usize>> {
223        AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
224    }
225}
226
227impl AsyncWrite for AsyncPtyMaster {
228    fn poll_write(
229        mut self: Pin<&mut Self>,
230        cx: &mut std::task::Context<'_>,
231        buf: &[u8],
232    ) -> std::task::Poll<Result<usize, io::Error>> {
233        AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
234    }
235
236    fn poll_flush(
237        mut self: Pin<&mut Self>,
238        cx: &mut std::task::Context<'_>,
239    ) -> std::task::Poll<Result<(), io::Error>> {
240        AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
241    }
242
243    fn poll_shutdown(
244        mut self: Pin<&mut Self>,
245        cx: &mut std::task::Context<'_>,
246    ) -> std::task::Poll<Result<(), io::Error>> {
247        AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
248    }
249}
250
251// Now, the async-ified child process framework.
252
253/// A child process that can be interacted with through a pseudo-TTY.
254#[must_use = "futures do nothing unless polled"]
255pub struct Child {
256    inner: process::Child,
257    kill_on_drop: bool,
258    reaped: bool,
259    sigchld: Signal,
260}
261
262impl fmt::Debug for Child {
263    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
264        fmt.debug_struct("Child")
265            .field("pid", &self.inner.id())
266            .field("inner", &self.inner)
267            .field("kill_on_drop", &self.kill_on_drop)
268            .field("reaped", &self.reaped)
269            .field("sigchld", &"..")
270            .finish()
271    }
272}
273
274impl Child {
275    fn new(inner: process::Child) -> Child {
276        Child {
277            inner: inner,
278            kill_on_drop: true,
279            reaped: false,
280            sigchld: signal(SignalKind::child()).expect("could not get sigchld signal"),
281        }
282    }
283
284    /// Returns the OS-assigned process identifier associated with this child.
285    pub fn id(&self) -> u32 {
286        self.inner.id()
287    }
288
289    /// Forces the child to exit.
290    ///
291    /// This is equivalent to sending a SIGKILL on unix platforms.
292    pub fn kill(&mut self) -> io::Result<()> {
293        if self.reaped {
294            Ok(())
295        } else {
296            self.inner.kill()
297        }
298    }
299
300    /// Drop this `Child` without killing the underlying process.
301    ///
302    /// Normally a `Child` is killed if it's still alive when dropped, but this
303    /// method will ensure that the child may continue running once the `Child`
304    /// instance is dropped.
305    pub fn forget(mut self) {
306        self.kill_on_drop = false;
307    }
308
309    /// Check whether this `Child` has exited yet.
310    pub fn poll_exit(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<ExitStatus>> {
311        assert!(!self.reaped);
312
313        loop {
314            match self.try_wait() {
315                Ok(Some(status)) => {
316                    self.reaped = true;
317                    return Poll::Ready(Ok(status));
318                }
319                Err(e) => return Poll::Ready(Err(e)),
320                _ => {}
321            }
322
323            // If the child hasn't exited yet, then it's our responsibility to
324            // ensure the current task gets notified when it might be able to
325            // make progress.
326            //
327            // As described in `spawn` above, we just indicate that we can
328            // next make progress once a SIGCHLD is received.
329            if self.sigchld.poll_recv(cx).is_pending() {
330                return Poll::Pending;
331            }
332        }
333    }
334
335    fn try_wait(&self) -> io::Result<Option<ExitStatus>> {
336        let id = self.id() as c_int;
337        let mut status = 0;
338
339        loop {
340            match unsafe { libc::waitpid(id, &mut status, libc::WNOHANG) } {
341                0 => return Ok(None),
342
343                n if n < 0 => {
344                    let err = io::Error::last_os_error();
345                    if err.kind() == io::ErrorKind::Interrupted {
346                        continue;
347                    }
348                    return Err(err);
349                }
350
351                n => {
352                    assert_eq!(n, id);
353                    return Ok(Some(ExitStatus::from_raw(status)));
354                }
355            }
356        }
357    }
358}
359
360impl Future for Child {
361    type Output = std::io::Result<ExitStatus>;
362
363    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
364        self.poll_exit(cx)
365    }
366}
367
368impl Drop for Child {
369    fn drop(&mut self) {
370        if self.kill_on_drop {
371            drop(self.kill());
372        }
373    }
374}
375
376/// A Future for getting the Pty file descriptor.
377pub struct AsyncPtyFd<T: AsAsyncPtyFd>(T);
378
379impl<T: AsAsyncPtyFd> AsyncPtyFd<T> {
380    /// Construct a new AsyncPtyFd future
381    pub fn from(inner: T) -> Self {
382        AsyncPtyFd(inner)
383    }
384}
385
386impl<T: AsAsyncPtyFd> Future for AsyncPtyFd<T> {
387    type Output = RawFd;
388
389    fn poll(
390        self: Pin<&mut Self>,
391        cx: &mut std::task::Context<'_>,
392    ) -> std::task::Poll<Self::Output> {
393        self.0.as_async_pty_fd(cx)
394    }
395}
396
397/// Trait to asynchronously get the `RawFd` of the master side of the PTY
398pub trait AsAsyncPtyFd {
399    /// Return a `Poll` containing the RawFd
400    fn as_async_pty_fd(&self, cx: &mut Context<'_>) -> Poll<RawFd>;
401}
402
403impl AsAsyncPtyFd for AsyncPtyMaster {
404    fn as_async_pty_fd(&self, _cx: &mut Context<'_>) -> Poll<RawFd> {
405        Poll::Ready(self.as_raw_fd())
406    }
407}
408
409/// An async-fn version of PollPtyMaster
410#[async_trait]
411pub trait PtyMaster: PollPtyMaster {
412    /// Resizes the pty
413    async fn resize(&self, dimensions: (u16, u16)) -> Result<(), io::Error>;
414
415    /// Retrieves the size of the pty
416    async fn size(&self) -> Result<(u16, u16), io::Error>;
417}
418
419#[async_trait]
420impl<T: Send + Sync> PtyMaster for T
421where
422    T: PollPtyMaster,
423{
424    async fn resize(&self, dimensions: (u16, u16)) -> Result<(), io::Error> {
425        let resize = Resize {
426            pty: self,
427            cols: dimensions.0,
428            rows: dimensions.1,
429        };
430
431        resize.await
432    }
433
434    async fn size(&self) -> Result<(u16, u16), io::Error> {
435        GetSize(self).await
436    }
437}
438/// Trait containing generalized methods for PTYs
439pub trait PollPtyMaster {
440    /// Return the full pathname of the slave device counterpart
441    fn poll_ptsname(&self, cx: &mut Context<'_>) -> Poll<Result<OsString, io::Error>>;
442
443    /// Resize the PTY
444    fn poll_resize(
445        &self,
446        cx: &mut Context<'_>,
447        rows: c_ushort,
448        cols: c_ushort,
449    ) -> Poll<Result<(), io::Error>>;
450
451    /// Get the PTY size
452    fn poll_winsize(&self, cx: &mut Context<'_>) -> Poll<Result<(c_ushort, c_ushort), io::Error>>;
453}
454
455impl<T: AsAsyncPtyFd> PollPtyMaster for T {
456    fn poll_ptsname(&self, cx: &mut Context<'_>) -> Poll<Result<OsString, io::Error>> {
457        let mut buf: [libc::c_char; 512] = [0; 512];
458        let fd = futures::ready!(self.as_async_pty_fd(cx));
459
460        #[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
461        {
462            if unsafe { libc::ptsname_r(fd, buf.as_mut_ptr(), buf.len()) } != 0 {
463                return Poll::Ready(Err(io::Error::last_os_error()));
464            }
465        }
466        #[cfg(any(target_os = "macos", target_os = "freebsd"))]
467        unsafe {
468            let st = libc::ptsname(fd);
469            if st.is_null() {
470                return Poll::Ready(Err(io::Error::last_os_error()));
471            }
472            libc::strncpy(buf.as_mut_ptr(), st, buf.len());
473        }
474        let ptsname = OsStr::from_bytes(unsafe { CStr::from_ptr(&buf as _) }.to_bytes());
475        Poll::Ready(Ok(ptsname.to_os_string()))
476    }
477
478    fn poll_winsize(&self, cx: &mut Context<'_>) -> Poll<Result<(c_ushort, c_ushort), io::Error>> {
479        let fd = futures::ready!(self.as_async_pty_fd(cx));
480        let mut winsz: libc::winsize = unsafe { std::mem::zeroed() };
481        if unsafe { libc::ioctl(fd, libc::TIOCGWINSZ.into(), &mut winsz) } != 0 {
482            return Poll::Ready(Err(io::Error::last_os_error()));
483        }
484        Poll::Ready(Ok((winsz.ws_col, winsz.ws_row)))
485    }
486
487    fn poll_resize(
488        &self,
489        cx: &mut Context<'_>,
490        rows: c_ushort,
491        cols: c_ushort,
492    ) -> Poll<Result<(), io::Error>> {
493        let fd = futures::ready!(self.as_async_pty_fd(cx));
494
495        let winsz = libc::winsize {
496            ws_row: rows,
497            ws_col: cols,
498            ws_xpixel: 0,
499            ws_ypixel: 0,
500        };
501        if unsafe { libc::ioctl(fd, libc::TIOCSWINSZ.into(), &winsz) } != 0 {
502            return Poll::Ready(Err(io::Error::last_os_error()));
503        }
504        Poll::Ready(Ok(()))
505    }
506}
507
508/// A private trait for the extending `std::process::Command`.
509trait CommandExtInternal {
510    fn spawn_pty_async_full(&mut self, ptymaster: &AsyncPtyMaster, raw: bool) -> io::Result<Child>;
511}
512
513impl CommandExtInternal for process::Command {
514    fn spawn_pty_async_full(&mut self, ptymaster: &AsyncPtyMaster, raw: bool) -> io::Result<Child> {
515        let master_fd = ptymaster.as_raw_fd();
516        let slave = ptymaster.open_sync_pty_slave()?;
517        let slave_fd = slave.as_raw_fd();
518
519        self.stdin(slave.try_clone()?);
520        self.stdout(slave.try_clone()?);
521        self.stderr(slave);
522
523        // XXX any need to close slave handles in the parent process beyond
524        // what's done here
525
526        unsafe {
527            self.pre_exec(move || {
528                if raw {
529                    let mut attrs: libc::termios = mem::zeroed();
530
531                    if libc::tcgetattr(slave_fd, &mut attrs as _) != 0 {
532                        return Err(io::Error::last_os_error());
533                    }
534
535                    libc::cfmakeraw(&mut attrs as _);
536
537                    if libc::tcsetattr(slave_fd, libc::TCSANOW, &attrs as _) != 0 {
538                        return Err(io::Error::last_os_error());
539                    }
540                }
541
542                // This is OK even though we don't own master since this process is
543                // about to become something totally different anyway.
544                if libc::close(master_fd) != 0 {
545                    return Err(io::Error::last_os_error());
546                }
547
548                if libc::setsid() < 0 {
549                    return Err(io::Error::last_os_error());
550                }
551
552                if libc::ioctl(0, libc::TIOCSCTTY.into(), 1) != 0 {
553                    return Err(io::Error::last_os_error());
554                }
555                Ok(())
556            });
557        }
558
559        Ok(Child::new(self.spawn()?))
560    }
561}
562
563/// An extension trait for the `std::process::Command` type.
564///
565/// This trait provides new `spawn_pty_async` and `spawn_pty_async_raw`
566/// methods that allow one to spawn a new process that is connected to the
567/// current process through a pseudo-TTY.
568pub trait CommandExt {
569    /// Spawn a subprocess that connects to the current one through a
570    /// pseudo-TTY in canonical (“cooked“, not “raw”) mode.
571    ///
572    /// This function creates the necessary PTY slave and uses
573    /// `std::process::Command::before_exec` to do the neccessary setup before
574    /// the child process is spawned. In particular, it calls `setsid()` to
575    /// launch a new TTY sesson.
576    ///
577    /// The child process’s standard input, standard output, and standard
578    /// error are all connected to the pseudo-TTY slave.
579    fn spawn_pty_async(&mut self, ptymaster: &AsyncPtyMaster) -> io::Result<Child>;
580
581    /// Spawn a subprocess that connects to the current one through a
582    /// pseudo-TTY in raw (“non-canonical”, not “cooked”) mode.
583    ///
584    /// This function creates the necessary PTY slave and uses
585    /// `std::process::Command::before_exec` to do the neccessary setup before
586    /// the child process is spawned. In particular, it sets the slave PTY
587    /// handle to raw mode and calls `setsid()` to launch a new TTY sesson.
588    ///
589    /// The child process’s standard input, standard output, and standard
590    /// error are all connected to the pseudo-TTY slave.
591    fn spawn_pty_async_raw(&mut self, ptymaster: &AsyncPtyMaster) -> io::Result<Child>;
592}
593
594impl CommandExt for process::Command {
595    fn spawn_pty_async(&mut self, ptymaster: &AsyncPtyMaster) -> io::Result<Child> {
596        self.spawn_pty_async_full(ptymaster, false)
597    }
598
599    fn spawn_pty_async_raw(&mut self, ptymaster: &AsyncPtyMaster) -> io::Result<Child> {
600        self.spawn_pty_async_full(ptymaster, true)
601    }
602}
603
604struct GetSize<'a, T: PtyMaster + Send>(&'a T);
605impl<'a, T: PtyMaster + Send> Future for GetSize<'a, T> {
606    type Output = io::Result<(c_ushort, c_ushort)>;
607    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
608        self.0.poll_winsize(cx)
609    }
610}
611
612struct Resize<'a, T: PtyMaster + Send> {
613    pub pty: &'a T,
614    pub rows: c_ushort,
615    pub cols: c_ushort,
616}
617
618impl<'a, T: PtyMaster + Send> Future for Resize<'a, T> {
619    type Output = io::Result<()>;
620
621    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
622        self.pty.poll_resize(cx, self.rows, self.cols)
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    extern crate errno;
629    extern crate libc;
630
631    use super::*;
632    use futures::executor::block_on;
633
634    /// Test that the PTY master file descriptor is in nonblocking mode. We do
635    /// this in a pretty hacky and dumb way, by creating the AsyncPtyMaster
636    /// and then just snarfing its FD and seeing whether a Unix `read(2)` call
637    /// errors out with EWOULDBLOCK (instead of blocking forever). In
638    /// principle it would be nice to actually spawn a subprogram and test
639    /// reading through the whole Tokio I/O subsystem, but that's annoying to
640    /// implement and can actually muddy the picture. Namely: if you try to
641    /// `master.read()` inside a Tokio event loop here, on Linux you'll get an
642    /// ErrorKind::WouldBlock I/O error from Tokio without it even attempting
643    /// the underlying `read(2)` system call, because Tokio uses epoll to test
644    /// the FD's readiness in a way that works orthogonal to whether it's set
645    /// to non-blocking mode.
646    #[tokio::test]
647    async fn basic_nonblocking() {
648        let master = AsyncPtyMaster::open().unwrap();
649
650        let fd = master.as_raw_fd();
651        let mut buf = [0u8; 128];
652        let rval = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 128) };
653        let errno: i32 = errno::errno().into();
654
655        assert_eq!(rval, -1);
656        assert_eq!(errno, libc::EWOULDBLOCK as i32);
657    }
658
659    #[tokio::test]
660    async fn test_winsize() {
661        let master = AsyncPtyMaster::open().expect("Could not open the PTY");
662
663        // On macos, it's only possible to resize a PTY with a child spawned
664        // On it, so let's just do that:
665        #[cfg(target_os = "macos")]
666        let mut child = std::process::Command::new("cat")
667            .spawn_pty_async(&master)
668            .expect("Could not spawn child");
669
670        // Set the size
671        block_on(Resize {
672            pty: &master,
673            cols: 80,
674            rows: 50,
675        })
676        .expect("Could not resize the PTY");
677
678        let (cols, rows) = block_on(GetSize(&master)).expect("Could not get PTY size");
679
680        assert_eq!(cols, 80);
681        assert_eq!(rows, 50);
682
683        #[cfg(target_os = "macos")]
684        child.kill().expect("Could not kill child");
685    }
686
687    #[tokio::test]
688    async fn test_size() {
689        let master = AsyncPtyMaster::open().expect("Could not open the PTY");
690
691        // On macos, it's only possible to resize a PTY with a child spawned
692        // On it, so let's just do that:
693        #[cfg(target_os = "macos")]
694        let mut child = std::process::Command::new("cat")
695            .spawn_pty_async(&master)
696            .expect("Could not spawn child");
697
698        let (_rows, _cols) = master.size().await.expect("Could not get PTY size");
699
700        #[cfg(target_os = "macos")]
701        child.kill().expect("Could not kill child");
702    }
703
704    #[tokio::test]
705    async fn test_resize() {
706        let master = AsyncPtyMaster::open().expect("Could not open the PTY");
707
708        // On macos, it's only possible to resize a PTY with a child spawned
709        // On it, so let's just do that:
710        #[cfg(target_os = "macos")]
711        let mut child = std::process::Command::new("cat")
712            .spawn_pty_async(&master)
713            .expect("Could not spawn child");
714
715        let _resize = master.resize((80, 50)).await.expect("resize failed");
716
717        #[cfg(target_os = "macos")]
718        child.kill().expect("Could not kill child");
719    }
720
721    #[tokio::test]
722    async fn test_from_fd() {
723        let master = AsyncPtyMaster::open().expect("Could not open the PTY");
724
725        let _fd = AsyncPtyFd::from(master).await;
726    }
727}