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