diff --git a/Cargo.toml b/Cargo.toml index be7b08745..4e6fcd794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,6 +166,9 @@ param = ["fs"] # Enable this to enable `rustix::io::proc_self_*` (on Linux) and `ttyname`. procfs = ["once_cell", "itoa", "fs"] +# Enable `rustix::pty::*`. +pty = ["itoa", "fs"] + # Enable `rustix::termios::*`. termios = [] @@ -188,6 +191,7 @@ all-apis = [ "param", "process", "procfs", + "pty", "rand", "runtime", "termios", diff --git a/README.md b/README.md index 8445a0286..0b254da88 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ by default. The rest of the API is conditional with cargo feature flags: | `net` | [`rustix::net`] and [`rustix::path`]—Network-related operations. | `param` | [`rustix::param`]—Process parameters. | `process` | [`rustix::process`]—Process-associated operations. +| `pty` | [`rustix::pty`]—Pseduoterminal operations. | `rand` | [`rustix::rand`]—Random-related operations. | `termios` | [`rustix::termios`]—Terminal I/O stream operations. | `thread` | [`rustix::thread`]—Thread-associated operations. @@ -76,6 +77,7 @@ by default. The rest of the API is conditional with cargo feature flags: [`rustix::net`]: https://docs.rs/rustix/*/rustix/net/index.html [`rustix::param`]: https://docs.rs/rustix/*/rustix/param/index.html [`rustix::process`]: https://docs.rs/rustix/*/rustix/process/index.html +[`rustix::pty`]: https://docs.rs/rustix/*/rustix/pty/index.html [`rustix::rand`]: https://docs.rs/rustix/*/rustix/rand/index.html [`rustix::termios`]: https://docs.rs/rustix/*/rustix/termios/index.html [`rustix::thread`]: https://docs.rs/rustix/*/rustix/thread/index.html diff --git a/src/backend/libc/mod.rs b/src/backend/libc/mod.rs index 9510dd210..e5f3bea45 100644 --- a/src/backend/libc/mod.rs +++ b/src/backend/libc/mod.rs @@ -72,6 +72,10 @@ pub(crate) mod param; #[cfg(not(windows))] pub(crate) mod process; #[cfg(not(windows))] +#[cfg(not(target_os = "wasi"))] +#[cfg(feature = "pty")] +pub(crate) mod pty; +#[cfg(not(windows))] #[cfg(feature = "rand")] pub(crate) mod rand; #[cfg(not(windows))] diff --git a/src/backend/libc/process/syscalls.rs b/src/backend/libc/process/syscalls.rs index 3c9e5d0f5..b96ea30f6 100644 --- a/src/backend/libc/process/syscalls.rs +++ b/src/backend/libc/process/syscalls.rs @@ -1,11 +1,9 @@ //! libc syscalls supporting `rustix::process`. use super::super::c; -#[cfg(not(any(target_os = "wasi", target_os = "fuchsia")))] -use super::super::conv::borrowed_fd; -use super::super::conv::{c_str, ret, ret_c_int, ret_discarded_char_ptr}; #[cfg(not(target_os = "wasi"))] -use super::super::conv::{ret_infallible, ret_pid_t, ret_usize}; +use super::super::conv::{borrowed_fd, ret_infallible, ret_pid_t, ret_usize}; +use super::super::conv::{c_str, ret, ret_c_int, ret_discarded_char_ptr}; #[cfg(any(target_os = "android", target_os = "linux"))] use super::super::conv::{syscall_ret, syscall_ret_u32}; #[cfg(any( @@ -15,7 +13,7 @@ use super::super::conv::{syscall_ret, syscall_ret_u32}; target_os = "linux", ))] use super::types::RawCpuSet; -#[cfg(not(any(target_os = "wasi", target_os = "fuchsia")))] +#[cfg(not(target_os = "wasi"))] use crate::fd::BorrowedFd; #[cfg(target_os = "linux")] use crate::fd::{AsRawFd, OwnedFd}; @@ -648,3 +646,9 @@ pub(crate) fn sethostname(name: &[u8]) -> io::Result<()> { )) } } + +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] +#[inline] +pub(crate) fn ioctl_tiocsctty(fd: BorrowedFd<'_>) -> io::Result<()> { + unsafe { ret(c::ioctl(borrowed_fd(fd), c::TIOCSCTTY as _, &0_u32)) } +} diff --git a/src/backend/libc/pty/mod.rs b/src/backend/libc/pty/mod.rs new file mode 100644 index 000000000..ef944f04d --- /dev/null +++ b/src/backend/libc/pty/mod.rs @@ -0,0 +1 @@ +pub(crate) mod syscalls; diff --git a/src/backend/libc/pty/syscalls.rs b/src/backend/libc/pty/syscalls.rs new file mode 100644 index 000000000..b95137fcc --- /dev/null +++ b/src/backend/libc/pty/syscalls.rs @@ -0,0 +1,87 @@ +//! libc syscalls supporting `rustix::pty`. + +use super::super::c; +use super::super::conv::{borrowed_fd, ret}; +use crate::fd::BorrowedFd; +use crate::io; +#[cfg(not(target_os = "android"))] +use {super::super::conv::ret_owned_fd, crate::fd::OwnedFd, crate::pty::OpenptFlags}; +#[cfg(any(apple, linux_like, target_os = "freebsd", target_os = "fuchsia"))] +use { + crate::ffi::{CStr, CString}, + crate::path::SMALL_PATH_BUFFER_SIZE, + alloc::borrow::ToOwned, + alloc::vec::Vec, +}; + +#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[inline] +pub(crate) fn openpt(flags: OpenptFlags) -> io::Result { + unsafe { ret_owned_fd(c::posix_openpt(flags.bits() as _)) } +} + +#[cfg(any(apple, linux_like, target_os = "freebsd", target_os = "fuchsia"))] +#[inline] +pub(crate) fn ptsname(fd: BorrowedFd, mut buffer: Vec) -> io::Result { + // This code would benefit from having a better way to read into + // uninitialized memory, but that requires `unsafe`. + buffer.clear(); + buffer.reserve(SMALL_PATH_BUFFER_SIZE); + buffer.resize(buffer.capacity(), 0_u8); + + loop { + // On platforms with `ptsname_r`, use it. + #[cfg(any(target_os = "freebsd", linux_like, target_os = "fuchsia"))] + let r = + unsafe { libc::ptsname_r(borrowed_fd(fd), buffer.as_mut_ptr().cast(), buffer.len()) }; + + // MacOS 10.13.4 has `ptsname_r`; use it if we have it, otherwise fall + // back to calling the underlying ioctl directly. + #[cfg(apple)] + let r = unsafe { + weak! { fn ptsname_r(c::c_int, *mut c::c_char, c::size_t) -> c::c_int } + + if let Some(libc_ptsname_r) = ptsname_r.get() { + libc_ptsname_r(borrowed_fd(fd), buffer.as_mut_ptr().cast(), buffer.len()) + } else { + // The size declared in the `TIOCPTYGNAME` macro in sys/ttycom.h is 128. + let mut name: [u8; 128] = [0_u8; 128]; + match libc::ioctl(borrowed_fd(fd), libc::TIOCPTYGNAME as u64, &mut name) { + 0 => { + let len = CStr::from_ptr(name.as_ptr().cast()).to_bytes().len(); + std::ptr::copy_nonoverlapping(name.as_ptr(), buffer.as_mut_ptr(), len + 1); + 0 + } + _ => libc_errno::errno().0, + } + } + }; + + if r == 0 { + return Ok(unsafe { CStr::from_ptr(buffer.as_ptr().cast()).to_owned() }); + } + if r != libc::ERANGE { + return Err(io::Errno::from_raw_os_error(r)); + } + + buffer.reserve(1); // use `Vec` reallocation strategy to grow capacity exponentially + buffer.resize(buffer.capacity(), 0_u8); + } +} + +#[inline] +pub(crate) fn unlockpt(fd: BorrowedFd) -> io::Result<()> { + unsafe { ret(c::unlockpt(borrowed_fd(fd))) } +} + +#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[inline] +pub(crate) fn grantpt(fd: BorrowedFd) -> io::Result<()> { + unsafe { ret(c::grantpt(borrowed_fd(fd))) } +} + +#[cfg(target_os = "linux")] +#[inline] +pub(crate) fn ioctl_tiocgptpeer(fd: BorrowedFd, flags: OpenptFlags) -> io::Result { + unsafe { ret_owned_fd(c::ioctl(borrowed_fd(fd), c::TIOCGPTPEER, flags.bits())) } +} diff --git a/src/backend/linux_raw/c.rs b/src/backend/linux_raw/c.rs index 0caa378c1..30ce1256c 100644 --- a/src/backend/linux_raw/c.rs +++ b/src/backend/linux_raw/c.rs @@ -32,4 +32,5 @@ pub(crate) use linux_raw_sys::general::{ SO_SNDTIMEO_NEW, SO_SNDTIMEO_OLD, SO_TYPE, TCP_NODELAY, }; pub(crate) use linux_raw_sys::general::{NFS_SUPER_MAGIC, PROC_SUPER_MAGIC, UTIME_NOW, UTIME_OMIT}; +pub(crate) use linux_raw_sys::general::{O_NOCTTY, O_RDWR}; pub(crate) use linux_raw_sys::general::{XATTR_CREATE, XATTR_REPLACE}; diff --git a/src/backend/linux_raw/mod.rs b/src/backend/linux_raw/mod.rs index e7e073e32..ae3ea4397 100644 --- a/src/backend/linux_raw/mod.rs +++ b/src/backend/linux_raw/mod.rs @@ -41,6 +41,8 @@ pub(crate) mod net; ))] pub(crate) mod param; pub(crate) mod process; +#[cfg(feature = "pty")] +pub(crate) mod pty; #[cfg(feature = "rand")] pub(crate) mod rand; #[cfg(feature = "runtime")] diff --git a/src/backend/linux_raw/process/syscalls.rs b/src/backend/linux_raw/process/syscalls.rs index d0546dd76..97215e369 100644 --- a/src/backend/linux_raw/process/syscalls.rs +++ b/src/backend/linux_raw/process/syscalls.rs @@ -31,6 +31,7 @@ use linux_raw_sys::general::{ __kernel_gid_t, __kernel_pid_t, __kernel_uid_t, membarrier_cmd, membarrier_cmd_flag, rlimit, rlimit64, PRIO_PGRP, PRIO_PROCESS, PRIO_USER, RLIM64_INFINITY, RLIM_INFINITY, }; +use linux_raw_sys::ioctl::TIOCSCTTY; #[cfg(not(target_os = "wasi"))] #[cfg(feature = "fs")] use {super::super::conv::ret_c_uint_infallible, crate::fs::Mode}; @@ -745,3 +746,15 @@ pub(crate) fn sethostname(name: &[u8]) -> io::Result<()> { let (ptr, len) = slice(name); unsafe { ret(syscall_readonly!(__NR_sethostname, ptr, len)) } } + +#[inline] +pub(crate) fn ioctl_tiocsctty(fd: BorrowedFd<'_>) -> io::Result<()> { + unsafe { + ret(syscall_readonly!( + __NR_ioctl, + fd, + c_uint(TIOCSCTTY), + by_ref(&0_u32) + )) + } +} diff --git a/src/backend/linux_raw/pty/mod.rs b/src/backend/linux_raw/pty/mod.rs new file mode 100644 index 000000000..ef944f04d --- /dev/null +++ b/src/backend/linux_raw/pty/mod.rs @@ -0,0 +1 @@ +pub(crate) mod syscalls; diff --git a/src/backend/linux_raw/pty/syscalls.rs b/src/backend/linux_raw/pty/syscalls.rs new file mode 100644 index 000000000..936c97f67 --- /dev/null +++ b/src/backend/linux_raw/pty/syscalls.rs @@ -0,0 +1,60 @@ +//! linux_raw syscalls supporting `rustix::pty`. +//! +//! # Safety +//! +//! See the `rustix::backend` module documentation for details. +#![allow(unsafe_code)] +#![allow(clippy::undocumented_unsafe_blocks)] + +use super::super::c; +use super::super::conv::{by_ref, c_uint, ret, ret_owned_fd}; +use crate::fd::{BorrowedFd, OwnedFd}; +use crate::ffi::CString; +use crate::io; +use crate::path::DecInt; +use crate::pty::OpenptFlags; +#[cfg(any(apple, freebsdlike, linux_like, target_os = "fuchsia"))] +use alloc::vec::Vec; +use core::mem::MaybeUninit; +use linux_raw_sys::ioctl::{TIOCGPTN, TIOCGPTPEER, TIOCSPTLCK}; + +#[cfg(any(apple, freebsdlike, linux_like, target_os = "fuchsia"))] +#[inline] +pub(crate) fn ptsname(fd: BorrowedFd, mut buffer: Vec) -> io::Result { + unsafe { + let mut n = MaybeUninit::::uninit(); + ret(syscall!(__NR_ioctl, fd, c_uint(TIOCGPTN), &mut n))?; + + buffer.clear(); + buffer.extend_from_slice(b"/dev/pts/"); + buffer.extend_from_slice(DecInt::new(n.assume_init()).as_bytes()); + // With Rust 1.58 we can append a '\0' ourselves and use + // `from_vec_with_nul_unchecked`. + Ok(CString::from_vec_unchecked(buffer)) + } +} + +#[inline] +pub(crate) fn unlockpt(fd: BorrowedFd) -> io::Result<()> { + unsafe { + ret(syscall_readonly!( + __NR_ioctl, + fd, + c_uint(TIOCSPTLCK), + by_ref(&0) + )) + } +} + +#[cfg(target_os = "linux")] +#[inline] +pub(crate) fn ioctl_tiocgptpeer(fd: BorrowedFd, flags: OpenptFlags) -> io::Result { + unsafe { + ret_owned_fd(syscall_readonly!( + __NR_ioctl, + fd, + c_uint(TIOCGPTPEER), + c_uint(flags.bits()) + )) + } +} diff --git a/src/lib.rs b/src/lib.rs index f56b5d878..5585c0b98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,11 @@ pub mod path; #[cfg_attr(doc_cfg, doc(cfg(feature = "process")))] pub mod process; #[cfg(not(windows))] +#[cfg(not(target_os = "wasi"))] +#[cfg(feature = "pty")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "pty")))] +pub mod pty; +#[cfg(not(windows))] #[cfg(feature = "rand")] #[cfg_attr(doc_cfg, doc(cfg(feature = "rand")))] pub mod rand; diff --git a/src/process/ioctl.rs b/src/process/ioctl.rs new file mode 100644 index 000000000..46dbbc59b --- /dev/null +++ b/src/process/ioctl.rs @@ -0,0 +1,21 @@ +use crate::{backend, io}; +use backend::fd::AsFd; + +/// `ioctl(fd, TIOCSCTTY, 0)`—Sets the controlling terminal for the processs. +/// +/// # References +/// - [Linux] +/// - [FreeBSD] +/// - [NetBSD] +/// - [OpenBSD] +/// +/// [Linux]: https://man7.org/linux/man-pages/man4/tty_ioctl.4.html +/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=tty&sektion=4 +/// [NetBSD]: https://man.netbsd.org/tty.4 +/// [OpenBSD]: https://man.openbsd.org/tty.4 +#[cfg(not(any(windows, target_os = "haiku", target_os = "redox", target_os = "wasi")))] +#[inline] +#[doc(alias = "TIOCSCTTY")] +pub fn ioctl_tiocsctty(fd: Fd) -> io::Result<()> { + backend::process::syscalls::ioctl_tiocsctty(fd.as_fd()) +} diff --git a/src/process/mod.rs b/src/process/mod.rs index 0002c0f46..67b08ddd5 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -7,6 +7,7 @@ mod chroot; mod exit; #[cfg(not(target_os = "wasi"))] // WASI doesn't have get[gpu]id. mod id; +mod ioctl; #[cfg(not(target_os = "wasi"))] mod kill; #[cfg(any(target_os = "android", target_os = "linux"))] @@ -43,6 +44,7 @@ pub use chroot::*; pub use exit::*; #[cfg(not(target_os = "wasi"))] pub use id::*; +pub use ioctl::*; #[cfg(not(target_os = "wasi"))] pub use kill::*; #[cfg(any(target_os = "android", target_os = "linux"))] diff --git a/src/pty.rs b/src/pty.rs new file mode 100644 index 000000000..d50296267 --- /dev/null +++ b/src/pty.rs @@ -0,0 +1,171 @@ +//! Pseudoterminal operations. +//! +//! For the `openpty` and `login_tty` functions, see the +//! [rustix-openpty crate]. +//! +//! [rustix-openpty crate]: https://crates.io/crates/rustix-openpty + +use crate::backend::c; +use crate::fd::{AsFd, OwnedFd}; +use crate::fs::OFlags; +use crate::{backend, io}; +#[cfg(any(apple, linux_like, target_os = "freebsd", target_os = "fuchsia"))] +use {crate::ffi::CString, alloc::vec::Vec}; + +bitflags::bitflags! { + /// Flags for use with [`openpt`] and [`ioctl_tiocgptpeer`]. + /// + /// [`ioctl_tiocgtpeer`]: https://docs.rs/rustix/*/x86_64-unknown-linux-gnu/rustix/pty/fn.ioctl_tiocgtpeer.html + pub struct OpenptFlags: u32 { + /// `O_RDWR` + const RDWR = c::O_RDWR as c::c_uint; + + /// `O_NOCTTY` + #[cfg(not(target_os = "redox"))] + const NOCTTY = c::O_NOCTTY as c::c_uint; + + /// `O_CLOEXEC` + /// + /// The standard `posix_openpt` function doesn't support `CLOEXEC`, but + /// rustix supports it on Linux, and FreeBSD and NetBSD support it. + #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] + const CLOEXEC = c::O_CLOEXEC as c::c_uint; + } +} + +impl From for OFlags { + #[inline] + fn from(flags: OpenptFlags) -> Self { + // SAFETY: `OpenptFlags` is a subset of `OFlags`. + #[allow(unsafe_code)] + unsafe { + Self::from_bits_unchecked(flags.bits() as _) + } + } +} + +/// `posix_openpt(flags)`—Open a pseudoterminal device. +/// +/// On Linux, an additional `CLOEXEC` flag value may be passed to request the +/// close-on-exec flag be set. +/// +/// On Linux, if the system has no free pseudoterminals available, the +/// underlying system call fails with [`io::Errno::NOSPC`], however this rustix +/// function translates that to [`io::Errno::AGAIN`], so that the linux_raw and +/// libc backends have the same behavior. +/// +/// # References +/// - [POSIX] +/// - [Linux] +/// - [Apple] +/// - [FreeBSD] +/// - [DragonFly BSD] +/// - [NetBSD] +/// - [OpenBSD] +/// - [illumos] +/// +/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/posix_openpt.html +/// [Linux]: https://man7.org/linux/man-pages/man3/posix_openpt.3.html +/// [Apple]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/posix_openpt.3.html +/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=posix_openpt&sektion=2 +/// [DragonFly BSD]: https://man.dragonflybsd.org/?command=posix_openpt§ion=3 +/// [NetBSD]: https://man.netbsd.org/posix_openpt.3 +/// [OpenBSD]: http://man.openbsd.org/posix_openpt +/// [illumos]: https://illumos.org/man/3C/posix_openpt +#[inline] +#[doc(alias = "posix_openpt")] +pub fn openpt(flags: OpenptFlags) -> io::Result { + // On Linux, open the device ourselves so that we can support `CLOEXEC`. + #[cfg(any(target_os = "android", target_os = "linux"))] + { + use crate::fs::{cwd, openat, Mode}; + match openat(cwd(), cstr!("/dev/ptmx"), flags.into(), Mode::empty()) { + // Match libc `openat` behavior with `ENOSPC`. + Err(io::Errno::NOSPC) => Err(io::Errno::AGAIN), + otherwise => otherwise, + } + } + + // On all other platforms, use `openpt`. + #[cfg(not(any(target_os = "android", target_os = "linux")))] + { + backend::pty::syscalls::openpt(flags) + } +} + +/// `ptsname(fd)`—Return the name of a pseudoterminal. +/// +/// # References +/// - [POSIX] +/// - [Linux] +/// - [glibc] +/// +/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/ptsname.html +/// [Linux]: https://man7.org/linux/man-pages/man3/ptsname.3.html +/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Allocation.html#index-ptsname +#[inline] +#[doc(alias = "ptsname_r")] +#[cfg(any(apple, linux_like, target_os = "freebsd", target_os = "fuchsia"))] +pub fn ptsname>>(fd: Fd, reuse: B) -> io::Result { + backend::pty::syscalls::ptsname(fd.as_fd(), reuse.into()) +} + +/// `unlockpt(fd)`—Unlock a pseudoterminal. +/// +/// # References +/// - [POSIX] +/// - [Linux] +/// - [glibc] +/// +/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/unlockpt.html +/// [Linux]: https://man7.org/linux/man-pages/man3/unlockpt.3.html +/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Allocation.html#index-unlockpt +#[inline] +pub fn unlockpt(fd: Fd) -> io::Result<()> { + backend::pty::syscalls::unlockpt(fd.as_fd()) +} + +/// `grantpt(fd)`—Grant access to the user side of a pseudoterminal. +/// +/// On Linux, calling this function has no effect, as the kernel is expected to +/// grant the appropriate access. On all other platorms, this function has +/// unspecified behavior if the calling process has a `SIGCHLD` signal handler +/// installed. +/// +/// # References +/// - [POSIX] +/// - [Linux] +/// - [glibc] +/// +/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/grantpt.html +/// [Linux]: https://man7.org/linux/man-pages/man3/grantpt.3.html +/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Allocation.html#index-grantpt +#[inline] +pub fn grantpt(fd: Fd) -> io::Result<()> { + #[cfg(not(any(target_os = "android", target_os = "linux")))] + { + backend::pty::syscalls::grantpt(fd.as_fd()) + } + + // On Linux, we assume the kernel has already granted the needed + // permissions to the user side of the pseudoterminal. + #[cfg(any(target_os = "android", target_os = "linux"))] + { + let _ = fd; + Ok(()) + } +} + +/// `ioctl(fd, TIOCGPTPEER)`—Open the user side of a pseduoterminal. +/// +/// This function is currently only implemented on Linux. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man2/ioctl_tty.2.html +#[cfg(target_os = "linux")] +#[inline] +pub fn ioctl_tiocgptpeer(fd: Fd, flags: OpenptFlags) -> io::Result { + backend::pty::syscalls::ioctl_tiocgptpeer(fd.as_fd(), flags) +} diff --git a/tests/pty/main.rs b/tests/pty/main.rs new file mode 100644 index 000000000..260a6c15b --- /dev/null +++ b/tests/pty/main.rs @@ -0,0 +1,7 @@ +//! Tests for [`rustix::pty`]. + +#![cfg_attr(io_lifetimes_use_std, feature(io_safety))] +#![cfg(feature = "pty")] + +#[cfg(any(apple, linux_like, target_os = "freebsd", target_os = "fuchsia"))] +mod openpty; diff --git a/tests/pty/openpty.rs b/tests/pty/openpty.rs new file mode 100644 index 000000000..194eec1f1 --- /dev/null +++ b/tests/pty/openpty.rs @@ -0,0 +1,92 @@ +use rustix::fs::{cwd, openat, Mode, OFlags}; +use rustix::pty::*; +use std::fs::File; +use std::io; +use std::io::{Read, Write}; + +#[test] +fn openpty_basic() -> io::Result<()> { + // Use `CLOEXEC` if we can. + #[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd" + ))] + let flags = OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC; + #[cfg(not(any( + target_os = "android", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd" + )))] + let flags = OpenptFlags::RDWR | OpenptFlags::NOCTTY; + + let controller = openpt(flags)?; + + grantpt(&controller)?; + unlockpt(&controller)?; + + let name = ptsname(&controller, Vec::new())?; + let user = openat( + cwd(), + name, + OFlags::RDWR | OFlags::NOCTTY | OFlags::CLOEXEC, + Mode::empty(), + )?; + + let mut controller = File::from(controller); + let mut user = File::from(user); + + // The '\x04' is Ctrl-D, the default EOF control code. + controller.write_all(b"Hello, world!\n\x04")?; + + let mut s = String::new(); + user.read_to_string(&mut s)?; + + assert_eq!(s, "Hello, world!\n"); + Ok(()) +} + +// Like `openpty_basic` but use `ioctl_tiocgptpeer` instead of `ptsname`. +#[cfg(target_os = "linux")] +#[test] +fn openpty_get_peer() -> io::Result<()> { + // Use `CLOEXEC` if we can. + #[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd" + ))] + let flags = OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC; + #[cfg(not(any( + target_os = "android", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd" + )))] + let flags = OpenptFlags::RDWR | OpenptFlags::NOCTTY; + + let controller = openpt(flags)?; + + grantpt(&controller)?; + unlockpt(&controller)?; + + let user = ioctl_tiocgptpeer( + &controller, + OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC, + )?; + + let mut controller = File::from(controller); + let mut user = File::from(user); + + // The '\x04' is Ctrl-D, the default EOF control code. + controller.write_all(b"Hello, world!\n\x04")?; + + let mut s = String::new(); + user.read_to_string(&mut s)?; + + assert_eq!(s, "Hello, world!\n"); + Ok(()) +}