diff --git a/Cargo.lock b/Cargo.lock index 786c1b23..dfa0e009 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,24 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -29,6 +47,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -41,12 +68,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", +] + [[package]] name = "comma" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "env_home" version = "0.1.0" @@ -72,6 +115,12 @@ dependencies = [ "instant", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -98,6 +147,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "libc" version = "0.2.172" @@ -122,6 +180,12 @@ version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "nix" version = "0.30.0" @@ -134,6 +198,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -194,6 +268,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" name = "rexpect" version = "0.6.2" dependencies = [ + "bindgen", "comma", "nix", "regex", @@ -202,6 +277,12 @@ dependencies = [ "which", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.37.27" @@ -229,6 +310,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "syn" version = "2.0.90" diff --git a/Cargo.toml b/Cargo.toml index d4c8d7af..fbf6d882 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ license.workspace = true edition.workspace = true rust-version.workspace = true include.workspace = true +build = "build/main.rs" [package.metadata.docs.rs] all-features = true @@ -123,5 +124,8 @@ tempfile = "3" thiserror = "2.0.0" which = { version = "8.0", optional = true } +[target.'cfg(target_os = "openbsd")'.build-dependencies] +bindgen = { version = "0.72.1", default-features = false } + [lints] workspace = true diff --git a/build/main.rs b/build/main.rs new file mode 100644 index 00000000..c024ad53 --- /dev/null +++ b/build/main.rs @@ -0,0 +1,23 @@ +fn main() { + #[cfg(target_os = "openbsd")] generate_openbsd_bindings(); +} + +#[cfg(target_os = "openbsd")] +fn generate_openbsd_bindings() { + use std::path::PathBuf; + use std::env; + + let bindings = bindgen::Builder::default() + .clang_macro_fallback() + .header("build/wrapper.h") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .unwrap_or_else(|e| + panic!("Unable to generate bindings: {e}") + ); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("openbsd_bindings.rs")) + .expect("couldn't write bindings"); +} diff --git a/build/wrapper.h b/build/wrapper.h new file mode 100644 index 00000000..88baa9fe --- /dev/null +++ b/build/wrapper.h @@ -0,0 +1 @@ +#include diff --git a/src/lib.rs b/src/lib.rs index 52c2dcca..7790929a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,7 @@ pub mod error; pub mod process; pub mod reader; pub mod session; +#[cfg(target_os = "openbsd")] pub mod openbsd; pub use reader::ReadUntil; pub use session::{spawn, spawn_bash, spawn_python, spawn_stream, spawn_with_options}; diff --git a/src/openbsd.rs b/src/openbsd.rs new file mode 100644 index 00000000..67b75d0c --- /dev/null +++ b/src/openbsd.rs @@ -0,0 +1,3 @@ +#![allow(warnings)] + +include!(concat!(env!("OUT_DIR"), "/openbsd_bindings.rs")); diff --git a/src/process.rs b/src/process.rs index 0dbf7e17..a8384f66 100644 --- a/src/process.rs +++ b/src/process.rs @@ -3,8 +3,8 @@ use crate::error::Error; use nix; use nix::fcntl::{open, OFlag}; -use nix::libc::STDERR_FILENO; -use nix::pty::{grantpt, posix_openpt, unlockpt, PtyMaster}; +use nix::libc::{ioctl, STDERR_FILENO, TIOCSCTTY}; +use nix::pty::{grantpt, unlockpt, PtyMaster}; pub use nix::sys::{signal, wait}; use nix::sys::{stat, termios}; use nix::unistd::{ @@ -17,6 +17,8 @@ use std::os::unix::io::AsRawFd; use std::os::unix::process::CommandExt; use std::process::Command; use std::{thread, time}; +use std::os::fd::OwnedFd; +use std::path::PathBuf; /// Start a process in a forked tty so you can interact with it the same as you would /// within a terminal @@ -85,18 +87,94 @@ fn ptsname_r(fd: &PtyMaster) -> nix::Result { } } +#[cfg(not(target_os = "openbsd"))] +fn open_pty() -> nix::Result { + // Open a new PTY master + let master_fd = nix::pty::posix_openpt(OFlag::O_RDWR)?; + + // Allow a slave to be generated for it + grantpt(&master_fd)?; + unlockpt(&master_fd)?; + + // on Linux this is the libc function, on OSX this is our implementation of ptsname_r + let slave_name = PathBuf::from(ptsname_r(&master_fd)?); + + Ok( + OpenPty { + master_fd, + slave_fd: None, + slave_name, + } + ) +} + +#[cfg(target_os = "openbsd")] +/// Open pty on OpenBSD +/// +/// OpenBSD does not have either ptsname_r or the Linux's ioctls. +/// pty(4) references and documents this method of pty creation +fn open_pty() -> nix::Result { + use crate::openbsd::{PATH_PTMDEV, PTMGET}; + use crate::openbsd::ptmget; + use nix::libc::{c_char, ioctl, open, O_RDWR}; + use std::ffi::CStr; + use std::os::fd::{FromRawFd, OwnedFd}; + + // ioctls on /dev/ptm is the underlying mechanism for pty management + // on OpenBSD + let fd = unsafe { + match open(PATH_PTMDEV.as_ptr() as *const c_char, O_RDWR) { + -1 => return Err(nix::Error::last()), + fd => OwnedFd::from_raw_fd(fd), + } + }; + + // here, we get the fds and names for the master and slave devices + // right away + let mut info = std::mem::MaybeUninit::::uninit(); + let info = unsafe { + match ioctl(fd.as_raw_fd(), PTMGET.into(), info.as_mut_ptr()) { + -1 => return Err(nix::Error::last()), + _ => info.assume_init(), + } + }; + + let master_fd = unsafe { + PtyMaster::from_owned_fd( + OwnedFd::from_raw_fd(info.cfd) + ) + }; + let slave_fd = Some( + unsafe { OwnedFd::from_raw_fd(info.sfd) } + ); + + // on OpenBSD these are no-ops (only checking for the argument fd + // to be a pty master), but they may become required some day + grantpt(&master_fd)?; + unlockpt(&master_fd)?; + + Ok( + OpenPty { + master_fd, + slave_fd, + slave_name: PathBuf::from( + unsafe { CStr::from_ptr(info.sn.as_ptr()) } + .to_string_lossy().into_owned() + ) + } + ) +} + +struct OpenPty { + master_fd: PtyMaster, + slave_fd: Option, + slave_name: PathBuf, +} + impl PtyProcess { /// Start a process in a forked pty pub fn new(mut command: Command) -> Result { - // Open a new PTY master - let master_fd = posix_openpt(OFlag::O_RDWR)?; - - // Allow a slave to be generated for it - grantpt(&master_fd)?; - unlockpt(&master_fd)?; - - // on Linux this is the libc function, on OSX this is our implementation of ptsname_r - let slave_name = ptsname_r(&master_fd)?; + let OpenPty { master_fd, slave_fd, slave_name } = open_pty()?; match unsafe { fork()? } { ForkResult::Child => { @@ -104,17 +182,28 @@ impl PtyProcess { close(master_fd.as_raw_fd())?; setsid()?; // create new session with child as session leader - let slave_fd = open( - std::path::Path::new(&slave_name), - OFlag::O_RDWR, - stat::Mode::empty(), - )?; + let slave_fd = slave_fd + .ok_or(()).or_else(|_| + open( + std::path::Path::new(&slave_name), + OFlag::O_RDWR, + stat::Mode::empty(), + ) + )?; // assign stdin, stdout, stderr to the tty, just like a terminal does dup2_stdin(&slave_fd)?; dup2_stdout(&slave_fd)?; dup2_stderr(&slave_fd)?; + // While Linux and macOS are lenient with the requirements + // for receiving signals through the pty, OpenBSD does check + // the process's controlling terminal and this is necessary. + #[cfg(target_os = "openbsd")] + if unsafe { ioctl(slave_fd.as_raw_fd(), TIOCSCTTY.into()) } == -1 { + return Err(nix::Error::last().into()) + } + // Avoid leaking slave fd if slave_fd.as_raw_fd() > STDERR_FILENO { close(slave_fd)?;