use std::{io::Write, sync::atomic::AtomicBool};
static LF: AtomicBool = AtomicBool::new(true);
static CR: AtomicBool = AtomicBool::new(false);
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,
}
impl PseudoTerminal {
pub fn new() -> 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 }
}
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 read(&self, buf: &mut [u8]) -> Option<nix::Result<usize>> {
let mut pollfd = nix::libc::pollfd { fd: self.fd, events: nix::libc::POLLIN, revents: 0 };
loop {
match unsafe { nix::libc::poll(&raw mut pollfd, 1, PRINT_LOG_TIMEOUT) } {
0 if crate::util::STOP.load(std::sync::atomic::Ordering::Relaxed) => break None,
0 => {}
1 => break Some(nix::unistd::read(self, buf)),
rc => panic!("unexpected return value from poll(): {rc}"),
}
}
}
fn get_prefix(process: &std::ffi::OsStr, out: crate::util::ConsoleOut) -> String {
let process = process.to_str().expect("to_str() returns None");
let process = {
if std::env::var("NO_COLOR").is_ok() {
console::style(process)
} else {
match out {
crate::util::ConsoleOut::Stdout => console::style(process).cyan(),
crate::util::ConsoleOut::Stderr => console::style(process).yellow(),
}
}
};
format!("{process} │ ")
}
#[tracing::instrument(skip(self))]
pub fn print_log(&self, process: &std::ffi::OsStr, out: crate::util::ConsoleOut) {
let prefix = Self::get_prefix(process, out);
let mut buf = [0u8; 256];
let mut newbuf = Vec::with_capacity(256);
let mut len;
while {
let Some(read) = self.read(&mut buf) else { return };
len = read.expect("read() failed");
len != 0
} {
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.as_bytes(),
);
std::io::stdout().write_all(&newbuf).expect("cannot write to stdout");
}
}
fn transform_log(buf: &[u8], newbuf: &mut Vec<u8>, prefix: &[u8]) {
let mut buf = buf.iter().peekable();
if Self::flag_was_true_then_set_false(&LF) {
newbuf.extend_from_slice(prefix);
}
if Self::flag_was_true_then_set_false(&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);
}
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(newbuf: &mut Vec<u8>, prefix: &[u8], buf: &mut PeekBuf<'_>) {
if buf.peek().is_none() {
LF.store(true, std::sync::atomic::Ordering::Relaxed);
} else {
newbuf.extend_from_slice(prefix);
}
}
fn transform_cr(newbuf: &mut Vec<u8>, prefix: &[u8], buf: &mut PeekBuf<'_>) {
let Some(&&next) = buf.peek() else {
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}");
}
}
}