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