ptyprocess/
lib.rs

1//! A library provides an interface for a unix [PTY/TTY](https://en.wikipedia.org/wiki/Pseudoterminal).
2//!
3//! It aims to work on all major Unix variants.
4//!
5//! The library was developed as a backend for a https://github.com/zhiburt/expectrl.
6//! If you're interested in a high level operations may you'd better take a look at `zhiburt/expectrl`.
7//!
8//! ## Usage
9//!
10//! ```rust
11//! use ptyprocess::PtyProcess;
12//! use std::process::Command;
13//! use std::io::{BufRead, Write, BufReader};
14//!
15//! // spawn a cat process
16//! let mut process = PtyProcess::spawn(Command::new("cat")).expect("failed to spawn a process");
17//!
18//! // create a communication stream
19//! let mut stream = process.get_raw_handle().expect("failed to create a stream");
20//!
21//! // send a message to process
22//! writeln!(stream, "Hello cat").expect("failed to write to a stream");
23//!
24//! // read a line from the stream
25//! let mut reader = BufReader::new(stream);
26//! let mut buf = String::new();
27//! reader.read_line(&mut buf).expect("failed to read a process output");
28//!
29//! println!("line={}", buf);
30//!
31//! // stop the process
32//! assert!(process.exit(true).expect("failed to stop the process"))
33//! ```
34
35pub mod stream;
36
37pub use nix::errno;
38pub use nix::sys::signal::Signal;
39pub use nix::sys::wait::WaitStatus;
40pub use nix::Error;
41
42use nix::fcntl::{fcntl, open, FcntlArg, FdFlag, OFlag};
43use nix::libc::{self, winsize, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
44use nix::pty::PtyMaster;
45use nix::pty::{grantpt, posix_openpt, unlockpt};
46use nix::sys::stat::Mode;
47use nix::sys::wait::{self, waitpid};
48use nix::sys::{signal, termios};
49use nix::unistd::{
50    self, close, dup, dup2, fork, isatty, pipe, setsid, sysconf, write, ForkResult, Pid, SysconfVar,
51};
52use nix::{ioctl_write_ptr_bad, Result};
53use signal::Signal::SIGKILL;
54use std::fs::File;
55use std::os::unix::prelude::{AsRawFd, CommandExt, FromRawFd, RawFd};
56use std::process::{self, Command};
57use std::thread;
58use std::time::{self, Duration};
59use stream::Stream;
60use termios::SpecialCharacterIndices;
61
62const DEFAULT_TERM_COLS: u16 = 80;
63const DEFAULT_TERM_ROWS: u16 = 24;
64
65const DEFAULT_VEOF_CHAR: u8 = 0x4; // ^D
66const DEFAULT_INTR_CHAR: u8 = 0x3; // ^C
67
68const DEFAULT_TERMINATE_DELAY: Duration = Duration::from_millis(100);
69
70/// PtyProcess controls a spawned process and communication with this.
71///
72/// It implements [std::io::Read] and [std::io::Write] to communicate with
73/// a child.
74///
75/// ```no_run,ignore
76/// use ptyprocess::PtyProcess;
77/// use std::io::Write;
78/// use std::process::Command;
79///
80/// let mut process = PtyProcess::spawn(Command::new("cat")).unwrap();
81/// process.write_all(b"Hello World").unwrap();
82/// process.flush().unwrap();
83/// ```
84#[derive(Debug)]
85pub struct PtyProcess {
86    master: Master,
87    child_pid: Pid,
88    eof_char: u8,
89    intr_char: u8,
90    terminate_delay: Duration,
91}
92
93impl PtyProcess {
94    /// Spawns a child process and create a [PtyProcess].
95    ///
96    /// ```no_run
97    ///   # use std::process::Command;
98    ///   # use ptyprocess::PtyProcess;
99    ///     let proc = PtyProcess::spawn(Command::new("bash"));
100    /// ```
101    pub fn spawn(mut command: Command) -> Result<Self> {
102        let master = Master::open()?;
103        master.grant_slave_access()?;
104        master.unlock_slave()?;
105
106        // handle errors in child executions by pipe
107        let (exec_err_pipe_r, exec_err_pipe_w) = pipe()?;
108
109        let fork = unsafe { fork()? };
110        match fork {
111            ForkResult::Child => {
112                let err = || -> Result<()> {
113                    make_controlling_tty(&master)?;
114
115                    let slave_fd = master.get_slave_fd()?;
116                    redirect_std_streams(slave_fd)?;
117
118                    set_echo(STDIN_FILENO, false)?;
119                    set_term_size(STDIN_FILENO, DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?;
120
121                    // Do not allow child to inherit open file descriptors from parent
122                    close_all_descriptors(&[
123                        0,
124                        1,
125                        2,
126                        slave_fd,
127                        exec_err_pipe_w,
128                        exec_err_pipe_r,
129                        master.as_raw_fd(),
130                    ])?;
131
132                    close(slave_fd)?;
133                    close(exec_err_pipe_r)?;
134                    drop(master);
135
136                    // close pipe on sucessfull exec
137                    fcntl(exec_err_pipe_w, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))?;
138
139                    let _ = command.exec();
140                    Err(Error::last())
141                }()
142                .unwrap_err();
143
144                let code = err as i32;
145
146                // Intentionally ignoring errors to exit the process properly
147                let _ = write(exec_err_pipe_w, &code.to_be_bytes());
148                let _ = close(exec_err_pipe_w);
149
150                process::exit(code);
151            }
152            ForkResult::Parent { child } => {
153                close(exec_err_pipe_w)?;
154
155                let mut pipe_buf = [0u8; 4];
156                unistd::read(exec_err_pipe_r, &mut pipe_buf)?;
157                close(exec_err_pipe_r)?;
158                let code = i32::from_be_bytes(pipe_buf);
159                if code != 0 {
160                    return Err(errno::from_i32(code));
161                }
162
163                // Some systems may work in this way? (not sure)
164                // that we need to set a terminal size in a parent.
165                set_term_size(master.as_raw_fd(), DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?;
166
167                let eof_char = get_eof_char();
168                let intr_char = get_intr_char();
169
170                Ok(Self {
171                    master,
172                    child_pid: child,
173                    eof_char,
174                    intr_char,
175                    terminate_delay: DEFAULT_TERMINATE_DELAY,
176                })
177            }
178        }
179    }
180
181    /// Returns a pid of a child process
182    pub fn pid(&self) -> Pid {
183        self.child_pid
184    }
185
186    /// Returns a file representation of a PTY, which can be used
187    /// to communicate with a spawned process.
188    ///
189    /// The file behaivor is platform dependent.
190    ///
191    /// # Safety
192    ///
193    /// Be carefull changing a descriptors inner state (e.g `fcntl`)
194    /// because it affects all structures which use it.
195    ///
196    /// Be carefull using this method in async mode.
197    /// Because descriptor is set to a non-blocking mode will affect all dublicated descriptors
198    /// which may be unexpected.
199    ///
200    /// # Example
201    ///
202    /// ```no_run
203    /// use ptyprocess::PtyProcess;
204    /// use std::{process::Command, io::{BufReader, LineWriter}};
205    ///
206    /// let mut process = PtyProcess::spawn(Command::new("cat")).unwrap();
207    /// let pty = process.get_raw_handle().unwrap();
208    /// let mut writer = LineWriter::new(&pty);
209    /// let mut reader = BufReader::new(&pty);
210    /// ```
211    pub fn get_raw_handle(&self) -> Result<File> {
212        self.master.get_file_handle()
213    }
214
215    /// Returns a stream representation of a PTY.
216    /// Which can be used to communicate with a spawned process.
217    ///
218    /// It differs from [Self::get_raw_handle] because it is
219    /// platform independent.
220    pub fn get_pty_stream(&self) -> Result<Stream> {
221        self.get_raw_handle().map(Stream::new)
222    }
223
224    /// Get a end of file character if set or a default.
225    pub fn get_eof_char(&self) -> u8 {
226        self.eof_char
227    }
228
229    /// Get a interapt character if set or a default.
230    pub fn get_intr_char(&self) -> u8 {
231        self.intr_char
232    }
233
234    /// Get window size of a terminal.
235    ///
236    /// Default size is 80x24.
237    pub fn get_window_size(&self) -> Result<(u16, u16)> {
238        get_term_size(self.master.as_raw_fd())
239    }
240
241    /// Sets a terminal size.
242    pub fn set_window_size(&mut self, cols: u16, rows: u16) -> Result<()> {
243        set_term_size(self.master.as_raw_fd(), cols, rows)
244    }
245
246    /// The function returns true if an echo setting is setup.
247    pub fn get_echo(&self) -> Result<bool> {
248        termios::tcgetattr(self.master.as_raw_fd())
249            .map(|flags| flags.local_flags.contains(termios::LocalFlags::ECHO))
250    }
251
252    /// Sets a echo setting for a terminal
253    pub fn set_echo(&mut self, on: bool, timeout: Option<Duration>) -> Result<bool> {
254        set_echo(self.master.as_raw_fd(), on)?;
255        self.wait_echo(on, timeout)
256    }
257
258    /// Returns true if a underline `fd` connected with a TTY.
259    pub fn isatty(&self) -> Result<bool> {
260        isatty(self.master.as_raw_fd())
261    }
262
263    /// Set the pty process's terminate approach delay.
264    pub fn set_terminate_delay(&mut self, terminate_approach_delay: Duration) {
265        self.terminate_delay = terminate_approach_delay;
266    }
267
268    /// Status returns a status a of child process.
269    pub fn status(&self) -> Result<WaitStatus> {
270        waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG))
271    }
272
273    /// Kill sends a signal to a child process.
274    ///
275    /// The operation is non-blocking.
276    pub fn kill(&mut self, signal: signal::Signal) -> Result<()> {
277        signal::kill(self.child_pid, signal)
278    }
279
280    /// Signal is an alias to [PtyProcess::kill].
281    ///
282    /// [PtyProcess::kill]: struct.PtyProcess.html#method.kill
283    pub fn signal(&mut self, signal: signal::Signal) -> Result<()> {
284        self.kill(signal)
285    }
286
287    /// Wait blocks until a child process exits.
288    ///
289    /// It returns a error if the child was DEAD or not exist
290    /// at the time of a call.
291    ///
292    /// If you need to verify that a process is dead in non-blocking way you can use
293    /// [is_alive] method.
294    ///
295    /// [is_alive]: struct.PtyProcess.html#method.is_alive
296    pub fn wait(&self) -> Result<WaitStatus> {
297        waitpid(self.child_pid, None)
298    }
299
300    /// Checks if a process is still exists.
301    ///
302    /// It's a non blocking operation.
303    ///
304    /// Keep in mind that after calling this method process might be marked as DEAD by kernel,
305    /// because a check of its status.
306    /// Therefore second call to [Self::status] or [Self::is_alive] might return a different status.
307    pub fn is_alive(&self) -> Result<bool> {
308        let status = self.status();
309        match status {
310            Ok(status) if status == WaitStatus::StillAlive => Ok(true),
311            Ok(_) | Err(Error::ECHILD) | Err(Error::ESRCH) => Ok(false),
312            Err(err) => Err(err),
313        }
314    }
315
316    /// Try to force a child to terminate.
317    ///
318    /// This returns true if the child was terminated. and returns false if the
319    /// child could not be terminated.
320    ///
321    /// It makes 4 tries getting more thorough.
322    ///
323    /// 1. SIGHUP
324    /// 2. SIGCONT
325    /// 3. SIGINT
326    /// 4. SIGTERM
327    ///
328    /// If "force" is `true` then moves onto SIGKILL.
329    pub fn exit(&mut self, force: bool) -> Result<bool> {
330        if !self.is_alive()? {
331            return Ok(true);
332        }
333
334        for &signal in &[
335            signal::SIGHUP,
336            signal::SIGCONT,
337            signal::SIGINT,
338            signal::SIGTERM,
339        ] {
340            if self.try_to_terminate(signal)? {
341                return Ok(true);
342            }
343        }
344
345        if !force {
346            return Ok(false);
347        }
348
349        self.try_to_terminate(SIGKILL)
350    }
351
352    fn try_to_terminate(&mut self, signal: signal::Signal) -> Result<bool> {
353        self.kill(signal)?;
354        thread::sleep(self.terminate_delay);
355
356        self.is_alive().map(|is_alive| !is_alive)
357    }
358
359    fn wait_echo(&self, on: bool, timeout: Option<Duration>) -> Result<bool> {
360        let now = time::Instant::now();
361        while timeout.is_none() || now.elapsed() < timeout.unwrap() {
362            if on == self.get_echo()? {
363                return Ok(true);
364            }
365
366            thread::sleep(Duration::from_millis(100));
367        }
368
369        Ok(false)
370    }
371}
372
373impl Drop for PtyProcess {
374    fn drop(&mut self) {
375        if let Ok(WaitStatus::StillAlive) = self.status() {
376            self.exit(true).unwrap();
377        }
378    }
379}
380
381fn set_term_size(fd: i32, cols: u16, rows: u16) -> Result<()> {
382    ioctl_write_ptr_bad!(_set_window_size, libc::TIOCSWINSZ, winsize);
383
384    let size = winsize {
385        ws_row: rows,
386        ws_col: cols,
387        ws_xpixel: 0,
388        ws_ypixel: 0,
389    };
390
391    let _ = unsafe { _set_window_size(fd, &size) }?;
392
393    Ok(())
394}
395
396fn get_term_size(fd: i32) -> Result<(u16, u16)> {
397    nix::ioctl_read_bad!(_get_window_size, libc::TIOCGWINSZ, winsize);
398
399    let mut size = winsize {
400        ws_col: 0,
401        ws_row: 0,
402        ws_xpixel: 0,
403        ws_ypixel: 0,
404    };
405
406    let _ = unsafe { _get_window_size(fd, &mut size) }?;
407
408    Ok((size.ws_col, size.ws_row))
409}
410
411#[derive(Debug)]
412struct Master {
413    fd: PtyMaster,
414}
415
416impl Master {
417    fn open() -> Result<Self> {
418        let master_fd = posix_openpt(OFlag::O_RDWR)?;
419        Ok(Self { fd: master_fd })
420    }
421
422    fn grant_slave_access(&self) -> Result<()> {
423        grantpt(&self.fd)
424    }
425
426    fn unlock_slave(&self) -> Result<()> {
427        unlockpt(&self.fd)
428    }
429
430    fn get_slave_name(&self) -> Result<String> {
431        get_slave_name(&self.fd)
432    }
433
434    #[cfg(not(target_os = "freebsd"))]
435    fn get_slave_fd(&self) -> Result<RawFd> {
436        let slave_name = self.get_slave_name()?;
437        let slave_fd = open(
438            slave_name.as_str(),
439            OFlag::O_RDWR | OFlag::O_NOCTTY,
440            Mode::empty(),
441        )?;
442        Ok(slave_fd)
443    }
444
445    #[cfg(target_os = "freebsd")]
446    fn get_slave_fd(&self) -> Result<RawFd> {
447        let slave_name = self.get_slave_name()?;
448        let slave_fd = open(
449            format!("/dev/{}", slave_name.as_str()).as_str(),
450            OFlag::O_RDWR | OFlag::O_NOCTTY,
451            Mode::empty(),
452        )?;
453        Ok(slave_fd)
454    }
455
456    fn get_file_handle(&self) -> Result<File> {
457        let fd = dup(self.as_raw_fd())?;
458        let file = unsafe { File::from_raw_fd(fd) };
459
460        Ok(file)
461    }
462}
463
464impl AsRawFd for Master {
465    fn as_raw_fd(&self) -> RawFd {
466        self.fd.as_raw_fd()
467    }
468}
469
470#[cfg(target_os = "linux")]
471fn get_slave_name(fd: &PtyMaster) -> Result<String> {
472    nix::pty::ptsname_r(fd)
473}
474
475#[cfg(target_os = "freebsd")]
476fn get_slave_name(fd: &PtyMaster) -> Result<String> {
477    use std::ffi::CStr;
478    use std::os::raw::c_char;
479    use std::os::unix::prelude::AsRawFd;
480
481    let fd = fd.as_raw_fd();
482
483    if !isptmaster(fd)? {
484        // never reached according current implementation of isptmaster
485        return Err(nix::Error::EINVAL);
486    }
487
488    // todo: Need to determine the correct size via some contstant like SPECNAMELEN in <sys/filio.h>
489    let mut buf: [c_char; 128] = [0; 128];
490
491    let _ = fdevname_r(fd, &mut buf)?;
492
493    // todo: determine how CStr::from_ptr handles not NUL terminated string.
494    let string = unsafe { CStr::from_ptr(buf.as_ptr()) }
495        .to_string_lossy()
496        .into_owned();
497
498    return Ok(string);
499}
500
501// https://github.com/freebsd/freebsd-src/blob/main/lib/libc/stdlib/ptsname.c#L52
502#[cfg(target_os = "freebsd")]
503fn isptmaster(fd: RawFd) -> Result<bool> {
504    use nix::libc::ioctl;
505    use nix::libc::TIOCPTMASTER;
506    match unsafe { ioctl(fd, TIOCPTMASTER as u64, 0) } {
507        0 => Ok(true),
508        _ => Err(Error::last()),
509    }
510}
511
512/* automatically generated by rust-bindgen 0.59.1 */
513// bindgen filio.h --allowlist-type fiodgname_arg -o bindings.rs
514// it may be worth to use a build.rs if we will need more FFI structures.
515#[cfg(target_os = "freebsd")]
516#[repr(C)]
517#[derive(Debug, Copy, Clone)]
518pub struct fiodgname_arg {
519    pub len: ::std::os::raw::c_int,
520    pub buf: *mut ::std::os::raw::c_void,
521}
522
523// https://github.com/freebsd/freebsd-src/blob/6ae38ab45396edaea26b4725e0c7db8cffa5f208/lib/libc/gen/fdevname.c#L39
524#[cfg(target_os = "freebsd")]
525fn fdevname_r(fd: RawFd, buf: &mut [std::os::raw::c_char]) -> Result<()> {
526    use nix::libc::{ioctl, FIODGNAME};
527
528    nix::ioctl_read_bad!(_ioctl_fiodgname, FIODGNAME, fiodgname_arg);
529
530    let mut fgn = fiodgname_arg {
531        len: buf.len() as i32,
532        buf: buf.as_mut_ptr() as *mut ::std::os::raw::c_void,
533    };
534
535    let _ = unsafe { _ioctl_fiodgname(fd, &mut fgn) }?;
536
537    Ok(())
538}
539
540/// Getting a slave name on darvin platform
541/// https://blog.tarq.io/ptsname-on-osx-with-rust/
542#[cfg(target_os = "macos")]
543fn get_slave_name(fd: &PtyMaster) -> Result<String> {
544    use nix::libc::ioctl;
545    use nix::libc::TIOCPTYGNAME;
546    use std::ffi::CStr;
547    use std::os::raw::c_char;
548    use std::os::unix::prelude::AsRawFd;
549
550    // ptsname_r is a linux extension but ptsname isn't thread-safe
551    // we could use a static mutex but instead we re-implemented ptsname_r with a syscall
552    // ioctl(fd, TIOCPTYGNAME, buf) manually
553    // the buffer size on OSX is 128, defined by sys/ttycom.h
554    let mut buf: [c_char; 128] = [0; 128];
555
556    let fd = fd.as_raw_fd();
557
558    match unsafe { ioctl(fd, TIOCPTYGNAME as u64, &mut buf) } {
559        0 => {
560            let string = unsafe { CStr::from_ptr(buf.as_ptr()) }
561                .to_string_lossy()
562                .into_owned();
563            return Ok(string);
564        }
565        _ => Err(Error::last()),
566    }
567}
568
569fn redirect_std_streams(fd: RawFd) -> Result<()> {
570    // If fildes2 is already a valid open file descriptor, it shall be closed first
571
572    close(STDIN_FILENO)?;
573    close(STDOUT_FILENO)?;
574    close(STDERR_FILENO)?;
575
576    // use slave fd as std[in/out/err]
577    dup2(fd, STDIN_FILENO)?;
578    dup2(fd, STDOUT_FILENO)?;
579    dup2(fd, STDERR_FILENO)?;
580
581    Ok(())
582}
583
584fn set_echo(fd: RawFd, on: bool) -> Result<()> {
585    // Set echo off
586    // Even though there may be something left behind https://stackoverflow.com/a/59034084
587    let mut flags = termios::tcgetattr(fd)?;
588    match on {
589        true => flags.local_flags |= termios::LocalFlags::ECHO,
590        false => flags.local_flags &= !termios::LocalFlags::ECHO,
591    }
592
593    termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?;
594    Ok(())
595}
596
597pub fn set_raw(fd: RawFd) -> Result<()> {
598    let mut flags = termios::tcgetattr(fd)?;
599
600    #[cfg(not(target_os = "macos"))]
601    {
602        termios::cfmakeraw(&mut flags);
603    }
604    #[cfg(target_os = "macos")]
605    {
606        // implementation is taken from https://github.com/python/cpython/blob/3.9/Lib/tty.py
607        use nix::libc::{VMIN, VTIME};
608        use termios::ControlFlags;
609        use termios::InputFlags;
610        use termios::LocalFlags;
611        use termios::OutputFlags;
612
613        flags.input_flags &= !(InputFlags::BRKINT
614            | InputFlags::ICRNL
615            | InputFlags::INPCK
616            | InputFlags::ISTRIP
617            | InputFlags::IXON);
618        flags.output_flags &= !OutputFlags::OPOST;
619        flags.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB);
620        flags.control_flags |= ControlFlags::CS8;
621        flags.local_flags &=
622            !(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG);
623        flags.control_chars[VMIN] = 1;
624        flags.control_chars[VTIME] = 0;
625    }
626
627    termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?;
628    Ok(())
629}
630
631fn get_this_term_char(char: SpecialCharacterIndices) -> Option<u8> {
632    for &fd in &[STDIN_FILENO, STDOUT_FILENO] {
633        if let Ok(char) = get_term_char(fd, char) {
634            return Some(char);
635        }
636    }
637
638    None
639}
640
641fn get_intr_char() -> u8 {
642    get_this_term_char(SpecialCharacterIndices::VINTR).unwrap_or(DEFAULT_INTR_CHAR)
643}
644
645fn get_eof_char() -> u8 {
646    get_this_term_char(SpecialCharacterIndices::VEOF).unwrap_or(DEFAULT_VEOF_CHAR)
647}
648
649fn get_term_char(fd: RawFd, char: SpecialCharacterIndices) -> Result<u8> {
650    let flags = termios::tcgetattr(fd)?;
651    let b = flags.control_chars[char as usize];
652    Ok(b)
653}
654
655fn make_controlling_tty(ptm: &Master) -> Result<()> {
656    #[cfg(not(any(target_os = "freebsd", target_os = "macos")))]
657    {
658        let pts_name = ptm.get_slave_name()?;
659        // https://github.com/pexpect/ptyprocess/blob/c69450d50fbd7e8270785a0552484182f486092f/ptyprocess/_fork_pty.py
660
661        // Disconnect from controlling tty, if any
662        //
663        // it may be a simmilar call to ioctl TIOCNOTTY
664        // https://man7.org/linux/man-pages/man4/tty_ioctl.4.html
665        let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty());
666        match fd {
667            Ok(fd) => {
668                close(fd)?;
669            }
670            Err(Error::ENXIO) => {
671                // Sometimes we get ENXIO right here which 'probably' means
672                // that we has been already disconnected from controlling tty.
673                // Specifically it was discovered on ubuntu-latest Github CI platform.
674            }
675            Err(err) => return Err(err),
676        }
677
678        // setsid() will remove the controlling tty. Also the ioctl TIOCNOTTY does this.
679        // https://www.win.tue.nl/~aeb/linux/lk/lk-10.html
680        setsid()?;
681
682        // Verify we are disconnected from controlling tty by attempting to open
683        // it again.  We expect that OSError of ENXIO should always be raised.
684        let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty());
685        match fd {
686            Err(Error::ENXIO) => {} // ok
687            Ok(fd) => {
688                close(fd)?;
689                return Err(Error::ENOTSUP);
690            }
691            Err(_) => return Err(Error::ENOTSUP),
692        }
693
694        // Verify we can open child pty.
695        let fd = open(pts_name.as_str(), OFlag::O_RDWR, Mode::empty())?;
696        close(fd)?;
697
698        // Verify we now have a controlling tty.
699        let fd = open("/dev/tty", OFlag::O_WRONLY, Mode::empty())?;
700        close(fd)?;
701    }
702
703    #[cfg(any(target_os = "freebsd", target_os = "macos"))]
704    {
705        let pts_fd = ptm.get_slave_fd()?;
706
707        // https://docs.freebsd.org/44doc/smm/01.setup/paper-3.html
708        setsid()?;
709
710        use nix::libc::ioctl;
711        use nix::libc::TIOCSCTTY;
712        match unsafe { ioctl(pts_fd, TIOCSCTTY as u64, 0) } {
713            0 => {}
714            _ => return Err(Error::last()),
715        }
716    }
717
718    Ok(())
719}
720
721// Except is used for cases like double free memory
722fn close_all_descriptors(except: &[RawFd]) -> Result<()> {
723    // On linux could be used getrlimit(RLIMIT_NOFILE, rlim) interface
724    let max_open_fds = sysconf(SysconfVar::OPEN_MAX)?.unwrap() as i32;
725    (0..max_open_fds)
726        .filter(|fd| !except.contains(fd))
727        .for_each(|fd| {
728            // We don't handle errors intentionally,
729            // because it will be hard to determine which descriptors closed already.
730            let _ = close(fd);
731        });
732
733    Ok(())
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739
740    #[test]
741    fn create_pty() -> Result<()> {
742        let master = Master::open()?;
743        master.grant_slave_access()?;
744        master.unlock_slave()?;
745        let slavename = master.get_slave_name()?;
746
747        let expected_path = if cfg!(target_os = "freebsd") {
748            "pts/"
749        } else if cfg!(target_os = "macos") {
750            "/dev/ttys"
751        } else {
752            "/dev/pts/"
753        };
754
755        if !slavename.starts_with(expected_path) {
756            assert_eq!(expected_path, slavename);
757        }
758
759        Ok(())
760    }
761
762    #[test]
763    #[ignore = "The test should be run in a sigle thread mode --jobs 1 or --test-threads 1"]
764    fn release_pty_master() -> Result<()> {
765        let master = Master::open()?;
766        let old_master_fd = master.fd.as_raw_fd();
767
768        drop(master);
769
770        let master = Master::open()?;
771
772        assert!(master.fd.as_raw_fd() == old_master_fd);
773
774        Ok(())
775    }
776}