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, 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 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}