1use anyhow::Context;
2use anyhow_source_location::{format_context, format_error};
3use indicatif::ProgressStyle;
4use owo_colors::{OwoColorize, Stream::Stdout};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::{
8 io::{BufRead, Write},
9 sync::{mpsc, Arc, Mutex},
10};
11use strum::Display;
12
13mod file_term;
14pub mod markdown;
15mod null_term;
16
17#[derive(
18 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Display, Default, Serialize, Deserialize,
19)]
20
21pub enum Level {
22 Trace,
23 Debug,
24 Message,
25 #[default]
26 Info,
27 App,
28 Passthrough,
29 Warning,
30 Error,
31 Silent,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct LogHeader {
36 command: Arc<str>,
37 working_directory: Option<Arc<str>>,
38 environment: HashMap<Arc<str>, HashMap<Arc<str>, Arc<str>>>,
39 arguments: Vec<Arc<str>>,
40 shell: Arc<str>,
41}
42
43#[derive(Debug, Clone, Copy, Default)]
44pub struct Verbosity {
45 pub level: Level,
46 pub is_show_progress_bars: bool,
47 pub is_show_elapsed_time: bool,
48 pub is_tty: bool,
49}
50
51const PROGRESS_PREFIX_WIDTH: usize = 0;
52
53fn is_verbosity_active(printer_level: Verbosity, verbosity: Level) -> bool {
54 verbosity >= printer_level.level
55}
56
57fn format_log(
58 indent: usize,
59 max_width: usize,
60 verbosity: Level,
61 message: &str,
62 is_show_elapsed_time: bool,
63 start_time: std::time::Instant,
64) -> String {
65 let timestamp: Arc<str> = if is_show_elapsed_time {
66 let elapsed = std::time::Instant::now() - start_time;
67 format!("{:.3}:", elapsed.as_secs_f64()).into()
68 } else {
69 "".into()
70 };
71 let mut result = if verbosity == Level::Passthrough {
72 format!("{timestamp}{message}")
73 } else {
74 format!(
75 "{timestamp}{}{}: {message}",
76 " ".repeat(indent),
77 verbosity
78 .to_string()
79 .if_supports_color(Stdout, |text| text.bold())
80 )
81 };
82 while result.len() < max_width {
83 result.push(' ');
84 }
85 result.push('\n');
86 result
87}
88
89pub struct Section<'a> {
90 pub printer: &'a mut Printer,
91}
92
93impl<'a> Section<'a> {
94 pub fn new(printer: &'a mut Printer, name: &str) -> anyhow::Result<Self> {
95 printer
96 .write(format!("{}{}:", " ".repeat(printer.indent), name.bold()).as_str())
97 .context(format_context!(""))?;
98 printer.shift_right();
99 Ok(Self { printer })
100 }
101}
102
103impl Drop for Section<'_> {
104 fn drop(&mut self) {
105 self.printer.shift_left();
106 }
107}
108
109pub struct MultiProgressBar {
110 lock: Arc<Mutex<()>>,
111 printer_verbosity: Verbosity,
112 start_time: std::time::Instant,
113 indent: usize,
114 max_width: usize,
115 progress_width: usize,
116 progress: Option<indicatif::ProgressBar>,
117 final_message: Option<Arc<str>>,
118 is_increasing: bool,
119}
120
121impl MultiProgressBar {
122 pub fn total(&self) -> Option<u64> {
123 if let Some(progress) = self.progress.as_ref() {
124 progress.length()
125 } else {
126 None
127 }
128 }
129
130 pub fn reset_elapsed(&mut self) {
131 if let Some(progress) = self.progress.as_mut() {
132 progress.reset_elapsed();
133 }
134 }
135
136 pub fn set_total(&mut self, total: u64) {
137 if let Some(progress) = self.progress.as_mut() {
138 if let Some(length) = progress.length() {
139 if length != total {
140 let _lock = self.lock.lock().unwrap();
141 progress.set_length(total);
142 progress.set_position(0);
143 }
144 }
145 }
146 }
147
148 pub fn log(&mut self, verbosity: Level, message: &str) {
149 if is_verbosity_active(self.printer_verbosity, verbosity) {
150 let formatted_message = format_log(
151 self.indent,
152 self.max_width,
153 verbosity,
154 message,
155 self.printer_verbosity.is_show_elapsed_time,
156 self.start_time,
157 );
158 let _lock = self.lock.lock().unwrap();
159 if let Some(progress) = self.progress.as_ref() {
160 progress.println(formatted_message.as_str());
161 } else {
162 print!("{formatted_message}");
163 }
164 }
165 }
166
167 pub fn set_prefix(&mut self, message: &str) {
168 if let Some(progress) = self.progress.as_mut() {
169 let _lock = self.lock.lock().unwrap();
170 progress.set_prefix(message.to_owned());
171 }
172 }
173
174 fn construct_message(&self, message: &str) -> String {
175 let prefix_size = if let Some(progress) = self.progress.as_ref() {
176 progress.prefix().len()
177 } else {
178 0_usize
179 };
180 let length = if self.max_width > self.progress_width + prefix_size {
181 self.max_width - self.progress_width - prefix_size
182 } else {
183 0_usize
184 };
185 sanitize_output(message, length)
186 }
187
188 pub fn set_message(&mut self, message: &str) {
189 let constructed_message = self.construct_message(message);
190 if let Some(progress) = self.progress.as_mut() {
191 let _lock = self.lock.lock().unwrap();
192 progress.set_message(constructed_message);
193 }
194 }
195
196 pub fn set_ending_message(&mut self, message: &str) {
197 self.final_message = Some(self.construct_message(message).into());
198 }
199
200 pub fn increment_with_overflow(&mut self, count: u64) {
201 let progress_total = self.total();
202 if let Some(progress) = self.progress.as_mut() {
203 let _lock = self.lock.lock().unwrap();
204 if self.is_increasing {
205 progress.inc(count);
206 if progress.position() == progress_total.unwrap_or(100) {
207 self.is_increasing = false;
208 }
209 } else if progress.position() >= count {
210 progress.set_position(progress.position() - count);
211 } else {
212 progress.set_position(0);
213 self.is_increasing = true;
214 }
215 }
216 }
217
218 pub fn decrement(&mut self, count: u64) {
219 if let Some(progress) = self.progress.as_mut() {
220 let _lock = self.lock.lock().unwrap();
221 if progress.position() >= count {
222 progress.set_position(progress.position() - count);
223 } else {
224 progress.set_position(0);
225 }
226 }
227 }
228
229 pub fn increment(&mut self, count: u64) {
230 if let Some(progress) = self.progress.as_mut() {
231 let _lock = self.lock.lock().unwrap();
232 progress.inc(count);
233 }
234 }
235
236 fn start_process(
237 &mut self,
238 command: &str,
239 options: &ExecuteOptions,
240 ) -> anyhow::Result<std::process::Child> {
241 if let Some(directory) = &options.working_directory {
242 if !std::path::Path::new(directory.as_ref()).exists() {
243 return Err(format_error!("Directory does not exist: {directory}"));
244 }
245 }
246
247 let child_process = options.spawn(command).context(format_context!(
248 "Failed to spawn a child process using {command}"
249 ))?;
250 Ok(child_process)
251 }
252
253 pub fn execute_process(
254 &mut self,
255 command: &str,
256 options: ExecuteOptions,
257 ) -> anyhow::Result<Option<String>> {
258 self.set_message(&options.get_full_command(command));
259 let child_process = self
260 .start_process(command, &options)
261 .context(format_context!("Failed to start process {command}"))?;
262 let result =
263 monitor_process(command, child_process, self, &options).context(format_context!(""))?;
264 Ok(result)
265 }
266}
267
268impl Drop for MultiProgressBar {
269 fn drop(&mut self) {
270 if let Some(message) = &self.final_message {
271 let constructed_message = self.construct_message(message);
272 if let Some(progress) = self.progress.as_mut() {
273 let _lock = self.lock.lock().unwrap();
274 progress.finish_with_message(constructed_message.bold().to_string());
275 }
276 }
277 }
278}
279
280pub struct MultiProgress<'a> {
281 pub printer: &'a mut Printer,
282 multi_progress: indicatif::MultiProgress,
283}
284
285impl<'a> MultiProgress<'a> {
286 pub fn new(printer: &'a mut Printer) -> Self {
287 let locker = printer.lock.clone();
288 let _lock = locker.lock().unwrap();
289
290 let draw_target = indicatif::ProgressDrawTarget::term_like_with_hz(
291 (printer.create_progress_printer)(),
292 10,
293 );
294
295 Self {
296 printer,
297 multi_progress: indicatif::MultiProgress::with_draw_target(draw_target),
298 }
299 }
300
301 pub fn add_progress(
302 &mut self,
303 prefix: &str,
304 total: Option<u64>,
305 finish_message: Option<&str>,
306 ) -> MultiProgressBar {
307 let _lock = self.printer.lock.lock().unwrap();
308
309 let template_string = "{elapsed_precise}|{bar:.cyan/blue}|{prefix} {msg}";
310
311 let (progress, progress_chars) = if let Some(total) = total {
312 let progress = indicatif::ProgressBar::new(total);
313 (progress, "#>-")
314 } else {
315 let progress = indicatif::ProgressBar::new(200);
316 (progress, "*>-")
317 };
318
319 progress.set_style(
320 ProgressStyle::with_template(template_string)
321 .unwrap()
322 .progress_chars(progress_chars),
323 );
324
325 let progress = if self.printer.verbosity.is_show_progress_bars {
326 let progress = self.multi_progress.add(progress);
327 let prefix = format!("{prefix}:");
328 progress.set_prefix(
329 format!("{prefix:PROGRESS_PREFIX_WIDTH$}")
330 .if_supports_color(Stdout, |text| text.bold())
331 .to_string(),
332 );
333 Some(progress)
334 } else {
335 None
336 };
337
338 MultiProgressBar {
339 lock: self.printer.lock.clone(),
340 printer_verbosity: self.printer.verbosity,
341 indent: self.printer.indent,
342 progress,
343 progress_width: 28, max_width: self.printer.max_width,
345 final_message: finish_message.map(|s| s.into()),
346 is_increasing: true,
347 start_time: self.printer.start_time,
348 }
349 }
350}
351
352pub struct Heading<'a> {
353 pub printer: &'a mut Printer,
354}
355
356impl<'a> Heading<'a> {
357 pub fn new(printer: &'a mut Printer, name: &str) -> anyhow::Result<Self> {
358 printer.newline().context(format_context!(""))?;
359 printer.enter_heading();
360 {
361 let heading = if printer.heading_count == 1 {
362 format!("{} {name}", "#".repeat(printer.heading_count))
363 .yellow()
364 .bold()
365 .to_string()
366 } else {
367 format!("{} {name}", "#".repeat(printer.heading_count))
368 .bold()
369 .to_string()
370 };
371 printer
372 .write(heading.as_str())
373 .context(format_context!(""))?;
374 printer.write("\n").context(format_context!(""))?;
375 }
376 Ok(Self { printer })
377 }
378}
379
380impl Drop for Heading<'_> {
381 fn drop(&mut self) {
382 self.printer.exit_heading();
383 }
384}
385
386#[derive(Clone, Debug)]
387pub struct ExecuteOptions {
388 pub label: Arc<str>,
389 pub is_return_stdout: bool,
390 pub working_directory: Option<Arc<str>>,
391 pub environment: Vec<(Arc<str>, Arc<str>)>,
392 pub arguments: Vec<Arc<str>>,
393 pub log_file_path: Option<Arc<str>>,
394 pub clear_environment: bool,
395 pub process_started_with_id: Option<fn(&str, u32)>,
396 pub log_level: Option<Level>,
397 pub timeout: Option<std::time::Duration>,
398}
399
400impl Default for ExecuteOptions {
401 fn default() -> Self {
402 Self {
403 label: "working".into(),
404 is_return_stdout: false,
405 working_directory: None,
406 environment: vec![],
407 arguments: vec![],
408 log_file_path: None,
409 clear_environment: false,
410 process_started_with_id: None,
411 log_level: None,
412 timeout: None,
413 }
414 }
415}
416
417impl ExecuteOptions {
418 fn process_child_output<OutputType: std::io::Read + Send + 'static>(
419 output: OutputType,
420 ) -> anyhow::Result<(std::thread::JoinHandle<()>, mpsc::Receiver<String>)> {
421 let (tx, rx) = mpsc::channel::<String>();
422
423 let thread = std::thread::spawn(move || {
424 use std::io::BufReader;
425 let reader = BufReader::new(output);
426 for line in reader.lines() {
427 let line = line.unwrap();
428 tx.send(line).unwrap();
429 }
430 });
431
432 Ok((thread, rx))
433 }
434
435 fn spawn(&self, command: &str) -> anyhow::Result<std::process::Child> {
436 use std::process::{Command, Stdio};
437 let mut process = Command::new(command);
438
439 if self.clear_environment {
440 process.env_clear();
441 }
442
443 for argument in &self.arguments {
444 process.arg(argument.as_ref());
445 }
446
447 if let Some(directory) = &self.working_directory {
448 process.current_dir(directory.as_ref());
449 }
450
451 for (key, value) in self.environment.iter() {
452 process.env(key.as_ref(), value.as_ref());
453 }
454
455 let result = process
456 .stdout(Stdio::piped())
457 .stderr(Stdio::piped())
458 .stdin(Stdio::null())
459 .spawn()
460 .context(format_context!("{command}"))?;
461
462 if let Some(callback) = self.process_started_with_id.as_ref() {
463 callback(self.label.as_ref(), result.id());
464 }
465
466 Ok(result)
467 }
468
469 pub fn get_full_command(&self, command: &str) -> String {
470 format!("{command} {}", self.arguments.join(" "))
471 }
472
473 pub fn get_full_command_in_working_directory(&self, command: &str) -> String {
474 format!(
475 "{} {command} {}",
476 if let Some(directory) = &self.working_directory {
477 directory
478 } else {
479 ""
480 },
481 self.arguments.join(" "),
482 )
483 }
484}
485
486trait PrinterTrait: std::io::Write + indicatif::TermLike {}
487impl<W: std::io::Write + indicatif::TermLike> PrinterTrait for W {}
488
489pub struct Printer {
490 pub verbosity: Verbosity,
491 lock: Arc<Mutex<()>>,
492 indent: usize,
493 heading_count: usize,
494 max_width: usize,
495 writer: Box<dyn PrinterTrait>,
496 start_time: std::time::Instant,
497 create_progress_printer: fn() -> Box<dyn PrinterTrait>,
498}
499
500impl Printer {
501 pub fn get_log_divider() -> Arc<str> {
502 "=".repeat(80).into()
503 }
504
505 pub fn get_terminal_width() -> usize {
506 const ASSUMED_WIDTH: usize = 80;
507 if let Some((width, _)) = terminal_size::terminal_size() {
508 width.0 as usize
509 } else {
510 ASSUMED_WIDTH
511 }
512 }
513
514 pub fn new_stdout() -> Self {
515 let max_width = Self::get_terminal_width();
516 Self {
517 indent: 0,
518 lock: Arc::new(Mutex::new(())),
519 verbosity: Verbosity::default(),
520 heading_count: 0,
521 max_width,
522 writer: Box::new(console::Term::stdout()),
523 create_progress_printer: || Box::new(console::Term::stdout()),
524 start_time: std::time::Instant::now(),
525 }
526 }
527
528 pub fn new_file(path: &str) -> anyhow::Result<Self> {
529 let file_writer = file_term::FileTerm::new(path)?;
530 Ok(Self {
531 indent: 0,
532 lock: Arc::new(Mutex::new(())),
533 verbosity: Verbosity::default(),
534 heading_count: 0,
535 max_width: 65535,
536 writer: Box::new(file_writer),
537 create_progress_printer: || Box::new(null_term::NullTerm {}),
538 start_time: std::time::Instant::now(),
539 })
540 }
541
542 pub fn new_null_term() -> Self {
543 Self {
544 indent: 0,
545 lock: Arc::new(Mutex::new(())),
546 verbosity: Verbosity::default(),
547 heading_count: 0,
548 max_width: 80,
549 writer: Box::new(null_term::NullTerm {}),
550 create_progress_printer: || Box::new(null_term::NullTerm {}),
551 start_time: std::time::Instant::now(),
552 }
553 }
554
555 pub(crate) fn write(&mut self, message: &str) -> anyhow::Result<()> {
556 let _lock = self.lock.lock().unwrap();
557 write!(self.writer, "{message}").context(format_context!(""))?;
558 Ok(())
559 }
560
561 pub fn newline(&mut self) -> anyhow::Result<()> {
562 self.write("\n")?;
563 Ok(())
564 }
565
566 pub fn trace<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
567 if is_verbosity_active(self.verbosity, Level::Trace) {
568 self.object(name, value)
569 } else {
570 Ok(())
571 }
572 }
573
574 pub fn debug<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
575 if is_verbosity_active(self.verbosity, Level::Debug) {
576 self.object(name, value)
577 } else {
578 Ok(())
579 }
580 }
581
582 pub fn message<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
583 if is_verbosity_active(self.verbosity, Level::Message) {
584 self.object(name, value)
585 } else {
586 Ok(())
587 }
588 }
589
590 pub fn info<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
591 if is_verbosity_active(self.verbosity, Level::Info) {
592 self.object(name, value)
593 } else {
594 Ok(())
595 }
596 }
597
598 pub fn warning<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
599 if is_verbosity_active(self.verbosity, Level::Warning) {
600 self.object(name.yellow().to_string().as_str(), value)
601 } else {
602 Ok(())
603 }
604 }
605
606 pub fn error<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
607 if is_verbosity_active(self.verbosity, Level::Error) {
608 self.object(name.red().to_string().as_str(), value)
609 } else {
610 Ok(())
611 }
612 }
613
614 pub fn log(&mut self, level: Level, message: &str) -> anyhow::Result<()> {
615 if is_verbosity_active(self.verbosity, level) {
616 self.write(
617 format_log(
618 self.indent,
619 self.max_width,
620 level,
621 message,
622 self.verbosity.is_show_elapsed_time,
623 self.start_time,
624 )
625 .as_str(),
626 )
627 } else {
628 Ok(())
629 }
630 }
631
632 pub fn code_block(&mut self, name: &str, content: &str) -> anyhow::Result<()> {
633 self.write(format!("```{name}\n{content}```\n").as_str())
634 .context(format_context!(""))?;
635 Ok(())
636 }
637
638 fn object<Type: Serialize>(&mut self, name: &str, value: &Type) -> anyhow::Result<()> {
639 let value = serde_json::to_value(value).context(format_context!(""))?;
640
641 if self.verbosity.level <= Level::Message && value == serde_json::Value::Null {
642 return Ok(());
643 }
644
645 self.write(
646 format!(
647 "{}{}: ",
648 " ".repeat(self.indent),
649 name.if_supports_color(Stdout, |text| text.bold())
650 )
651 .as_str(),
652 )?;
653
654 self.print_value(&value).context(format_context!(""))?;
655 Ok(())
656 }
657
658 fn enter_heading(&mut self) {
659 self.heading_count += 1;
660 }
661
662 fn exit_heading(&mut self) {
663 self.heading_count -= 1;
664 }
665
666 fn shift_right(&mut self) {
667 self.indent += 2;
668 }
669
670 fn shift_left(&mut self) {
671 self.indent -= 2;
672 }
673
674 fn print_value(&mut self, value: &serde_json::Value) -> anyhow::Result<()> {
675 match value {
676 serde_json::Value::Object(map) => {
677 self.write("\n").context(format_context!(""))?;
678 self.shift_right();
679 for (key, value) in map {
680 let is_skip =
681 *value == serde_json::Value::Null && self.verbosity.level > Level::Message;
682 if !is_skip {
683 {
684 self.write(
685 format!(
686 "{}{}: ",
687 " ".repeat(self.indent),
688 key.if_supports_color(Stdout, |text| text.bold())
689 )
690 .as_str(),
691 )
692 .context(format_context!(""))?;
693 }
694 self.print_value(value).context(format_context!(""))?;
695 }
696 }
697 self.shift_left();
698 }
699 serde_json::Value::Array(array) => {
700 self.write("\n").context(format_context!(""))?;
701 self.shift_right();
702 for (index, value) in array.iter().enumerate() {
703 self.write(format!("{}[{index}]: ", " ".repeat(self.indent)).as_str())?;
704 self.print_value(value).context(format_context!(""))?;
705 }
706 self.shift_left();
707 }
708 serde_json::Value::Null => {
709 self.write("null\n").context(format_context!(""))?;
710 }
711 serde_json::Value::Bool(value) => {
712 self.write(format!("{value}\n").as_str())
713 .context(format_context!(""))?;
714 }
715 serde_json::Value::Number(value) => {
716 self.write(format!("{value}\n").as_str())
717 .context(format_context!(""))?;
718 }
719 serde_json::Value::String(value) => {
720 self.write(format!("{value}\n").as_str())
721 .context(format_context!(""))?;
722 }
723 }
724
725 Ok(())
726 }
727
728 pub fn start_process(
729 &mut self,
730 command: &str,
731 options: &ExecuteOptions,
732 ) -> anyhow::Result<std::process::Child> {
733 let args = options.arguments.join(" ");
734 let full_command = format!("{command} {args}");
735
736 self.info("execute", &full_command)
737 .context(format_context!(""))?;
738 if let Some(directory) = &options.working_directory {
739 self.info("directory", &directory)
740 .context(format_context!(""))?;
741 if !std::path::Path::new(directory.as_ref()).exists() {
742 return Err(format_error!("Directory does not exist: {directory}"));
743 }
744 }
745
746 let child_process = options
747 .spawn(command)
748 .context(format_context!("{command}"))?;
749 Ok(child_process)
750 }
751
752 pub fn execute_process(
753 &mut self,
754 command: &str,
755 options: ExecuteOptions,
756 ) -> anyhow::Result<Option<String>> {
757 let section = Section::new(self, command).context(format_context!(""))?;
758 let child_process = section
759 .printer
760 .start_process(command, &options)
761 .context(format_context!("Faild to execute process: {command}"))?;
762 let mut multi_progress = MultiProgress::new(section.printer);
763 let mut progress_bar = multi_progress.add_progress("progress", None, None);
764 let result = monitor_process(command, child_process, &mut progress_bar, &options)
765 .context(format_context!(""))?;
766
767 Ok(result)
768 }
769}
770
771fn sanitize_output(input: &str, max_length: usize) -> String {
772 let escaped: Vec<_> = input.chars().flat_map(|c| c.escape_default()).collect();
775
776 let mut result = String::new();
777 let mut length = 0usize;
778 for character in escaped.into_iter() {
779 if length < max_length {
780 result.push(character);
781 length += 1;
782 }
783 }
784 while result.len() < max_length {
785 result.push(' ');
786 }
787
788 result
789}
790
791fn format_monitor_log_message(level: Level, source: &str, command: &str, message: &str) -> String {
792 if level == Level::Passthrough {
793 message.to_string()
794 } else {
795 format!("[{source}:{command}] {message}")
796 }
797}
798
799fn monitor_process(
800 command: &str,
801 mut child_process: std::process::Child,
802 progress_bar: &mut MultiProgressBar,
803 options: &ExecuteOptions,
804) -> anyhow::Result<Option<String>> {
805 let start_time = std::time::Instant::now();
806
807 let child_stdout = child_process
808 .stdout
809 .take()
810 .ok_or(format_error!("Internal Error: Child has no stdout"))?;
811
812 let child_stderr = child_process
813 .stderr
814 .take()
815 .ok_or(format_error!("Internal Error: Child has no stderr"))?;
816
817 let log_level_stdout = options.log_level;
818 let log_level_stderr = options.log_level;
819
820 let (stdout_thread, stdout_rx) = ExecuteOptions::process_child_output(child_stdout)?;
821 let (stderr_thread, stderr_rx) = ExecuteOptions::process_child_output(child_stderr)?;
822
823 let handle_stdout = |progress: &mut MultiProgressBar,
824 writer: Option<&mut std::fs::File>,
825 content: Option<&mut String>|
826 -> anyhow::Result<()> {
827 let mut stdout = String::new();
828 while let Ok(message) = stdout_rx.try_recv() {
829 if writer.is_some() || content.is_some() {
830 stdout.push_str(message.as_str());
831 stdout.push('\n');
832 }
833 progress.set_message(message.as_str());
834 if let Some(level) = log_level_stdout.as_ref() {
835 progress.log(
836 *level,
837 format_monitor_log_message(*level, "stdout", command, message.as_str())
838 .as_str(),
839 );
840 }
841 }
842
843 if let Some(content) = content {
844 content.push_str(stdout.as_str());
845 }
846
847 if let Some(writer) = writer {
848 let _ = writer.write_all(stdout.as_bytes());
849 }
850 Ok(())
851 };
852
853 let handle_stderr = |progress: &mut MultiProgressBar,
854 writer: Option<&mut std::fs::File>,
855 content: &mut String|
856 -> anyhow::Result<()> {
857 let mut stderr = String::new();
858 while let Ok(message) = stderr_rx.try_recv() {
859 stderr.push_str(message.as_str());
860 stderr.push('\n');
861 progress.set_message(message.as_str());
862 if let Some(level) = log_level_stderr.as_ref() {
863 progress.log(
864 *level,
865 format_monitor_log_message(*level, "stdout", command, message.as_str())
866 .as_str(),
867 );
868 }
869 }
870 content.push_str(stderr.as_str());
871
872 if let Some(writer) = writer {
873 let _ = writer.write_all(stderr.as_bytes());
874 }
875 Ok(())
876 };
877
878 let exit_status;
879
880 let mut stderr_content = String::new();
881 let mut stdout_content = String::new();
882
883 let mut output_file = if let Some(log_path) = options.log_file_path.as_ref() {
884 let mut file = std::fs::File::create(log_path.as_ref())
885 .context(format_context!("while creating {log_path}"))?;
886
887 let mut environment = HashMap::new();
888 const INHERITED: &str = "inherited";
889 const GIVEN: &str = "given";
890 environment.insert(INHERITED.into(), HashMap::new());
891 environment.insert(GIVEN.into(), HashMap::new());
892 let env_inherited = environment.get_mut(INHERITED).unwrap();
893 if !options.clear_environment {
894 for (key, value) in std::env::vars() {
895 env_inherited.insert(key.into(), value.into());
896 }
897 }
898 let env_given = environment.get_mut(GIVEN).unwrap();
899 for (key, value) in options.environment.iter() {
900 env_given.insert(key.clone(), value.clone());
901 }
902
903 let arguments = options.arguments.join(" ");
904 let arguments_escaped: Vec<_> =
905 arguments.chars().flat_map(|c| c.escape_default()).collect();
906 let args = arguments_escaped.into_iter().collect::<String>();
907 let shell = format!("{command} {args}").into();
908
909 let log_header = LogHeader {
910 command: command.into(),
911 working_directory: options.working_directory.clone(),
912 environment,
913 arguments: options.arguments.clone(),
914 shell,
915 };
916
917 let log_header_serialized = serde_yaml::to_string(&log_header).context(format_context!(
918 "Internal Error: failed to yamlize log header"
919 ))?;
920
921 let divider = Printer::get_log_divider();
922
923 file.write(format!("{log_header_serialized}{divider}\n").as_bytes())
924 .context(format_context!("while writing {log_path}"))?;
925
926 Some(file)
927 } else {
928 None
929 };
930
931 loop {
932 if let Ok(Some(status)) = child_process.try_wait() {
933 exit_status = Some(status);
934 break;
935 }
936
937 let stdout_content = if options.is_return_stdout {
938 Some(&mut stdout_content)
939 } else {
940 None
941 };
942
943 handle_stdout(progress_bar, output_file.as_mut(), stdout_content)
944 .context(format_context!("failed to handle stdout"))?;
945 handle_stderr(progress_bar, output_file.as_mut(), &mut stderr_content)
946 .context(format_context!("failed to handle stderr"))?;
947 std::thread::sleep(std::time::Duration::from_millis(100));
948 progress_bar.increment_with_overflow(1);
949
950 let now = std::time::Instant::now();
951
952 if let Some(timeout) = options.timeout {
953 if now - start_time > timeout {
954 child_process
955 .kill()
956 .context(format_context!("Failed to kill process"))?;
957 }
958 }
959 }
960
961 let _ = stdout_thread.join();
962 let _ = stderr_thread.join();
963
964 {
965 let stdout_content = if options.is_return_stdout {
966 Some(&mut stdout_content)
967 } else {
968 None
969 };
970
971 handle_stdout(progress_bar, output_file.as_mut(), stdout_content)
972 .context(format_context!("while handling stdout"))?;
973 }
974
975 handle_stderr(progress_bar, output_file.as_mut(), &mut stderr_content)
976 .context(format_context!("while handling stderr"))?;
977
978 if let Some(exit_status) = exit_status {
979 if !exit_status.success() {
980 if let Some(code) = exit_status.code() {
981 let exit_message = format!("Command failed with exit code: {code}");
982 return Err(format_error!("{exit_message} : {stderr_content}"));
983 } else {
984 return Err(format_error!(
985 "Command failed with unknown exit code: {stderr_content}"
986 ));
987 }
988 }
989 }
990
991 Ok(if options.is_return_stdout {
992 Some(stdout_content)
993 } else {
994 None
995 })
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001
1002 #[derive(Serialize)]
1003 pub struct Test {
1004 pub name: String,
1005 pub age: u32,
1006 pub alive: bool,
1007 pub dead: bool,
1008 pub children: f64,
1009 }
1010
1011 #[test]
1012 fn printer() {
1013 let mut printer = Printer::new_stdout();
1014 let mut options = ExecuteOptions::default();
1015 options.arguments.push("-alt".into());
1016
1017 let runtime =
1018 tokio::runtime::Runtime::new().expect("Internal Error: Failed to create runtime");
1019
1020 let (async_sender, sync_receiver) = flume::bounded(1);
1021 runtime.spawn(async move {
1022 async_sender.send_async(10).await.expect("Failed to send");
1023 });
1024 let received = sync_receiver.recv().expect("Failed to receive");
1025
1026 drop(runtime);
1027
1028 printer.info("Received", &received).unwrap();
1029
1030 printer.execute_process("/bin/ls", options).unwrap();
1031
1032 {
1033 let mut heading = Heading::new(&mut printer, "First").unwrap();
1034 {
1035 let section = Section::new(&mut heading.printer, "PersonWrapper").unwrap();
1036 section
1037 .printer
1038 .object(
1039 "Person",
1040 &Test {
1041 name: "John".to_string(),
1042 age: 30,
1043 alive: true,
1044 dead: false,
1045 children: 2.5,
1046 },
1047 )
1048 .unwrap();
1049 }
1050
1051 let mut sub_heading = Heading::new(&mut heading.printer, "Second").unwrap();
1052
1053 let mut sub_section = Section::new(&mut sub_heading.printer, "PersonWrapper").unwrap();
1054 sub_section.printer.object("Hello", &"World").unwrap();
1055
1056 {
1057 let mut multi_progress = MultiProgress::new(&mut sub_section.printer);
1058 let mut first = multi_progress.add_progress("First", Some(10), None);
1059 let mut second = multi_progress.add_progress("Second", Some(50), None);
1060 let mut third = multi_progress.add_progress("Third", Some(100), None);
1061
1062 let first_handle = std::thread::spawn(move || {
1063 first.set_ending_message("Done!");
1064 for index in 0..10 {
1065 first.increment(1);
1066 if index == 5 {
1067 first.set_message("half way");
1068 }
1069 std::thread::sleep(std::time::Duration::from_millis(100));
1070 }
1071 });
1072
1073 let second_handle = std::thread::spawn(move || {
1074 for index in 0..50 {
1075 second.increment(1);
1076 if index == 25 {
1077 second.set_message("half way");
1078 }
1079 std::thread::sleep(std::time::Duration::from_millis(10));
1080 }
1081 });
1082
1083 for _ in 0..100 {
1084 third.increment(1);
1085 std::thread::sleep(std::time::Duration::from_millis(10));
1086 }
1087
1088 first_handle.join().unwrap();
1089 second_handle.join().unwrap();
1090 }
1091 }
1092
1093 {
1094 let runtime =
1095 tokio::runtime::Runtime::new().expect("Internal Error: Failed to create runtime");
1096
1097 let heading = Heading::new(&mut printer, "Async").unwrap();
1098
1099 let mut multi_progress = MultiProgress::new(heading.printer);
1100
1101 let mut handles = Vec::new();
1102
1103 let task1_progress = multi_progress.add_progress("Task1", Some(30), None);
1104 let task2_progress = multi_progress.add_progress("Task2", Some(30), None);
1105 let task1 = async move {
1106 let mut progress = task1_progress;
1107 progress.set_message("Task1a");
1108 for _ in 0..10 {
1109 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1110 progress.increment(1);
1111 }
1112
1113 progress.set_message("Task1b");
1114 for _ in 0..10 {
1115 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1116 progress.increment(1);
1117 }
1118
1119 progress.set_message("Task1c");
1120 for _ in 0..10 {
1121 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1122 progress.increment(1);
1123 }
1124 ()
1125 };
1126 handles.push(runtime.spawn(task1));
1127
1128 let task2 = async move {
1129 let mut progress = task2_progress;
1130 progress.set_message("Task2a");
1131 for _ in 0..10 {
1132 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1133 progress.increment(1);
1134 }
1135
1136 progress.set_message("Task2b");
1137 for _ in 0..10 {
1138 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1139 progress.increment(1);
1140 }
1141
1142 progress.set_message("Task2c");
1143 for _ in 0..10 {
1144 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1145 progress.increment(1);
1146 }
1147 ()
1148 };
1149 handles.push(runtime.spawn(task2));
1150
1151 for handle in handles {
1152 runtime.block_on(handle).unwrap();
1153 }
1154 }
1155 }
1156}