lurk_cli/
args.rs

1use crate::arch::{
2    TRACE_CLOCK, TRACE_CREDS, TRACE_DESC, TRACE_FILE, TRACE_FSTAT, TRACE_FSTATFS, TRACE_IPC,
3    TRACE_LSTAT, TRACE_MEMORY, TRACE_NETWORK, TRACE_PROCESS, TRACE_PURE, TRACE_SIGNAL, TRACE_STAT,
4    TRACE_STATFS, TRACE_STATFS_LIKE, TRACE_STAT_LIKE,
5};
6use crate::syscall_info::RetCode;
7use anyhow::bail;
8use clap::{Parser, Subcommand};
9use libc::pid_t;
10use regex::Regex;
11use std::collections::HashMap;
12use std::path::PathBuf;
13use std::str::FromStr;
14use syscalls::{Sysno, SysnoSet};
15
16#[derive(Parser, Debug, Default)]
17#[command(name = "lurk", about, version, allow_external_subcommands = true)]
18pub struct Args {
19    /// Display system call numbers
20    #[arg(short = 'n', long)]
21    pub syscall_number: bool,
22    /// Attach to a running process
23    #[arg(short = 'p', long)]
24    pub attach: Option<pid_t>,
25    /// Print un-abbreviated versions of strings
26    #[arg(short = 'v', long)]
27    pub no_abbrev: bool,
28    /// Maximum string argument size to print
29    #[arg(short, long, conflicts_with = "no_abbrev")]
30    pub string_limit: Option<usize>,
31    /// Name of the file to print output to
32    #[arg(short = 'o', long)]
33    pub file: Option<PathBuf>,
34    /// Report a summary instead of the regular output
35    #[arg(short = 'c', long)]
36    pub summary_only: bool,
37    /// Report a summary in addition to the regular output
38    #[arg(short = 'C', long, conflicts_with = "summary_only")]
39    pub summary: bool,
40    /// Print only syscalls that returned without an error code
41    #[arg(short = 'z', long)]
42    pub successful_only: bool,
43    /// Print only syscalls that returned with an error code
44    #[arg(short = 'Z', long, conflicts_with = "successful_only")]
45    pub failed_only: bool,
46    /// --env var=val adds an environment variable. --env var removes an environment variable.
47    #[arg(short = 'E', long)]
48    pub env: Vec<String>,
49    /// Run the command with uid, gid and supplementary groups of username.
50    #[arg(short, long)]
51    pub username: Option<String>,
52    /// Trace child processes as they are created by currently traced processes.
53    #[arg(short, long)]
54    pub follow_forks: bool,
55    /// Show the time spent in system calls in ms.
56    #[arg(short = 'T', long)]
57    pub syscall_times: bool,
58    /// A qualifying expression which modifies which events to trace or how to trace them.
59    #[arg(short, long)]
60    pub expr: Vec<String>,
61    /// Display output in JSON format
62    #[arg(short, long)]
63    pub json: bool,
64    #[command(subcommand)]
65    pub command: Option<ArgCommand>,
66}
67
68// The command/subcommand is a bit hacky, but gets the job done:
69// https://github.com/clap-rs/clap/discussions/4560#discussioncomment-5392780
70
71#[derive(Subcommand, Debug, PartialEq)]
72pub enum ArgCommand {
73    /// Trace command
74    #[command(external_subcommand)]
75    Command(Vec<String>),
76}
77
78#[derive(Parser, Debug, PartialEq)]
79pub struct ArgAttach {
80    /// Attach to a running process with the given pid.
81    #[arg(short = 'p', long)]
82    pub attach: pid_t,
83}
84
85impl Args {
86    pub fn create_filter(&self) -> anyhow::Result<Filter> {
87        let all_syscall_names: HashMap<&'static str, Sysno> =
88            SysnoSet::all().iter().map(|v| (v.name(), v)).collect();
89        let mut expr_negation = false;
90        let mut system_calls = SysnoSet::empty();
91
92        // Sort system calls listed with --expr into their category to handle them accordingly
93        for token in &self.expr {
94            let mut tokens = token.splitn(2, '=');
95            match (tokens.next(), tokens.next()) {
96                (Some(token_key), Some(mut token_value))
97                    if token_key == "t" || token_key == "trace" =>
98                {
99                    if let Some(v) = token_value.strip_prefix('!') {
100                        token_value = v;
101                        expr_negation = true;
102                    }
103
104                    for part in token_value.split(',') {
105                        if let Some(part) = part.strip_prefix('/') {
106                            // The '/' prefix followed by a regex pattern to match system calls
107                            if let Ok(pattern) = Regex::new(part) {
108                                for (syscall, sysno) in &all_syscall_names {
109                                    if pattern.is_match(syscall) {
110                                        system_calls.insert(*sysno);
111                                    }
112                                }
113                            } else {
114                                bail!("Invalid regex pattern: {part}");
115                            }
116                        } else if let Some(part) = part.strip_prefix('%') {
117                            // The '%' prefix followed by the name of a syscalls category to trace
118                            system_calls = system_calls.union(match part {
119                                "file" => &TRACE_FILE,
120                                "process" => &TRACE_PROCESS,
121                                "network" | "net" => &TRACE_NETWORK,
122                                "signal" => &TRACE_SIGNAL,
123                                "ipc" => &TRACE_IPC,
124                                "desc" => &TRACE_DESC,
125                                "memory" => &TRACE_MEMORY,
126                                "creds" => &TRACE_CREDS,
127                                "stat" => &TRACE_STAT,
128                                "lstat" => &TRACE_LSTAT,
129                                "fstat" => &TRACE_FSTAT,
130                                "%stat" => &TRACE_STAT_LIKE,
131                                "statfs" => &TRACE_STATFS,
132                                "fstatfs" => &TRACE_FSTATFS,
133                                "%statfs" => &TRACE_STATFS_LIKE,
134                                "clock" => &TRACE_CLOCK,
135                                "pure" => &TRACE_PURE,
136                                v => bail!("Category '{v}' is not valid!"),
137                            });
138                        } else {
139                            // The optional '?' prefix will ignore unknown system calls
140                            let mut ignore_unknown = false;
141                            if let Some(v) = token_value.strip_prefix('?') {
142                                token_value = v;
143                                ignore_unknown = true;
144                            }
145                            if let Ok(val) = Sysno::from_str(part) {
146                                system_calls.insert(val);
147                            } else if !ignore_unknown {
148                                bail!("System call '{part}' is not valid!");
149                            }
150                        }
151                    }
152                }
153                _ => bail!("expr {token} is not supported. Please have a look at the syntax."),
154            }
155        }
156        Ok(Filter {
157            ret_code_filter: if self.successful_only {
158                FilterRetCode::Oks
159            } else if self.failed_only {
160                FilterRetCode::Errs
161            } else {
162                FilterRetCode::All
163            },
164            sysno_filter: if system_calls.count() == 0 {
165                FilterSysno::All
166            } else if expr_negation {
167                FilterSysno::Except(system_calls)
168            } else {
169                FilterSysno::Only(system_calls)
170            },
171        })
172    }
173}
174
175enum FilterRetCode {
176    All,
177    Oks,
178    Errs,
179}
180
181enum FilterSysno {
182    All,
183    Only(SysnoSet),
184    Except(SysnoSet),
185}
186
187pub struct Filter {
188    ret_code_filter: FilterRetCode,
189    sysno_filter: FilterSysno,
190}
191
192impl Filter {
193    pub fn matches(&mut self, sys_no: Sysno, res: RetCode) -> bool {
194        (
195            // Should this result code be printed?
196            match self.ret_code_filter {
197                FilterRetCode::All => true,
198                FilterRetCode::Oks => matches!(res, RetCode::Ok(_) | RetCode::Address(_)),
199                FilterRetCode::Errs => matches!(res, RetCode::Err(_)),
200            }
201        ) && (
202            // Should this sys_no be printed?
203            match &self.sysno_filter {
204                FilterSysno::All => true,
205                FilterSysno::Only(sysno_set) => sysno_set.contains(sys_no),
206                FilterSysno::Except(sysno_set) => !sysno_set.contains(sys_no),
207            }
208        )
209    }
210
211    pub fn all_enabled(&self) -> SysnoSet {
212        match &self.sysno_filter {
213            FilterSysno::All => SysnoSet::all(),
214            FilterSysno::Only(sysno_set) => sysno_set.clone(),
215            FilterSysno::Except(sysno_set) => SysnoSet::all().difference(sysno_set),
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_args_simple() {
226        let args = Args::parse_from(["lurk", "app"]);
227        assert_eq!(
228            args.command,
229            Some(ArgCommand::Command(vec!["app".to_string()])),
230        );
231    }
232}