use std::{
io::Write,
os::fd::FromRawFd,
sync::{atomic::AtomicBool, Arc},
};
use color_eyre::eyre::{eyre, Context};
use nix::sys::signal;
type PeekBuf<'a> = std::iter::Peekable<std::slice::Iter<'a, u8>>;
const PRINT_LOG_TIMEOUT: i32 = 50;
pub struct PseudoTerminal {
fd: std::ffi::c_int,
stop: Arc<AtomicBool>,
lf: Arc<AtomicBool>,
cr: Arc<AtomicBool>,
}
impl PseudoTerminal {
pub fn new(lf: Arc<AtomicBool>, cr: Arc<AtomicBool>) -> Self {
let fd = unsafe { nix::libc::posix_openpt(nix::libc::O_RDWR | nix::libc::O_NONBLOCK) };
assert_eq!(unsafe { nix::libc::grantpt(fd) }, 0, "grantpt failed");
assert_eq!(unsafe { nix::libc::unlockpt(fd) }, 0, "unlockpt failed");
Self { fd, stop: Arc::new(AtomicBool::new(false)), lf, cr }
}
pub fn set_winsize(&self, winsize: nix::libc::winsize) {
assert_eq!(
unsafe {
nix::libc::ioctl(self.fd, nix::libc::TIOCSWINSZ, std::ptr::from_ref(&winsize))
},
0,
"ioctl failed"
);
}
pub fn slave(&self) -> std::ffi::c_int {
let slave_name = unsafe { nix::libc::ptsname(self.fd) };
let slave_fd = unsafe { nix::libc::open(slave_name, nix::libc::O_RDWR) };
assert_eq!(
unsafe { nix::libc::fcntl(slave_fd, nix::libc::F_SETFD, nix::libc::FD_CLOEXEC) },
0,
"cannot set fcntl FD_CLOEXEC"
);
slave_fd
}
pub fn slave_stdio(&self) -> std::process::Stdio {
unsafe { std::process::Stdio::from_raw_fd(self.slave()) }
}
pub fn read(&self, buf: &mut [u8]) -> Option<nix::Result<usize>> {
let mut pollfd = nix::libc::pollfd { fd: self.fd, events: nix::libc::POLLIN, revents: 0 };
while !self.stop.load(std::sync::atomic::Ordering::Relaxed) {
match unsafe { nix::libc::poll(&raw mut pollfd, 1, PRINT_LOG_TIMEOUT) } {
0 => {}
1 => return Some(nix::unistd::read(self, buf)),
rc => panic!("unexpected return value from poll(): {rc}"),
}
}
None
}
#[tracing::instrument(skip(self))]
pub fn print_log(&self, prefix: &[u8]) {
let mut buf = [0u8; 256];
let mut newbuf = Vec::with_capacity(256);
while let Ok(Some(len @ 1..)) =
self.read(&mut buf).transpose().inspect_err(|e| tracing::error!("read(): {e:?}"))
{
newbuf.clear();
newbuf.reserve(len.saturating_add_signed(64));
self.transform_log(
buf.get(..len).expect("out of range buf slicing from read()"),
&mut newbuf,
prefix,
);
std::io::stdout().write_all(&newbuf).expect("cannot write to stdout");
}
}
fn transform_log(&self, buf: &[u8], newbuf: &mut Vec<u8>, prefix: &[u8]) {
let mut buf = buf.iter().peekable();
if Self::flag_was_true_then_set_false(&self.lf) {
newbuf.extend_from_slice(prefix);
}
if Self::flag_was_true_then_set_false(&self.cr) {
if buf.peek() == Some(&&b'\n') {
buf.next(); newbuf.push(b'\n');
}
newbuf.extend_from_slice(prefix);
}
while let Some(&c) = buf.next() {
newbuf.push(c);
if c == b'\n' {
self.transform_lf(newbuf, prefix, &mut buf);
} else if c == b'\r' {
self.transform_cr(newbuf, prefix, &mut buf);
}
}
}
fn flag_was_true_then_set_false(flag: &AtomicBool) -> bool {
flag.fetch_update(
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
|b| b.then_some(false),
) == Ok(true)
}
fn transform_lf(&self, newbuf: &mut Vec<u8>, prefix: &[u8], buf: &mut PeekBuf<'_>) {
if buf.peek().is_none() {
self.lf.store(true, std::sync::atomic::Ordering::Relaxed);
} else {
newbuf.extend_from_slice(prefix);
}
}
fn transform_cr(&self, newbuf: &mut Vec<u8>, prefix: &[u8], buf: &mut PeekBuf<'_>) {
let Some(&&next) = buf.peek() else {
self.cr.store(true, std::sync::atomic::Ordering::Relaxed);
return;
};
if next == b'\n' {
buf.next(); *newbuf.last_mut().unwrap() = b'\n'; self.transform_lf(newbuf, prefix, buf);
}
}
}
impl std::os::fd::AsFd for PseudoTerminal {
fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
unsafe { std::os::fd::BorrowedFd::borrow_raw(self.fd) }
}
}
impl std::os::fd::AsRawFd for PseudoTerminal {
fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
self.fd
}
}
impl Drop for PseudoTerminal {
fn drop(&mut self) {
let c = unsafe { nix::libc::close(self.fd) };
if c != 0 {
tracing::error!("cannot close fd: libc::close() returned {c}");
}
}
}
pub struct PseudoTerminalCtl {
stdout: PseudoTerminal,
stderr: PseudoTerminal,
}
impl PseudoTerminalCtl {
pub fn new(process: &std::ffi::OsStr) -> Self {
let (lf, cr) = (Arc::new(AtomicBool::new(true)), Arc::new(AtomicBool::new(false)));
let ret = Self {
stdout: PseudoTerminal::new(Arc::clone(&lf), Arc::clone(&cr)),
stderr: PseudoTerminal::new(Arc::clone(&lf), Arc::clone(&cr)),
};
let mut winsize = Self::real_winsize();
winsize.ws_col -= u16::try_from(process.len()).expect("process name too long");
winsize.ws_col -= 3; ret.stdout.set_winsize(winsize);
ret.stderr.set_winsize(winsize);
ret
}
fn real_winsize() -> nix::libc::winsize {
let mut winsize = nix::libc::winsize { ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0 };
if unsafe {
nix::libc::ioctl(nix::libc::STDOUT_FILENO, nix::libc::TIOCGWINSZ, &raw mut winsize)
} != 0
{
winsize = nix::libc::winsize { ws_row: 24, ws_col: 80, ws_xpixel: 0, ws_ypixel: 0 };
}
winsize
}
pub async fn run(self, cmd: &mut tokio::process::Command) -> color_eyre::Result<()> {
let process = cmd.as_std().get_program();
let process_str = process.to_string_lossy().into_owned();
let (stdout_prefix, stderr_prefix) = Self::get_prefix(process.as_encoded_bytes());
let mut out = cmd
.stdout(self.stdout.slave_stdio())
.stderr(self.stderr.slave_stdio())
.spawn()
.context(eyre!("Cannot run {process_str}"))?;
let (stdout_stop, stderr_stop) =
(Arc::clone(&self.stdout.stop), Arc::clone(&self.stderr.stop));
tokio::join!(
tokio::task::spawn_blocking(move || {
self.stdout.print_log(&stdout_prefix);
}),
tokio::task::spawn_blocking(move || {
self.stderr.print_log(&stderr_prefix);
}),
tokio::spawn(async move {
let res = tokio::select! {
_ = tokio::signal::ctrl_c() => {
tracing::info!("Received ctrl-c, sending sigint to child process");
#[allow(clippy::cast_possible_wrap)]
signal::kill(nix::unistd::Pid::from_raw(out.id().unwrap() as i32), signal::Signal::SIGINT).unwrap();
Err(eyre!("Received ctrl-c, exiting"))
}
w = out.wait() => {
let status = w.unwrap();
if status.success() {
tracing::info!("Command exited successfully");
Ok(())
} else {
Err(eyre!("Command exited with status: {status}"))
}
}
};
stdout_stop.store(true, std::sync::atomic::Ordering::Relaxed);
stderr_stop.store(true, std::sync::atomic::Ordering::Relaxed);
println!();
res
})
).2?
}
fn get_prefix(process: &[u8]) -> (Vec<u8>, Vec<u8>) {
const CYAN: &[u8] = b"\x1b[36m";
const YELLOW: &[u8] = b"\x1b[33m";
const RESET: &[u8] = b"\x1b[0m";
if std::env::var("NO_COLOR").is_ok() {
return (process.to_owned(), process.to_owned());
}
(
[CYAN, process, RESET, " │ ".as_bytes()].concat(),
[YELLOW, process, RESET, " │ ".as_bytes()].concat(),
)
}
}