use std::{collections::HashMap, io::IsTerminal, path::Path, sync::Arc};
use anyhow::Result;
use crate::redact::Redactor;
#[derive(Debug, PartialEq, Eq)]
enum Mode {
Plain,
Pty,
Piped,
}
fn select_mode(has_redactor: bool, stdin_tty: bool, stdout_tty: bool) -> Mode {
if !has_redactor {
return Mode::Plain;
}
if stdin_tty && stdout_tty {
Mode::Pty
} else {
Mode::Piped
}
}
pub fn run(
shell: &str,
command: &str,
env: HashMap<String, String>,
cwd: &Path,
redactor: Option<Redactor>,
) -> Result<i32> {
let mode = select_mode(
redactor.is_some(),
std::io::stdin().is_terminal(),
std::io::stdout().is_terminal(),
);
match mode {
Mode::Plain => run_plain(shell, command, env, cwd),
Mode::Pty => run_pty(shell, command, env, cwd, Arc::new(redactor.unwrap())),
Mode::Piped => run_piped(shell, command, env, cwd, Arc::new(redactor.unwrap())),
}
}
fn run_plain(shell: &str, command: &str, env: HashMap<String, String>, cwd: &Path) -> Result<i32> {
let status = std::process::Command::new(shell)
.args(["-c", command])
.current_dir(cwd)
.envs(std::env::vars())
.envs(env)
.status()?;
Ok(status.code().unwrap_or(1))
}
#[cfg(not(unix))]
fn run_pty(
shell: &str,
command: &str,
env: HashMap<String, String>,
cwd: &Path,
redactor: Arc<Redactor>,
) -> Result<i32> {
run_piped(shell, command, env, cwd, redactor)
}
#[cfg(unix)]
fn run_pty(
shell: &str,
command: &str,
env: HashMap<String, String>,
cwd: &Path,
redactor: Arc<Redactor>,
) -> Result<i32> {
use nix::pty::{OpenptyResult, openpty};
use std::fs::File;
use std::os::fd::{AsRawFd, OwnedFd};
use std::process::{Command, Stdio};
let size = pty_winsize();
let OpenptyResult { master, slave } = openpty(&size, None)?;
let slave_out: OwnedFd = slave.try_clone()?;
let slave_err: OwnedFd = slave.try_clone()?;
let _raw_guard = RawStdinGuard::enter();
let mut child = Command::new(shell)
.args(["-c", command])
.current_dir(cwd)
.envs(std::env::vars())
.envs(env)
.stdin(Stdio::inherit())
.stdout(Stdio::from(slave_out))
.stderr(Stdio::from(slave_err))
.spawn()?;
let master_fd = master.as_raw_fd();
drop(slave);
setup_sigwinch_resize(master_fd);
let master_for_read = master.try_clone()?;
let mut master_reader = File::from(master_for_read);
let _ = redactor.stream(&mut master_reader, &mut std::io::stdout().lock());
let status = child.wait()?;
drop(master);
Ok(status.code().unwrap_or(1))
}
fn run_piped(
shell: &str,
command: &str,
env: HashMap<String, String>,
cwd: &Path,
redactor: Arc<Redactor>,
) -> Result<i32> {
use std::process::{Command, Stdio};
let mut child = Command::new(shell)
.args(["-c", command])
.current_dir(cwd)
.envs(std::env::vars())
.envs(env)
.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let child_stdout = child.stdout.take().unwrap();
let child_stderr = child.stderr.take().unwrap();
let redactor_stderr = Arc::clone(&redactor);
let stdout_thread = std::thread::spawn(move || {
redactor
.stream(child_stdout, &mut std::io::stdout().lock())
.ok();
});
let stderr_thread = std::thread::spawn(move || {
redactor_stderr
.stream(child_stderr, &mut std::io::stderr().lock())
.ok();
});
let status = child.wait()?;
stdout_thread.join().ok();
stderr_thread.join().ok();
Ok(status.code().unwrap_or(1))
}
#[cfg(unix)]
fn pty_winsize() -> nix::pty::Winsize {
let mut ws: nix::libc::winsize = unsafe { std::mem::zeroed() };
if unsafe { nix::libc::ioctl(nix::libc::STDOUT_FILENO, nix::libc::TIOCGWINSZ, &mut ws) } == 0
&& ws.ws_row > 0
&& ws.ws_col > 0
{
return nix::pty::Winsize {
ws_row: ws.ws_row,
ws_col: ws.ws_col,
ws_xpixel: ws.ws_xpixel,
ws_ypixel: ws.ws_ypixel,
};
}
nix::pty::Winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
}
}
#[cfg(unix)]
fn setup_sigwinch_resize(master_fd: std::os::fd::RawFd) {
use nix::sys::signal::{SigHandler, Signal};
use std::sync::atomic::{AtomicBool, Ordering};
static SIGWINCH_PENDING: AtomicBool = AtomicBool::new(false);
extern "C" fn handler(_: nix::libc::c_int) {
SIGWINCH_PENDING.store(true, Ordering::Relaxed);
}
unsafe { nix::sys::signal::signal(Signal::SIGWINCH, SigHandler::Handler(handler)).ok() };
std::thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_millis(50));
if SIGWINCH_PENDING.swap(false, Ordering::Relaxed) {
let mut ws: nix::libc::winsize = unsafe { std::mem::zeroed() };
if unsafe {
nix::libc::ioctl(nix::libc::STDOUT_FILENO, nix::libc::TIOCGWINSZ, &mut ws)
} == 0
{
unsafe { nix::libc::ioctl(master_fd, nix::libc::TIOCSWINSZ, &ws) };
}
}
}
});
}
#[cfg(unix)]
struct RawStdinGuard {
original: Option<nix::sys::termios::Termios>,
}
#[cfg(unix)]
impl RawStdinGuard {
fn enter() -> Self {
use nix::sys::termios::{SetArg, cfmakeraw, tcgetattr, tcsetattr};
let stdin = std::io::stdin();
let Ok(original) = tcgetattr(&stdin) else {
return Self { original: None };
};
let mut raw = original.clone();
cfmakeraw(&mut raw);
if tcsetattr(stdin, SetArg::TCSANOW, &raw).is_err() {
return Self { original: None };
}
Self {
original: Some(original),
}
}
}
#[cfg(unix)]
impl Drop for RawStdinGuard {
fn drop(&mut self) {
if let Some(original) = &self.original {
let _ = nix::sys::termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
original,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn select_mode_plain_without_redactor() {
assert_eq!(select_mode(false, true, true), Mode::Plain);
assert_eq!(select_mode(false, false, false), Mode::Plain);
}
#[test]
fn select_mode_pty_only_when_both_tty() {
assert_eq!(select_mode(true, true, true), Mode::Pty);
}
#[test]
fn select_mode_piped_when_stdin_not_tty() {
assert_eq!(select_mode(true, false, true), Mode::Piped);
}
#[test]
fn select_mode_piped_when_stdout_not_tty() {
assert_eq!(select_mode(true, true, false), Mode::Piped);
assert_eq!(select_mode(true, false, false), Mode::Piped);
}
}