tracexec_tui/
event.rs

1use std::borrow::Cow;
2
3use itertools::{
4  Itertools,
5  chain,
6};
7use nix::fcntl::OFlag;
8use ratatui::{
9  style::Styled,
10  text::{
11    Line,
12    Span,
13  },
14};
15use tracexec_core::{
16  cli::args::ModifierArgs,
17  event::{
18    EventStatus,
19    ExecEvent,
20    RuntimeModifier,
21    TracerEventDetails,
22    TracerEventMessage,
23  },
24  proc::{
25    BaselineInfo,
26    FileDescriptorInfoCollection,
27  },
28  timestamp::Timestamp,
29};
30
31use crate::{
32  action::CopyTarget,
33  event::private::Sealed,
34  event_line::{
35    EventLine,
36    Mask,
37  },
38  output::OutputMsgTuiExt,
39  theme::THEME,
40};
41
42mod private {
43  use tracexec_core::event::TracerEventDetails;
44
45  pub trait Sealed {}
46
47  impl Sealed for TracerEventDetails {}
48}
49
50pub trait TracerEventDetailsTuiExt: Sealed {
51  fn to_tui_line(
52    &self,
53    baseline: &BaselineInfo,
54    cmdline_only: bool,
55    modifier: &ModifierArgs,
56    rt_modifier: RuntimeModifier,
57    event_status: Option<EventStatus>,
58  ) -> Line<'static>;
59
60  /// Convert the event to a EventLine
61  ///
62  /// This method is resource intensive and the caller should cache the result
63  #[allow(clippy::too_many_arguments)]
64  fn to_event_line(
65    &self,
66    baseline: &BaselineInfo,
67    cmdline_only: bool,
68    modifier: &ModifierArgs,
69    rt_modifier: RuntimeModifier,
70    event_status: Option<EventStatus>,
71    enable_mask: bool,
72    extra_prefix: Option<Span<'static>>,
73    full_env: bool,
74  ) -> EventLine;
75
76  fn text_for_copy<'a>(
77    &'a self,
78    baseline: &BaselineInfo,
79    target: CopyTarget,
80    modifier_args: &ModifierArgs,
81    rt_modifier: RuntimeModifier,
82  ) -> Cow<'a, str>;
83}
84
85impl TracerEventDetailsTuiExt for TracerEventDetails {
86  fn to_tui_line(
87    &self,
88    baseline: &BaselineInfo,
89    cmdline_only: bool,
90    modifier: &ModifierArgs,
91    rt_modifier: RuntimeModifier,
92    event_status: Option<EventStatus>,
93  ) -> Line<'static> {
94    self
95      .to_event_line(
96        baseline,
97        cmdline_only,
98        modifier,
99        rt_modifier,
100        event_status,
101        false,
102        None,
103        false,
104      )
105      .line
106  }
107
108  /// Convert the event to a EventLine
109  ///
110  /// This method is resource intensive and the caller should cache the result
111  #[allow(clippy::too_many_arguments)]
112  fn to_event_line(
113    &self,
114    baseline: &BaselineInfo,
115    cmdline_only: bool,
116    modifier: &ModifierArgs,
117    rt_modifier: RuntimeModifier,
118    event_status: Option<EventStatus>,
119    enable_mask: bool,
120    extra_prefix: Option<Span<'static>>,
121    full_env: bool,
122  ) -> EventLine {
123    fn handle_stdio_fd(
124      fd: i32,
125      baseline: &BaselineInfo,
126      curr: &FileDescriptorInfoCollection,
127      spans: &mut Vec<Span>,
128    ) {
129      let (fdstr, redir) = match fd {
130        0 => (" 0", "<"),
131        1 => (" 1", ">"),
132        2 => (" 2", "2>"),
133        _ => unreachable!(),
134      };
135
136      let space: Span = " ".into();
137      let fdinfo_orig = baseline.fdinfo.get(fd).unwrap();
138      if let Some(fdinfo) = curr.get(fd) {
139        if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
140          // stdio fd will be closed
141          spans.push(fdstr.set_style(THEME.cloexec_fd_in_cmdline));
142          spans.push(">&-".set_style(THEME.cloexec_fd_in_cmdline));
143        } else if fdinfo.not_same_file_as(fdinfo_orig) {
144          spans.push(space.clone());
145          spans.push(redir.set_style(THEME.modified_fd_in_cmdline));
146          spans.push(
147            fdinfo
148              .path
149              .bash_escaped_with_style(THEME.modified_fd_in_cmdline),
150          );
151        }
152      } else {
153        // stdio fd is closed
154        spans.push(fdstr.set_style(THEME.cloexec_fd_in_cmdline));
155        spans.push(">&-".set_style(THEME.removed_fd_in_cmdline));
156      }
157    }
158
159    let mut env_range = None;
160    let mut cwd_range = None;
161
162    let rt_modifier_effective = if enable_mask {
163      // Enable all modifiers so that the mask can be toggled later
164      RuntimeModifier::default()
165    } else {
166      rt_modifier
167    };
168
169    let ts_formatter = |ts: Timestamp| {
170      if modifier.timestamp {
171        let fmt = modifier.inline_timestamp_format.as_deref().unwrap();
172        Some(Span::styled(
173          format!("{} ", ts.format(fmt)),
174          THEME.inline_timestamp,
175        ))
176      } else {
177        None
178      }
179    };
180
181    let mut line = match self {
182      Self::Info(TracerEventMessage {
183        msg,
184        pid,
185        timestamp,
186      }) => chain!(
187        extra_prefix,
188        timestamp.and_then(ts_formatter),
189        pid
190          .map(|p| [p.to_string().set_style(THEME.pid_in_msg)])
191          .unwrap_or_default(),
192        ["[info]".set_style(THEME.tracer_info)],
193        [": ".into(), msg.clone().set_style(THEME.tracer_info)]
194      )
195      .collect(),
196      Self::Warning(TracerEventMessage {
197        msg,
198        pid,
199        timestamp,
200      }) => chain!(
201        extra_prefix,
202        timestamp.and_then(ts_formatter),
203        pid
204          .map(|p| [p.to_string().set_style(THEME.pid_in_msg)])
205          .unwrap_or_default(),
206        ["[warn]".set_style(THEME.tracer_warning)],
207        [": ".into(), msg.clone().set_style(THEME.tracer_warning)]
208      )
209      .collect(),
210      Self::Error(TracerEventMessage {
211        msg,
212        pid,
213        timestamp,
214      }) => chain!(
215        extra_prefix,
216        timestamp.and_then(ts_formatter),
217        pid
218          .map(|p| [p.to_string().set_style(THEME.pid_in_msg)])
219          .unwrap_or_default(),
220        ["error".set_style(THEME.tracer_error)],
221        [": ".into(), msg.clone().set_style(THEME.tracer_error)]
222      )
223      .collect(),
224      Self::NewChild {
225        ppid,
226        pcomm,
227        pid,
228        timestamp,
229      } => [
230        extra_prefix,
231        ts_formatter(*timestamp),
232        Some(ppid.to_string().set_style(THEME.pid_success)),
233        event_status.map(|s| <&'static str>::from(s).into()),
234        Some(format!("<{pcomm}>").set_style(THEME.comm)),
235        Some(": ".into()),
236        Some("new child ".set_style(THEME.tracer_event)),
237        Some(pid.to_string().set_style(THEME.new_child_pid)),
238      ]
239      .into_iter()
240      .flatten()
241      .collect(),
242      Self::Exec(exec) => {
243        let ExecEvent {
244          pid,
245          cwd,
246          comm,
247          filename,
248          argv,
249          interpreter: _,
250          env_diff,
251          result,
252          fdinfo,
253          envp,
254          ..
255        } = exec.as_ref();
256        let mut spans = extra_prefix
257          .into_iter()
258          .chain(ts_formatter(exec.timestamp))
259          .collect_vec();
260        if !cmdline_only {
261          spans.extend(
262            [
263              Some(pid.to_string().set_style(if *result == 0 {
264                THEME.pid_success
265              } else if *result == (-nix::libc::ENOENT) as i64 {
266                THEME.pid_enoent
267              } else {
268                THEME.pid_failure
269              })),
270              event_status.map(|s| <&'static str>::from(s).into()),
271              Some(format!("<{comm}>").set_style(THEME.comm)),
272              Some(": ".into()),
273              Some("env".set_style(THEME.tracer_event)),
274            ]
275            .into_iter()
276            .flatten(),
277          )
278        } else {
279          spans.push("env".set_style(THEME.tracer_event));
280        };
281        let space: Span = " ".into();
282
283        // Handle argv[0]
284        let _ = argv.as_deref().inspect(|v| {
285          v.first().inspect(|&arg0| {
286            if filename != arg0 {
287              spans.push(space.clone());
288              spans.push("-a ".set_style(THEME.arg0));
289              spans.push(arg0.bash_escaped_with_style(THEME.arg0));
290            }
291          });
292        });
293        // Handle cwd
294        if cwd != &baseline.cwd && rt_modifier_effective.show_cwd {
295          let range_start = spans.len();
296          spans.push(space.clone());
297          spans.push("-C ".set_style(THEME.cwd));
298          spans.push(cwd.bash_escaped_with_style(THEME.cwd));
299          cwd_range = Some(range_start..(spans.len()))
300        }
301        if rt_modifier_effective.show_env {
302          env_range = Some((spans.len(), 0));
303          if !full_env {
304            if let Ok(env_diff) = env_diff {
305              // Handle env diff
306              for k in env_diff.removed.iter() {
307                spans.push(space.clone());
308                spans.push("-u ".set_style(THEME.deleted_env_var));
309                spans.push(k.bash_escaped_with_style(THEME.deleted_env_var));
310              }
311              if env_diff.need_env_argument_separator() {
312                spans.push(space.clone());
313                spans.push("--".into());
314              }
315              for (k, v) in env_diff.added.iter() {
316                // Added env vars
317                spans.push(space.clone());
318                spans.push(k.bash_escaped_with_style(THEME.added_env_var));
319                spans.push("=".set_style(THEME.added_env_var));
320                spans.push(v.bash_escaped_with_style(THEME.added_env_var));
321              }
322              for (k, v) in env_diff.modified.iter() {
323                // Modified env vars
324                spans.push(space.clone());
325                spans.push(k.bash_escaped_with_style(THEME.modified_env_var));
326                spans.push("=".set_style(THEME.modified_env_var));
327                spans.push(v.bash_escaped_with_style(THEME.modified_env_var));
328              }
329            }
330          } else if let Ok(envp) = &**envp {
331            spans.push(space.clone());
332            spans.push("-i --".into()); // TODO: style
333            for (k, v) in envp.iter() {
334              spans.push(space.clone());
335              spans.push(k.bash_escaped_with_style(THEME.unchanged_env_key));
336              spans.push("=".set_style(THEME.unchanged_env_key));
337              spans.push(v.bash_escaped_with_style(THEME.unchanged_env_val));
338            }
339          }
340
341          if let Some(r) = env_range.as_mut() {
342            r.1 = spans.len();
343          }
344        }
345        spans.push(space.clone());
346        // Filename
347        spans.push(filename.bash_escaped_with_style(THEME.filename));
348        // Argv[1..]
349        match argv.as_ref() {
350          Ok(argv) => {
351            for arg in argv.iter().skip(1) {
352              spans.push(space.clone());
353              spans.push(arg.bash_escaped_with_style(THEME.argv));
354            }
355          }
356          Err(_) => {
357            spans.push(space.clone());
358            spans.push("[failed to read argv]".set_style(THEME.inline_tracer_error));
359          }
360        }
361
362        // Handle file descriptors
363        if modifier.stdio_in_cmdline {
364          for fd in 0..=2 {
365            handle_stdio_fd(fd, baseline, fdinfo, &mut spans);
366          }
367        }
368
369        if modifier.fd_in_cmdline {
370          for (&fd, fdinfo) in fdinfo.fdinfo.iter() {
371            if fd < 3 {
372              continue;
373            }
374            if fdinfo.flags.intersects(OFlag::O_CLOEXEC) {
375              // Skip fds that will be closed upon exec
376              continue;
377            }
378            spans.push(space.clone());
379            spans.push(fd.to_string().set_style(THEME.added_fd_in_cmdline));
380            spans.push("<>".set_style(THEME.added_fd_in_cmdline));
381            spans.push(
382              fdinfo
383                .path
384                .bash_escaped_with_style(THEME.added_fd_in_cmdline),
385            )
386          }
387        }
388
389        Line::default().spans(spans)
390      }
391      Self::TraceeExit {
392        signal,
393        exit_code,
394        timestamp,
395      } => chain!(
396        ts_formatter(*timestamp),
397        Some(format!("tracee exit: signal: {signal:?}, exit_code: {exit_code}").into())
398      )
399      .collect(),
400      Self::TraceeSpawn { pid, timestamp } => chain!(
401        ts_formatter(*timestamp),
402        Some(format!("tracee spawned: {pid}").into())
403      )
404      .collect(),
405    };
406    let mut cwd_mask = None;
407    let mut env_mask = None;
408    if enable_mask {
409      if let Some(range) = cwd_range {
410        let mut mask = Mask::new(range);
411        if !rt_modifier.show_cwd {
412          mask.toggle(&mut line);
413        }
414        cwd_mask.replace(mask);
415      }
416      if let Some((start, end)) = env_range {
417        let mut mask = Mask::new(start..end);
418        if !rt_modifier.show_env {
419          mask.toggle(&mut line);
420        }
421        env_mask.replace(mask);
422      }
423    }
424    EventLine {
425      line,
426      cwd_mask,
427      env_mask,
428    }
429  }
430
431  fn text_for_copy<'a>(
432    &'a self,
433    baseline: &BaselineInfo,
434    target: CopyTarget,
435    modifier_args: &ModifierArgs,
436    rt_modifier: RuntimeModifier,
437  ) -> Cow<'a, str> {
438    if CopyTarget::Line == target {
439      return self
440        .to_event_line(
441          baseline,
442          false,
443          modifier_args,
444          rt_modifier,
445          None,
446          false,
447          None,
448          false,
449        )
450        .to_string()
451        .into();
452    }
453    // Other targets are only available for Exec events
454    let Self::Exec(event) = self else {
455      panic!("Copy target {target:?} is only available for Exec events");
456    };
457    let mut modifier_args = ModifierArgs::default();
458    match target {
459      CopyTarget::Commandline(_) => self
460        .to_event_line(
461          baseline,
462          true,
463          &modifier_args,
464          Default::default(),
465          None,
466          false,
467          None,
468          false,
469        )
470        .to_string()
471        .into(),
472      CopyTarget::CommandlineWithFullEnv(_) => self
473        .to_event_line(
474          baseline,
475          true,
476          &modifier_args,
477          Default::default(),
478          None,
479          false,
480          None,
481          true,
482        )
483        .to_string()
484        .into(),
485      CopyTarget::CommandlineWithStdio(_) => {
486        modifier_args.stdio_in_cmdline = true;
487        self
488          .to_event_line(
489            baseline,
490            true,
491            &modifier_args,
492            Default::default(),
493            None,
494            false,
495            None,
496            false,
497          )
498          .to_string()
499          .into()
500      }
501      CopyTarget::CommandlineWithFds(_) => {
502        modifier_args.fd_in_cmdline = true;
503        modifier_args.stdio_in_cmdline = true;
504        self
505          .to_event_line(
506            baseline,
507            true,
508            &modifier_args,
509            Default::default(),
510            None,
511            false,
512            None,
513            false,
514          )
515          .to_string()
516          .into()
517      }
518      CopyTarget::Env => match event.envp.as_ref() {
519        Ok(envp) => envp
520          .iter()
521          .map(|(k, v)| format!("{k}={v}"))
522          .join("\n")
523          .into(),
524        Err(e) => format!("[failed to read envp: {e}]").into(),
525      },
526      CopyTarget::EnvDiff => {
527        let Ok(env_diff) = event.env_diff.as_ref() else {
528          return "[failed to read envp]".into();
529        };
530        let mut result = String::new();
531        result.push_str("# Added:\n");
532        for (k, v) in env_diff.added.iter() {
533          result.push_str(&format!("{k}={v}\n"));
534        }
535        result.push_str("# Modified: (original first)\n");
536        for (k, v) in env_diff.modified.iter() {
537          result.push_str(&format!(
538            "{}={}\n{}={}\n",
539            k,
540            baseline.env.get(k).unwrap(),
541            k,
542            v
543          ));
544        }
545        result.push_str("# Removed:\n");
546        for k in env_diff.removed.iter() {
547          result.push_str(&format!("{}={}\n", k, baseline.env.get(k).unwrap()));
548        }
549        result.into()
550      }
551      CopyTarget::Argv => Self::argv_to_string(&event.argv).into(),
552      CopyTarget::Filename => Cow::Borrowed(event.filename.as_ref()),
553      CopyTarget::SyscallResult => event.result.to_string().into(),
554      CopyTarget::Line => unreachable!(),
555    }
556  }
557}