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