spaces_printer/
lib.rs

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, // This is the default from indicatif?
344            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    //remove all backspaces and truncate
773
774    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}