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