use anyhow::Context;
use indicatif::ProgressStyle;
use owo_colors::{OwoColorize, Stream::Stdout};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::{
io::{BufRead, Write},
sync::{Arc, Mutex, mpsc},
};
use strum::Display;
mod file_term;
pub mod markdown;
mod null_term;
mod secrets;
mod util;
pub use secrets::{DEFAULT_MAX_REDACTIONS, DEFAULT_MIN_SECRET_LENGTH, Secrets};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Display, Default, Serialize, Deserialize,
)]
pub enum Level {
Trace,
Debug,
Message,
#[default]
Info,
App,
Passthrough,
Warning,
Error,
Silent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogHeader {
pub(crate) command: Arc<str>,
pub(crate) working_directory: Option<Arc<str>>,
pub(crate) environment: HashMap<Arc<str>, HashMap<Arc<str>, Arc<str>>>,
pub(crate) arguments: Vec<Arc<str>>,
pub(crate) shell: Arc<str>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Verbosity {
pub level: Level,
pub is_show_progress_bars: bool,
pub is_show_elapsed_time: bool,
pub is_tty: bool,
}
const PROGRESS_PREFIX_WIDTH: usize = 0;
pub struct Section<'a> {
pub printer: &'a mut Printer,
}
impl<'a> Section<'a> {
pub fn new(printer: &'a mut Printer, name: &str) -> anyhow::Result<Self> {
printer.write(format!("{}{}:", " ".repeat(printer.indent), name.bold()).as_str())?;
printer.shift_right();
Ok(Self { printer })
}
}
impl Drop for Section<'_> {
fn drop(&mut self) {
self.printer.shift_left();
}
}
pub struct MultiProgressBar {
lock: Arc<Mutex<()>>,
printer_verbosity: Verbosity,
start_time: std::time::Instant,
indent: usize,
max_width: usize,
progress_width: usize,
progress: Option<indicatif::ProgressBar>,
final_message: Option<Arc<str>>,
is_increasing: bool,
secrets: Secrets,
}
impl MultiProgressBar {
pub fn total(&self) -> Option<u64> {
if let Some(progress) = self.progress.as_ref() {
progress.length()
} else {
None
}
}
pub fn reset_elapsed(&mut self) {
if let Some(progress) = self.progress.as_mut() {
progress.reset_elapsed();
}
}
pub fn set_total(&mut self, total: u64) {
if let Some(progress) = self.progress.as_mut()
&& let Some(length) = progress.length()
&& length != total
{
let _lock = self.lock.lock().unwrap();
progress.set_length(total);
progress.set_position(0);
}
}
pub fn log(&mut self, verbosity: Level, message: &str) {
if util::is_verbosity_active(self.printer_verbosity, verbosity) {
let formatted_message = util::format_log(
self.indent,
self.max_width,
verbosity,
message,
self.printer_verbosity.is_show_elapsed_time,
self.start_time,
);
let _lock = self.lock.lock().unwrap();
if let Some(progress) = self.progress.as_ref() {
progress.println(formatted_message.as_str());
} else {
print!("{formatted_message}");
}
}
}
pub fn set_prefix(&mut self, message: &str) {
if let Some(progress) = self.progress.as_mut() {
let _lock = self.lock.lock().unwrap();
progress.set_prefix(message.to_owned());
}
}
fn construct_message(&self, message: &str) -> String {
let prefix_size = if let Some(progress) = self.progress.as_ref() {
progress.prefix().len()
} else {
0_usize
};
let length = if self.max_width > self.progress_width + prefix_size {
self.max_width - self.progress_width - prefix_size
} else {
0_usize
};
util::sanitize_output(message, length)
}
pub fn set_message(&mut self, message: &str) {
let constructed_message = self.construct_message(message);
if let Some(progress) = self.progress.as_mut() {
let _lock = self.lock.lock().unwrap();
progress.set_message(constructed_message);
}
}
pub fn set_ending_message(&mut self, message: &str) {
self.final_message = Some(self.construct_message(message).into());
}
pub fn set_ending_message_none(&mut self) {
self.final_message = None;
}
pub fn increment_with_overflow(&mut self, count: u64) {
let progress_total = self.total();
if let Some(progress) = self.progress.as_mut() {
let _lock = self.lock.lock().unwrap();
if self.is_increasing {
progress.inc(count);
if progress.position() == progress_total.unwrap_or(100) {
self.is_increasing = false;
}
} else if progress.position() >= count {
progress.set_position(progress.position() - count);
} else {
progress.set_position(0);
self.is_increasing = true;
}
}
}
pub fn decrement(&mut self, count: u64) {
if let Some(progress) = self.progress.as_mut() {
let _lock = self.lock.lock().unwrap();
if progress.position() >= count {
progress.set_position(progress.position() - count);
} else {
progress.set_position(0);
}
}
}
pub fn increment(&mut self, count: u64) {
if let Some(progress) = self.progress.as_mut() {
let _lock = self.lock.lock().unwrap();
progress.inc(count);
}
}
fn start_process(
&mut self,
command: &str,
options: &ExecuteOptions,
) -> anyhow::Result<std::process::Child> {
if let Some(directory) = &options.working_directory
&& !std::path::Path::new(directory.as_ref()).exists()
{
return Err(anyhow::anyhow!("Directory does not exist: {directory}"));
}
let child_process = options
.spawn(command)
.context(format!("Failed to spawn a child process using {command}"))?;
Ok(child_process)
}
pub fn execute_process(
&mut self,
command: &str,
options: ExecuteOptions,
) -> anyhow::Result<Option<String>> {
self.set_message(&options.get_full_command(command));
let child_process = self
.start_process(command, &options)
.context(format!("Failed to start process {command}"))?;
let secrets = self.secrets.clone();
let result = util::monitor_process(command, child_process, self, &options, &secrets)
.context(format!("Command `{command}` failed to execute"))?;
Ok(result)
}
}
impl Drop for MultiProgressBar {
fn drop(&mut self) {
if let Some(message) = &self.final_message {
let constructed_message = self.construct_message(message);
if let Some(progress) = self.progress.as_mut() {
let _lock = self.lock.lock().unwrap();
progress.finish_with_message(constructed_message.bold().to_string());
}
}
}
}
pub struct MultiProgress<'a> {
pub printer: &'a mut Printer,
multi_progress: indicatif::MultiProgress,
}
impl<'a> MultiProgress<'a> {
pub fn new(printer: &'a mut Printer) -> Self {
let locker = printer.lock.clone();
let _lock = locker.lock().unwrap();
let draw_target = indicatif::ProgressDrawTarget::term_like_with_hz(
(printer.create_progress_printer)(),
10,
);
Self {
printer,
multi_progress: indicatif::MultiProgress::with_draw_target(draw_target),
}
}
pub fn add_progress(
&mut self,
prefix: &str,
total: Option<u64>,
finish_message: Option<&str>,
) -> MultiProgressBar {
let _lock = self.printer.lock.lock().unwrap();
let template_string = "{elapsed_precise}|{bar:.cyan/blue}|{prefix} {msg}";
let (progress, progress_chars) = if let Some(total) = total {
let progress = indicatif::ProgressBar::new(total);
(progress, "#>-")
} else {
let progress = indicatif::ProgressBar::new(200);
(progress, "*>-")
};
progress.set_style(
ProgressStyle::with_template(template_string)
.unwrap()
.progress_chars(progress_chars),
);
let progress = if self.printer.verbosity.is_show_progress_bars {
let progress = self.multi_progress.add(progress);
let prefix = format!("{prefix}:");
progress.set_prefix(
format!("{prefix:PROGRESS_PREFIX_WIDTH$}")
.if_supports_color(Stdout, |text| text.bold())
.to_string(),
);
Some(progress)
} else {
None
};
MultiProgressBar {
lock: self.printer.lock.clone(),
printer_verbosity: self.printer.verbosity,
indent: self.printer.indent,
progress,
progress_width: 28, max_width: self.printer.max_width,
final_message: finish_message.map(|s| s.into()),
is_increasing: true,
start_time: self.printer.start_time,
secrets: self.printer.get_secrets(),
}
}
}
pub struct Heading<'a> {
pub printer: &'a mut Printer,
}
impl<'a> Heading<'a> {
pub fn new(printer: &'a mut Printer, name: &str) -> anyhow::Result<Self> {
printer.newline()?;
printer.enter_heading();
{
let heading = if printer.heading_count == 1 {
format!("{} {name}", "#".repeat(printer.heading_count))
.yellow()
.bold()
.to_string()
} else {
format!("{} {name}", "#".repeat(printer.heading_count))
.bold()
.to_string()
};
printer.write(heading.as_str())?;
printer.write("\n")?;
}
Ok(Self { printer })
}
}
impl Drop for Heading<'_> {
fn drop(&mut self) {
self.printer.exit_heading();
}
}
#[derive(Clone, Debug)]
pub struct ExecuteOptions {
pub label: Arc<str>,
pub is_return_stdout: bool,
pub working_directory: Option<Arc<str>>,
pub environment: Vec<(Arc<str>, Arc<str>)>,
pub arguments: Vec<Arc<str>>,
pub log_file_path: Option<Arc<str>>,
pub clear_environment: bool,
pub process_started_with_id: Option<fn(&str, u32)>,
pub log_level: Option<Level>,
pub timeout: Option<std::time::Duration>,
}
impl Default for ExecuteOptions {
fn default() -> Self {
Self {
label: "working".into(),
is_return_stdout: false,
working_directory: None,
environment: vec![],
arguments: vec![],
log_file_path: None,
clear_environment: false,
process_started_with_id: None,
log_level: None,
timeout: None,
}
}
}
impl ExecuteOptions {
pub(crate) fn process_child_output<OutputType: std::io::Read + Send + 'static>(
output: OutputType,
) -> anyhow::Result<(std::thread::JoinHandle<()>, mpsc::Receiver<String>)> {
let (tx, rx) = mpsc::channel::<String>();
let thread = std::thread::spawn(move || {
use std::io::BufReader;
let reader = BufReader::new(output);
for line in reader.lines() {
let line = line.unwrap();
tx.send(line).unwrap();
}
});
Ok((thread, rx))
}
fn spawn(&self, command: &str) -> anyhow::Result<std::process::Child> {
use std::process::{Command, Stdio};
let mut process = Command::new(command);
if self.clear_environment {
process.env_clear();
}
for argument in &self.arguments {
process.arg(argument.as_ref());
}
if let Some(directory) = &self.working_directory {
process.current_dir(directory.as_ref());
}
for (key, value) in self.environment.iter() {
process.env(key.as_ref(), value.as_ref());
}
let result = process
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.spawn()
.context(format!("while spawning piped {command}"))?;
if let Some(callback) = self.process_started_with_id.as_ref() {
callback(self.label.as_ref(), result.id());
}
Ok(result)
}
pub fn get_full_command(&self, command: &str) -> String {
format!("{command} {}", self.arguments.join(" "))
}
pub fn get_full_command_in_working_directory(&self, command: &str) -> String {
format!(
"{} {command} {}",
if let Some(directory) = &self.working_directory {
directory
} else {
""
},
self.arguments.join(" "),
)
}
}
trait PrinterTrait: std::io::Write + indicatif::TermLike {}
impl<W: std::io::Write + indicatif::TermLike> PrinterTrait for W {}
pub struct Printer {
pub verbosity: Verbosity,
pub secrets: Vec<Arc<str>>,
pub redacted: Arc<str>,
pub max_redactions: usize,
pub min_secret_length: usize,
lock: Arc<Mutex<()>>,
indent: usize,
heading_count: usize,
max_width: usize,
writer: Box<dyn PrinterTrait>,
start_time: std::time::Instant,
create_progress_printer: fn() -> Box<dyn PrinterTrait>,
}
impl Printer {
pub fn get_log_divider() -> Arc<str> {
"=".repeat(80).into()
}
pub fn get_terminal_width() -> usize {
const ASSUMED_WIDTH: usize = 80;
if let Some((width, _)) = terminal_size::terminal_size() {
width.0 as usize
} else {
ASSUMED_WIDTH
}
}
pub fn new_stdout() -> Self {
let max_width = Self::get_terminal_width();
Self {
indent: 0,
lock: Arc::new(Mutex::new(())),
verbosity: Verbosity::default(),
heading_count: 0,
max_width,
writer: Box::new(console::Term::stdout()),
create_progress_printer: || Box::new(console::Term::stdout()),
start_time: std::time::Instant::now(),
secrets: Vec::new(),
redacted: "<REDACTED>".into(),
max_redactions: DEFAULT_MAX_REDACTIONS,
min_secret_length: DEFAULT_MIN_SECRET_LENGTH,
}
}
pub fn new_file(path: &str) -> anyhow::Result<Self> {
let file_writer = file_term::FileTerm::new(path)?;
Ok(Self {
indent: 0,
lock: Arc::new(Mutex::new(())),
verbosity: Verbosity::default(),
heading_count: 0,
max_width: 65535,
writer: Box::new(file_writer),
create_progress_printer: || Box::new(null_term::NullTerm {}),
start_time: std::time::Instant::now(),
secrets: Vec::new(),
redacted: "<REDACTED>".into(),
max_redactions: DEFAULT_MAX_REDACTIONS,
min_secret_length: DEFAULT_MIN_SECRET_LENGTH,
})
}
pub fn new_null_term() -> Self {
Self {
indent: 0,
lock: Arc::new(Mutex::new(())),
verbosity: Verbosity::default(),
heading_count: 0,
max_width: 80,
writer: Box::new(null_term::NullTerm {}),
create_progress_printer: || Box::new(null_term::NullTerm {}),
start_time: std::time::Instant::now(),
secrets: Vec::new(),
redacted: "<REDACTED>".into(),
max_redactions: DEFAULT_MAX_REDACTIONS,
min_secret_length: DEFAULT_MIN_SECRET_LENGTH,
}
}
pub fn raw(&mut self, message: &str) -> anyhow::Result<()> {
let _lock = self.lock.lock().unwrap();
write!(self.writer, "{message}")?;
Ok(())
}
pub(crate) fn write(&mut self, message: &str) -> anyhow::Result<()> {
let redacted = self.get_secrets().redact(message.into());
let _lock = self.lock.lock().unwrap();
write!(self.writer, "{redacted}")?;
Ok(())
}
pub fn newline(&mut self) -> anyhow::Result<()> {
self.write("\n")?;
Ok(())
}
pub fn trace<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
if util::is_verbosity_active(self.verbosity, Level::Trace) {
self.object(name, value)
} else {
Ok(())
}
}
pub fn debug<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
if util::is_verbosity_active(self.verbosity, Level::Debug) {
self.object(name, value)
} else {
Ok(())
}
}
pub fn message<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
if util::is_verbosity_active(self.verbosity, Level::Message) {
self.object(name, value)
} else {
Ok(())
}
}
pub fn info<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
if util::is_verbosity_active(self.verbosity, Level::Info) {
self.object(name, value)
} else {
Ok(())
}
}
pub fn warning<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
if util::is_verbosity_active(self.verbosity, Level::Warning) {
self.object(name.yellow().to_string().as_str(), value)
} else {
Ok(())
}
}
pub fn error<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
if util::is_verbosity_active(self.verbosity, Level::Error) {
self.object(name.red().to_string().as_str(), value)
} else {
Ok(())
}
}
pub fn log(&mut self, level: Level, message: &str) -> anyhow::Result<()> {
if util::is_verbosity_active(self.verbosity, level) {
self.write(
util::format_log(
self.indent,
self.max_width,
level,
message,
self.verbosity.is_show_elapsed_time,
self.start_time,
)
.as_str(),
)
} else {
Ok(())
}
}
pub fn code_block(&mut self, name: &str, content: &str) -> anyhow::Result<()> {
self.write(format!("```{name}\n{content}```\n").as_str())?;
Ok(())
}
fn get_secrets(&self) -> Secrets {
Secrets {
secrets: self.secrets.clone(),
redacted: self.redacted.clone(),
max_redactions: self.max_redactions,
min_secret_length: self.min_secret_length,
}
}
fn object<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
let value = serde_json::to_value(value).context("failed to serialize as JSON")?;
if self.verbosity.level <= Level::Message && value == serde_json::Value::Null {
return Ok(());
}
self.write(
format!(
"{}{}: ",
" ".repeat(self.indent),
name.if_supports_color(Stdout, |text| text.bold())
)
.as_str(),
)?;
self.print_value(&value)?;
Ok(())
}
fn enter_heading(&mut self) {
self.heading_count += 1;
}
fn exit_heading(&mut self) {
self.heading_count -= 1;
}
fn shift_right(&mut self) {
self.indent += 2;
}
fn shift_left(&mut self) {
self.indent -= 2;
}
fn print_value(&mut self, value: &serde_json::Value) -> anyhow::Result<()> {
match value {
serde_json::Value::Object(map) => {
self.write("\n")?;
self.shift_right();
for (key, value) in map {
let is_skip =
*value == serde_json::Value::Null && self.verbosity.level > Level::Message;
if !is_skip {
{
self.write(
format!(
"{}{}: ",
" ".repeat(self.indent),
key.if_supports_color(Stdout, |text| text.bold())
)
.as_str(),
)?;
}
self.print_value(value)?;
}
}
self.shift_left();
}
serde_json::Value::Array(array) => {
self.write("\n")?;
self.shift_right();
for (index, value) in array.iter().enumerate() {
self.write(format!("{}[{index}]: ", " ".repeat(self.indent)).as_str())?;
self.print_value(value)?;
}
self.shift_left();
}
serde_json::Value::Null => {
self.write("null\n")?;
}
serde_json::Value::Bool(value) => {
self.write(format!("{value}\n").as_str())?;
}
serde_json::Value::Number(value) => {
self.write(format!("{value}\n").as_str())?;
}
serde_json::Value::String(value) => {
self.write(format!("{value}\n").as_str())?;
}
}
Ok(())
}
pub fn start_process(
&mut self,
command: &str,
options: &ExecuteOptions,
) -> anyhow::Result<std::process::Child> {
let args = options.arguments.join(" ");
let full_command = format!("{command} {args}");
self.info("execute", &full_command)?;
if let Some(directory) = &options.working_directory {
self.info("directory", &directory)?;
if !std::path::Path::new(directory.as_ref()).exists() {
return Err(anyhow::anyhow!("Directory does not exist: {directory}"));
}
}
let child_process = options
.spawn(command)
.context(format!("while spawning {command}"))?;
Ok(child_process)
}
pub fn execute_process(
&mut self,
command: &str,
options: ExecuteOptions,
) -> anyhow::Result<Option<String>> {
let section = Section::new(self, command)?;
let child_process = section
.printer
.start_process(command, &options)
.context(format!("Faild to execute process: {command}"))?;
let mut multi_progress = MultiProgress::new(section.printer);
let mut progress_bar = multi_progress.add_progress("progress", None, None);
let secrets = multi_progress.printer.get_secrets();
let result = util::monitor_process(
command,
child_process,
&mut progress_bar,
&options,
&secrets,
)
.context(format!("Command `{command}` failed to execute"))?;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize)]
pub struct Test {
pub name: String,
pub age: u32,
pub alive: bool,
pub dead: bool,
pub children: f64,
}
#[test]
fn printer() {
let mut printer = Printer::new_stdout();
let mut options = ExecuteOptions::default();
options.arguments.push("-alt".into());
let runtime =
tokio::runtime::Runtime::new().expect("Internal Error: Failed to create runtime");
let (async_sender, sync_receiver) = flume::bounded(1);
runtime.spawn(async move {
async_sender.send_async(10).await.expect("Failed to send");
});
let received = sync_receiver.recv().expect("Failed to receive");
drop(runtime);
printer.info("Received", &received).unwrap();
printer.execute_process("/bin/ls", options).unwrap();
{
let mut heading = Heading::new(&mut printer, "First").unwrap();
{
let section = Section::new(&mut heading.printer, "PersonWrapper").unwrap();
section
.printer
.object(
"Person",
&Test {
name: "John".to_string(),
age: 30,
alive: true,
dead: false,
children: 2.5,
},
)
.unwrap();
}
let mut sub_heading = Heading::new(&mut heading.printer, "Second").unwrap();
let mut sub_section = Section::new(&mut sub_heading.printer, "PersonWrapper").unwrap();
sub_section.printer.object("Hello", &"World").unwrap();
{
let mut multi_progress = MultiProgress::new(&mut sub_section.printer);
let mut first = multi_progress.add_progress("First", Some(10), None);
let mut second = multi_progress.add_progress("Second", Some(50), None);
let mut third = multi_progress.add_progress("Third", Some(100), None);
let first_handle = std::thread::spawn(move || {
first.set_ending_message("Done!");
for index in 0..10 {
first.increment(1);
if index == 5 {
first.set_message("half way");
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
});
let second_handle = std::thread::spawn(move || {
for index in 0..50 {
second.increment(1);
if index == 25 {
second.set_message("half way");
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
});
for _ in 0..100 {
third.increment(1);
std::thread::sleep(std::time::Duration::from_millis(10));
}
first_handle.join().unwrap();
second_handle.join().unwrap();
}
}
{
let runtime =
tokio::runtime::Runtime::new().expect("Internal Error: Failed to create runtime");
let heading = Heading::new(&mut printer, "Async").unwrap();
let mut multi_progress = MultiProgress::new(heading.printer);
let mut handles = Vec::new();
let task1_progress = multi_progress.add_progress("Task1", Some(30), None);
let task2_progress = multi_progress.add_progress("Task2", Some(30), None);
let task1 = async move {
let mut progress = task1_progress;
progress.set_message("Task1a");
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
progress.increment(1);
}
progress.set_message("Task1b");
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
progress.increment(1);
}
progress.set_message("Task1c");
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
progress.increment(1);
}
()
};
handles.push(runtime.spawn(task1));
let task2 = async move {
let mut progress = task2_progress;
progress.set_message("Task2a");
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
progress.increment(1);
}
progress.set_message("Task2b");
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
progress.increment(1);
}
progress.set_message("Task2c");
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
progress.increment(1);
}
()
};
handles.push(runtime.spawn(task2));
for handle in handles {
runtime.block_on(handle).unwrap();
}
}
}
}