#![allow(unused)]
use color_eyre::eyre::{bail, Error};
use filedescriptor::FileDescriptor;
use nix::libc::{self, pid_t, winsize};
use nix::unistd::Pid;
use std::cell::RefCell;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use std::{io, mem, ptr};
use std::io::Result as IoResult;
use crate::cmdbuilder::CommandBuilder;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PtySize {
pub rows: u16,
pub cols: u16,
pub pixel_width: u16,
pub pixel_height: u16,
}
impl Default for PtySize {
fn default() -> Self {
Self {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
}
}
}
pub trait MasterPty: Send {
fn resize(&self, size: PtySize) -> Result<(), Error>;
fn get_size(&self) -> Result<PtySize, Error>;
fn try_clone_reader(&self) -> Result<Box<dyn std::io::Read + Send>, Error>;
fn take_writer(&self) -> Result<Box<dyn std::io::Write + Send>, Error>;
#[cfg(unix)]
fn process_group_leader(&self) -> Option<libc::pid_t>;
#[cfg(unix)]
fn as_raw_fd(&self) -> Option<RawFd>;
#[cfg(unix)]
fn tty_name(&self) -> Option<std::path::PathBuf>;
#[cfg(unix)]
fn get_termios(&self) -> Option<nix::sys::termios::Termios> {
None
}
}
pub trait Child: std::fmt::Debug + ChildKiller + Send {
fn try_wait(&mut self) -> IoResult<Option<ExitStatus>>;
fn wait(&mut self) -> IoResult<ExitStatus>;
fn process_id(&self) -> Pid;
}
pub trait ChildKiller: std::fmt::Debug + Send {
fn kill(&mut self) -> IoResult<()>;
fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync>;
}
#[derive(Debug, Clone)]
pub struct ExitStatus {
code: u32,
signal: Option<String>,
}
impl ExitStatus {
pub fn with_exit_code(code: u32) -> Self {
Self { code, signal: None }
}
pub fn with_signal(signal: &str) -> Self {
Self {
code: 1,
signal: Some(signal.to_string()),
}
}
pub fn success(&self) -> bool {
match self.signal {
None => self.code == 0,
Some(_) => false,
}
}
pub fn exit_code(&self) -> u32 {
self.code
}
pub fn signal(&self) -> Option<&str> {
self.signal.as_deref()
}
}
impl From<std::process::ExitStatus> for ExitStatus {
fn from(status: std::process::ExitStatus) -> Self {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = status.signal() {
let signame = unsafe { libc::strsignal(signal) };
let signal = if signame.is_null() {
format!("Signal {}", signal)
} else {
let signame = unsafe { std::ffi::CStr::from_ptr(signame) };
signame.to_string_lossy().to_string()
};
return Self {
code: status.code().map(|c| c as u32).unwrap_or(1),
signal: Some(signal),
};
}
}
let code = status
.code()
.map(|c| c as u32)
.unwrap_or_else(|| if status.success() { 0 } else { 1 });
Self { code, signal: None }
}
}
impl std::fmt::Display for ExitStatus {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.success() {
write!(fmt, "Success")
} else {
match &self.signal {
Some(sig) => write!(fmt, "Terminated by {}", sig),
None => write!(fmt, "Exited with code {}", self.code),
}
}
}
}
pub struct PtyPair {
pub slave: UnixSlavePty,
pub master: UnixMasterPty,
}
pub trait PtySystem {
fn openpty(&self, size: PtySize) -> color_eyre::Result<PtyPair>;
}
impl Child for std::process::Child {
fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> {
Self::try_wait(self).map(|s| s.map(Into::into))
}
fn wait(&mut self) -> IoResult<ExitStatus> {
Self::wait(self).map(Into::into)
}
fn process_id(&self) -> Pid {
Pid::from_raw(self.id() as pid_t)
}
}
#[derive(Debug)]
struct ProcessSignaller {
pid: Option<Pid>,
}
impl ChildKiller for ProcessSignaller {
fn kill(&mut self) -> IoResult<()> {
if let Some(pid) = self.pid {
let result = unsafe { libc::kill(pid.as_raw(), libc::SIGHUP) };
if result != 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
}
fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
Box::new(Self { pid: self.pid })
}
}
impl ChildKiller for std::process::Child {
fn kill(&mut self) -> IoResult<()> {
#[cfg(unix)]
{
let result = unsafe { libc::kill(self.id() as i32, libc::SIGHUP) };
if result != 0 {
return Err(std::io::Error::last_os_error());
}
for attempt in 0..5 {
if attempt > 0 {
std::thread::sleep(std::time::Duration::from_millis(50));
}
if let Ok(Some(_)) = self.try_wait() {
return Ok(());
}
}
}
Self::kill(self)
}
fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
Box::new(ProcessSignaller {
pid: Some(self.process_id()),
})
}
}
pub fn native_pty_system() -> NativePtySystem {
NativePtySystem::default()
}
pub type NativePtySystem = UnixPtySystem;
#[derive(Default)]
pub struct UnixPtySystem {}
fn openpty(size: PtySize) -> color_eyre::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 {
libc::openpty(
&mut master,
&mut slave,
ptr::null_mut(),
ptr::null_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) }),
};
cloexec(master.fd.as_raw_fd())?;
cloexec(slave.fd.as_raw_fd())?;
Ok((master, slave))
}
impl PtySystem for UnixPtySystem {
fn openpty(&self, size: PtySize) -> color_eyre::Result<PtyPair> {
let (master, slave) = openpty(size)?;
Ok(PtyPair { master, slave })
}
}
pub 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) => {
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 {
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
};
}
}
pub fn close_random_fds() {
if let Ok(dir) = std::fs::read_dir("/proc/self/fd").or_else(|_| std::fs::read_dir("/dev/fd")) {
let mut fds = vec![];
for entry in dir {
if let Some(num) = entry
.ok()
.map(|e| e.file_name())
.and_then(|s| s.into_string().ok())
.and_then(|n| n.parse::<libc::c_int>().ok())
{
if num > 2 {
fds.push(num);
}
}
}
for fd in fds {
let _ = nix::unistd::close(fd);
}
}
}
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,
command: CommandBuilder,
command_hook: impl FnOnce(&mut Command) -> color_eyre::Result<()> + Send + Sync + 'static,
pre_exec: impl Fn(&OsStr) -> color_eyre::Result<()> + Send + Sync + 'static,
) -> color_eyre::Result<std::process::Child> {
spawn_command_from_pty_fd(Some(self), command, command_hook, pre_exec)
}
}
pub fn spawn_command(
pts: Option<&UnixSlavePty>,
command: CommandBuilder,
command_hook: impl FnOnce(&mut Command) -> color_eyre::Result<()> + Send + Sync + 'static,
pre_exec: impl Fn(&OsStr) -> color_eyre::Result<()> + Send + Sync + 'static,
) -> color_eyre::Result<std::process::Child> {
if let Some(pts) = pts {
pts.spawn_command(command, command_hook, pre_exec)
} else {
spawn_command_from_pty_fd(None, command, command_hook, pre_exec)
}
}
fn spawn_command_from_pty_fd(
pty: Option<&PtyFd>,
command: CommandBuilder,
command_hook: impl FnOnce(&mut Command) -> color_eyre::Result<()> + Send + Sync + 'static,
pre_exec: impl Fn(&OsStr) -> color_eyre::Result<()> + Send + Sync + 'static,
) -> color_eyre::Result<std::process::Child> {
let configured_umask = command.umask;
let mut cmd = command.as_command()?;
if let Some(pty) = pty {
cmd
.stdin(pty.as_stdio()?)
.stdout(pty.as_stdio()?)
.stderr(pty.as_stdio()?);
}
command_hook(&mut cmd)?;
unsafe {
let program_path = cmd.get_program().to_owned();
cmd.pre_exec(move || {
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());
close_random_fds();
if let Some(mask) = configured_umask {
libc::umask(mask);
}
pre_exec(program_path.as_os_str()).unwrap();
Ok(())
})
};
let mut child = cmd.spawn()?;
child.stdin.take();
child.stdout.take();
child.stderr.take();
Ok(child)
}
pub struct UnixMasterPty {
fd: PtyFd,
took_writer: RefCell<bool>,
tty_name: Option<PathBuf>,
}
pub struct UnixSlavePty {
pub fd: PtyFd,
}
impl UnixSlavePty {
pub fn spawn_command(
&self,
command: CommandBuilder,
command_hook: impl FnOnce(&mut Command) -> color_eyre::Result<()> + Send + Sync + 'static,
pre_exec: impl Fn(&OsStr) -> color_eyre::Result<()> + Send + Sync + 'static,
) -> color_eyre::Result<std::process::Child> {
self.fd.spawn_command(command, command_hook, pre_exec)
}
}
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 MasterPty for 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() {
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,
}
}
fn get_termios(&self) -> Option<nix::sys::termios::Termios> {
nix::sys::termios::tcgetattr(unsafe { File::from_raw_fd(self.fd.0.as_raw_fd()) }).ok()
}
}
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 {
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()
}
}