diff --git a/Cargo.lock b/Cargo.lock index 58737f927..907d9d9e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -753,6 +759,12 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dryoc" version = "0.6.2" @@ -854,6 +866,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "flate2" version = "1.1.2" @@ -1537,6 +1560,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.1", + "cfg_aliases 0.1.1", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1748,6 +1783,27 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -1797,7 +1853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -1837,7 +1893,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2", @@ -2007,12 +2063,15 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3 1.5.3", + "filedescriptor", "ignore", + "libc", "object", "pbr", "pkgar 0.1.19", "pkgar-core 0.1.19", "pkgar-keys 0.1.19", + "portable-pty", "ratatui", "redox-pkg", "redoxer", @@ -2434,6 +2493,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serial2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" +dependencies = [ + "cfg-if 1.0.1", + "libc", + "winapi", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2445,6 +2515,22 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -3445,6 +3531,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 8b28c1c1e..ae5e18c94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,14 @@ doctest = false [dependencies] anyhow = "1" blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar +libc = "0.2" ignore = "0.4" object = { version = "0.36", features = ["build_core"] } pbr = "1.0.2" pkgar = { path = "pkgar/pkgar" } pkgar-core = { path = "pkgar/pkgar-core" } pkgar-keys = { path = "pkgar/pkgar-keys" } +portable-pty = "0.9.0" redox-pkg = "0.2.8" redoxer = "0.2" regex = "1.11" @@ -36,6 +38,7 @@ serde = { version = "=1.0.197", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" +filedescriptor = "0.8.3" [dependencies.ratatui] version = "0.29.0" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 9e4f2865a..6e643f4aa 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,5 +1,5 @@ use std::collections::{HashMap, VecDeque}; -use std::io::{BufRead, BufReader, PipeReader, Write, stderr, stdin, stdout}; +use std::io::{BufRead, BufReader, Read, Write, stderr, stdin, stdout}; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; @@ -14,8 +14,9 @@ use cookbook::WALK_DEPTH; use cookbook::config::{CookConfig, get_config, init_config}; use cookbook::cook::cook_build::build; use cookbook::cook::fetch::{fetch, fetch_offline}; -use cookbook::cook::fs::{Stdout, create_target_dir}; +use cookbook::cook::fs::create_target_dir; use cookbook::cook::package::package; +use cookbook::cook::pty::{setup_pty, PtyOut, UnixSlavePty}; use cookbook::recipe::CookRecipe; use pkg::PackageName; use pkg::package::PackageError; @@ -355,7 +356,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec anyhow::Result { let recipe_dir = &recipe.dir; let source_dir = match config.cook.offline { @@ -372,7 +373,7 @@ fn handle_cook( config: &CliConfig, source_dir: PathBuf, is_deps: bool, - logger: &Stdout, + logger: &PtyOut, ) -> anyhow::Result<()> { let recipe_dir = &recipe.dir; let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; @@ -1212,13 +1213,15 @@ fn draw_prompt(f: &mut ratatui::Frame, prompt: &FailurePrompt) { f.render_widget(paragraph, popup_area); } -fn spawn_log_reader( - mut pipe_reader: PipeReader, +fn spawn_log_reader( + mut reader: R, package_name: PackageName, status_tx: mpsc::Sender, -) { +) where + R: Read + Send + 'static, +{ thread::spawn(move || { - let reader = BufReader::new(&mut pipe_reader); + let reader = BufReader::new(&mut reader); for line in reader.lines() { let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); if status_tx @@ -1235,12 +1238,12 @@ fn spawn_log_reader( fn setup_logger( status_tx: &mpsc::Sender, name: &PackageName, -) -> (std::io::PipeWriter, std::io::PipeWriter) { - let (stdout_reader, stdout_writer) = std::io::pipe().expect("Failed to create stdout pipe"); - let (stderr_reader, stderr_writer) = std::io::pipe().expect("Failed to create stderr pipe"); - spawn_log_reader(stdout_reader, name.clone(), status_tx.clone()); - spawn_log_reader(stderr_reader, name.clone(), status_tx.clone()); - (stdout_writer, stderr_writer) +) -> (UnixSlavePty, std::io::PipeWriter) { + let (pty_reader, log_reader, pipes) = setup_pty(); + + spawn_log_reader(pty_reader, name.clone(), status_tx.clone()); + spawn_log_reader(log_reader, name.clone(), status_tx.clone()); + pipes } #[derive(PartialEq, Clone, Copy)] diff --git a/src/cook.rs b/src/cook.rs index 4ffa8a7bd..14c752f88 100644 --- a/src/cook.rs +++ b/src/cook.rs @@ -3,4 +3,5 @@ pub mod cook_build; pub mod fetch; pub mod fs; pub mod package; +pub mod pty; pub mod script; diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index a9c921644..e379fce62 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -3,6 +3,7 @@ use pkg::{Package, PackageName}; use redoxer::target; use crate::cook::fs::*; +use crate::cook::pty::PtyOut; use crate::cook::script::*; use crate::recipe::AutoDeps; use crate::recipe::BuildKind; @@ -39,7 +40,7 @@ macro_rules! log_warn { fn auto_deps( stage_dir: &Path, dep_pkgars: &BTreeSet<(PackageName, PathBuf)>, - logger: &Stdout, + logger: &PtyOut, ) -> BTreeSet { let mut paths = BTreeSet::new(); let mut visited = BTreeSet::new(); @@ -167,7 +168,7 @@ pub fn build( recipe: &Recipe, offline_mode: bool, check_source: bool, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(PathBuf, BTreeSet), String> { let sysroot_dir = target_dir.join("sysroot"); let stage_dir = target_dir.join("stage"); @@ -379,7 +380,7 @@ fn build_auto_deps( target_dir: &Path, stage_dir: &PathBuf, dep_pkgars: BTreeSet<(PackageName, PathBuf)>, - logger: &Stdout, + logger: &PtyOut, ) -> Result, String> { let auto_deps_path = target_dir.join("auto_deps.toml"); if auto_deps_path.is_file() && modified(&auto_deps_path)? < modified(stage_dir)? { @@ -412,7 +413,7 @@ pub fn build_remote( target_dir: &Path, name: &PackageName, offline_mode: bool, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(PathBuf, BTreeSet), String> { // download straight from remote source then declare pkg dependencies as autodeps dependency let stage_dir = target_dir.join("stage"); diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index 6295cae74..7f569fb57 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -1,5 +1,6 @@ use crate::config::translate_mirror; use crate::cook::fs::*; +use crate::cook::pty::PtyOut; use crate::cook::script::*; use crate::is_redox; use crate::recipe::BuildKind; @@ -43,7 +44,7 @@ pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { @@ -103,7 +104,7 @@ pub fn fetch_offline( Ok(source_dir) } -pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result { +pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { // the build function doesn't need source dir exists @@ -374,7 +375,7 @@ pub(crate) fn fetch_resolve_canon( pub(crate) fn fetch_extract_tar( source_tar: PathBuf, source_dir_tmp: &PathBuf, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(), String> { let mut command = Command::new("tar"); if is_redox() { @@ -420,7 +421,7 @@ pub(crate) fn fetch_apply_patches( patches: &Vec, script: &Option, source_dir_tmp: &PathBuf, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(), String> { for patch_name in patches { let patch_file = recipe_dir.join(patch_name); diff --git a/src/cook/fs.rs b/src/cook/fs.rs index 6a5e8b883..db1a0fa2c 100644 --- a/src/cook/fs.rs +++ b/src/cook/fs.rs @@ -8,7 +8,10 @@ use std::{ }; use walkdir::{DirEntry, WalkDir}; -use crate::config::translate_mirror; +use crate::{ + config::translate_mirror, + cook::pty::{PtyOut, spawn_to_pipe}, +}; //TODO: pub(crate) for all of these functions @@ -146,27 +149,10 @@ pub fn rename(src: &Path, dst: &Path) -> Result<(), String> { }) } -pub type Stdout<'a> = Option<(&'a mut PipeWriter, &'a mut PipeWriter)>; - -fn pipe_to_cmd(command: &mut Command, stdout_pipe: &Stdout) -> Result<(), String> { - Ok(if let Some((stdout, stderr)) = stdout_pipe { - command.stdout::( - stdout - .try_clone() - .map_err(|e| format!("unable to clone stdout fd: {:?}", e))?, - ); - command.stderr( - stderr - .try_clone() - .map_err(|e| format!("unable to clone stderr fd: {:?}", e))?, - ); - }) -} - -pub fn run_command(mut command: process::Command, stdout_pipe: &Stdout) -> Result<(), String> { - pipe_to_cmd(&mut command, stdout_pipe)?; - let status = command - .status() +pub fn run_command(mut command: process::Command, stdout_pipe: &PtyOut) -> Result<(), String> { + let status = spawn_to_pipe(&mut command, stdout_pipe) + .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))? + .wait() .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))?; if !status.success() { @@ -182,13 +168,10 @@ pub fn run_command(mut command: process::Command, stdout_pipe: &Stdout) -> Resul pub fn run_command_stdin( mut command: process::Command, stdin_data: &[u8], - stdout_pipe: &Stdout, + stdout_pipe: &PtyOut, ) -> Result<(), String> { command.stdin(Stdio::piped()); - pipe_to_cmd(&mut command, stdout_pipe)?; - - let mut child = command - .spawn() + let mut child = spawn_to_pipe(&mut command, stdout_pipe) .map_err(|err| format!("failed to spawn {:?}: {}\n{:#?}", command, err, err))?; if let Some(ref mut stdin) = child.stdin { @@ -240,7 +223,7 @@ pub fn offline_check_exists(path: &PathBuf) -> Result<(), String> { Ok(()) } -pub fn download_wget(url: &str, dest: &PathBuf, logger: &Stdout) -> Result<(), String> { +pub fn download_wget(url: &str, dest: &PathBuf, logger: &PtyOut) -> Result<(), String> { if !dest.is_file() { let dest_tmp = PathBuf::from(format!("{}.tmp", dest.display())); let mut command = Command::new("wget"); diff --git a/src/cook/package.rs b/src/cook/package.rs index 3103c13fa..fa97817f1 100644 --- a/src/cook/package.rs +++ b/src/cook/package.rs @@ -3,32 +3,18 @@ use std::{collections::BTreeSet, env, path::Path}; use pkg::{Package, PackageName}; use crate::{ - cook::fs::*, + cook::{fs::*, pty::PtyOut}, + log_to_pty, recipe::{BuildKind, Recipe}, }; -macro_rules! log_warn { - ($logger:expr, $($arg:tt)+) => { - use std::io::Write; - - if $logger.is_some() { - let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( - format!($($arg)+) - .as_bytes(), - ); - } else { - eprintln!($($arg)+); - } - }; -} - pub fn package( stage_dir: &Path, target_dir: &Path, name: &PackageName, recipe: &Recipe, auto_deps: &BTreeSet, - logger: &Stdout, + logger: &PtyOut, ) -> Result<(), String> { if recipe.build.kind == BuildKind::None { // metapackages don't have stage dir @@ -58,7 +44,7 @@ pub fn package( if package_file.is_file() { let stage_modified = modified_dir(stage_dir)?; if modified(&package_file)? < stage_modified { - log_warn!( + log_to_pty!( logger, "DEBUG: '{}' newer than '{}'", stage_dir.display(), diff --git a/src/cook/pty.rs b/src/cook/pty.rs new file mode 100644 index 000000000..42876e9cf --- /dev/null +++ b/src/cook/pty.rs @@ -0,0 +1,403 @@ +use anyhow::{Error, bail}; +use filedescriptor::FileDescriptor; +use libc::{self, winsize}; +use portable_pty::PtySize; +use std::cell::RefCell; +use std::ffi::OsStr; +use std::io::{Read, Write}; +use std::os::fd::FromRawFd; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::io::AsRawFd; +use std::os::unix::process::CommandExt; +use std::path::PathBuf; +use std::process::Child; +use std::{io, mem, ptr}; +use std::{ + io::{PipeReader, PipeWriter}, + process::Command, +}; + +pub use std::os::unix::io::RawFd; + +#[macro_export] +macro_rules! log_to_pty { + ($logger:expr, $($arg:tt)+) => { + use std::io::Write; + + if $logger.is_some() { + let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( + format!($($arg)+) + .as_bytes(), + ); + } else { + eprintln!($($arg)+); + } + }; +} + +pub type PtyOut<'a> = Option<(&'a mut UnixSlavePty, &'a mut PipeWriter)>; + +pub fn setup_pty() -> ( + Box, + PipeReader, + (UnixSlavePty, std::io::PipeWriter), +) { + let pty_system = UnixPtySystem::default(); + let pair = pty_system + .openpty(PtySize { + rows: 24, // Standard terminal size + cols: 80, // Standard terminal size + ..Default::default() + }) + .expect("Unable to open pty"); + + // TODO: There's no way to handle stdin + let pty_reader = pair + .master + .try_clone_reader() + .expect("Unable to clone pty reader"); + + let (log_reader, log_writer) = std::io::pipe().expect("Failed to create log pipe"); + let pipes = (pair.slave, log_writer); + (pty_reader, log_reader, pipes) +} + +pub fn spawn_to_pipe(command: &mut Command, stdout_pipe: &PtyOut) -> Result { + match stdout_pipe { + Some(stdout) => stdout.0.spawn_command(command.into()), + None => Ok(command.spawn()?), + } +} + +// +// based on portable-pty crate +// copied here since it isn't flexible enough +// + +#[derive(Default)] +pub struct UnixPtySystem {} + +fn openpty(size: PtySize) -> anyhow::Result<(UnixMasterPty, UnixSlavePty)> { + let mut master: RawFd = -1; + let mut slave: RawFd = -1; + + let mut size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + let result = unsafe { + // BSDish systems may require mut pointers to some args + #[allow(clippy::unnecessary_mut_passed)] + libc::openpty( + &mut master, + &mut slave, + ptr::null_mut(), + ptr::null_mut(), + &mut size, + ) + }; + + if result != 0 { + bail!("failed to openpty: {:?}", io::Error::last_os_error()); + } + + let tty_name = tty_name(slave); + + let master = UnixMasterPty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(master) }), + took_writer: RefCell::new(false), + tty_name, + }; + let slave = UnixSlavePty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(slave) }), + }; + + // Ensure that these descriptors will get closed when we execute + // the child process. This is done after constructing the Pty + // instances so that we ensure that the Ptys get drop()'d if + // the cloexec() functions fail (unlikely!). + cloexec(master.fd.as_raw_fd())?; + cloexec(slave.fd.as_raw_fd())?; + + Ok((master, slave)) +} + +pub struct PtyPair { + // slave is listed first so that it is dropped first. + // The drop order is stable and specified by rust rfc 1857 + pub slave: UnixSlavePty, + pub master: UnixMasterPty, +} + +impl UnixPtySystem { + fn openpty(&self, size: PtySize) -> anyhow::Result { + let (master, slave) = openpty(size)?; + Ok(PtyPair { + master: master, + slave: slave, + }) + } +} + +struct PtyFd(pub FileDescriptor); +impl std::ops::Deref for PtyFd { + type Target = FileDescriptor; + fn deref(&self) -> &FileDescriptor { + &self.0 + } +} +impl std::ops::DerefMut for PtyFd { + fn deref_mut(&mut self) -> &mut FileDescriptor { + &mut self.0 + } +} + +impl Read for PtyFd { + fn read(&mut self, buf: &mut [u8]) -> Result { + match self.0.read(buf) { + Err(ref e) if e.raw_os_error() == Some(libc::EIO) => { + // EIO indicates that the slave pty has been closed. + // Treat this as EOF so that std::io::Read::read_to_string + // and similar functions gracefully terminate when they + // encounter this condition + Ok(0) + } + x => x, + } + } +} + +fn tty_name(fd: RawFd) -> Option { + let mut buf = vec![0 as std::ffi::c_char; 128]; + + loop { + let res = unsafe { libc::ttyname_r(fd, buf.as_mut_ptr(), buf.len()) }; + + if res == libc::ERANGE { + if buf.len() > 64 * 1024 { + // on macOS, if the buf is "too big", ttyname_r can + // return ERANGE, even though that is supposed to + // indicate buf is "too small". + return None; + } + buf.resize(buf.len() * 2, 0 as std::ffi::c_char); + continue; + } + + return if res == 0 { + let cstr = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }; + let osstr = OsStr::from_bytes(cstr.to_bytes()); + Some(PathBuf::from(osstr)) + } else { + None + }; + } +} + +impl PtyFd { + fn resize(&self, size: PtySize) -> Result<(), Error> { + let ws_size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCSWINSZ as _, + &ws_size as *const _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCSWINSZ): {:?}", + io::Error::last_os_error() + ); + } + + Ok(()) + } + + fn get_size(&self) -> Result { + let mut size: winsize = unsafe { mem::zeroed() }; + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCGWINSZ as _, + &mut size as *mut _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCGWINSZ): {:?}", + io::Error::last_os_error() + ); + } + Ok(PtySize { + rows: size.ws_row, + cols: size.ws_col, + pixel_width: size.ws_xpixel, + pixel_height: size.ws_ypixel, + }) + } + + fn spawn_command(&self, cmd: &mut Command) -> anyhow::Result { + unsafe { + cmd + // .stdin(self.as_stdio()?) + .stdout(self.as_stdio()?) + .stderr(self.as_stdio()?) + .pre_exec(move || { + // Clean up a few things before we exec the program + // Clear out any potentially problematic signal + // dispositions that we might have inherited + for signo in &[ + libc::SIGCHLD, + libc::SIGHUP, + libc::SIGINT, + libc::SIGQUIT, + libc::SIGTERM, + libc::SIGALRM, + ] { + libc::signal(*signo, libc::SIG_DFL); + } + + let empty_set: libc::sigset_t = std::mem::zeroed(); + libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut()); + + // Establish ourselves as a session leader. + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + + Ok(()) + }) + }; + + let mut child = cmd.spawn()?; + + // Ensure that we close out the slave fds that Child retains; + // they are not what we need (we need the master side to reference + // them) and won't work in the usual way anyway. + // In practice these are None, but it seems best to be move them + // out in case the behavior of Command changes in the future. + child.stdin.take(); + child.stdout.take(); + child.stderr.take(); + + Ok(child) + } +} + +/// Represents the master end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +pub struct UnixMasterPty { + fd: PtyFd, + took_writer: RefCell, + tty_name: Option, +} + +/// Represents the slave end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +pub struct UnixSlavePty { + fd: PtyFd, +} + +/// Helper function to set the close-on-exec flag for a raw descriptor +fn cloexec(fd: RawFd) -> Result<(), Error> { + let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; + if flags == -1 { + bail!( + "fcntl to read flags failed: {:?}", + io::Error::last_os_error() + ); + } + let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) }; + if result == -1 { + bail!( + "fcntl to set CLOEXEC failed: {:?}", + io::Error::last_os_error() + ); + } + Ok(()) +} + +impl UnixSlavePty { + fn spawn_command(&self, builder: &mut Command) -> Result { + Ok(self.fd.spawn_command(builder)?) + } +} + +impl UnixMasterPty { + fn resize(&self, size: PtySize) -> Result<(), Error> { + self.fd.resize(size) + } + + fn get_size(&self) -> Result { + self.fd.get_size() + } + + fn try_clone_reader(&self) -> Result, Error> { + let fd = PtyFd(self.fd.try_clone()?); + Ok(Box::new(fd)) + } + + fn take_writer(&self) -> Result, Error> { + if *self.took_writer.borrow() { + anyhow::bail!("cannot take writer more than once"); + } + *self.took_writer.borrow_mut() = true; + let fd = PtyFd(self.fd.try_clone()?); + Ok(Box::new(UnixMasterWriter { fd })) + } + + fn as_raw_fd(&self) -> Option { + Some(self.fd.0.as_raw_fd()) + } + + fn tty_name(&self) -> Option { + self.tty_name.clone() + } + + fn process_group_leader(&self) -> Option { + match unsafe { libc::tcgetpgrp(self.fd.0.as_raw_fd()) } { + pid if pid > 0 => Some(pid), + _ => None, + } + } +} + +/// Represents the master end of a pty. +/// EOT will be sent, and then the file descriptor will be closed when +/// the Pty is dropped. +struct UnixMasterWriter { + fd: PtyFd, +} + +impl Drop for UnixMasterWriter { + fn drop(&mut self) { + let mut t: libc::termios = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + if unsafe { libc::tcgetattr(self.fd.0.as_raw_fd(), &mut t) } == 0 { + // EOF is only interpreted after a newline, so if it is set, + // we send a newline followed by EOF. + let eot = t.c_cc[libc::VEOF]; + if eot != 0 { + let _ = self.fd.0.write_all(&[b'\n', eot]); + } + } + } +} + +impl Write for UnixMasterWriter { + fn write(&mut self, buf: &[u8]) -> Result { + self.fd.write(buf) + } + fn flush(&mut self) -> Result<(), io::Error> { + self.fd.flush() + } +}