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, },
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 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 _ => 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 #[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 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 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 #[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 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 if self.args.trace_interpreter
559 && result == 0
560 && let Some(interpreters) = exec_data.interpreters.as_ref()
561 {
562 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 self.print_fd(out, &exec_data.fdinfo)?;
587
588 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 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 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 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 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 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 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 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 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 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}