quinoa/src/pty.rs

154 lines
4.3 KiB
Rust

use nix::fcntl::{fcntl, FcntlArg, OFlag};
use nix::pty::{grantpt, posix_openpt, ptsname, unlockpt, Winsize};
use nix::sys::stat::Mode;
use nix::unistd::{ForkResult, Pid};
use nix::{ioctl_none_bad, ioctl_write_ptr_bad};
use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, TIOCSCTTY};
// ioctl request code to set window size of pty:
use libc::TIOCSWINSZ;
use std::ffi::CStr;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::path::Path;
use std::os::unix::io::{FromRawFd, IntoRawFd};
// nix macro that generates an ioctl call to set window size of pty:
ioctl_write_ptr_bad!(set_window_size, TIOCSWINSZ, Winsize);
// request to "Make the given terminal the controlling terminal of the calling process"
ioctl_none_bad!(set_controlling_terminal, TIOCSCTTY);
pub struct Child {
pub pty: tokio::fs::File,
pub proc: Proc,
}
impl Child {
pub fn set_nodelay(&mut self) -> nix::Result<()> {
fcntl(self.pty.as_raw_fd(), FcntlArg::F_SETFL(OFlag::O_NDELAY)).map(|_| ())
}
}
pub struct Proc(pub Pid);
impl Proc {
// copied from
// https://doc.rust-lang.org/nightly/src/std/sys/unix/process/process_unix.rs.html#744-757
pub fn try_wait(&self) -> std::io::Result<Option<i32>> {
let mut status = 0;
let pid = cvt(unsafe { libc::waitpid(self.0.as_raw(), &mut status, libc::WNOHANG) })?;
if pid == 0 {
Ok(None)
} else {
Ok(Some(status))
}
}
}
impl Drop for Proc {
fn drop(&mut self) {
_ = nix::sys::signal::kill(self.0, nix::sys::signal::Signal::SIGTERM);
}
}
pub struct PreExec<T>(T);
impl<T> PreExec<T> {
pub unsafe fn new(t: T) -> Self {
Self(t)
}
}
pub fn create_pty<S, F>(path: &CStr, argv: &[S], pre_exec: PreExec<F>) -> nix::Result<Child>
where
S: AsRef<CStr> + std::fmt::Debug,
F: FnOnce() -> anyhow::Result<()>,
{
/* Create a new master */
let master_fd = posix_openpt(OFlag::O_RDWR)?;
/* For some reason, you have to give permission to the master to have a
* pty. What is it good for otherwise? */
grantpt(&master_fd)?;
unlockpt(&master_fd)?;
/* Get the path of the slave */
let slave_name = unsafe { ptsname(&master_fd) }?;
/* Try to open the slave */
let _slave_fd = nix::fcntl::open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?;
trace!("master opened the slave_fd!");
/* Launch our child process. The main application loop can inspect and then
pass the stdin data to it. */
let child_pid = match unsafe { nix::unistd::fork() } {
Ok(ForkResult::Child) => {
let t = init_child(path, argv, pre_exec, &slave_name).unwrap();
#[allow(unreachable_code)]
match t {}
},
Ok(ForkResult::Parent { child }) => child,
Err(e) => panic!("{}", e),
};
let winsize = Winsize {
ws_row: 25,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
let master_fd = master_fd.into_raw_fd();
/* Tell the master the size of the terminal */
unsafe { set_window_size(master_fd, &winsize)? };
let master_file = unsafe { File::from_raw_fd(master_fd) };
Ok(Child {
pty: master_file.into(),
proc: Proc(child_pid),
})
}
fn init_child<S, F>(
path: &CStr,
argv: &[S],
pre_exec: PreExec<F>,
slave_name: &str,
) -> anyhow::Result<std::convert::Infallible>
where
S: AsRef<CStr> + std::fmt::Debug,
F: FnOnce() -> anyhow::Result<()>,
{
debug!("we are going to execute: {:?} {:?}", path, argv);
init_child_common(slave_name)?;
(pre_exec.0)()?;
trace!("running exec");
match nix::unistd::execv(path, argv)? {}
}
fn init_child_common(slave_name: &str) -> anyhow::Result<()> {
/* Open slave end for pseudoterminal */
let slave_fd = nix::fcntl::open(Path::new(slave_name), OFlag::O_RDWR, Mode::empty())?;
trace!("child opened the slave_fd!");
// assign stdin, stdout, stderr to the tty
nix::unistd::dup2(slave_fd, STDIN_FILENO)?;
nix::unistd::dup2(slave_fd, STDOUT_FILENO)?;
nix::unistd::dup2(slave_fd, STDERR_FILENO)?;
nix::unistd::setsid().unwrap();
unsafe { set_controlling_terminal(slave_fd) }.unwrap();
Ok(())
}
// copied from... somewhere in std
pub fn cvt(t: i32) -> std::io::Result<i32> {
if t == -1 {
Err(std::io::Error::last_os_error())
} else {
Ok(t)
}
}