Skip to content

Commit

Permalink
Add various pty functions
Browse files Browse the repository at this point in the history
  * grantpt
  * ptsname/ptsname_r
  * posix_openpt
  * unlockpt
  • Loading branch information
Susurrus committed May 16, 2017
1 parent 8498bd9 commit dc2dfb8
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -92,6 +94,9 @@ pub type Result<T> = result::Result<T, Error>;
pub enum Error {
Sys(errno::Errno),
InvalidPath,
/// The operation involved a conversion to Rust's native String type, which failed because the
/// string did not contain all valid UTF-8.
InvalidUtf8,
}

impl Error {
Expand All @@ -114,8 +119,9 @@ impl Error {
/// Get the errno associated with this error
pub fn errno(&self) -> errno::Errno {
match *self {
Error::Sys(errno) => errno,
Error::InvalidPath => errno::Errno::EINVAL,
Error::InvalidUtf8 => errno::Errno::UnknownErrno,
Error::Sys(errno) => errno,
}
}
}
Expand All @@ -124,10 +130,15 @@ impl From<errno::Errno> for Error {
fn from(errno: errno::Errno) -> Error { Error::from_errno(errno) }
}

impl From<std::string::FromUtf8Error> for Error {
fn from(_: std::string::FromUtf8Error) -> Error { Error::InvalidUtf8 }
}

impl error::Error for Error {
fn description(&self) -> &str {
match self {
&Error::InvalidPath => "Invalid path",
&Error::InvalidUtf8 => "Invalid UTF-8 string",
&Error::Sys(ref errno) => errno.desc(),
}
}
Expand All @@ -137,6 +148,7 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
&Error::InvalidPath => write!(f, "Invalid path"),
&Error::InvalidUtf8 => write!(f, "Invalid UTF-8 string"),
&Error::Sys(errno) => write!(f, "{:?}: {}", errno, errno.desc()),
}
}
Expand All @@ -146,7 +158,9 @@ impl From<Error> for io::Error {
fn from(err: Error) -> Self {
match err {
Error::InvalidPath => io::Error::new(io::ErrorKind::InvalidInput, err),
Error::InvalidUtf8 => io::Error::new(io::ErrorKind::Other, err),
Error::Sys(errno) => io::Error::from_raw_os_error(errno as i32),

}
}
}
Expand Down
166 changes: 166 additions & 0 deletions src/pty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! 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<PtyMaster> {
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<String> {
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<String> {
let mut name_buf = vec![0u8; 64];
let name_buf_ptr = name_buf.as_mut_ptr() as *mut libc::c_char;
println!("{}/{}", name_buf.len(), name_buf.capacity());
if unsafe { libc::ptsname_r(fd.as_raw_fd(), name_buf_ptr, name_buf.capacity()) } != 0 {
return Err(Error::last().into());
}
println!("{}/{}", name_buf.len(), name_buf.capacity());

// Find the first null-character terminating this string. This is guaranteed to succeed if the
// return value of `libc::ptsname_r` is 0.
let null_index = name_buf.iter().position(|c| *c == b'\0').unwrap();
name_buf.truncate(null_index);

let name = String::from_utf8(name_buf)?;
Ok(name)
}

/// 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(())
}
1 change: 1 addition & 0 deletions test/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
92 changes: 92 additions & 0 deletions test/test_pty.rs
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit dc2dfb8

Please sign in to comment.