Skip to content

Commit ba5c837

Browse files
committed
Merge #556
556: Add various pty functions r=asomers * grantpt * posix_openpt * ptsname/ptsname_r * unlockpt I've refactored my code to use these functions and they work correctly at least in the non-error case. Only issue is what to do for failure in `ptsname` when converting strings. This should effectively never happen. I think maybe adding an `Unknown` to the `Error` type and returning that might be useful.
2 parents 8498bd9 + 4e97520 commit ba5c837

File tree

5 files changed

+273
-1
lines changed

5 files changed

+273
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1010
([#582](https://github.com/nix-rust/nix/pull/582)
1111
- Added `nix::unistd::{openat, fstatat, readlink, readlinkat}`
1212
([#551](https://github.com/nix-rust/nix/pull/551))
13+
- Added `nix::pty::{grantpt, posix_openpt, ptsname/ptsname_r, unlockpt}`
14+
([#556](https://github.com/nix-rust/nix/pull/556)
1315

1416
### Changed
1517
- Marked `sys::mman::{ mmap, munmap, madvise, munlock, msync }` as unsafe.

src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub mod mount;
4444
#[cfg(target_os = "linux")]
4545
pub mod mqueue;
4646

47+
pub mod pty;
48+
4749
#[cfg(any(target_os = "linux", target_os = "macos"))]
4850
pub mod poll;
4951

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

97102
impl Error {
@@ -114,8 +119,9 @@ impl Error {
114119
/// Get the errno associated with this error
115120
pub fn errno(&self) -> errno::Errno {
116121
match *self {
117-
Error::Sys(errno) => errno,
118122
Error::InvalidPath => errno::Errno::EINVAL,
123+
Error::InvalidUtf8 => errno::Errno::UnknownErrno,
124+
Error::Sys(errno) => errno,
119125
}
120126
}
121127
}
@@ -124,10 +130,15 @@ impl From<errno::Errno> for Error {
124130
fn from(errno: errno::Errno) -> Error { Error::from_errno(errno) }
125131
}
126132

133+
impl From<std::string::FromUtf8Error> for Error {
134+
fn from(_: std::string::FromUtf8Error) -> Error { Error::InvalidUtf8 }
135+
}
136+
127137
impl error::Error for Error {
128138
fn description(&self) -> &str {
129139
match self {
130140
&Error::InvalidPath => "Invalid path",
141+
&Error::InvalidUtf8 => "Invalid UTF-8 string",
131142
&Error::Sys(ref errno) => errno.desc(),
132143
}
133144
}
@@ -137,6 +148,7 @@ impl fmt::Display for Error {
137148
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138149
match self {
139150
&Error::InvalidPath => write!(f, "Invalid path"),
151+
&Error::InvalidUtf8 => write!(f, "Invalid UTF-8 string"),
140152
&Error::Sys(errno) => write!(f, "{:?}: {}", errno, errno.desc()),
141153
}
142154
}
@@ -146,6 +158,7 @@ impl From<Error> for io::Error {
146158
fn from(err: Error) -> Self {
147159
match err {
148160
Error::InvalidPath => io::Error::new(io::ErrorKind::InvalidInput, err),
161+
Error::InvalidUtf8 => io::Error::new(io::ErrorKind::Other, err),
149162
Error::Sys(errno) => io::Error::from_raw_os_error(errno as i32),
150163
}
151164
}

src/pty.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//! Create master and slave virtual pseudo-terminals (PTYs)
2+
3+
use std::ffi::CStr;
4+
use std::mem;
5+
use std::os::unix::prelude::*;
6+
7+
use libc;
8+
9+
use {Error, fcntl, Result};
10+
11+
/// Representation of the Master device in a master/slave pty pair
12+
///
13+
/// While this datatype is a thin wrapper around `RawFd`, it enforces that the available PTY
14+
/// functions are given the correct file descriptor. Additionally this type implements `Drop`,
15+
/// so that when it's consumed or goes out of scope, it's automatically cleaned-up.
16+
#[derive(Debug)]
17+
pub struct PtyMaster(RawFd);
18+
19+
impl AsRawFd for PtyMaster {
20+
fn as_raw_fd(&self) -> RawFd {
21+
self.0
22+
}
23+
}
24+
25+
impl IntoRawFd for PtyMaster {
26+
fn into_raw_fd(self) -> RawFd {
27+
let fd = self.0;
28+
mem::forget(self);
29+
fd
30+
}
31+
}
32+
33+
impl Drop for PtyMaster {
34+
fn drop(&mut self) {
35+
// Errors when closing are ignored because we don't actually know if the file descriptor
36+
// was closed. If we retried, it's possible that descriptor was reallocated in the mean
37+
// time and the wrong file descriptor could be closed.
38+
let _ = ::unistd::close(self.0);
39+
}
40+
}
41+
42+
/// Grant access to a slave pseudoterminal (see
43+
/// [grantpt(3)](http://man7.org/linux/man-pages/man3/grantpt.3.html))
44+
///
45+
/// `grantpt()` changes the mode and owner of the slave pseudoterminal device corresponding to the
46+
/// master pseudoterminal referred to by `fd`. This is a necessary step towards opening the slave.
47+
#[inline]
48+
pub fn grantpt(fd: &PtyMaster) -> Result<()> {
49+
if unsafe { libc::grantpt(fd.as_raw_fd()) } < 0 {
50+
return Err(Error::last().into());
51+
}
52+
53+
Ok(())
54+
}
55+
56+
/// Open a pseudoterminal device (see
57+
/// [posix_openpt(3)](http://man7.org/linux/man-pages/man3/posix_openpt.3.html))
58+
///
59+
/// `posix_openpt()` returns a file descriptor to an existing unused pseuterminal master device.
60+
///
61+
/// # Examples
62+
///
63+
/// A common use case with this function is to open both a master and slave PTY pair. This can be
64+
/// done as follows:
65+
///
66+
/// ```
67+
/// use std::path::Path;
68+
/// use nix::fcntl::{O_RDWR, open};
69+
/// use nix::pty::*;
70+
/// use nix::sys::stat;
71+
///
72+
/// # #[allow(dead_code)]
73+
/// # fn run() -> nix::Result<()> {
74+
/// // Open a new PTY master
75+
/// let master_fd = posix_openpt(O_RDWR)?;
76+
///
77+
/// // Allow a slave to be generated for it
78+
/// grantpt(&master_fd)?;
79+
/// unlockpt(&master_fd)?;
80+
///
81+
/// // Get the name of the slave
82+
/// let slave_name = ptsname(&master_fd)?;
83+
///
84+
/// // Try to open the slave
85+
/// # #[allow(unused_variables)]
86+
/// let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty())?;
87+
/// # Ok(())
88+
/// # }
89+
/// ```
90+
#[inline]
91+
pub fn posix_openpt(flags: fcntl::OFlag) -> Result<PtyMaster> {
92+
let fd = unsafe {
93+
libc::posix_openpt(flags.bits())
94+
};
95+
96+
if fd < 0 {
97+
return Err(Error::last().into());
98+
}
99+
100+
Ok(PtyMaster(fd))
101+
}
102+
103+
/// Get the name of the slave pseudoterminal (see
104+
/// [ptsname(3)](http://man7.org/linux/man-pages/man3/ptsname.3.html))
105+
///
106+
/// `ptsname()` returns the name of the slave pseudoterminal device corresponding to the master
107+
/// referred to by `fd`. Note that this function is *not* threadsafe. For that see `ptsname_r()`.
108+
///
109+
/// This value is useful for opening the slave pty once the master has already been opened with
110+
/// `posix_openpt()`.
111+
#[inline]
112+
pub fn ptsname(fd: &PtyMaster) -> Result<String> {
113+
let name_ptr = unsafe { libc::ptsname(fd.as_raw_fd()) };
114+
if name_ptr.is_null() {
115+
return Err(Error::last().into());
116+
}
117+
118+
let name = unsafe {
119+
CStr::from_ptr(name_ptr)
120+
};
121+
Ok(name.to_string_lossy().into_owned())
122+
}
123+
124+
/// Get the name of the slave pseudoterminal (see
125+
/// [ptsname(3)](http://man7.org/linux/man-pages/man3/ptsname.3.html))
126+
///
127+
/// `ptsname_r()` returns the name of the slave pseudoterminal device corresponding to the master
128+
/// referred to by `fd`. This is the threadsafe version of `ptsname()`, but it is not part of the
129+
/// POSIX standard and is instead a Linux-specific extension.
130+
///
131+
/// This value is useful for opening the slave ptty once the master has already been opened with
132+
/// `posix_openpt()`.
133+
#[cfg(any(target_os = "android", target_os = "linux"))]
134+
#[inline]
135+
pub fn ptsname_r(fd: &PtyMaster) -> Result<String> {
136+
let mut name_buf = vec![0u8; 64];
137+
let name_buf_ptr = name_buf.as_mut_ptr() as *mut libc::c_char;
138+
if unsafe { libc::ptsname_r(fd.as_raw_fd(), name_buf_ptr, name_buf.capacity()) } != 0 {
139+
return Err(Error::last().into());
140+
}
141+
142+
// Find the first null-character terminating this string. This is guaranteed to succeed if the
143+
// return value of `libc::ptsname_r` is 0.
144+
let null_index = name_buf.iter().position(|c| *c == b'\0').unwrap();
145+
name_buf.truncate(null_index);
146+
147+
let name = String::from_utf8(name_buf)?;
148+
Ok(name)
149+
}
150+
151+
/// Unlock a pseudoterminal master/slave pseudoterminal pair (see
152+
/// [unlockpt(3)](http://man7.org/linux/man-pages/man3/unlockpt.3.html))
153+
///
154+
/// `unlockpt()` unlocks the slave pseudoterminal device corresponding to the master pseudoterminal
155+
/// referred to by `fd`. This must be called before trying to open the slave side of a
156+
/// pseuoterminal.
157+
#[inline]
158+
pub fn unlockpt(fd: &PtyMaster) -> Result<()> {
159+
if unsafe { libc::unlockpt(fd.as_raw_fd()) } < 0 {
160+
return Err(Error::last().into());
161+
}
162+
163+
Ok(())
164+
}

test/test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mod test_mq;
2323

2424
#[cfg(any(target_os = "linux", target_os = "macos"))]
2525
mod test_poll;
26+
mod test_pty;
2627

2728
use nixtest::assert_size_of;
2829

test/test_pty.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use std::path::Path;
2+
use std::os::unix::prelude::*;
3+
use nix::fcntl::{O_RDWR, open};
4+
use nix::pty::*;
5+
use nix::sys::stat;
6+
7+
/// Test equivalence of `ptsname` and `ptsname_r`
8+
#[test]
9+
#[cfg(any(target_os = "android", target_os = "linux"))]
10+
fn test_ptsname_equivalence() {
11+
// Open a new PTTY master
12+
let master_fd = posix_openpt(O_RDWR).unwrap();
13+
assert!(master_fd.as_raw_fd() > 0);
14+
15+
// Get the name of the slave
16+
let slave_name = ptsname(&master_fd).unwrap();
17+
let slave_name_r = ptsname_r(&master_fd).unwrap();
18+
assert_eq!(slave_name, slave_name_r);
19+
}
20+
21+
/// Test data copying of `ptsname`
22+
#[test]
23+
#[cfg(any(target_os = "android", target_os = "linux"))]
24+
fn test_ptsname_copy() {
25+
// Open a new PTTY master
26+
let master_fd = posix_openpt(O_RDWR).unwrap();
27+
assert!(master_fd.as_raw_fd() > 0);
28+
29+
// Get the name of the slave
30+
let slave_name1 = ptsname(&master_fd).unwrap();
31+
let slave_name2 = ptsname(&master_fd).unwrap();
32+
assert!(slave_name1 == slave_name2);
33+
// Also make sure that the string was actually copied and they point to different parts of
34+
// memory.
35+
assert!(slave_name1.as_ptr() != slave_name2.as_ptr());
36+
}
37+
38+
/// Test data copying of `ptsname_r`
39+
#[test]
40+
#[cfg(any(target_os = "android", target_os = "linux"))]
41+
fn test_ptsname_r_copy() {
42+
// Open a new PTTY master
43+
let master_fd = posix_openpt(O_RDWR).unwrap();
44+
assert!(master_fd.as_raw_fd() > 0);
45+
46+
// Get the name of the slave
47+
let slave_name1 = ptsname_r(&master_fd).unwrap();
48+
let slave_name2 = ptsname_r(&master_fd).unwrap();
49+
assert!(slave_name1 == slave_name2);
50+
assert!(slave_name1.as_ptr() != slave_name2.as_ptr());
51+
}
52+
53+
/// Test that `ptsname` returns different names for different devices
54+
#[test]
55+
#[cfg(any(target_os = "android", target_os = "linux"))]
56+
fn test_ptsname_unique() {
57+
// Open a new PTTY master
58+
let master1_fd = posix_openpt(O_RDWR).unwrap();
59+
assert!(master1_fd.as_raw_fd() > 0);
60+
61+
// Open a second PTTY master
62+
let master2_fd = posix_openpt(O_RDWR).unwrap();
63+
assert!(master2_fd.as_raw_fd() > 0);
64+
65+
// Get the name of the slave
66+
let slave_name1 = ptsname(&master1_fd).unwrap();
67+
let slave_name2 = ptsname(&master2_fd).unwrap();
68+
assert!(slave_name1 != slave_name2);
69+
}
70+
71+
/// Test opening a master/slave PTTY pair
72+
///
73+
/// This is a single larger test because much of these functions aren't useful by themselves. So for
74+
/// this test we perform the basic act of getting a file handle for a connect master/slave PTTY
75+
/// pair.
76+
#[test]
77+
fn test_open_ptty_pair() {
78+
// Open a new PTTY master
79+
let master_fd = posix_openpt(O_RDWR).unwrap();
80+
assert!(master_fd.as_raw_fd() > 0);
81+
82+
// Allow a slave to be generated for it
83+
grantpt(&master_fd).unwrap();
84+
unlockpt(&master_fd).unwrap();
85+
86+
// Get the name of the slave
87+
let slave_name = ptsname(&master_fd).unwrap();
88+
89+
// Open the slave device
90+
let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty()).unwrap();
91+
assert!(slave_fd > 0);
92+
}

0 commit comments

Comments
 (0)