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 #[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 #[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 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 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 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 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 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 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 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 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()); 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 spans.push(filename.bash_escaped_with_style(THEME.filename));
348 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 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 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 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}