use std::error::Error;
use std::io::ErrorKind;
use std::os::fd::{FromRawFd, RawFd};
use std::os::unix::prelude::CommandExt;
use std::process::{Command, Stdio};
use std::thread;
use nix::errno::errno;
use nix::libc::{self, EBADFD, EINTR, F_GETFD, F_GETFL, F_SETFL, O_NONBLOCK, POLLERR, POLLHUP, POLLIN, POLLNVAL, TIOCSCTTY, winsize};
use nix::poll::{PollFd, PollFlags};
use nix::pty::openpty;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use nix::sys::termios::{self, InputFlags, SetArg};
use nix::unistd;
use crate::error::PtyError;
use crate::unix::shell::ShellUser;
use crate::unix::window::WindowSize;
pub(crate) fn spawn() -> Result<RawFd, Box<dyn Error>> {
let ends = openpty(None, None)?;
let (master, slave) = (ends.master, ends.slave);
#[cfg(any(target_os = "linux", target_os = "macos"))]
if let Ok(mut termios) = termios::tcgetattr(master) {
termios.input_flags.set(InputFlags::IUTF8, true);
let _ = termios::tcsetattr(master, SetArg::TCSANOW, &termios);
}
let user = ShellUser::from_env()?;
let mut builder = Command::new(user.shell);
builder
.stdin (unsafe { Stdio::from_raw_fd(slave) })
.stderr(unsafe { Stdio::from_raw_fd(slave) })
.stdout(unsafe { Stdio::from_raw_fd(slave) })
.env("USER", user.user)
.env("HOME", user.home);
unsafe {
builder.pre_exec(move || {
if libc::setsid() < 0 {
return Err(std::io::Error::new(ErrorKind::Other, "failed to set session id"));
}
#[allow(clippy::cast_lossless)]
if libc::ioctl(slave, TIOCSCTTY as _, 0) < 0 {
return Err(std::io::Error::new(ErrorKind::Other, "ioctl failure on TIOCSCTTY"));
}
libc::close(slave);
libc::close(master);
libc::signal(libc::SIGCHLD, libc::SIG_DFL);
libc::signal(libc::SIGHUP, libc::SIG_DFL);
libc::signal(libc::SIGINT, libc::SIG_DFL);
libc::signal(libc::SIGQUIT, libc::SIG_DFL);
libc::signal(libc::SIGTERM, libc::SIG_DFL);
libc::signal(libc::SIGALRM, libc::SIG_DFL);
Ok(())
});
}
match builder.spawn() {
Ok(_child) => unsafe {
let res = libc::fcntl(master, F_SETFL, libc::fcntl(master, F_GETFL, 0) | O_NONBLOCK);
assert_eq!(res, 0);
Ok(master)
},
Err(err) => Err(Box::new(std::io::Error::new(
err.kind(),
format!(
"failed to spawn command '{}': {}",
builder.get_program().to_string_lossy(),
err
)
)))
}
}
pub(crate) fn poll<F, G>(fd: RawFd, mut on_read: F, mut on_death: G) -> Result<(), Box<dyn Error>>
where
F: FnMut(RawFd, Result<String, Box<dyn Error>>) + Send + 'static,
G: FnMut(RawFd) + Send + 'static {
const ERR_BITS: i16 = POLLERR | POLLHUP | POLLNVAL;
validate_fd(fd)?;
thread::spawn(move || {
let flags = PollFlags::from_bits(POLLIN).unwrap();
let mut fds = [PollFd::new(fd, flags)];
while let Ok(n) = nix::poll::ppoll(&mut fds, None, None) {
if n <= 0 {
if errno() == EINTR { continue } else { break }
}
match fds[0].revents() {
Some(events) => {
if events.bits() & ERR_BITS != 0 { break }
if events.bits() & POLLIN == 0 { continue }
},
None => continue
};
on_read(fd, read(fd));
}
on_death(fd);
let _ = unistd::close(fd);
});
Ok(())
}
pub(crate) fn read(fd: RawFd) -> Result<String, Box<dyn Error>> {
let mut buf: [u8; 0x1000] = [0; 0x1000];
match unistd::read(fd, &mut buf) {
Ok(r) => Ok(String::from_utf8_lossy(&buf[..r]).into()),
Err(e) => Err(Box::new(PtyError(format!("Read failure {e}"))))
}
}
pub(crate) fn write(fd: RawFd, buf: &[u8]) -> Result<(), Box<dyn Error>> {
match unistd::write(fd, buf) {
Ok(_) => Ok(()),
Err(e) => Err(Box::new(PtyError(format!("Write failure {e}"))))
}
}
pub(crate) fn resize(fd: RawFd, window_size: WindowSize) -> Result<(), Box<dyn Error>> {
let window_size: winsize = window_size.to_winsize();
if unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, &window_size as *const _) } < 0 {
return Err(Box::new(PtyError("Window resize failure".into())));
}
Ok(())
}
pub(crate) fn kill(fd: RawFd) {
let _ = write(fd, "exit\r".as_bytes());
}
fn validate_fd(fd: RawFd) -> Result<(), Box<dyn Error>> {
unsafe {
if libc::fcntl(fd, F_GETFD) != -1 || errno() != EBADFD {
Ok(())
} else {
Err(Box::new(PtyError(format!("Invalid file descriptor: {fd}"))))
}
}
}