diff --git a/CHANGELOG.md b/CHANGELOG.md index a4310d47ff..7d59d217bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ([#582](https://github.com/nix-rust/nix/pull/582) - Added `nix::unistd::{openat, fstatat, readlink, readlinkat}` ([#551](https://github.com/nix-rust/nix/pull/551)) +- Added `nix::pty::{grantpt, posix_openpt, ptsname/ptsname_r, unlockpt}` + ([#556](https://github.com/nix-rust/nix/pull/556) ### Changed - Marked `sys::mman::{ mmap, munmap, madvise, munlock, msync }` as unsafe. diff --git a/src/lib.rs b/src/lib.rs index ecbf139dc6..56ed5c3fa8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,8 @@ pub mod mount; #[cfg(target_os = "linux")] pub mod mqueue; +pub mod pty; + #[cfg(any(target_os = "linux", target_os = "macos"))] pub mod poll; diff --git a/src/pty.rs b/src/pty.rs new file mode 100644 index 0000000000..231d037169 --- /dev/null +++ b/src/pty.rs @@ -0,0 +1,160 @@ +//! Create master and slave virtual pseudo-terminals (PTYs) + +use std::ffi::CStr; +use std::mem; +use std::os::unix::prelude::*; + +use libc; + +use {Error, fcntl, Result}; + +/// Representation of the Master device in a master/slave pty pair +/// +/// While this datatype is a thin wrapper around `RawFd`, it enforces that the available PTY +/// functions are given the correct file descriptor. Additionally this type implements `Drop`, +/// so that when it's consumed or goes out of scope, it's automatically cleaned-up. +#[derive(Debug)] +pub struct PtyMaster(RawFd); + +impl AsRawFd for PtyMaster { + fn as_raw_fd(&self) -> RawFd { + self.0 + } +} + +impl IntoRawFd for PtyMaster { + fn into_raw_fd(self) -> RawFd { + let fd = self.0; + mem::forget(self); + fd + } +} + +impl Drop for PtyMaster { + fn drop(&mut self) { + // Errors when closing are ignored because we don't actually know if the file descriptor + // was closed. If we retried, it's possible that descriptor was reallocated in the mean + // time and the wrong file descriptor could be closed. + let _ = ::unistd::close(self.0); + } +} + +/// Grant access to a slave pseudoterminal (see +/// [grantpt(3)](http://man7.org/linux/man-pages/man3/grantpt.3.html)) +/// +/// `grantpt()` changes the mode and owner of the slave pseudoterminal device corresponding to the +/// master pseudoterminal referred to by `fd`. This is a necessary step towards opening the slave. +#[inline] +pub fn grantpt(fd: &PtyMaster) -> Result<()> { + if unsafe { libc::grantpt(fd.as_raw_fd()) } < 0 { + return Err(Error::last().into()); + } + + Ok(()) +} + +/// Open a pseudoterminal device (see +/// [posix_openpt(3)](http://man7.org/linux/man-pages/man3/posix_openpt.3.html)) +/// +/// `posix_openpt()` returns a file descriptor to an existing unused pseuterminal master device. +/// +/// # Examples +/// +/// A common use case with this function is to open both a master and slave PTY pair. This can be +/// done as follows: +/// +/// ``` +/// use std::path::Path; +/// use nix::fcntl::{O_RDWR, open}; +/// use nix::pty::*; +/// use nix::sys::stat; +/// +/// # #[allow(dead_code)] +/// # fn run() -> nix::Result<()> { +/// // Open a new PTY master +/// let master_fd = posix_openpt(O_RDWR)?; +/// +/// // Allow a slave to be generated for it +/// grantpt(&master_fd)?; +/// unlockpt(&master_fd)?; +/// +/// // Get the name of the slave +/// let slave_name = ptsname(&master_fd)?; +/// +/// // Try to open the slave +/// # #[allow(unused_variables)] +/// let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty())?; +/// # Ok(()) +/// # } +/// ``` +#[inline] +pub fn posix_openpt(flags: fcntl::OFlag) -> Result { + let fd = unsafe { + libc::posix_openpt(flags.bits()) + }; + + if fd < 0 { + return Err(Error::last().into()); + } + + Ok(PtyMaster(fd)) +} + +/// Get the name of the slave pseudoterminal (see +/// [ptsname(3)](http://man7.org/linux/man-pages/man3/ptsname.3.html)) +/// +/// `ptsname()` returns the name of the slave pseudoterminal device corresponding to the master +/// referred to by `fd`. Note that this function is *not* threadsafe. For that see `ptsname_r()`. +/// +/// This value is useful for opening the slave pty once the master has already been opened with +/// `posix_openpt()`. +#[inline] +pub fn ptsname(fd: &PtyMaster) -> Result { + let name_ptr = unsafe { libc::ptsname(fd.as_raw_fd()) }; + if name_ptr.is_null() { + return Err(Error::last().into()); + } + + let name = unsafe { + CStr::from_ptr(name_ptr) + }; + Ok(name.to_string_lossy().into_owned()) +} + +/// Get the name of the slave pseudoterminal (see +/// [ptsname(3)](http://man7.org/linux/man-pages/man3/ptsname.3.html)) +/// +/// `ptsname_r()` returns the name of the slave pseudoterminal device corresponding to the master +/// referred to by `fd`. This is the threadsafe version of `ptsname()`, but it is not part of the +/// POSIX standard and is instead a Linux-specific extension. +/// +/// This value is useful for opening the slave ptty once the master has already been opened with +/// `posix_openpt()`. +#[cfg(any(target_os = "android", target_os = "linux"))] +#[inline] +pub fn ptsname_r(fd: &PtyMaster) -> Result { + let mut name_buf = [0 as libc::c_char; 64]; + if unsafe { libc::ptsname_r(fd.as_raw_fd(), name_buf.as_mut_ptr(), name_buf.len()) } != 0 { + return Err(Error::last().into()); + } + + let name = unsafe { + CStr::from_ptr(name_buf.as_ptr()) + }; + Ok(name.to_string_lossy().into_owned()) +} + +/// Unlock a pseudoterminal master/slave pseudoterminal pair (see +/// [unlockpt(3)](http://man7.org/linux/man-pages/man3/unlockpt.3.html)) +/// +/// `unlockpt()` unlocks the slave pseudoterminal device corresponding to the master pseudoterminal +/// referred to by `fd`. This must be called before trying to open the slave side of a +/// pseuoterminal. +#[inline] +pub fn unlockpt(fd: &PtyMaster) -> Result<()> { + if unsafe { libc::unlockpt(fd.as_raw_fd()) } < 0 { + return Err(Error::last().into()); + } + + Ok(()) +} diff --git a/test/test.rs b/test/test.rs index 1357642ef6..1f87171da5 100644 --- a/test/test.rs +++ b/test/test.rs @@ -23,6 +23,7 @@ mod test_mq; #[cfg(any(target_os = "linux", target_os = "macos"))] mod test_poll; +mod test_pty; use nixtest::assert_size_of; diff --git a/test/test_pty.rs b/test/test_pty.rs new file mode 100644 index 0000000000..61780421b7 --- /dev/null +++ b/test/test_pty.rs @@ -0,0 +1,92 @@ +use std::path::Path; +use std::os::unix::prelude::*; +use nix::fcntl::{O_RDWR, open}; +use nix::pty::*; +use nix::sys::stat; + +/// Test equivalence of `ptsname` and `ptsname_r` +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_equivalence() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name = ptsname(&master_fd).unwrap(); + let slave_name_r = ptsname_r(&master_fd).unwrap(); + assert_eq!(slave_name, slave_name_r); +} + +/// Test data copying of `ptsname` +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_copy() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name1 = ptsname(&master_fd).unwrap(); + let slave_name2 = ptsname(&master_fd).unwrap(); + assert!(slave_name1 == slave_name2); + // Also make sure that the string was actually copied and they point to different parts of + // memory. + assert!(slave_name1.as_ptr() != slave_name2.as_ptr()); +} + +/// Test data copying of `ptsname_r` +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_r_copy() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name1 = ptsname_r(&master_fd).unwrap(); + let slave_name2 = ptsname_r(&master_fd).unwrap(); + assert!(slave_name1 == slave_name2); + assert!(slave_name1.as_ptr() != slave_name2.as_ptr()); +} + +/// Test that `ptsname` returns different names for different devices +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_unique() { + // Open a new PTTY master + let master1_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master1_fd.as_raw_fd() > 0); + + // Open a second PTTY master + let master2_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master2_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name1 = ptsname(&master1_fd).unwrap(); + let slave_name2 = ptsname(&master2_fd).unwrap(); + assert!(slave_name1 != slave_name2); +} + +/// Test opening a master/slave PTTY pair +/// +/// This is a single larger test because much of these functions aren't useful by themselves. So for +/// this test we perform the basic act of getting a file handle for a connect master/slave PTTY +/// pair. +#[test] +fn test_open_ptty_pair() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Allow a slave to be generated for it + grantpt(&master_fd).unwrap(); + unlockpt(&master_fd).unwrap(); + + // Get the name of the slave + let slave_name = ptsname(&master_fd).unwrap(); + + // Open the slave device + let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty()).unwrap(); + assert!(slave_fd > 0); +}