Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add various pty functions #556

Merged
merged 1 commit into from
May 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
15 changes: 14 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,6 +158,7 @@ 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
164 changes: 164 additions & 0 deletions src/pty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//! 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test that calls ptsname on two separate pseudoterminals, and checks that their names are not the same? That would enforce that this function performs a data copy (as I think it probably should).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't you instead want to call ptsname() on the same ptty and check that they don't point to the same underlying buffer? I'm unclear what the test you suggested actually tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that could do it. I'm really just trying to think up ways to improve the coverage.

};
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;
if unsafe { libc::ptsname_r(fd.as_raw_fd(), name_buf_ptr, name_buf.capacity()) } != 0 {
return Err(Error::last().into());
}

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/PTTY/PTY here and below

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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test leaks its file descriptors.