1use std::{
2 io::{
3 BufWriter,
4 stderr,
5 stdout,
6 },
7 path::PathBuf,
8};
9
10use args::{
11 DebuggerArgs,
12 PtraceArgs,
13 TuiModeArgs,
14};
15use clap::{
16 CommandFactory,
17 Parser,
18 Subcommand,
19};
20use config::Config;
21use options::ExportFormat;
22use tracing::debug;
23
24use self::{
25 args::{
26 LogModeArgs,
27 ModifierArgs,
28 TracerEventArgs,
29 },
30 options::Color,
31};
32use crate::{
33 cli::args::ExporterArgs,
34 output::Output,
35};
36
37pub mod args;
38pub mod config;
39pub mod keys;
40pub mod options;
41pub mod theme;
42pub mod tui_theme;
43
44#[derive(Parser, Debug)]
45#[clap(
46 name = "tracexec",
47 author,
48 version,
49 about = "A small utility for tracing execve{,at} and pre-exec behavior."
50)]
51pub struct Cli {
52 #[arg(long, default_value_t = Color::Auto, help = "Control whether colored output is enabled. This flag has no effect on TUI mode.")]
53 pub color: Color,
54 #[arg(
55 short = 'C',
56 long,
57 help = "Change current directory to this path before doing anything"
58 )]
59 pub cwd: Option<PathBuf>,
60 #[arg(
61 short = 'P',
62 long,
63 help = "Load profile from this path",
64 conflicts_with = "no_profile"
65 )]
66 pub profile: Option<PathBuf>,
67 #[arg(long, help = "Do not load profiles")]
68 pub no_profile: bool,
69 #[arg(
70 short,
71 long,
72 help = "Run as user. This option is only available when running tracexec as root",
73 conflicts_with = "elevate"
74 )]
75 pub user: Option<String>,
76 #[arg(
77 long,
78 help = "Re-execute tracexec with privilege elevation (e.g. via sudo). \
79 The original user credentials are saved and the tracee will run as the original user.",
80 conflicts_with = "user"
81 )]
82 pub elevate: bool,
83 #[arg(
84 long,
85 hide = true,
86 conflicts_with = "elevate",
87 requires = "user",
88 help = "Internal: abstract Unix socket name used to request the original environment from --elevate"
89 )]
90 pub restore_env_socket: Option<String>,
91 #[arg(
92 long,
93 hide = true,
94 conflicts_with = "elevate",
95 requires_all = ["elevated_data_dir", "elevated_data_local_dir"],
96 help = "Internal: original user's config directory from --elevate"
97 )]
98 pub elevated_config_dir: Option<PathBuf>,
99 #[arg(
100 long,
101 hide = true,
102 conflicts_with = "elevate",
103 requires_all = ["elevated_config_dir", "elevated_data_local_dir"],
104 help = "Internal: original user's data directory from --elevate"
105 )]
106 pub elevated_data_dir: Option<PathBuf>,
107 #[arg(
108 long,
109 hide = true,
110 conflicts_with = "elevate",
111 requires_all = ["elevated_config_dir", "elevated_data_dir"],
112 help = "Internal: original user's local data directory from --elevate"
113 )]
114 pub elevated_data_local_dir: Option<PathBuf>,
115 #[clap(subcommand)]
116 pub cmd: CliCommand,
117}
118
119#[derive(Subcommand, Debug)]
120pub enum CliCommand {
121 #[clap(about = "Run tracexec in logging mode")]
122 Log {
123 #[arg(last = true, required = true, help = "command to be executed")]
124 cmd: Vec<String>,
125 #[clap(flatten)]
126 tracing_args: LogModeArgs,
127 #[clap(flatten)]
128 modifier_args: ModifierArgs,
129 #[clap(flatten)]
130 ptrace_args: PtraceArgs,
131 #[clap(flatten)]
132 tracer_event_args: TracerEventArgs,
133 #[clap(
134 short,
135 long,
136 help = "Output, stderr by default. A single hyphen '-' represents stdout."
137 )]
138 output: Option<PathBuf>,
139 },
140 #[clap(about = "Run tracexec in TUI mode with a pseudo terminal by default")]
141 Tui {
142 #[arg(last = true, required = true, help = "command to be executed")]
143 cmd: Vec<String>,
144 #[clap(flatten)]
145 modifier_args: ModifierArgs,
146 #[clap(flatten)]
147 ptrace_args: PtraceArgs,
148 #[clap(flatten)]
149 tracer_event_args: TracerEventArgs,
150 #[clap(flatten)]
151 tui_args: TuiModeArgs,
152 #[clap(flatten)]
153 debugger_args: DebuggerArgs,
154 },
155 #[clap(about = "Generate shell completions for tracexec")]
156 GenerateCompletions {
157 #[arg(required = true, help = "The shell to generate completions for")]
158 shell: clap_complete::Shell,
159 },
160 #[clap(about = "Collect exec events and export them")]
161 Collect {
162 #[arg(last = true, required = true, help = "command to be executed")]
163 cmd: Vec<String>,
164 #[clap(flatten)]
165 modifier_args: ModifierArgs,
166 #[clap(flatten)]
167 ptrace_args: PtraceArgs,
168 #[clap(flatten)]
169 exporter_args: ExporterArgs,
170 #[clap(short = 'F', long, help = "the format for exported exec events")]
171 format: ExportFormat,
172 #[clap(
173 short,
174 long,
175 help = "Output, stderr by default. A single hyphen '-' represents stdout."
176 )]
177 output: Option<PathBuf>,
178 #[clap(
179 long,
180 help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
181 conflicts_with = "no_foreground"
182 )]
183 foreground: bool,
184 #[clap(
185 long,
186 help = "Do not set the terminal foreground process group to tracee",
187 conflicts_with = "foreground"
188 )]
189 no_foreground: bool,
190 },
191 #[cfg(feature = "ebpf")]
192 #[clap(about = "Experimental ebpf mode")]
193 Ebpf {
194 #[clap(subcommand)]
195 command: EbpfCommand,
196 },
197}
198
199#[derive(Subcommand, Debug)]
200#[cfg(feature = "ebpf")]
201pub enum EbpfCommand {
202 #[clap(about = "Run tracexec in logging mode")]
203 Log {
204 #[arg(
205 last = true,
206 help = "command to be executed. Leave it empty to trace all exec on system"
207 )]
208 cmd: Vec<String>,
209 #[clap(
210 short,
211 long,
212 help = "Output, stderr by default. A single hyphen '-' represents stdout."
213 )]
214 output: Option<PathBuf>,
215 #[clap(flatten)]
216 modifier_args: ModifierArgs,
217 #[clap(flatten)]
218 log_args: LogModeArgs,
219 },
220 #[clap(about = "Run tracexec in TUI mode, with a pseudo terminal when following a command")]
221 Tui {
222 #[arg(
223 last = true,
224 help = "command to be executed. Leave it empty to trace all exec on system"
225 )]
226 cmd: Vec<String>,
227 #[clap(flatten)]
228 modifier_args: ModifierArgs,
229 #[clap(flatten)]
230 tracer_event_args: TracerEventArgs,
231 #[clap(flatten)]
232 tui_args: TuiModeArgs,
233 },
234 #[clap(about = "Collect exec events and export them")]
235 Collect {
236 #[arg(
237 last = true,
238 help = "command to be executed. Leave it empty to trace all exec on system"
239 )]
240 cmd: Vec<String>,
241 #[clap(flatten)]
242 modifier_args: ModifierArgs,
243 #[clap(short = 'F', long, help = "the format for exported exec events")]
244 format: ExportFormat,
245 #[clap(flatten)]
246 exporter_args: ExporterArgs,
247 #[clap(
248 short,
249 long,
250 help = "Output, stderr by default. A single hyphen '-' represents stdout."
251 )]
252 output: Option<PathBuf>,
253 #[clap(
254 long,
255 help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
256 conflicts_with = "no_foreground"
257 )]
258 foreground: bool,
259 #[clap(
260 long,
261 help = "Do not set the terminal foreground process group to tracee",
262 conflicts_with = "foreground"
263 )]
264 no_foreground: bool,
265 },
266}
267
268impl Cli {
269 pub fn get_output(path: Option<PathBuf>, color: Color) -> std::io::Result<Box<Output>> {
270 Ok(match path {
271 None => Box::new(stderr()),
272 Some(ref x) if x.as_os_str() == "-" => Box::new(stdout()),
273 Some(path) => {
274 let file = std::fs::OpenOptions::new()
275 .create(true)
276 .truncate(true)
277 .write(true)
278 .open(path)?;
279 if color != Color::Always {
280 owo_colors::control::set_should_colorize(false);
282 }
283 Box::new(BufWriter::new(file))
284 }
285 })
286 }
287
288 pub fn generate_completions(shell: clap_complete::Shell) {
289 let mut cmd = Self::command();
290 clap_complete::generate(shell, &mut cmd, env!("CARGO_CRATE_NAME"), &mut stdout())
291 }
292
293 pub fn merge_config(&mut self, config: Config) {
294 debug!("Merging config: {config:?}");
295 match &mut self.cmd {
296 CliCommand::Log {
297 tracing_args,
298 modifier_args,
299 ptrace_args,
300 ..
301 } => {
302 if let Some(c) = config.ptrace {
303 ptrace_args.merge_config(c);
304 }
305 if let Some(c) = config.modifier {
306 modifier_args.merge_config(c);
307 }
308 if let Some(c) = config.log {
309 tracing_args.merge_config(c);
310 }
311 }
312 CliCommand::Tui {
313 modifier_args,
314 ptrace_args,
315 tui_args,
316 debugger_args,
317 ..
318 } => {
319 if let Some(c) = config.ptrace {
320 ptrace_args.merge_config(c);
321 }
322 if let Some(c) = config.modifier {
323 modifier_args.merge_config(c);
324 }
325 if let Some(c) = config.tui {
326 tui_args.merge_config(c);
327 }
328 if let Some(c) = config.debugger {
329 debugger_args.merge_config(c);
330 }
331 }
332 CliCommand::Collect {
333 foreground,
334 no_foreground,
335 ptrace_args,
336 ..
337 } => {
338 if let Some(c) = config.ptrace {
339 ptrace_args.merge_config(c);
340 }
341 if let Some(c) = config.log
342 && (!*foreground)
343 && (!*no_foreground)
344 && let Some(x) = c.foreground
345 {
346 if x {
347 *foreground = true;
348 } else {
349 *no_foreground = true;
350 }
351 }
352 }
353 #[cfg(feature = "ebpf")]
354 CliCommand::Ebpf { command } => command.merge_config(config),
355 CliCommand::GenerateCompletions { .. } => (),
356 }
357 }
358}
359
360#[cfg(feature = "ebpf")]
361impl EbpfCommand {
362 fn merge_config(&mut self, config: Config) {
363 let modifier_args = match self {
364 Self::Log { modifier_args, .. }
365 | Self::Tui { modifier_args, .. }
366 | Self::Collect { modifier_args, .. } => modifier_args,
367 };
368 if let Some(c) = config.modifier {
369 modifier_args.merge_config(c);
370 }
371 match self {
372 Self::Log { log_args, .. } => {
373 if let Some(c) = config.log {
374 log_args.merge_config(c);
375 }
376 }
377 Self::Tui { tui_args, .. } => {
378 if let Some(c) = config.tui {
379 tui_args.merge_config(c);
380 }
381 }
382 Self::Collect {
383 foreground,
384 no_foreground,
385 ..
386 } => {
387 if let Some(c) = config.log
388 && (!*foreground)
389 && (!*no_foreground)
390 && let Some(x) = c.foreground
391 {
392 if x {
393 *foreground = true;
394 } else {
395 *no_foreground = true;
396 }
397 }
398 }
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use std::{
406 fs,
407 io::Write,
408 path::PathBuf,
409 };
410
411 use test_that::prelude::*;
412
413 use super::*;
414 use crate::cli::{
415 args::{
416 DebuggerArgs,
417 LogModeArgs,
418 ModifierArgs,
419 PtraceArgs,
420 TracerEventArgs,
421 TuiModeArgs,
422 },
423 config::{
424 Config,
425 DebuggerConfig,
426 LogModeConfig,
427 ModifierConfig,
428 PtraceConfig,
429 TuiModeConfig,
430 },
431 options::{
432 Color,
433 ExportFormat,
434 SeccompBpf,
435 },
436 };
437
438 #[test]
439 fn test_cli_parse_log() {
440 let args = vec![
441 "tracexec",
442 "log",
443 "--show-interpreter",
444 "--successful-only",
445 "--",
446 "echo",
447 "hello",
448 ];
449 let cli = Cli::parse_from(args);
450
451 if let CliCommand::Log {
452 cmd,
453 tracing_args,
454 modifier_args,
455 ..
456 } = cli.cmd
457 {
458 assert_eq!(cmd, vec!["echo", "hello"]);
459 assert!(tracing_args.show_interpreter);
460 assert!(modifier_args.successful_only);
461 } else {
462 panic!("Expected Log command");
463 }
464 }
465
466 #[test]
467 fn test_cli_parse_tui() {
468 let args = vec!["tracexec", "tui", "--follow", "--", "bash"];
469 let cli = Cli::parse_from(args);
470
471 if let CliCommand::Tui { cmd, tui_args, .. } = cli.cmd {
472 assert_eq!(cmd, vec!["bash"]);
473 assert!(tui_args.tty());
474 assert!(tui_args.follow);
475 } else {
476 panic!("Expected Tui command");
477 }
478 }
479
480 #[test]
481 fn test_cli_parse_elevate_tui() {
482 let args = vec!["tracexec", "--elevate", "tui", "--", "sudo", "ls"];
483 let cli = Cli::parse_from(args);
484 assert!(cli.elevate);
485 assert_that!(cli.user, none());
486 if let CliCommand::Tui { cmd, .. } = cli.cmd {
487 assert_eq!(cmd, vec!["sudo", "ls"]);
488 } else {
489 panic!("Expected Tui command");
490 }
491 }
492
493 #[test]
494 fn test_cli_parse_elevate_log() {
495 let args = vec!["tracexec", "--elevate", "log", "--", "ls"];
496 let cli = Cli::parse_from(args);
497 assert!(cli.elevate);
498 if let CliCommand::Log { cmd, .. } = cli.cmd {
499 assert_eq!(cmd, vec!["ls"]);
500 } else {
501 panic!("Expected Log command");
502 }
503 }
504
505 #[test]
506 fn test_cli_parse_elevate_conflicts_with_user() {
507 let args = vec!["tracexec", "--elevate", "--user", "root", "log", "--", "ls"];
508 let result = Cli::try_parse_from(args);
509 assert_that!(result, err(anything()));
510 }
511
512 #[test]
513 fn test_cli_parse_restore_env_socket_requires_user() {
514 let args = vec![
515 "tracexec",
516 "--restore-env-socket",
517 "socket-name",
518 "log",
519 "--",
520 "ls",
521 ];
522 let result = Cli::try_parse_from(args);
523 assert_that!(result, err(anything()));
524 }
525
526 #[test]
527 fn test_cli_parse_tui_theme_file_cli_source() {
528 let args = vec![
529 "tracexec",
530 "--no-profile",
531 "tui",
532 "--theme",
533 "cli.toml",
534 "--",
535 "bash",
536 ];
537 let cli = Cli::parse_from(args);
538
539 assert!(cli.no_profile);
540 if let CliCommand::Tui { tui_args, .. } = cli.cmd {
541 assert_eq!(
542 tui_args.theme_file,
543 Some(crate::cli::args::ThemeFileValue::Cli(PathBuf::from(
544 "cli.toml"
545 )))
546 );
547 } else {
548 panic!("Expected Tui command");
549 }
550 }
551
552 #[test]
553 fn test_get_output_stderr_stdout_file() {
554 let out = Cli::get_output(None, Color::Auto).unwrap();
556 let _ = out; let out = Cli::get_output(Some(PathBuf::from("-")), Color::Auto).unwrap();
560 let _ = out;
561
562 let dir = tempfile::tempdir().unwrap();
564 let path = dir.path().join("test_output.txt");
565 let mut out = Cli::get_output(Some(path.clone()), Color::Auto).unwrap();
566 writeln!(out, "Hello world").unwrap();
567 drop(out);
568
569 let content = fs::read_to_string(path).unwrap();
570 assert_that!(content, contains_substring("Hello world"));
571 }
572
573 #[test]
574 fn test_merge_config_log() {
575 let mut cli = Cli {
576 color: Color::Auto,
577 cwd: None,
578 profile: None,
579 no_profile: false,
580 user: None,
581 elevate: false,
582 restore_env_socket: None,
583 elevated_config_dir: None,
584 elevated_data_dir: None,
585 elevated_data_local_dir: None,
586 cmd: CliCommand::Log {
587 cmd: vec!["ls".into()],
588 tracing_args: LogModeArgs {
589 show_interpreter: false,
590 ..Default::default()
591 },
592 modifier_args: ModifierArgs::default(),
593 ptrace_args: PtraceArgs::default(),
594 tracer_event_args: TracerEventArgs::all(),
595 output: None,
596 },
597 };
598
599 let config = Config {
600 ptrace: Some(PtraceConfig {
601 seccomp_bpf: Some(SeccompBpf::On),
602 }),
603 modifier: Some(ModifierConfig {
604 successful_only: Some(true),
605 ..Default::default()
606 }),
607 log: Some(LogModeConfig {
608 show_interpreter: Some(true),
609 ..Default::default()
610 }),
611 tui: None,
612 debugger: None,
613 };
614
615 cli.merge_config(config);
616
617 if let CliCommand::Log {
618 tracing_args,
619 modifier_args,
620 ptrace_args,
621 ..
622 } = cli.cmd
623 {
624 assert!(tracing_args.show_interpreter);
625 assert!(modifier_args.successful_only);
626 assert_eq!(ptrace_args.seccomp_bpf, SeccompBpf::On);
627 } else {
628 panic!("Expected Log command");
629 }
630 }
631
632 #[test]
633 fn test_merge_config_tui() {
634 let mut cli = Cli {
635 color: Color::Auto,
636 cwd: None,
637 profile: None,
638 no_profile: false,
639 user: None,
640 elevate: false,
641 restore_env_socket: None,
642 elevated_config_dir: None,
643 elevated_data_dir: None,
644 elevated_data_local_dir: None,
645 cmd: CliCommand::Tui {
646 cmd: vec!["bash".into()],
647 modifier_args: ModifierArgs::default(),
648 ptrace_args: PtraceArgs::default(),
649 tracer_event_args: TracerEventArgs::all(),
650 tui_args: TuiModeArgs::default(),
651 debugger_args: DebuggerArgs::default(),
652 },
653 };
654
655 let config = Config {
656 ptrace: Some(PtraceConfig {
657 seccomp_bpf: Some(SeccompBpf::Off),
658 }),
659 modifier: Some(ModifierConfig {
660 successful_only: Some(true),
661 ..Default::default()
662 }),
663 log: None,
664 tui: Some(TuiModeConfig {
665 follow: Some(true),
666 frame_rate: Some(30.0),
667 ..Default::default()
668 }),
669 debugger: Some(DebuggerConfig {
670 default_external_command: Some("echo hello".into()),
671 }),
672 };
673
674 cli.merge_config(config);
675
676 if let CliCommand::Tui {
677 modifier_args,
678 ptrace_args,
679 tui_args,
680 debugger_args,
681 ..
682 } = cli.cmd
683 {
684 assert!(modifier_args.successful_only);
685 assert_eq!(ptrace_args.seccomp_bpf, SeccompBpf::Off);
686 assert_eq!(tui_args.frame_rate.unwrap(), 30.0);
687 assert_eq!(
688 debugger_args.default_external_command.as_ref().unwrap(),
689 "echo hello"
690 );
691 } else {
692 panic!("Expected Tui command");
693 }
694 }
695
696 #[test]
697 fn test_merge_config_collect_foreground() {
698 let mut cli = Cli {
699 color: Color::Auto,
700 cwd: None,
701 profile: None,
702 no_profile: false,
703 user: None,
704 elevate: false,
705 restore_env_socket: None,
706 elevated_config_dir: None,
707 elevated_data_dir: None,
708 elevated_data_local_dir: None,
709 cmd: CliCommand::Collect {
710 cmd: vec!["ls".into()],
711 modifier_args: ModifierArgs::default(),
712 ptrace_args: PtraceArgs::default(),
713 exporter_args: Default::default(),
714 format: ExportFormat::Json,
715 output: None,
716 foreground: false,
717 no_foreground: false,
718 },
719 };
720
721 let config = Config {
722 log: Some(LogModeConfig {
723 foreground: Some(true),
724 ..Default::default()
725 }),
726 ptrace: None,
727 modifier: None,
728 tui: None,
729 debugger: None,
730 };
731
732 cli.merge_config(config);
733
734 if let CliCommand::Collect {
735 foreground,
736 no_foreground,
737 ..
738 } = cli.cmd
739 {
740 assert!(foreground);
741 assert!(!no_foreground);
742 } else {
743 panic!("Expected Collect command");
744 }
745 }
746
747 #[test]
748 fn test_generate_completions_smoke() {
749 Cli::generate_completions(clap_complete::Shell::Bash);
751 }
752
753 #[cfg(feature = "ebpf")]
754 mod ebpf {
755 use super::*;
756 use crate::cli::args::ThemeFileValue;
757
758 fn load_profile(args: &[&str], profile: &str) -> color_eyre::Result<Cli> {
759 let mut file = tempfile::NamedTempFile::new()?;
760 file.write_all(profile.as_bytes())?;
761 let mut cli = Cli::try_parse_from(args)?;
762 cli.merge_config(Config::load(Some(file.path().to_path_buf()))?);
763 Ok(cli)
764 }
765
766 #[test]
767 fn test_merge_config_ebpf_log() -> color_eyre::Result<()> {
768 for with_command in [false, true] {
769 for override_config in [false, true] {
770 let mut args = vec!["tracexec", "ebpf", "log"];
771 if override_config {
772 args.extend(["--no-show-interpreter", "--foreground", "--no-timestamp"]);
773 }
774 if with_command {
775 args.extend(["--", "true"]);
776 }
777 let cli = load_profile(
778 &args,
779 r#"
780 [log]
781 show_interpreter = true
782 foreground = false
783 [modifier]
784 successful_only = true
785 timestamp = { enable = true }
786 "#,
787 )?;
788 let CliCommand::Ebpf {
789 command:
790 EbpfCommand::Log {
791 log_args,
792 modifier_args,
793 ..
794 },
795 } = cli.cmd
796 else {
797 panic!("Expected eBPF Log command");
798 };
799 assert_eq!(log_args.show_interpreter, !override_config);
800 assert_eq!(log_args.foreground(), override_config);
801 let modifier_args = modifier_args.processed();
802 assert!(modifier_args.successful_only);
803 assert_eq!(modifier_args.timestamp, !override_config);
804 }
805 }
806 Ok(())
807 }
808
809 #[test]
810 fn test_merge_config_ebpf_tui() -> color_eyre::Result<()> {
811 for with_command in [false, true] {
812 for override_config in [false, true] {
813 let mut args = vec!["tracexec", "ebpf", "tui"];
814 if override_config {
815 args.extend([
816 "--frame-rate",
817 "45",
818 "--theme",
819 "cli.toml",
820 "--no-timestamp",
821 ]);
822 }
823 if with_command {
824 args.extend(["--", "true"]);
825 }
826 let cli = load_profile(
827 &args,
828 r#"
829 [tui]
830 follow = true
831 frame_rate = 30.0
832 theme-file = "profile.toml"
833 [tui.keys]
834 quit = "q"
835 [modifier]
836 timestamp = { enable = true }
837 "#,
838 )?;
839 let CliCommand::Ebpf {
840 command:
841 EbpfCommand::Tui {
842 tui_args,
843 modifier_args,
844 ..
845 },
846 } = cli.cmd
847 else {
848 panic!("Expected eBPF Tui command");
849 };
850 assert!(tui_args.follow);
851 assert_eq!(
852 tui_args.frame_rate,
853 Some(if override_config { 45.0 } else { 30.0 })
854 );
855 assert_eq!(
856 tui_args.theme_file,
857 Some(if override_config {
858 ThemeFileValue::Cli("cli.toml".into())
859 } else {
860 ThemeFileValue::Config("profile.toml".into())
861 })
862 );
863 assert!(tui_args.keys.is_some_and(|keys| keys.quit.is_some()));
864 assert_eq!(modifier_args.processed().timestamp, !override_config);
865 }
866 }
867 Ok(())
868 }
869
870 #[test]
871 fn test_merge_config_ebpf_collect() -> color_eyre::Result<()> {
872 for with_command in [false, true] {
873 for configured_foreground in [false, true] {
874 for cli_foreground in [None, Some(false), Some(true)] {
875 let mut args = vec!["tracexec", "ebpf", "collect", "--format", "json"];
876 if let Some(foreground) = cli_foreground {
877 args.push(if foreground {
878 "--foreground"
879 } else {
880 "--no-foreground"
881 });
882 }
883 if with_command {
884 args.extend(["--", "true"]);
885 }
886 let cli = load_profile(
887 &args,
888 &format!(
889 "[log]\nforeground = {configured_foreground}\n[modifier]\ncollect_cgroup = true"
890 ),
891 )?;
892 let CliCommand::Ebpf {
893 command:
894 EbpfCommand::Collect {
895 foreground,
896 no_foreground,
897 modifier_args,
898 ..
899 },
900 } = cli.cmd
901 else {
902 panic!("Expected eBPF Collect command");
903 };
904 let expected = cli_foreground.unwrap_or(configured_foreground);
905 assert_eq!((foreground, no_foreground), (expected, !expected));
906 assert!(modifier_args.processed().collect_cgroup);
907 }
908 }
909 }
910 Ok(())
911 }
912 }
913}