use super::cli::{Command as Launch, Options};
use std::collections::VecDeque;
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::time::{Duration, Instant};
const MAX_PENDING_INPUT: usize = 64 * 1024;
pub const READ_BUDGET: usize = 64 * 1024;
pub struct Pty {
master: File,
child: Child,
pending: VecDeque<u8>,
pub eof: bool,
status: Option<ExitStatus>,
}
impl Pty {
pub fn spawn(options: &Options, rows: u16, cols: u16) -> io::Result<Self> {
let mut master = -1;
let mut slave = -1;
let size = winsize(rows, cols);
if unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
&size,
)
} < 0
{
return Err(io::Error::last_os_error());
}
let master = unsafe { OwnedFd::from_raw_fd(master) };
let slave = unsafe { OwnedFd::from_raw_fd(slave) };
for fd in [&master, &slave] {
if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } < 0 {
return Err(io::Error::last_os_error());
}
}
let shell = std::env::var_os("SHELL")
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "/bin/sh".into());
let mut command = match &options.command {
Launch::Shell => {
let mut c = Command::new(&shell);
c.arg("-i");
c
}
Launch::Script(script) => {
let mut c = Command::new(&shell);
c.arg("-c").arg(script);
c
}
Launch::Argv(argv) => {
let mut c = Command::new(&argv[0]);
c.args(&argv[1..]);
c
}
};
if let Some(path) = &options.directory {
command.current_dir(path);
}
command
.env("TERM", "xterm-256color")
.env("COLORTERM", "truecolor")
.env("TERM_PROGRAM", "lios")
.env("TERM_PROGRAM_VERSION", env!("CARGO_PKG_VERSION"))
.env_remove("DISPLAY")
.env_remove("WAYLAND_DISPLAY")
.env_remove("WAYLAND_SOCKET")
.stdin(Stdio::from(slave.try_clone()?))
.stdout(Stdio::from(slave.try_clone()?))
.stderr(Stdio::from(slave));
unsafe {
command.pre_exec(|| {
if libc::setsid() < 0 || libc::ioctl(0, libc::TIOCSCTTY, 0) < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
let flags = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_GETFL) };
if flags < 0
|| unsafe { libc::fcntl(master.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) }
< 0
{
return Err(io::Error::last_os_error());
}
let child = command.spawn()?;
Ok(Self {
master: File::from(master),
child,
pending: VecDeque::new(),
eof: false,
status: None,
})
}
pub fn resize(&self, rows: u16, cols: u16) -> io::Result<()> {
let size = winsize(rows, cols);
if unsafe { libc::ioctl(self.master.as_raw_fd(), libc::TIOCSWINSZ, &size) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
if self.eof {
return Ok(0);
}
match self.master.read(buffer) {
Ok(0) => {
self.eof = true;
Ok(0)
}
Err(error) if error.raw_os_error() == Some(libc::EIO) => {
self.eof = true;
Ok(0)
}
other => other,
}
}
pub fn enqueue(&mut self, bytes: &[u8]) -> io::Result<()> {
if self.eof {
return Ok(());
}
if bytes.len() > MAX_PENDING_INPUT.saturating_sub(self.pending.len()) {
return Err(io::Error::other("Lios PTY input queue exceeded its limit"));
}
self.pending.extend(bytes);
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
while !self.pending.is_empty() {
let (bytes, _) = self.pending.as_slices();
match self.master.write(bytes) {
Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
Ok(count) => {
self.pending.drain(..count);
}
Err(error) if error.kind() == io::ErrorKind::WouldBlock => break,
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) if error.raw_os_error() == Some(libc::EIO) => {
self.pending.clear();
self.eof = true;
break;
}
Err(error) => return Err(error),
}
}
Ok(())
}
pub fn available_input_bytes(&self) -> usize {
if self.eof {
0
} else {
MAX_PENDING_INPUT.saturating_sub(self.pending.len())
}
}
pub fn wants_write(&self) -> bool {
!self.pending.is_empty() && !self.eof
}
pub fn status(&mut self) -> io::Result<Option<ExitStatus>> {
if self.status.is_none() {
self.status = self.child.try_wait()?;
}
Ok(self.status)
}
}
impl AsRawFd for Pty {
fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.master.as_raw_fd()
}
}
impl Drop for Pty {
fn drop(&mut self) {
if self.status.is_some() {
return;
}
let group = self.child.id() as i32;
unsafe {
let foreground = libc::tcgetpgrp(self.master.as_raw_fd());
if foreground > 0 && foreground != libc::getpgrp() {
libc::kill(-foreground, libc::SIGHUP);
}
libc::kill(-group, libc::SIGHUP);
}
let deadline = Instant::now() + Duration::from_millis(100);
while Instant::now() < deadline {
if self.child.try_wait().ok().flatten().is_some() {
return;
}
std::thread::sleep(Duration::from_millis(5));
}
unsafe {
libc::kill(-group, libc::SIGKILL);
}
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn winsize(rows: u16, cols: u16) -> libc::winsize {
libc::winsize {
ws_row: rows.max(1),
ws_col: cols.max(1),
ws_xpixel: 0,
ws_ypixel: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn child_has_controlling_pty_correct_size_and_native_environment() {
let super::super::cli::Action::Run(options) = super::super::cli::parse([
"--", "/bin/sh", "-c", "test -t 0 && test -t 1 && test -t 2 && stty size && printf '%s|%s|%s\\n' \"$TERM\" \"$TERM_PROGRAM\" \"${WAYLAND_DISPLAY-unset}\""
].map(Into::into)).unwrap() else { panic!() };
let mut pty = Pty::spawn(&options, 31, 91).unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
let mut output = Vec::new();
let mut buffer = [0; 1024];
while !pty.eof {
match pty.read(&mut buffer) {
Ok(n) => output.extend_from_slice(&buffer[..n]),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(2))
}
Err(e) => panic!("{e}"),
}
assert!(Instant::now() < deadline);
}
let text = String::from_utf8(output).unwrap();
assert!(text.contains("31 91"), "{text:?}");
assert!(text.contains("xterm-256color|lios|unset"), "{text:?}");
pty.resize(20, 60).unwrap();
}
#[test]
fn close_reaps_a_running_child_and_queue_is_bounded() {
let super::super::cli::Action::Run(options) =
super::super::cli::parse(["--", "/bin/sh", "-c", "exec sleep 20"].map(Into::into))
.unwrap()
else {
panic!()
};
let mut pty = Pty::spawn(&options, 24, 80).unwrap();
let pid = pty.child.id() as i32;
assert!(pty.enqueue(&vec![0; MAX_PENDING_INPUT + 1]).is_err());
drop(pty);
assert_eq!(unsafe { libc::kill(pid, 0) }, -1);
assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::ESRCH));
}
}