Try use pty

This commit is contained in:
Wildan M 2025-10-26 22:06:59 +07:00
parent 66d7a520e1
commit a943426bde
9 changed files with 546 additions and 70 deletions

99
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
fn handle_fetch(
recipe: &CookRecipe,
config: &CliConfig,
logger: &Stdout,
logger: &PtyOut,
) -> anyhow::Result<PathBuf> {
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<R>(
mut reader: R,
package_name: PackageName,
status_tx: mpsc::Sender<StatusUpdate>,
) {
) 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<StatusUpdate>,
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)]

View File

@ -3,4 +3,5 @@ pub mod cook_build;
pub mod fetch;
pub mod fs;
pub mod package;
pub mod pty;
pub mod script;

View File

@ -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<PackageName> {
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<PackageName>), 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<BTreeSet<PackageName>, 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<PackageName>), String> {
// download straight from remote source then declare pkg dependencies as autodeps dependency
let stage_dir = target_dir.join("stage");

View File

@ -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<String,
pub fn fetch_offline(
recipe_dir: &Path,
recipe: &Recipe,
logger: &Stdout,
logger: &PtyOut,
) -> Result<PathBuf, String> {
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<PathBuf, String> {
pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result<PathBuf, String> {
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<String>,
script: &Option<String>,
source_dir_tmp: &PathBuf,
logger: &Stdout,
logger: &PtyOut,
) -> Result<(), String> {
for patch_name in patches {
let patch_file = recipe_dir.join(patch_name);

View File

@ -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::<PipeWriter>(
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");

View File

@ -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<PackageName>,
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(),

403
src/cook/pty.rs Normal file
View File

@ -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<dyn Read + Send>,
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<Child, Error> {
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<PtyPair> {
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<usize, io::Error> {
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<PathBuf> {
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<PtySize, Error> {
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<std::process::Child> {
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<bool>,
tty_name: Option<PathBuf>,
}
/// 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<std::process::Child, Error> {
Ok(self.fd.spawn_command(builder)?)
}
}
impl UnixMasterPty {
fn resize(&self, size: PtySize) -> Result<(), Error> {
self.fd.resize(size)
}
fn get_size(&self) -> Result<PtySize, Error> {
self.fd.get_size()
}
fn try_clone_reader(&self) -> Result<Box<dyn Read + Send>, Error> {
let fd = PtyFd(self.fd.try_clone()?);
Ok(Box::new(fd))
}
fn take_writer(&self) -> Result<Box<dyn Write + Send>, 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<RawFd> {
Some(self.fd.0.as_raw_fd())
}
fn tty_name(&self) -> Option<PathBuf> {
self.tty_name.clone()
}
fn process_group_leader(&self) -> Option<libc::pid_t> {
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<usize, io::Error> {
self.fd.write(buf)
}
fn flush(&mut self) -> Result<(), io::Error> {
self.fd.flush()
}
}