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 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, };
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, };
108 self.timestamp = match (self.timestamp, self.no_timestamp) {
109 (true, false) => true,
110 (false, true) => false,
111 _ => false, };
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 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 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 #[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 #[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 }
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 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}