Skip to main content

tracexec_core/
printer.rs

1use std::{
2  cell::RefCell,
3  collections::BTreeMap,
4  fmt::{
5    Debug,
6    Display,
7  },
8  io::{
9    self,
10    Write,
11  },
12  sync::Arc,
13};
14
15use itertools::chain;
16use nix::{
17  fcntl::OFlag,
18  libc::ENOENT,
19  unistd::Pid,
20};
21use owo_colors::{
22  OwoColorize,
23  Style,
24};
25
26use crate::{
27  cache::ArcStr,
28  cli::{
29    args::{
30      LogModeArgs,
31      ModifierArgs,
32    },
33    theme::THEME,
34  },
35  event::{
36    FriendlyError,
37    OutputMsg,
38  },
39  proc::{
40    BaselineInfo,
41    FileDescriptorInfo,
42    FileDescriptorInfoCollection,
43    Interpreter,
44    diff_env,
45  },
46  timestamp::TimestampFormat,
47  tracer::ExecData,
48};
49
50macro_rules! escape_str_for_bash {
51  ($x:expr) => {{
52    let result: String = shell_quote::QuoteRefExt::quoted($x, shell_quote::Bash);
53    result
54  }};
55}
56
57#[derive(Debug, Clone, Copy)]
58pub enum EnvPrintFormat {
59  Diff,
60  Raw,
61  None,
62}
63
64#[derive(Debug, Clone, Copy)]
65pub enum FdPrintFormat {
66  Diff,
67  Raw,
68  None,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
72pub enum ColorLevel {
73  Less,
74  Normal,
75  More,
76}
77
78#[derive(Debug, Clone)]
79pub struct PrinterArgs {
80  pub trace_comm: bool,
81  pub trace_argv: bool,
82  pub trace_env: EnvPrintFormat,
83  pub trace_fd: FdPrintFormat,
84  pub trace_cwd: bool,
85  pub print_cmdline: bool,
86  pub successful_only: bool,
87  pub trace_interpreter: bool,
88  pub trace_filename: bool,
89  pub decode_errno: bool,
90  pub color: ColorLevel,
91  pub stdio_in_cmdline: bool,
92  pub fd_in_cmdline: bool,
93  pub hide_cloexec_fds: bool,
94  pub inline_timestamp_format: Option<TimestampFormat>,
95}
96
97impl PrinterArgs {
98  pub fn from_cli(tracing_args: &LogModeArgs, modifier_args: &ModifierArgs) -> Self {
99    Self {
100      trace_comm: !tracing_args.no_show_comm,
101      trace_argv: !tracing_args.no_show_argv && !tracing_args.show_cmdline,
102      trace_env: match (
103        tracing_args.show_cmdline,
104        tracing_args.diff_env,
105        tracing_args.no_diff_env,
106        tracing_args.show_env,
107        tracing_args.no_show_env,
108      ) {
109        (true, ..) | (.., true) => EnvPrintFormat::None,
110        (false, .., true, _) | (false, _, true, ..) => EnvPrintFormat::Raw,
111        _ => EnvPrintFormat::Diff, // diff_env is enabled by default
112      },
113      trace_fd: match (
114        tracing_args.diff_fd,
115        tracing_args.no_diff_fd,
116        tracing_args.show_fd,
117        tracing_args.no_show_fd,
118      ) {
119        (false, _, true, false) => FdPrintFormat::Raw,
120        (_, true, _, _) => FdPrintFormat::None,
121        (true, _, _, _) => FdPrintFormat::Diff,
122        _ => {
123          // The default is diff fd,
124          // but if fd_in_cmdline or stdio_in_cmdline is enabled, we disable diff fd by default
125          if modifier_args.fd_in_cmdline || modifier_args.stdio_in_cmdline {
126            FdPrintFormat::None
127          } else {
128            FdPrintFormat::Diff
129          }
130        }
131      },
132      trace_cwd: tracing_args.show_cwd,
133      print_cmdline: tracing_args.show_cmdline,
134      successful_only: modifier_args.successful_only,
135      trace_interpreter: tracing_args.show_interpreter,
136      trace_filename: match (tracing_args.show_filename, tracing_args.no_show_filename) {
137        (_, true) => false,
138        (true, _) => true,
139        // default
140        _ => true,
141      },
142      decode_errno: !tracing_args.no_decode_errno,
143      color: match (tracing_args.more_colors, tracing_args.less_colors) {
144        (false, false) => ColorLevel::Normal,
145        (true, false) => ColorLevel::More,
146        (false, true) => ColorLevel::Less,
147        _ => unreachable!(),
148      },
149      stdio_in_cmdline: modifier_args.stdio_in_cmdline,
150      fd_in_cmdline: modifier_args.fd_in_cmdline,
151      hide_cloexec_fds: modifier_args.hide_cloexec_fds,
152      inline_timestamp_format: modifier_args.timestamp.then(|| {
153        // We ensure a default is set in modifier_args
154        #[expect(clippy::unwrap_used)]
155        let value = modifier_args.inline_timestamp_format.clone().unwrap();
156        value
157      }),
158    }
159  }
160}
161
162pub type PrinterOut = dyn Write + Send + Sync + 'static;
163
164enum DeferredWarningKind {
165  NoArgv0,
166  FailedReadingArgv(FriendlyError),
167  FailedReadingFilename(FriendlyError),
168  FailedReadingEnvp(FriendlyError),
169}
170
171struct DeferredWarnings {
172  warning: DeferredWarningKind,
173  pid: Pid,
174}
175
176impl Drop for DeferredWarnings {
177  #[expect(clippy::unwrap_used)]
178  fn drop(&mut self) {
179    Printer::OUT.with_borrow_mut(|out| {
180      if let Some(out) = out {
181        write!(out, "{}", self.pid.bright_red()).unwrap();
182        write!(out, "[{}]: ", "warning".bright_yellow()).unwrap();
183        match self.warning {
184          DeferredWarningKind::NoArgv0 => {
185            write!(
186              out,
187              "No argv[0] provided! The printed commandline might be incorrect!"
188            )
189            .unwrap();
190          }
191          DeferredWarningKind::FailedReadingArgv(e) => {
192            write!(out, "Failed to read argv: {e}").unwrap();
193          }
194          DeferredWarningKind::FailedReadingFilename(e) => {
195            write!(out, "Failed to read filename: {e}").unwrap();
196          }
197          DeferredWarningKind::FailedReadingEnvp(e) => {
198            write!(out, "Failed to read envp: {e}").unwrap();
199          }
200        };
201        writeln!(out).unwrap();
202      };
203    })
204  }
205}
206
207pub struct ListPrinter {
208  style: owo_colors::Style,
209}
210
211impl ListPrinter {
212  pub fn new(color: ColorLevel) -> Self {
213    if color > ColorLevel::Normal {
214      Self {
215        style: Style::new().bright_white().bold(),
216      }
217    } else {
218      Self {
219        style: Style::new(),
220      }
221    }
222  }
223
224  pub fn begin(&self, out: &mut dyn Write) -> io::Result<()> {
225    write!(out, "{}", "[".style(self.style))
226  }
227
228  pub fn end(&self, out: &mut dyn Write) -> io::Result<()> {
229    write!(out, "{}", "]".style(self.style))
230  }
231
232  pub fn comma(&self, out: &mut dyn Write) -> io::Result<()> {
233    write!(out, "{}", ", ".style(self.style))
234  }
235
236  pub fn print_string_list(&self, out: &mut dyn Write, list: &[impl Display]) -> io::Result<()> {
237    self.begin(out)?;
238    if let Some((last, rest)) = list.split_last() {
239      #[allow(clippy::branches_sharing_code)]
240      if rest.is_empty() {
241        write!(out, "{last}")?;
242      } else {
243        for s in rest {
244          write!(out, "{s}")?;
245          self.comma(out)?;
246        }
247        write!(out, "{last}")?;
248      }
249    }
250    self.end(out)
251  }
252
253  pub fn print_env(
254    &self,
255    out: &mut dyn Write,
256    env: &BTreeMap<OutputMsg, OutputMsg>,
257  ) -> io::Result<()> {
258    self.begin(out)?;
259    let mut first_item_written = false;
260    let mut write_separator = |out: &mut dyn Write| -> io::Result<()> {
261      if first_item_written {
262        self.comma(out)?;
263      } else {
264        first_item_written = true;
265      }
266      Ok(())
267    };
268    for (k, v) in env.iter() {
269      write_separator(out)?;
270      write!(out, "{k}={v}")?;
271    }
272    self.end(out)
273  }
274}
275
276pub struct Printer {
277  pub args: PrinterArgs,
278  baseline: Arc<BaselineInfo>,
279}
280
281impl Printer {
282  pub fn new(args: PrinterArgs, baseline: Arc<BaselineInfo>) -> Self {
283    Self { args, baseline }
284  }
285
286  thread_local! {
287    pub static OUT: RefCell<Option<Box<PrinterOut>>> = RefCell::new(None);
288  }
289
290  pub fn init_thread_local(&self, output: Option<Box<PrinterOut>>) {
291    Self::OUT.with(|out| {
292      *out.borrow_mut() = output;
293    });
294  }
295
296  pub fn print_new_child(&self, parent: Pid, comm: &str, child: Pid) -> color_eyre::Result<()> {
297    Self::OUT.with_borrow_mut(|out| {
298      let Some(out) = out else {
299        return Ok(());
300      };
301      write!(out, "{}", parent.bright_green())?;
302      if self.args.trace_comm {
303        write!(out, "<{}>", comm.cyan())?;
304      }
305      writeln!(out, ": {}: {}", "new child".purple(), child.bright_green())?;
306      out.flush()?;
307      Ok(())
308    })
309  }
310
311  fn print_stdio_fd(
312    &self,
313    out: &mut dyn Write,
314    fd: i32,
315    orig_fd: &FileDescriptorInfo,
316    curr_fd: Option<&FileDescriptorInfo>,
317    fd_collection_reliable: bool,
318    list_printer: &ListPrinter,
319  ) -> io::Result<()> {
320    let desc = match fd {
321      0 => "stdin",
322      1 => "stdout",
323      2 => "stderr",
324      _ => unreachable!(),
325    };
326    if let Some(fdinfo) = curr_fd {
327      if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
328        if !self.args.hide_cloexec_fds {
329          write!(
330            out,
331            "{}{}",
332            "cloexec: ".bright_red().bold(),
333            desc.bright_red().bold()
334          )?;
335        } else {
336          write!(
337            out,
338            "{}{}",
339            "closed: ".bright_red().bold(),
340            desc.bright_red().bold()
341          )?;
342        }
343        list_printer.comma(out)?;
344      } else if fdinfo.not_same_file_as(orig_fd) {
345        write!(out, "{}", desc.bright_yellow().bold())?;
346        write!(out, "={}", fdinfo.path.bright_yellow())?;
347        list_printer.comma(out)?;
348      }
349    } else if fd_collection_reliable {
350      write!(
351        out,
352        "{}{}",
353        "closed: ".bright_red().bold(),
354        desc.bright_red().bold()
355      )?;
356      list_printer.comma(out)?;
357    }
358    Ok(())
359  }
360
361  fn print_stdio_fds_in_cmdline(
362    &self,
363    out: &mut dyn Write,
364    curr_fds: &FileDescriptorInfoCollection,
365  ) -> io::Result<()> {
366    for (fd, orig_fd) in self.baseline.fdinfo.stdio() {
367      let (redirect, closed) = match fd {
368        0 => ("<", "0>&-"),
369        1 => (">", "1>&-"),
370        2 => ("2>", "2>&-"),
371        _ => unreachable!(),
372      };
373
374      if let Some(fdinfo) = curr_fds.get(fd) {
375        if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
376          write!(out, " {}", closed.bright_red().bold().italic())?;
377        } else if fdinfo.not_same_file_as(orig_fd) {
378          write!(
379            out,
380            " {}{}",
381            redirect.bright_yellow().bold(),
382            fdinfo.path.cli_bash_escaped_with_style(THEME.modified_fd)
383          )?;
384        }
385      } else if curr_fds.is_reliable() {
386        // If the fd collection is reliable, we can be sure that the file is closed
387        write!(out, " {}", closed.bright_red().bold())?;
388      }
389    }
390
391    Ok(())
392  }
393
394  pub fn print_fd(
395    &self,
396    out: &mut dyn Write,
397    fds: &FileDescriptorInfoCollection,
398  ) -> io::Result<()> {
399    match self.args.trace_fd {
400      FdPrintFormat::Diff => {
401        write!(out, " {} ", "fd".purple())?;
402        let list_printer = ListPrinter::new(self.args.color);
403        list_printer.begin(out)?;
404        if let Some(error) = &fds.error {
405          write!(out, "{}", <&'static str>::from(error).bright_red())?;
406          list_printer.comma(out)?;
407        }
408        // Stdio
409        for (fd, fdinfo_orig) in self.baseline.fdinfo.stdio() {
410          self.print_stdio_fd(
411            out,
412            fd,
413            fdinfo_orig,
414            fds.fdinfo.get(&fd),
415            fds.is_reliable(),
416            &list_printer,
417          )?;
418        }
419        for (&fd, fdinfo) in fds.fdinfo.iter() {
420          if fd < 3 {
421            continue;
422          }
423          if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
424            if !self.args.hide_cloexec_fds {
425              write!(
426                out,
427                "{} {}",
428                "cloexec:".bright_red().bold(),
429                fd.bright_green().bold()
430              )?;
431              write!(out, "={}", fdinfo.path.bright_red())?;
432              list_printer.comma(out)?;
433            }
434          } else {
435            write!(out, "{}", fd.bright_green().bold())?;
436            write!(out, "={}", fdinfo.path.bright_green())?;
437            list_printer.comma(out)?;
438          }
439        }
440        list_printer.end(out)?;
441      }
442      FdPrintFormat::Raw => {
443        write!(out, " {} ", "fd".purple())?;
444        let list_printer = ListPrinter::new(self.args.color);
445        list_printer.begin(out)?;
446        if let Some(error) = &fds.error {
447          write!(out, "{}", <&'static str>::from(error).bright_red())?;
448          if !fds.fdinfo.is_empty() {
449            list_printer.comma(out)?;
450          }
451        }
452        let last = fds.fdinfo.len().saturating_sub(1);
453        for (idx, (fd, fdinfo)) in fds.fdinfo.iter().enumerate() {
454          if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
455            if self.args.hide_cloexec_fds {
456              continue;
457            }
458            write!(out, "{}", fd.bright_red().bold())?;
459          } else {
460            write!(out, "{}", fd.bright_cyan().bold())?;
461          }
462          write!(out, "={}", fdinfo.path)?;
463          if idx != last {
464            list_printer.comma(out)?;
465          }
466        }
467        list_printer.end(out)?;
468      }
469      FdPrintFormat::None => {}
470    }
471    Ok(())
472  }
473
474  pub fn print_exec_trace(
475    &self,
476    pid: Pid,
477    comm: ArcStr,
478    result: i64,
479    exec_data: &ExecData,
480    env: &BTreeMap<OutputMsg, OutputMsg>,
481    cwd: &OutputMsg,
482  ) -> color_eyre::Result<()> {
483    // Preconditions:
484    // 1. execve syscall exit, which leads to 2
485    // 2. state.exec_data is Some
486
487    // Defer the warnings so that they are printed after the main message
488    #[allow(clippy::collection_is_never_read)]
489    let mut _deferred_warnings = vec![];
490
491    Self::OUT.with_borrow_mut(|out| {
492      let Some(out) = out else {
493        return Ok(());
494      };
495      let list_printer = ListPrinter::new(self.args.color);
496      if let Some(f) = self.args.inline_timestamp_format.as_deref() {
497        write!(out, "{} ", exec_data.timestamp.format(f).bright_cyan())?;
498      }
499      if result == 0 {
500        write!(out, "{}", pid.bright_green())?;
501      } else if result == -ENOENT as i64 {
502        write!(out, "{}", pid.bright_yellow())?;
503      } else {
504        write!(out, "{}", pid.bright_red())?;
505      }
506      if self.args.trace_comm {
507        write!(out, "<{}>", comm.cyan())?;
508      }
509      write!(out, ":")?;
510
511      if self.args.trace_filename {
512        write!(
513          out,
514          " {}",
515          exec_data.filename.cli_escaped_styled(THEME.filename)
516        )?;
517      }
518      if let OutputMsg::Err(e) = exec_data.filename {
519        _deferred_warnings.push(DeferredWarnings {
520          warning: DeferredWarningKind::FailedReadingFilename(e),
521          pid,
522        });
523      }
524      match exec_data.argv.as_ref() {
525        Err(e) => {
526          _deferred_warnings.push(DeferredWarnings {
527            warning: DeferredWarningKind::FailedReadingArgv(FriendlyError::InspectError(*e)),
528            pid,
529          });
530        }
531        Ok(argv) => {
532          if self.args.trace_argv {
533            write!(out, " ")?;
534            list_printer.print_string_list(out, argv)?;
535          }
536        }
537      }
538
539      // CWD
540
541      if self.args.trace_cwd {
542        write!(
543          out,
544          " {} {}",
545          "at".purple(),
546          exec_data
547            .cwd
548            .cli_escaped_styled(if self.args.color >= ColorLevel::Normal {
549              THEME.cwd
550            } else {
551              THEME.plain
552            })
553        )?;
554      }
555
556      // Interpreter
557
558      if self.args.trace_interpreter
559        && result == 0
560        && let Some(interpreters) = exec_data.interpreters.as_ref()
561      {
562        // FIXME: show interpreter for errnos other than ENOENT
563        write!(out, " {} ", "interpreter".purple(),)?;
564        match interpreters.len() {
565          0 => {
566            write!(out, "{}", Interpreter::None)?;
567          }
568          1 => {
569            write!(out, "{}", interpreters[0])?;
570          }
571          _ => {
572            list_printer.begin(out)?;
573            for (idx, interpreter) in interpreters.iter().enumerate() {
574              if idx != 0 {
575                list_printer.comma(out)?;
576              }
577              write!(out, "{interpreter}")?;
578            }
579            list_printer.end(out)?;
580          }
581        }
582      }
583
584      // File descriptors
585
586      self.print_fd(out, &exec_data.fdinfo)?;
587
588      // Environment
589
590      match exec_data.envp.as_ref() {
591        Ok(envp) => {
592          match self.args.trace_env {
593            EnvPrintFormat::Diff => {
594              write!(out, " {} ", "with".purple())?;
595              list_printer.begin(out)?;
596              let env = env.clone();
597              let mut first_item_written = false;
598              let mut write_separator = |out: &mut dyn Write| -> io::Result<()> {
599                if first_item_written {
600                  list_printer.comma(out)?;
601                } else {
602                  first_item_written = true;
603                }
604                Ok(())
605              };
606
607              let diff = diff_env(&env, envp);
608              for (k, v) in &diff.added {
609                write_separator(out)?;
610                write!(
611                  out,
612                  "{}{}{}{}",
613                  "+".bright_green().bold(),
614                  k.cli_escaped_styled(THEME.added_env_var),
615                  "=".bright_green().bold(),
616                  v.cli_escaped_styled(THEME.added_env_var)
617                )?;
618              }
619              for (k, _, v) in diff.modified_with_values(&env) {
620                write_separator(out)?;
621                write!(
622                  out,
623                  "{}{}{}{}",
624                  "M".bright_yellow().bold(),
625                  k.cli_escaped_styled(THEME.modified_env_key),
626                  "=".bright_yellow().bold(),
627                  v.cli_escaped_styled(THEME.modified_env_val)
628                )?;
629              }
630              // Now we have the tracee removed entries in env
631              for (k, value) in diff.removed_with_values(&env) {
632                write_separator(out)?;
633                write!(
634                  out,
635                  "{}{}{}{}",
636                  "-".bright_red().bold(),
637                  k.cli_escaped_styled(THEME.removed_env_var),
638                  "=".bright_red().strikethrough(),
639                  value.cli_escaped_styled(THEME.removed_env_var)
640                )?;
641              }
642              list_printer.end(out)?;
643              // Avoid trailing color
644              // https://unix.stackexchange.com/questions/212933/background-color-whitespace-when-end-of-the-terminal-reached
645              if owo_colors::control::should_colorize() {
646                write!(out, "\x1B[49m\x1B[K")?;
647              }
648            }
649            EnvPrintFormat::Raw => {
650              write!(out, " {} ", "with".purple())?;
651              list_printer.print_env(out, envp)?;
652            }
653            EnvPrintFormat::None => (),
654          }
655        }
656        Err(e) => {
657          match self.args.trace_env {
658            EnvPrintFormat::Diff | EnvPrintFormat::Raw => {
659              write!(
660                out,
661                " {} {}",
662                "with".purple(),
663                format!("[Failed to read envp: {e}]")
664                  .bright_red()
665                  .blink()
666                  .bold()
667              )?;
668            }
669            EnvPrintFormat::None => {}
670          }
671          _deferred_warnings.push(DeferredWarnings {
672            warning: DeferredWarningKind::FailedReadingEnvp(FriendlyError::InspectError(*e)),
673            pid,
674          });
675        }
676      }
677
678      // Command line
679
680      if self.args.print_cmdline {
681        write!(out, " {}", "cmdline".purple())?;
682        write!(out, " env")?;
683
684        if self.args.stdio_in_cmdline {
685          self.print_stdio_fds_in_cmdline(out, &exec_data.fdinfo)?;
686        }
687
688        if self.args.fd_in_cmdline {
689          for (&fd, fdinfo) in exec_data.fdinfo.fdinfo.iter() {
690            if fd < 3 {
691              continue;
692            }
693            if fdinfo.flags.ok().is_none() || fdinfo.flags.contains(OFlag::O_CLOEXEC) {
694              // Don't show fds that will be closed upon exec
695              continue;
696            }
697            write!(
698              out,
699              " {}{}{}",
700              fd.bright_green().bold(),
701              "<>".bright_green().bold(),
702              fdinfo.path.cli_bash_escaped_with_style(THEME.added_fd)
703            )?;
704          }
705        }
706
707        match exec_data.argv.as_ref() {
708          Ok(argv) => {
709            if let Some(arg0) = argv.first() {
710              // filename warning is already handled
711              if &exec_data.filename != arg0 {
712                write!(
713                  out,
714                  " {} {}",
715                  "-a".bright_white().italic(),
716                  escape_str_for_bash!(arg0.as_ref()).bright_white().italic()
717                )?;
718              }
719            } else {
720              _deferred_warnings.push(DeferredWarnings {
721                warning: DeferredWarningKind::NoArgv0,
722                pid,
723              });
724            }
725            if cwd != &exec_data.cwd {
726              if self.args.color >= ColorLevel::Normal {
727                write!(
728                  out,
729                  " -C {}",
730                  exec_data.cwd.cli_bash_escaped_with_style(THEME.cwd)
731                )?;
732              } else {
733                write!(out, " -C {}", exec_data.cwd.bash_escaped())?;
734              }
735            }
736            // envp warning is already handled
737            if let Ok(envp) = exec_data.envp.as_ref() {
738              let diff = diff_env(env, envp);
739              let need_env_argument_separator =
740                diff.need_env_argument_separator(&exec_data.filename);
741              // Now we have the tracee removed entries in env
742              for k in diff.removed.into_iter() {
743                if self.args.color >= ColorLevel::Normal {
744                  write!(
745                    out,
746                    " {}{}",
747                    "-u ".bright_red(),
748                    k.cli_bash_escaped_with_style(THEME.removed_env_key)
749                  )?;
750                } else {
751                  write!(out, " -u {}", k.bash_escaped())?;
752                }
753              }
754              if need_env_argument_separator {
755                write!(out, " --")?;
756              }
757              if self.args.color >= ColorLevel::Normal {
758                for (k, v) in diff.added.into_iter() {
759                  write!(
760                    out,
761                    " {}{}{}",
762                    k.cli_bash_escaped_with_style(THEME.added_env_var),
763                    "=".green().bold(),
764                    v.cli_bash_escaped_with_style(THEME.added_env_var)
765                  )?;
766                }
767                for (k, v) in diff.modified.into_iter() {
768                  write!(
769                    out,
770                    " {}{}{}",
771                    k.bash_escaped(),
772                    "=".bright_yellow().bold(),
773                    v.cli_bash_escaped_with_style(THEME.modified_env_val)
774                  )?;
775                }
776              } else {
777                for (k, v) in chain!(diff.added.into_iter(), diff.modified.into_iter()) {
778                  write!(out, " {}={}", k.bash_escaped(), v.bash_escaped())?;
779                }
780              }
781            }
782            write!(out, " {}", exec_data.filename.bash_escaped())?;
783            for arg in argv.iter().skip(1) {
784              write!(out, " {}", arg.bash_escaped())?;
785            }
786          }
787          Err(e) => {
788            _deferred_warnings.push(DeferredWarnings {
789              warning: DeferredWarningKind::FailedReadingArgv(FriendlyError::InspectError(*e)),
790              pid,
791            });
792          }
793        }
794      }
795
796      // Result
797
798      if result == 0 {
799        writeln!(out)?;
800      } else {
801        write!(out, " {} ", "=".purple())?;
802        if self.args.decode_errno {
803          writeln!(
804            out,
805            "{} ({})",
806            result.bright_red().bold(),
807            nix::errno::Errno::from_raw(-result as i32).red()
808          )?;
809        } else {
810          writeln!(out, "{}", result.bright_red().bold())?;
811        }
812      }
813      // It is critical to call [flush] before BufWriter<W> is dropped.
814      // Though dropping will attempt to flush the contents of the buffer, any errors that happen in the process of dropping will be ignored.
815      // Calling [flush] ensures that the buffer is empty and thus dropping will not even attempt file operations.
816      out.flush()?;
817      Ok(())
818    })
819  }
820}
821
822#[cfg(test)]
823mod tests {
824  use std::{
825    collections::BTreeMap,
826    io,
827    sync::{
828      Arc,
829      Mutex,
830    },
831  };
832
833  use chrono::Local;
834  use nix::{
835    errno::Errno,
836    fcntl::OFlag,
837    unistd::Pid,
838  };
839  use test_that::prelude::*;
840
841  use super::*;
842  use crate::{
843    proc::{
844      CgroupInfo,
845      Cred,
846    },
847    timestamp::TimestampFormat,
848  };
849
850  #[derive(Clone, Default)]
851  struct CaptureWriter(Arc<Mutex<Vec<u8>>>);
852
853  impl Write for CaptureWriter {
854    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
855      self.0.lock().unwrap().extend_from_slice(buf);
856      Ok(buf.len())
857    }
858
859    fn flush(&mut self) -> io::Result<()> {
860      Ok(())
861    }
862  }
863
864  fn msg(value: &str) -> OutputMsg {
865    OutputMsg::Ok(ArcStr::from(value))
866  }
867
868  fn fd(fd: i32, path: &str, ino: u64, flags: OFlag) -> FileDescriptorInfo {
869    FileDescriptorInfo {
870      fd,
871      path: msg(path),
872      pos: 0.into(),
873      flags: flags.into(),
874      mnt_id: 1.into(),
875      ino: ino.into(),
876      mnt: ArcStr::from("mnt"),
877      extra: Vec::new(),
878    }
879  }
880
881  fn fd_collection(
882    entries: impl IntoIterator<Item = FileDescriptorInfo>,
883  ) -> FileDescriptorInfoCollection {
884    FileDescriptorInfoCollection {
885      fdinfo: entries.into_iter().map(|info| (info.fd, info)).collect(),
886      error: None,
887    }
888  }
889
890  fn baseline() -> Arc<BaselineInfo> {
891    let mut env = BTreeMap::new();
892    env.insert(msg("KEEP"), msg("same"));
893    env.insert(msg("MODIFIED"), msg("old"));
894    env.insert(msg("REMOVED"), msg("gone"));
895
896    Arc::new(BaselineInfo {
897      cwd: msg("/baseline"),
898      env,
899      fdinfo: fd_collection([
900        fd(0, "/dev/stdin", 10, OFlag::empty()),
901        fd(1, "/dev/stdout", 11, OFlag::empty()),
902        fd(2, "/dev/stderr", 12, OFlag::empty()),
903      ]),
904    })
905  }
906
907  fn printer_args() -> PrinterArgs {
908    PrinterArgs {
909      trace_comm: true,
910      trace_argv: true,
911      trace_env: EnvPrintFormat::Diff,
912      trace_fd: FdPrintFormat::Diff,
913      trace_cwd: true,
914      print_cmdline: false,
915      successful_only: false,
916      trace_interpreter: true,
917      trace_filename: true,
918      decode_errno: true,
919      color: ColorLevel::Less,
920      stdio_in_cmdline: false,
921      fd_in_cmdline: false,
922      hide_cloexec_fds: false,
923      inline_timestamp_format: None,
924    }
925  }
926
927  fn exec_data(
928    argv: Result<Vec<OutputMsg>, Errno>,
929    envp: Result<BTreeMap<OutputMsg, OutputMsg>, Errno>,
930    fdinfo: FileDescriptorInfoCollection,
931  ) -> ExecData {
932    ExecData::new(
933      Pid::from_raw(123),
934      msg("/bin/echo"),
935      argv,
936      envp,
937      false,
938      Ok(Cred::default()),
939      msg("/exec-cwd"),
940      Some(vec![
941        Interpreter::Shebang(ArcStr::from("/usr/bin/env sh")),
942        Interpreter::None,
943      ]),
944      fdinfo,
945      Local::now(),
946      CgroupInfo::NotCollected,
947    )
948  }
949
950  fn run_with_output(printer: &Printer, f: impl FnOnce() -> color_eyre::Result<()>) -> String {
951    let bytes = Arc::new(Mutex::new(Vec::new()));
952    printer.init_thread_local(Some(Box::new(CaptureWriter(bytes.clone()))));
953    owo_colors::control::set_should_colorize(false);
954    f().unwrap();
955    printer.init_thread_local(None);
956    String::from_utf8(bytes.lock().unwrap().clone()).unwrap()
957  }
958
959  #[test]
960  fn list_printer_formats_lists_and_env_maps() {
961    let list = ListPrinter::new(ColorLevel::Less);
962    let mut out = Vec::new();
963
964    list.print_string_list(&mut out, &["one", "two"]).unwrap();
965    assert_eq!(String::from_utf8(out).unwrap(), "[one, two]");
966
967    let mut env = BTreeMap::new();
968    env.insert(msg("A"), msg("1"));
969    env.insert(msg("B"), msg("2"));
970    let mut out = Vec::new();
971    list.print_env(&mut out, &env).unwrap();
972    assert_eq!(
973      String::from_utf8(out).unwrap(),
974      "[\"A\"=\"1\", \"B\"=\"2\"]"
975    );
976  }
977
978  #[test]
979  fn print_fd_covers_diff_raw_hidden_and_none_modes() {
980    owo_colors::control::set_should_colorize(false);
981    let baseline = baseline();
982    let fds = fd_collection([
983      fd(1, "/tmp/stdout.log", 20, OFlag::empty()),
984      fd(2, "/dev/stderr", 12, OFlag::O_CLOEXEC),
985      fd(3, "/tmp/extra", 30, OFlag::empty()),
986      fd(4, "/tmp/closed-on-exec", 40, OFlag::O_CLOEXEC),
987    ]);
988
989    let mut args = printer_args();
990    args.trace_fd = FdPrintFormat::Diff;
991    let printer = Printer::new(args.clone(), baseline.clone());
992    let mut out = Vec::new();
993    printer.print_fd(&mut out, &fds).unwrap();
994    let rendered = String::from_utf8(out).unwrap();
995    dbg!(&rendered);
996    assert_that!(rendered, contains_substring("closed: stdin"));
997    assert_that!(rendered, contains_substring("stdout=\"/tmp/stdout.log\""));
998    assert_that!(rendered, contains_substring("cloexec: stderr"));
999    assert_that!(rendered, contains_substring("3=\"/tmp/extra\""));
1000    assert_that!(
1001      rendered,
1002      contains_substring("cloexec: 4=\"/tmp/closed-on-exec\"")
1003    );
1004
1005    args.trace_fd = FdPrintFormat::Raw;
1006    args.hide_cloexec_fds = true;
1007    let printer = Printer::new(args.clone(), baseline.clone());
1008    let mut out = Vec::new();
1009    printer.print_fd(&mut out, &fds).unwrap();
1010    let rendered = String::from_utf8(out).unwrap();
1011    assert_that!(rendered, contains_substring("1=\"/tmp/stdout.log\""));
1012    assert_that!(rendered, not(contains_substring("closed-on-exec")));
1013
1014    args.trace_fd = FdPrintFormat::None;
1015    let printer = Printer::new(args, baseline);
1016    let mut out = Vec::new();
1017    printer.print_fd(&mut out, &fds).unwrap();
1018    assert_that!(out, empty());
1019  }
1020
1021  #[test]
1022  fn print_stdio_fds_in_cmdline_handles_partial_baseline() {
1023    owo_colors::control::set_should_colorize(false);
1024    let mut baseline = baseline();
1025    Arc::make_mut(&mut baseline).fdinfo.fdinfo.remove(&0);
1026    let fds = fd_collection([
1027      fd(0, "/tmp/stdin", 20, OFlag::empty()),
1028      fd(1, "/tmp/stdout", 21, OFlag::empty()),
1029    ]);
1030    let printer = Printer::new(printer_args(), baseline);
1031    let mut out = Vec::new();
1032
1033    printer.print_stdio_fds_in_cmdline(&mut out, &fds).unwrap();
1034
1035    assert_eq!(String::from_utf8(out).unwrap(), " >/tmp/stdout 2>&-");
1036  }
1037
1038  #[test]
1039  fn print_exec_trace_renders_diff_env_interpreters_and_errno() {
1040    owo_colors::control::set_should_colorize(false);
1041    let baseline = baseline();
1042    let mut envp = BTreeMap::new();
1043    envp.insert(msg("KEEP"), msg("same"));
1044    envp.insert(msg("MODIFIED"), msg("new"));
1045    envp.insert(msg("ADDED"), msg("value"));
1046    let fds = fd_collection([
1047      fd(0, "/dev/stdin", 10, OFlag::empty()),
1048      fd(1, "/dev/stdout", 11, OFlag::empty()),
1049      fd(2, "/dev/stderr", 12, OFlag::empty()),
1050    ]);
1051    let exec = exec_data(
1052      Ok(vec![msg("/bin/echo"), msg("hello world")]),
1053      Ok(envp),
1054      fds,
1055    );
1056    let mut args = printer_args();
1057    args.inline_timestamp_format = Some(TimestampFormat::try_new("%H:%M:%S".to_string()).unwrap());
1058    let printer = Printer::new(args, baseline.clone());
1059
1060    let rendered = run_with_output(&printer, || {
1061      printer.print_exec_trace(
1062        Pid::from_raw(123),
1063        ArcStr::from("echo"),
1064        0,
1065        &exec,
1066        &baseline.env,
1067        &baseline.cwd,
1068      )
1069    });
1070
1071    dbg!(&rendered);
1072
1073    assert_that!(rendered, contains_substring("123<echo>: \"/bin/echo\""));
1074    assert_that!(
1075      rendered,
1076      contains_substring("[\"/bin/echo\", \"hello world\"]")
1077    );
1078    assert_that!(rendered, contains_substring("at \"/exec-cwd\""));
1079    assert_that!(rendered, contains_substring("interpreter"));
1080    assert_that!(rendered, contains_substring("+\"ADDED\"=\"value\""));
1081    assert_that!(rendered, contains_substring("M\"MODIFIED\"=\"new\""));
1082    assert_that!(rendered, contains_substring("-\"REMOVED\"=\"gone\""));
1083  }
1084
1085  #[test]
1086  fn print_exec_trace_renders_raw_env_and_warning_paths() {
1087    owo_colors::control::set_should_colorize(false);
1088    let baseline = baseline();
1089    let fds = fd_collection([
1090      fd(0, "/dev/stdin", 10, OFlag::empty()),
1091      fd(1, "/dev/stdout", 11, OFlag::empty()),
1092      fd(2, "/dev/stderr", 12, OFlag::empty()),
1093    ]);
1094    let exec = exec_data(Err(Errno::EACCES), Err(Errno::EPERM), fds);
1095    let mut args = printer_args();
1096    args.trace_env = EnvPrintFormat::Raw;
1097    args.trace_argv = false;
1098    let printer = Printer::new(args, baseline.clone());
1099
1100    let rendered = run_with_output(&printer, || {
1101      printer.print_exec_trace(
1102        Pid::from_raw(321),
1103        ArcStr::from("bad"),
1104        1,
1105        &exec,
1106        &baseline.env,
1107        &baseline.cwd,
1108      )
1109    });
1110
1111    assert_that!(rendered, contains_substring("Failed to read envp"));
1112    assert_that!(rendered, contains_substring("Failed to read argv"));
1113    assert_that!(rendered, contains_substring("warning"));
1114  }
1115
1116  #[test]
1117  fn print_exec_trace_renders_reconstructed_cmdline() {
1118    owo_colors::control::set_should_colorize(false);
1119    let baseline = baseline();
1120    let mut envp = BTreeMap::new();
1121    envp.insert(msg("KEEP"), msg("same"));
1122    envp.insert(msg("MODIFIED"), msg("new value"));
1123    envp.insert(msg("ADDED"), msg("value"));
1124    let fds = fd_collection([
1125      fd(0, "/tmp/stdin", 20, OFlag::empty()),
1126      fd(1, "/tmp/stdout", 21, OFlag::empty()),
1127      fd(2, "/tmp/stderr", 22, OFlag::O_CLOEXEC),
1128      fd(5, "/tmp/fd5", 25, OFlag::empty()),
1129      fd(6, "/tmp/fd6", 26, OFlag::O_CLOEXEC),
1130    ]);
1131    let exec = exec_data(
1132      Ok(vec![msg("custom-argv0"), msg("hello world")]),
1133      Ok(envp),
1134      fds,
1135    );
1136    let mut args = printer_args();
1137    args.trace_argv = false;
1138    args.trace_env = EnvPrintFormat::None;
1139    args.trace_fd = FdPrintFormat::None;
1140    args.print_cmdline = true;
1141    args.stdio_in_cmdline = true;
1142    args.fd_in_cmdline = true;
1143    args.trace_interpreter = false;
1144    let printer = Printer::new(args, baseline.clone());
1145
1146    let rendered = run_with_output(&printer, || {
1147      printer.print_exec_trace(
1148        Pid::from_raw(456),
1149        ArcStr::from("echo"),
1150        0,
1151        &exec,
1152        &baseline.env,
1153        &baseline.cwd,
1154      )
1155    });
1156
1157    assert_that!(rendered, contains_substring("cmdline env"));
1158    assert_that!(rendered, contains_substring("</tmp/stdin"));
1159    assert_that!(rendered, contains_substring(">/tmp/stdout"));
1160    assert_that!(rendered, contains_substring("2>&-"));
1161    assert_that!(rendered, contains_substring("5<>/tmp/fd5"));
1162    assert_that!(rendered, contains_substring("-a custom-argv0"));
1163    assert_that!(rendered, contains_substring("-C /exec-cwd"));
1164    assert_that!(rendered, contains_substring("-u REMOVED"));
1165    assert_that!(rendered, contains_substring("ADDED=value"));
1166    assert_that!(rendered, contains_substring("MODIFIED=$'new value'"));
1167    assert_that!(rendered, contains_substring("/bin/echo $'hello world'"));
1168  }
1169
1170  #[test]
1171  fn reconstructed_cmdline_separates_dash_filename_when_env_is_unchanged() {
1172    owo_colors::control::set_should_colorize(false);
1173    let baseline = baseline();
1174    let filename = msg("--ignore-signal");
1175    let mut exec = exec_data(
1176      Ok(vec![filename.clone()]),
1177      Ok(baseline.env.clone()),
1178      FileDescriptorInfoCollection::default(),
1179    );
1180    exec.filename = filename;
1181    exec.cwd = baseline.cwd.clone();
1182    let mut args = printer_args();
1183    args.trace_argv = false;
1184    args.trace_env = EnvPrintFormat::None;
1185    args.trace_fd = FdPrintFormat::None;
1186    args.trace_cwd = false;
1187    args.trace_filename = false;
1188    args.trace_interpreter = false;
1189    args.print_cmdline = true;
1190    let printer = Printer::new(args, baseline.clone());
1191
1192    let rendered = run_with_output(&printer, || {
1193      printer.print_exec_trace(
1194        Pid::from_raw(456),
1195        ArcStr::from("false"),
1196        0,
1197        &exec,
1198        &baseline.env,
1199        &baseline.cwd,
1200      )
1201    });
1202
1203    assert_that!(
1204      rendered,
1205      contains_substring("cmdline env -- --ignore-signal")
1206    );
1207  }
1208
1209  #[test]
1210  fn print_new_child_respects_missing_output_and_comm_flag() {
1211    owo_colors::control::set_should_colorize(false);
1212    let baseline = baseline();
1213    let mut args = printer_args();
1214    args.trace_comm = false;
1215    let printer = Printer::new(args, baseline);
1216    printer.init_thread_local(None);
1217    printer
1218      .print_new_child(Pid::from_raw(1), "ignored", Pid::from_raw(2))
1219      .unwrap();
1220
1221    let rendered = run_with_output(&printer, || {
1222      printer.print_new_child(Pid::from_raw(1), "ignored", Pid::from_raw(2))
1223    });
1224    assert_eq!(rendered, "1: new child: 2\n");
1225  }
1226}