use std::ffi::{CStr, OsStr};
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::PathBuf;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
struct Pty {
leader: File,
follower: File,
}
fn check(result: libc::c_int) -> io::Result<()> {
if result == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
impl Pty {
fn open() -> io::Result<Self> {
let open = |path: &OsStr| {
OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NOCTTY)
.open(path)
};
let leader = open(OsStr::new("/dev/ptmx"))?;
let fd = leader.as_raw_fd();
let path = unsafe {
check(libc::grantpt(fd))?;
check(libc::unlockpt(fd))?;
static NAME: Mutex<()> = Mutex::new(());
let _name = NAME.lock().unwrap();
let name = libc::ptsname(fd);
if name.is_null() {
return Err(io::Error::last_os_error());
}
PathBuf::from(OsStr::from_bytes(CStr::from_ptr(name).to_bytes()))
};
let follower = open(path.as_os_str())?;
let fd = follower.as_raw_fd();
unsafe {
let mut size: libc::winsize = std::mem::zeroed();
if libc::ioctl(1, libc::TIOCGWINSZ, &mut size) == -1 || size.ws_col == 0 {
size = libc::winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
}
check(libc::ioctl(fd, libc::TIOCSWINSZ, &size))?;
let mut termios: libc::termios = std::mem::zeroed();
check(libc::tcgetattr(fd, &mut termios))?;
termios.c_oflag &= !libc::OPOST;
check(libc::tcsetattr(fd, libc::TCSANOW, &termios))?;
}
Ok(Self { leader, follower })
}
}
pub fn spawn(command: &mut Command, input: bool) -> io::Result<(Session, File)> {
let pty = Pty::open()?;
if input {
command.stdin(pty.follower.try_clone()?);
}
command
.stdout(pty.follower.try_clone()?)
.stderr(pty.follower.try_clone()?);
unsafe {
command.pre_exec(|| {
check(libc::setsid())?;
check(libc::ioctl(1, libc::TIOCSCTTY as _, 0))
});
}
let child = command.spawn()?;
if input {
command.stdin(Stdio::null());
}
command.stdout(Stdio::null()).stderr(Stdio::null());
drop(pty.follower);
Ok((
Session {
child,
status: None,
},
pty.leader,
))
}
pub struct Session {
child: Child,
status: Option<ExitStatus>,
}
impl Session {
pub fn id(&self) -> u32 {
self.child.id()
}
pub fn inner(&mut self) -> &mut Child {
&mut self.child
}
pub fn signal(&self, signal: libc::c_int) -> io::Result<()> {
check(unsafe { libc::killpg(self.child.id() as libc::pid_t, signal) })
}
pub fn kill(&mut self) -> io::Result<()> {
self.signal(libc::SIGKILL)
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
self.reap(libc::WNOHANG)?;
Ok(self.status)
}
pub fn wait(&mut self) -> io::Result<ExitStatus> {
self.reap(0)?;
self.status
.ok_or_else(|| io::Error::other("command exited without a status"))
}
fn reap(&mut self, flags: libc::c_int) -> io::Result<()> {
let group = self.child.id() as libc::pid_t;
loop {
let mut status = 0;
match unsafe { libc::waitpid(-group, &mut status, flags) } {
0 => return Ok(()),
-1 => {
let error = io::Error::last_os_error();
return match error.raw_os_error() {
Some(libc::ECHILD) => Ok(()),
Some(libc::EINTR) => continue,
_ => Err(error),
};
}
pid if pid == group => self.status = Some(ExitStatus::from_raw(status)),
_ => {}
}
}
}
}
pub struct Output(pub File);
impl Read for Output {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
match self.0.read(buffer) {
Err(error) if error.raw_os_error() == Some(libc::EIO) => Ok(0),
result => result,
}
}
}
pub fn forwards_input() -> bool {
io::IsTerminal::is_terminal(&io::stdin())
}
pub struct Forwarder {
stop: Arc<AtomicBool>,
thread: Option<std::thread::JoinHandle<()>>,
saved: Option<libc::termios>,
}
impl Forwarder {
pub fn start(mut leader: File, raw: bool) -> Self {
let saved = raw.then(raw_input).flatten();
let stop = Arc::new(AtomicBool::new(false));
let stopped = stop.clone();
let thread = std::thread::spawn(move || {
let mut buffer = [0u8; 1024];
while !stopped.load(Ordering::SeqCst) {
let mut poll = libc::pollfd {
fd: 0,
events: libc::POLLIN,
revents: 0,
};
match unsafe { libc::poll(&mut poll, 1, 50) } {
0 => continue,
-1 if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted => {
continue;
}
-1 => return,
_ => {}
}
let count = unsafe { libc::read(0, buffer.as_mut_ptr().cast(), buffer.len()) };
match count {
-1 if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted => {}
0 | -1 => return,
count => {
if leader.write_all(&buffer[..count as usize]).is_err() {
return;
}
}
}
}
});
Self {
stop,
thread: Some(thread),
saved,
}
}
}
impl Drop for Forwarder {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
if let Some(saved) = &self.saved {
unsafe {
libc::tcsetattr(0, libc::TCSANOW, saved);
}
}
}
}
fn raw_input() -> Option<libc::termios> {
if !crate::process::terminal::in_foreground() {
return None;
}
unsafe {
let mut saved: libc::termios = std::mem::zeroed();
if libc::tcgetattr(0, &mut saved) == -1 {
return None;
}
let mut raw = saved;
raw.c_iflag &= !(libc::ICRNL | libc::IXON);
raw.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG | libc::IEXTEN);
raw.c_cc[libc::VMIN] = 1;
raw.c_cc[libc::VTIME] = 0;
(libc::tcsetattr(0, libc::TCSANOW, &raw) != -1).then_some(saved)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn commands_see_a_terminal_that_passes_lines_through() {
let mut command = Command::new("/bin/sh");
command.arg("-c").arg(
"[ -t 1 ] && [ -t 2 ] && echo terminal; [ -t 0 ] || echo no input; \
stty size <&1; (exec 3</dev/tty) && echo controlling",
);
command.stdin(std::process::Stdio::null());
let (mut session, leader) = spawn(&mut command, false).unwrap();
let mut output = Vec::new();
Output(leader).read_to_end(&mut output).unwrap();
assert!(session.wait().unwrap().success());
let output = String::from_utf8(output).unwrap();
assert!(!output.contains('\r'), "{output:?}");
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines[..2], ["terminal", "no input"], "{output:?}");
assert!(lines[2].split(' ').all(|n| n.parse::<u32>().unwrap() > 0));
assert_eq!(lines[3], "controlling", "{output:?}");
}
#[test]
fn input_reaches_the_command_through_the_terminal() {
let mut command = Command::new("/bin/sh");
command
.arg("-c")
.arg("[ -t 0 ] && echo terminal; read line; echo got $line");
let (mut session, mut leader) = spawn(&mut command, true).unwrap();
leader.write_all(b"typed\n").unwrap();
let mut output = Vec::new();
Output(leader).read_to_end(&mut output).unwrap();
assert!(session.wait().unwrap().success());
let output = String::from_utf8(output).unwrap();
assert!(output.contains("terminal\n"), "{output:?}");
assert!(output.contains("got typed\n"), "{output:?}");
}
#[test]
fn signals_reach_the_whole_session() {
let mut command = Command::new("/bin/sh");
command.arg("-c").arg("sleep 30 & sleep 30");
let (mut session, leader) = spawn(&mut command, false).unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
session.kill().unwrap();
assert!(session.wait().unwrap().signal() == Some(libc::SIGKILL));
let mut output = Vec::new();
Output(leader).read_to_end(&mut output).unwrap();
assert!(session.try_wait().unwrap().is_some());
}
}