spaces_printer/
lib.rs

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, // This is the default from indicatif?
340            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    //remove all backspaces and truncate
760
761    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}