tracexec_core/cli/
args.rs

1use std::{borrow::Cow, num::ParseFloatError};
2
3use clap::{Args, ValueEnum};
4use color_eyre::eyre::bail;
5use enumflags2::BitFlags;
6use snafu::{ResultExt, Snafu};
7
8use crate::{
9  breakpoint::BreakPoint,
10  cli::config::{ColorLevel, EnvDisplay, FileDescriptorDisplay},
11  event::TracerEventDetailsKind,
12  timestamp::TimestampFormat,
13};
14
15use super::options::{AppLayout, SeccompBpf};
16use super::{
17  config::{
18    DebuggerConfig, ExitHandling, LogModeConfig, ModifierConfig, PtraceConfig, TuiModeConfig,
19  },
20  options::ActivePane,
21};
22
23#[derive(Args, Debug, Default, Clone)]
24pub struct PtraceArgs {
25  #[clap(long, help = "Controls whether to enable seccomp-bpf optimization, which greatly improves performance", default_value_t = SeccompBpf::Auto)]
26  pub seccomp_bpf: SeccompBpf,
27  #[clap(
28    long,
29    help = "Polling interval, in microseconds. -1(default) disables polling."
30  )]
31  pub polling_interval: Option<i64>,
32}
33
34#[derive(Args, Debug, Default, Clone)]
35pub struct ModifierArgs {
36  #[clap(long, help = "Only show successful calls", default_value_t = false)]
37  pub successful_only: bool,
38  #[clap(
39    long,
40    help = "[Experimental] Try to reproduce file descriptors in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
41    default_value_t = false
42  )]
43  pub fd_in_cmdline: bool,
44  #[clap(
45    long,
46    help = "[Experimental] Try to reproduce stdio in commandline. This might result in an unexecutable cmdline if pipes, sockets, etc. are involved.",
47    default_value_t = false
48  )]
49  pub stdio_in_cmdline: bool,
50  #[clap(long, help = "Resolve /proc/self/exe symlink", default_value_t = false)]
51  pub resolve_proc_self_exe: bool,
52  #[clap(
53    long,
54    help = "Do not resolve /proc/self/exe symlink",
55    default_value_t = false,
56    conflicts_with = "resolve_proc_self_exe"
57  )]
58  pub no_resolve_proc_self_exe: bool,
59  #[clap(long, help = "Hide CLOEXEC fds", default_value_t = false)]
60  pub hide_cloexec_fds: bool,
61  #[clap(
62    long,
63    help = "Do not hide CLOEXEC fds",
64    default_value_t = false,
65    conflicts_with = "hide_cloexec_fds"
66  )]
67  pub no_hide_cloexec_fds: bool,
68  #[clap(long, help = "Show timestamp information", default_value_t = false)]
69  pub timestamp: bool,
70  #[clap(
71    long,
72    help = "Do not show timestamp information",
73    default_value_t = false,
74    conflicts_with = "timestamp"
75  )]
76  pub no_timestamp: bool,
77  #[clap(
78    long,
79    help = "Set the format of inline timestamp. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for available options."
80  )]
81  pub inline_timestamp_format: Option<TimestampFormat>,
82}
83
84impl PtraceArgs {
85  pub fn merge_config(&mut self, config: PtraceConfig) {
86    // seccomp-bpf
87    if let Some(setting) = config.seccomp_bpf
88      && self.seccomp_bpf == SeccompBpf::Auto
89    {
90      self.seccomp_bpf = setting;
91    }
92  }
93}
94
95impl ModifierArgs {
96  pub fn processed(mut self) -> Self {
97    self.stdio_in_cmdline = self.fd_in_cmdline || self.stdio_in_cmdline;
98    self.resolve_proc_self_exe = match (self.resolve_proc_self_exe, self.no_resolve_proc_self_exe) {
99      (true, false) => true,
100      (false, true) => false,
101      _ => true, // default
102    };
103    self.hide_cloexec_fds = match (self.hide_cloexec_fds, self.no_hide_cloexec_fds) {
104      (true, false) => true,
105      (false, true) => false,
106      _ => true, // default
107    };
108    self.timestamp = match (self.timestamp, self.no_timestamp) {
109      (true, false) => true,
110      (false, true) => false,
111      _ => false, // default
112    };
113    self.inline_timestamp_format = self
114      .inline_timestamp_format
115      .or_else(|| Some(TimestampFormat::try_new("%H:%M:%S").unwrap()));
116    self
117  }
118
119  pub fn merge_config(&mut self, config: ModifierConfig) {
120    // false by default flags
121    self.successful_only = self.successful_only || config.successful_only.unwrap_or_default();
122    self.fd_in_cmdline |= config.fd_in_cmdline.unwrap_or_default();
123    self.stdio_in_cmdline |= config.stdio_in_cmdline.unwrap_or_default();
124    // flags that have negation counterparts
125    if (!self.no_resolve_proc_self_exe) && (!self.resolve_proc_self_exe) {
126      self.resolve_proc_self_exe = config.resolve_proc_self_exe.unwrap_or_default();
127    }
128    if (!self.no_hide_cloexec_fds) && (!self.hide_cloexec_fds) {
129      self.hide_cloexec_fds = config.hide_cloexec_fds.unwrap_or_default();
130    }
131    if let Some(c) = config.timestamp {
132      if (!self.timestamp) && (!self.no_timestamp) {
133        self.timestamp = c.enable;
134      }
135      if self.inline_timestamp_format.is_none() {
136        self.inline_timestamp_format = c.inline_format;
137      }
138    }
139  }
140}
141
142#[derive(Args, Debug)]
143pub struct TracerEventArgs {
144  // TODO:
145  //   This isn't really compatible with logging mode
146  #[clap(
147    long,
148    help = "Set the default filter to show all events. This option can be used in combination with --filter-exclude to exclude some unwanted events.",
149    conflicts_with = "filter"
150  )]
151  pub show_all_events: bool,
152  #[clap(
153    long,
154    help = "Set the default filter for events.",
155    value_parser = tracer_event_filter_parser,
156    default_value = "warning,error,exec,tracee-exit"
157  )]
158  pub filter: BitFlags<TracerEventDetailsKind>,
159  #[clap(
160    long,
161    help = "Aside from the default filter, also include the events specified here.",
162    required = false,
163    value_parser = tracer_event_filter_parser,
164    default_value_t = BitFlags::empty()
165  )]
166  pub filter_include: BitFlags<TracerEventDetailsKind>,
167  #[clap(
168    long,
169    help = "Exclude the events specified here from the default filter.",
170    value_parser = tracer_event_filter_parser,
171    default_value_t = BitFlags::empty()
172  )]
173  pub filter_exclude: BitFlags<TracerEventDetailsKind>,
174}
175
176fn tracer_event_filter_parser(filter: &str) -> Result<BitFlags<TracerEventDetailsKind>, String> {
177  let mut result = BitFlags::empty();
178  if filter == "<empty>" {
179    return Ok(result);
180  }
181  for f in filter.split(',') {
182    let kind = TracerEventDetailsKind::from_str(f, false)?;
183    if result.contains(kind) {
184      return Err(format!(
185        "Event kind '{kind}' is already included in the filter"
186      ));
187    }
188    result |= kind;
189  }
190  Ok(result)
191}
192
193impl TracerEventArgs {
194  pub fn all() -> Self {
195    Self {
196      show_all_events: true,
197      filter: Default::default(),
198      filter_include: Default::default(),
199      filter_exclude: Default::default(),
200    }
201  }
202
203  pub fn filter(&self) -> color_eyre::Result<BitFlags<TracerEventDetailsKind>> {
204    let default_filter = if self.show_all_events {
205      BitFlags::all()
206    } else {
207      self.filter
208    };
209    if self.filter_include.intersects(self.filter_exclude) {
210      bail!("filter_include and filter_exclude cannot contain common events");
211    }
212    let mut filter = default_filter | self.filter_include;
213    filter.remove(self.filter_exclude);
214    Ok(filter)
215  }
216}
217
218#[derive(Args, Debug, Default, Clone)]
219pub struct LogModeArgs {
220  #[clap(long, help = "More colors", conflicts_with = "less_colors")]
221  pub more_colors: bool,
222  #[clap(long, help = "Less colors", conflicts_with = "more_colors")]
223  pub less_colors: bool,
224  // BEGIN ugly: https://github.com/clap-rs/clap/issues/815
225  #[clap(
226    long,
227    help = "Print commandline that (hopefully) reproduces what was executed. Note: file descriptors are not handled for now.",
228    conflicts_with_all = ["show_env", "diff_env", "show_argv", "no_show_cmdline"]
229  )]
230  pub show_cmdline: bool,
231  #[clap(
232    long,
233    help = "Don't print commandline that (hopefully) reproduces what was executed."
234  )]
235  pub no_show_cmdline: bool,
236  #[clap(
237    long,
238    help = "Try to show script interpreter indicated by shebang",
239    conflicts_with = "no_show_interpreter"
240  )]
241  pub show_interpreter: bool,
242  #[clap(
243    long,
244    help = "Do not show script interpreter indicated by shebang",
245    conflicts_with = "show_interpreter"
246  )]
247  pub no_show_interpreter: bool,
248  #[clap(
249    long,
250    help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
251    conflicts_with = "no_foreground"
252  )]
253  pub foreground: bool,
254  #[clap(
255    long,
256    help = "Do not set the terminal foreground process group to tracee",
257    conflicts_with = "foreground"
258  )]
259  pub no_foreground: bool,
260  #[clap(
261    long,
262    help = "Diff file descriptors with the original std{in/out/err}",
263    conflicts_with = "no_diff_fd"
264  )]
265  pub diff_fd: bool,
266  #[clap(
267    long,
268    help = "Do not diff file descriptors",
269    conflicts_with = "diff_fd"
270  )]
271  pub no_diff_fd: bool,
272  #[clap(long, help = "Show file descriptors", conflicts_with = "diff_fd")]
273  pub show_fd: bool,
274  #[clap(
275    long,
276    help = "Do not show file descriptors",
277    conflicts_with = "show_fd"
278  )]
279  pub no_show_fd: bool,
280  #[clap(
281    long,
282    help = "Diff environment variables with the original environment",
283    conflicts_with = "no_diff_env",
284    conflicts_with = "show_env",
285    conflicts_with = "no_show_env"
286  )]
287  pub diff_env: bool,
288  #[clap(
289    long,
290    help = "Do not diff environment variables",
291    conflicts_with = "diff_env"
292  )]
293  pub no_diff_env: bool,
294  #[clap(
295    long,
296    help = "Show environment variables",
297    conflicts_with = "no_show_env",
298    conflicts_with = "diff_env"
299  )]
300  pub show_env: bool,
301  #[clap(
302    long,
303    help = "Do not show environment variables",
304    conflicts_with = "show_env"
305  )]
306  pub no_show_env: bool,
307  #[clap(long, help = "Show comm", conflicts_with = "no_show_comm")]
308  pub show_comm: bool,
309  #[clap(long, help = "Do not show comm", conflicts_with = "show_comm")]
310  pub no_show_comm: bool,
311  #[clap(long, help = "Show argv", conflicts_with = "no_show_argv")]
312  pub show_argv: bool,
313  #[clap(long, help = "Do not show argv", conflicts_with = "show_argv")]
314  pub no_show_argv: bool,
315  #[clap(long, help = "Show filename", conflicts_with = "no_show_filename")]
316  pub show_filename: bool,
317  #[clap(long, help = "Do not show filename", conflicts_with = "show_filename")]
318  pub no_show_filename: bool,
319  #[clap(long, help = "Show cwd", conflicts_with = "no_show_cwd")]
320  pub show_cwd: bool,
321  #[clap(long, help = "Do not show cwd", conflicts_with = "show_cwd")]
322  pub no_show_cwd: bool,
323  #[clap(long, help = "Decode errno values", conflicts_with = "no_decode_errno")]
324  pub decode_errno: bool,
325  #[clap(
326    long,
327    help = "Do not decode errno values",
328    conflicts_with = "decode_errno"
329  )]
330  pub no_decode_errno: bool,
331  // END ugly
332}
333
334impl LogModeArgs {
335  pub fn foreground(&self) -> bool {
336    match (self.foreground, self.no_foreground) {
337      (false, true) => false,
338      (true, false) => true,
339      _ => true,
340    }
341  }
342
343  pub fn merge_config(&mut self, config: LogModeConfig) {
344    /// fallback to config value if both --x and --no-x are not set
345    macro_rules! fallback {
346      ($x:ident) => {
347        ::paste::paste! {
348          if (!self.$x) && (!self.[<no_ $x>]) {
349            if let Some(x) = config.$x {
350              if x {
351                self.$x = true;
352              } else {
353                self.[<no_ $x>] = true;
354              }
355            }
356          }
357        }
358      };
359    }
360    fallback!(show_interpreter);
361    fallback!(foreground);
362    fallback!(show_comm);
363    fallback!(show_filename);
364    fallback!(show_cwd);
365    fallback!(decode_errno);
366    match config.fd_display {
367      Some(FileDescriptorDisplay::Show) => {
368        if (!self.no_show_fd) && (!self.diff_fd) {
369          self.show_fd = true;
370        }
371      }
372      Some(FileDescriptorDisplay::Diff) => {
373        if (!self.show_fd) && (!self.no_diff_fd) {
374          self.diff_fd = true;
375        }
376      }
377      Some(FileDescriptorDisplay::Hide) => {
378        if (!self.diff_fd) && (!self.show_fd) {
379          self.no_diff_fd = true;
380          self.no_show_fd = true;
381        }
382      }
383      _ => (),
384    }
385    fallback!(show_cmdline);
386    if !self.show_cmdline {
387      fallback!(show_argv);
388      tracing::warn!("{}", self.show_argv);
389      match config.env_display {
390        Some(EnvDisplay::Show) => {
391          if (!self.diff_env) && (!self.no_show_env) {
392            self.show_env = true;
393          }
394        }
395        Some(EnvDisplay::Diff) => {
396          if (!self.show_env) && (!self.no_diff_env) {
397            self.diff_env = true;
398          }
399        }
400        Some(EnvDisplay::Hide) => {
401          if (!self.show_env) && (!self.diff_env) {
402            self.no_diff_env = true;
403            self.no_show_env = true;
404          }
405        }
406        _ => (),
407      }
408    }
409    match config.color_level {
410      Some(ColorLevel::Less) => {
411        if !self.more_colors {
412          self.less_colors = true;
413        }
414      }
415      Some(ColorLevel::More) => {
416        if !self.less_colors {
417          self.more_colors = true;
418        }
419      }
420      _ => (),
421    }
422  }
423}
424
425#[derive(Args, Debug, Default, Clone)]
426pub struct TuiModeArgs {
427  #[clap(
428    long,
429    short,
430    help = "Allocate a pseudo terminal and show it alongside the TUI"
431  )]
432  pub tty: bool,
433  #[clap(long, short, help = "Keep the event list scrolled to the bottom")]
434  pub follow: bool,
435  #[clap(
436    long,
437    help = "Instead of waiting for the root child to exit, terminate when the TUI exits",
438    conflicts_with = "kill_on_exit"
439  )]
440  pub terminate_on_exit: bool,
441  #[clap(
442    long,
443    help = "Instead of waiting for the root child to exit, kill when the TUI exits"
444  )]
445  pub kill_on_exit: bool,
446  #[clap(
447    long,
448    short = 'A',
449    help = "Set the default active pane to use when TUI launches",
450    requires = "tty"
451  )]
452  pub active_pane: Option<ActivePane>,
453  #[clap(
454    long,
455    short = 'L',
456    help = "Set the layout of the TUI when it launches",
457    requires = "tty"
458  )]
459  pub layout: Option<AppLayout>,
460  #[clap(
461    long,
462    short = 'F',
463    help = "Set the frame rate of the TUI (60 by default)",
464    value_parser = frame_rate_parser
465  )]
466  pub frame_rate: Option<f64>,
467  #[clap(
468    long,
469    short = 'm',
470    help = "Max number of events to keep in TUI (0=unlimited)"
471  )]
472  pub max_events: Option<u64>,
473}
474
475#[derive(Args, Debug, Default, Clone)]
476pub struct DebuggerArgs {
477  #[clap(
478    long,
479    short = 'D',
480    help = "Set the default external command to run when using \"Detach, Stop and Run Command\" feature in Hit Manager"
481  )]
482  pub default_external_command: Option<String>,
483  #[clap(
484    long = "add-breakpoint",
485    short = 'b',
486    value_parser = breakpoint_parser,
487    help = "Add a new breakpoint to the tracer. This option can be used multiple times. The format is <syscall-stop>:<pattern-type>:<pattern>, where syscall-stop can be sysenter or sysexit, pattern-type can be argv-regex, in-filename or exact-filename. For example, sysexit:in-filename:/bash",
488  )]
489  pub breakpoints: Vec<BreakPoint>,
490}
491
492impl TuiModeArgs {
493  pub fn merge_config(&mut self, config: TuiModeConfig) {
494    self.active_pane = self.active_pane.or(config.active_pane);
495    self.layout = self.layout.or(config.layout);
496    self.frame_rate = self.frame_rate.or(config.frame_rate);
497    self.max_events = self.max_events.or(config.max_events);
498    self.follow |= config.follow.unwrap_or_default();
499    if (!self.terminate_on_exit) && (!self.kill_on_exit) {
500      match config.exit_handling {
501        Some(ExitHandling::Kill) => self.kill_on_exit = true,
502        Some(ExitHandling::Terminate) => self.terminate_on_exit = true,
503        _ => (),
504      }
505    }
506  }
507}
508
509impl DebuggerArgs {
510  pub fn merge_config(&mut self, config: DebuggerConfig) {
511    if self.default_external_command.is_none() {
512      self.default_external_command = config.default_external_command;
513    }
514  }
515}
516
517fn frame_rate_parser(s: &str) -> Result<f64, ParseFrameRateError> {
518  let v = s.parse::<f64>().with_context(|_| ParseFloatSnafu {
519    value: s.to_string(),
520  })?;
521  if v < 0.0 || v.is_nan() || v.is_infinite() {
522    Err(ParseFrameRateError::Invalid)
523  } else if v < 5.0 {
524    Err(ParseFrameRateError::TooLow)
525  } else {
526    Ok(v)
527  }
528}
529
530fn breakpoint_parser(s: &str) -> Result<BreakPoint, Cow<'static, str>> {
531  BreakPoint::try_from(s)
532}
533
534#[derive(Snafu, Debug)]
535enum ParseFrameRateError {
536  #[snafu(display("Failed to parse frame rate {value} as a floating point number"))]
537  ParseFloat {
538    source: ParseFloatError,
539    value: String,
540  },
541  #[snafu(display("Invalid frame rate"))]
542  Invalid,
543  #[snafu(display("Frame rate too low, must be at least 5.0"))]
544  TooLow,
545}
546
547#[derive(Args, Debug, Default, Clone)]
548pub struct ExporterArgs {
549  #[clap(short, long, help = "prettify the output if supported")]
550  pub pretty: bool,
551}