use std::{
collections::HashMap,
io::{BufRead, BufReader},
process::{Command, ExitStatus, Stdio},
thread,
time::{Duration, Instant},
};
use serde::Serialize;
use shellish_parse::ParseOptions;
use termcolor::Color;
use crate::{
cwrite, cwriteln,
output::Lines,
script::{ScriptKillReceiver, ScriptKillSender, ScriptLocation},
};
#[derive(Copy, Clone, derive_more::Debug, derive_more::Display, PartialEq, Eq)]
pub enum CommandResult {
#[debug("{_0:?}")]
#[display("{_0}")]
Exit(ExitStatus),
#[debug("timed out")]
#[display("timed out")]
TimedOut,
}
impl CommandResult {
pub fn success(&self) -> bool {
match self {
CommandResult::Exit(status) => status.success(),
CommandResult::TimedOut => false,
}
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(transparent)]
pub struct CommandLine {
pub command: String,
#[serde(skip)]
pub location: ScriptLocation,
#[serde(skip)]
pub line_count: usize,
}
impl CommandLine {
pub fn new(command: String, location: ScriptLocation, line_count: usize) -> Self {
Self {
command,
location,
line_count,
}
}
#[allow(clippy::too_many_arguments)]
pub fn run(
&self,
writer: &mut dyn termcolor::WriteColor,
show_line_numbers: bool,
runner: Option<String>,
timeout: Duration,
envs: &HashMap<String, String>,
kill_receiver: &ScriptKillReceiver,
kill_sender: &ScriptKillSender,
) -> Result<(Lines, CommandResult), std::io::Error> {
let start = Instant::now();
let warn_time = timeout.saturating_mul(90) / 100;
let timeout = timeout.saturating_mul(110) / 100;
thread::scope(|s| {
let mut command = if let Some(runner) = runner {
let bits = shellish_parse::parse(&runner, ParseOptions::default())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
let mut cmd = Command::new(&bits[0]);
cmd.args(&bits[1..]);
cmd
} else {
let mut cmd = Command::new("sh");
cmd.arg("-c");
cmd
};
command.arg(&self.command);
command.envs(envs);
if let Some(pwd) = envs.get("PWD") {
command.current_dir(pwd);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.process_group(0);
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_SUSPENDED: u32 = 0x00000004;
command.creation_flags(CREATE_SUSPENDED);
}
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
command.stdin(Stdio::null());
let mut output = command.spawn().map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to spawn command {command:?}: {e}"),
)
})?;
let (tx, rx) = std::sync::mpsc::channel();
let stdout_lines = tx.clone();
let stdout = output.stdout.take().unwrap();
let stdout = s.spawn(move || {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
while reader.read_line(&mut line).unwrap() > 0 {
if line.is_empty() {
continue;
}
if line.ends_with('\n') {
line.pop();
}
if line.ends_with('\r') {
line.pop();
}
_ = stdout_lines.send((true, std::mem::take(&mut line)));
}
});
let stderr_lines = tx;
let stderr = output.stderr.take().unwrap();
let stderr = s.spawn(move || {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
while reader.read_line(&mut line).unwrap() > 0 {
if line.is_empty() {
continue;
}
if line.ends_with('\n') {
line.pop();
}
if line.ends_with('\r') {
line.pop();
}
_ = stderr_lines.send((false, std::mem::take(&mut line)));
}
});
let runner = s.spawn(move || kill_receiver.run_cmd(output, warn_time));
let mut line_number = 1;
let mut output_lines = vec![];
while let Ok((is_stdout, line)) = rx.recv_timeout(timeout) {
if show_line_numbers {
cwrite!(
writer,
fg = Color::White,
dimmed = true,
"{line_number:>3} "
);
}
let line_out = fast_strip_ansi::strip_ansi_string(&line);
if is_stdout {
cwriteln!(writer, fg = Color::White, "{line_out}");
} else {
cwriteln!(writer, fg = Color::Yellow, "{line_out}");
}
output_lines.push(line);
line_number += 1;
}
let mut handles = vec![stdout, stderr];
while !handles.is_empty() {
if start.elapsed() > timeout {
cwriteln!(writer, fg = Color::Yellow, "Process took too long!");
kill_sender.kill();
return Ok((Lines::new(output_lines), CommandResult::TimedOut));
}
let mut new_handles = vec![];
for handle in handles.drain(..) {
if handle.is_finished() {
handle
.join()
.map_err(|_| std::io::Error::other("thread panicked"))?;
} else {
new_handles.push(handle);
}
}
handles = new_handles;
std::thread::sleep(std::time::Duration::from_millis(10));
}
Ok((
Lines::new(output_lines),
CommandResult::Exit(runner.join().unwrap()?),
))
})
}
}