use std::ffi::{CString, OsStr, OsString};
use std::fs::File;
use std::io::Write;
use std::os::fd::{AsFd, BorrowedFd, IntoRawFd, OwnedFd};
use std::os::unix::prelude::{AsRawFd, OpenOptionsExt, OsStrExt};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{env, io};
use nix::errno::Errno;
use nix::libc::{login_tty, O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, VEOF};
use nix::pty::{openpty, Winsize};
use nix::sys::select::{select, FdSet};
use nix::sys::signal::{killpg, Signal};
use nix::sys::stat::Mode;
use nix::sys::termios::{
cfmakeraw, tcgetattr, tcsetattr, LocalFlags, OutputFlags, SetArg, Termios,
};
use nix::sys::time::TimeVal;
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{dup2, execvp, fork, isatty, mkfifo, read, tcgetpgrp, write, ForkResult, Pid};
use signal_hook::consts::SIGWINCH;
pub struct TtySpawn {
options: Option<SpawnOptions>,
}
impl TtySpawn {
pub fn new<S: AsRef<OsStr>>(cmd: S) -> TtySpawn {
TtySpawn {
options: Some(SpawnOptions {
command: vec![cmd.as_ref().to_os_string()],
stdin_file: None,
stdout_file: None,
script_mode: false,
no_flush: false,
no_echo: false,
no_pager: false,
no_raw: false,
}),
}
}
pub fn new_cmdline<S: AsRef<OsStr>, I: Iterator<Item = S>>(mut cmdline: I) -> Self {
let mut rv = TtySpawn::new(cmdline.next().expect("empty cmdline"));
rv.args(cmdline);
rv
}
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.options_mut().command.push(arg.as_ref().to_os_string());
self
}
pub fn args<S: AsRef<OsStr>, I: Iterator<Item = S>>(&mut self, args: I) -> &mut Self {
for arg in args {
self.arg(arg);
}
self
}
pub fn stdin_file(&mut self, f: File) -> &mut Self {
self.options_mut().stdin_file = Some(f);
self
}
pub fn stdin_path<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Self, io::Error> {
let path = path.as_ref();
mkfifo_atomic(path)?;
Ok(self.stdin_file(
File::options()
.read(true)
.write(true)
.custom_flags(O_NONBLOCK)
.open(path)?,
))
}
pub fn stdout_file(&mut self, f: File) -> &mut Self {
self.options_mut().stdout_file = Some(f);
self
}
pub fn stdout_path<P: AsRef<Path>>(
&mut self,
path: P,
truncate: bool,
) -> Result<&mut Self, io::Error> {
Ok(self.stdout_file(if !truncate {
File::options().append(true).create(true).open(path)?
} else {
File::options()
.create(true)
.truncate(true)
.write(true)
.open(path)?
}))
}
pub fn script_mode(&mut self, yes: bool) -> &mut Self {
self.options_mut().script_mode = yes;
self
}
pub fn flush(&mut self, yes: bool) -> &mut Self {
self.options_mut().no_flush = !yes;
self
}
pub fn echo(&mut self, yes: bool) -> &mut Self {
self.options_mut().no_echo = !yes;
self
}
pub fn pager(&mut self, yes: bool) -> &mut Self {
self.options_mut().no_pager = !yes;
self
}
pub fn raw(&mut self, yes: bool) -> &mut Self {
self.options_mut().no_raw = !yes;
self
}
pub fn spawn(&mut self) -> Result<i32, io::Error> {
Ok(spawn(
self.options.take().expect("builder only works once"),
)?)
}
fn options_mut(&mut self) -> &mut SpawnOptions {
self.options.as_mut().expect("builder only works once")
}
}
struct SpawnOptions {
command: Vec<OsString>,
stdin_file: Option<File>,
stdout_file: Option<File>,
script_mode: bool,
no_flush: bool,
no_echo: bool,
no_pager: bool,
no_raw: bool,
}
fn spawn(mut opts: SpawnOptions) -> Result<i32, Errno> {
let term_attrs = tcgetattr(io::stdin()).ok();
let winsize = term_attrs
.as_ref()
.and_then(|_| get_winsize(io::stdin().as_fd()));
let pty = openpty(&winsize, &term_attrs)?;
let (_restore_term, stderr_pty) = if opts.script_mode {
let term_attrs = tcgetattr(io::stderr()).ok();
let winsize = term_attrs
.as_ref()
.and_then(|_| get_winsize(io::stderr().as_fd()));
let stderr_pty = openpty(&winsize, &term_attrs)?;
(None, Some(stderr_pty))
} else if !opts.no_raw {
(
term_attrs.as_ref().map(|term_attrs| {
let mut raw_attrs = term_attrs.clone();
cfmakeraw(&mut raw_attrs);
raw_attrs.local_flags.remove(LocalFlags::ECHO);
tcsetattr(io::stdin(), SetArg::TCSAFLUSH, &raw_attrs).ok();
RestoreTerm(term_attrs.clone())
}),
None,
)
} else {
(None, None)
};
if let Ok(mut term_attrs) = tcgetattr(&pty.master) {
if opts.script_mode {
term_attrs.output_flags.remove(OutputFlags::OPOST);
}
if opts.no_echo || (opts.script_mode && !isatty(io::stdin().as_raw_fd()).unwrap_or(false)) {
term_attrs.local_flags.remove(LocalFlags::ECHO);
}
tcsetattr(&pty.master, SetArg::TCSAFLUSH, &term_attrs).ok();
}
if let ForkResult::Parent { child } = unsafe { fork()? } {
drop(pty.slave);
let stderr_pty = if let Some(stderr_pty) = stderr_pty {
drop(stderr_pty.slave);
Some(stderr_pty.master)
} else {
None
};
return communication_loop(
pty.master,
child,
term_attrs.is_some(),
opts.stdout_file.as_mut(),
opts.stdin_file.as_mut(),
stderr_pty,
!opts.no_flush,
);
}
if opts.no_pager || opts.script_mode {
unsafe {
env::set_var("PAGER", "cat");
}
}
let args = opts
.command
.iter()
.filter_map(|x| CString::new(x.as_bytes()).ok())
.collect::<Vec<_>>();
drop(pty.master);
unsafe {
login_tty(pty.slave.into_raw_fd());
if let Some(stderr_pty) = stderr_pty {
dup2(stderr_pty.slave.into_raw_fd(), io::stderr().as_raw_fd())?;
}
}
match execvp(&args[0], &args)? {}
}
fn communication_loop(
master: OwnedFd,
child: Pid,
is_tty: bool,
mut out_file: Option<&mut File>,
in_file: Option<&mut File>,
stderr: Option<OwnedFd>,
flush: bool,
) -> Result<i32, Errno> {
let mut buf = [0; 4096];
let mut read_stdin = true;
let mut done = false;
let stdin = io::stdin();
let got_winch = Arc::new(AtomicBool::new(false));
if is_tty {
signal_hook::flag::register(SIGWINCH, Arc::clone(&got_winch)).ok();
}
while !done {
if got_winch.load(Ordering::Relaxed) {
forward_winsize(master.as_fd(), stderr.as_ref().map(|x| x.as_fd()))?;
got_winch.store(false, Ordering::Relaxed);
}
let mut read_fds = FdSet::new();
let mut timeout = TimeVal::new(1, 0);
read_fds.insert(master.as_fd());
if !read_stdin && is_tty {
read_stdin = true;
}
if read_stdin {
read_fds.insert(stdin.as_fd());
}
if let Some(ref f) = in_file {
read_fds.insert(f.as_fd());
}
if let Some(ref fd) = stderr {
read_fds.insert(fd.as_fd());
}
match select(None, Some(&mut read_fds), None, None, Some(&mut timeout)) {
Ok(0) | Err(Errno::EINTR | Errno::EAGAIN) => continue,
Ok(_) => {}
Err(err) => return Err(err),
}
if read_fds.contains(stdin.as_fd()) {
match read(stdin.as_raw_fd(), &mut buf) {
Ok(0) => {
send_eof_sequence(master.as_fd());
read_stdin = false;
}
Ok(n) => {
write_all(master.as_fd(), &buf[..n])?;
}
Err(Errno::EINTR | Errno::EAGAIN) => {}
Err(Errno::EIO) => {
done = true;
}
Err(err) => return Err(err),
};
}
if let Some(ref f) = in_file {
if read_fds.contains(f.as_fd()) {
match read(f.as_raw_fd(), &mut buf) {
Ok(0) | Err(Errno::EAGAIN | Errno::EINTR) => {}
Err(err) => return Err(err),
Ok(n) => {
write_all(master.as_fd(), &buf[..n])?;
}
}
}
}
if let Some(ref fd) = stderr {
if read_fds.contains(fd.as_fd()) {
match read(fd.as_raw_fd(), &mut buf) {
Ok(0) | Err(_) => {}
Ok(n) => {
forward_and_log(io::stderr().as_fd(), &mut out_file, &buf[..n], flush)?;
}
}
}
}
if read_fds.contains(master.as_fd()) {
match read(master.as_raw_fd(), &mut buf) {
Ok(0) | Err(Errno::EIO) => {
done = true;
}
Ok(n) => forward_and_log(io::stdout().as_fd(), &mut out_file, &buf[..n], flush)?,
Err(Errno::EAGAIN | Errno::EINTR) => {}
Err(err) => return Err(err),
};
}
}
Ok(match waitpid(child, None)? {
WaitStatus::Exited(_, status) => status,
WaitStatus::Signaled(_, signal, _) => 128 + signal as i32,
_ => 1,
})
}
fn forward_and_log(
fd: BorrowedFd,
out_file: &mut Option<&mut File>,
buf: &[u8],
flush: bool,
) -> Result<(), Errno> {
if let Some(logfile) = out_file {
logfile.write_all(buf).map_err(|x| match x.raw_os_error() {
Some(errno) => Errno::from_raw(errno),
None => Errno::EINVAL,
})?;
if flush {
logfile.flush().ok();
}
}
write_all(fd, buf)?;
Ok(())
}
fn forward_winsize(master: BorrowedFd, stderr_master: Option<BorrowedFd>) -> Result<(), Errno> {
if let Some(winsize) = get_winsize(io::stdin().as_fd()) {
set_winsize(master, winsize).ok();
if let Some(second_master) = stderr_master {
set_winsize(second_master, winsize).ok();
}
if let Ok(pgrp) = tcgetpgrp(master) {
killpg(pgrp, Signal::SIGWINCH).ok();
}
}
Ok(())
}
fn get_winsize(fd: BorrowedFd) -> Option<Winsize> {
nix::ioctl_read_bad!(_get_window_size, TIOCGWINSZ, Winsize);
let mut size: Winsize = unsafe { std::mem::zeroed() };
unsafe { _get_window_size(fd.as_raw_fd(), &mut size).ok()? };
Some(size)
}
fn set_winsize(fd: BorrowedFd, winsize: Winsize) -> Result<(), Errno> {
nix::ioctl_write_ptr_bad!(_set_window_size, TIOCSWINSZ, Winsize);
unsafe { _set_window_size(fd.as_raw_fd(), &winsize) }?;
Ok(())
}
fn send_eof_sequence(fd: BorrowedFd) {
if let Ok(attrs) = tcgetattr(fd) {
if attrs.local_flags.contains(LocalFlags::ICANON) {
write(fd, &[attrs.control_chars[VEOF]]).ok();
}
}
}
fn write_all(fd: BorrowedFd, mut buf: &[u8]) -> Result<(), Errno> {
while !buf.is_empty() {
let n = write(fd, buf)?;
buf = &buf[n..];
}
Ok(())
}
fn mkfifo_atomic(path: &Path) -> Result<(), Errno> {
match mkfifo(path, Mode::S_IRUSR | Mode::S_IWUSR) {
Ok(()) | Err(Errno::EEXIST) => Ok(()),
Err(err) => Err(err),
}
}
struct RestoreTerm(Termios);
impl Drop for RestoreTerm {
fn drop(&mut self) {
tcsetattr(io::stdin(), SetArg::TCSAFLUSH, &self.0).ok();
}
}