diff options
Diffstat (limited to 'system/fatrace/patches/0003-Add-option-d-dir.patch')
-rw-r--r-- | system/fatrace/patches/0003-Add-option-d-dir.patch | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/system/fatrace/patches/0003-Add-option-d-dir.patch b/system/fatrace/patches/0003-Add-option-d-dir.patch new file mode 100644 index 0000000000..6a251555ba --- /dev/null +++ b/system/fatrace/patches/0003-Add-option-d-dir.patch @@ -0,0 +1,320 @@ +From 21d7e40b2ccc2a3209341264bae97cc0a64cc068 Mon Sep 17 00:00:00 2001 +From: Axel Svensson <mail@axelsvensson.com> +Date: Sun, 29 Jun 2025 17:30:53 +0200 +Subject: [PATCH 3/3] Add option -d,--dir + +Fixes #48 +--- + fatrace.8 | 27 ++++++++++++- + fatrace.c | 50 ++++++++++++++++++++++- + tests/test.py | 107 ++++++++++++++++++++++++++++++++++++++++++++------ + 3 files changed, 168 insertions(+), 16 deletions(-) + +diff --git a/fatrace.8 b/fatrace.8 +index 7117ee2..aa355b1 100644 +--- a/fatrace.8 ++++ b/fatrace.8 +@@ -1,4 +1,4 @@ +-.TH fatrace 8 "August 20, 2020" "Martin Pitt" ++.TH fatrace 8 "September 5, 2025" "Martin Pitt" + + .SH NAME + +@@ -10,6 +10,10 @@ fatrace \- report system wide file access events + [ + .I OPTIONS + ] ++[ ++-- ++] ++[ \fIDIR\fR... ] + + .SH DESCRIPTION + +@@ -212,6 +216,27 @@ Print information about all parent processes. + .B \-e\fR, \fB\-\-exe + Print executable path. + ++.TP ++.B \-d \fIDIR\fR, \fB\-\-dir=\fIDIR\fR ++Show only events where the affected file is \fIdirectly\fR under this directory. ++Can be specified multiple times to include events from several directories. ++.IP ++This is \fInot\fR recursive. For example, \fB\-d /home/user\fR will show events ++for \fB/home/user/file\fR but not for \fB/home/user/subdir/file\fR. ++.IP ++\fIDIR\fRs can also be specified at the end of the command line, advisably ++preceded by the \fB\-\-\fR separator. As long as no directories will be created ++or moved under a subtree, it's possible to watch that subtree like so: ++.RS ++.IP "" 4 ++fatrace -- $(find /path/to/subtree -type d) ++.RE ++.IP ++The attachment is to a directory inode, not the path. For example, this means ++that 1) If you move a watched directory while fatrace runs, you may receive ++events for a path that is not listed on the command line; 2) If you delete and ++recreate a watched directory you will no longer receive events. ++ + .TP + .B \-h \fR, \fB\-\-help + Print help and exit. +diff --git a/fatrace.c b/fatrace.c +index 836c431..1c9042d 100644 +--- a/fatrace.c ++++ b/fatrace.c +@@ -48,6 +48,9 @@ + + #define BUFSIZE 256*1024 + ++/* Likely to be less than /proc/sys/fs/fanotify/max_user_marks */ ++#define MAX_DIRS 4096 ++ + /* https://man7.org/linux/man-pages/man5/proc_pid_comm.5.html ; not defined in any include file */ + #ifndef TASK_COMM_LEN + #define TASK_COMM_LEN 16 +@@ -73,6 +76,8 @@ static char* option_comm = NULL; + static bool option_json = false; + static bool option_parents = false; + static bool option_exe = false; ++static const char *option_dirs[MAX_DIRS]; ++static unsigned int option_dirs_len = 0; + + /* --time alarm sets this to 0 */ + static volatile int running = 1; +@@ -594,6 +599,26 @@ do_mark (int fan_fd, const char *dir, bool fatal) + static void + setup_fanotify (int fan_fd) + { ++ if (option_dirs_len > 0) { ++ mark_mode = FAN_MARK_ADD; ++ char resolved[PATH_MAX]; ++ struct stat st; ++ for (unsigned i = 0; i < option_dirs_len; i++) { ++ if (realpath(option_dirs[i], resolved) && ++ stat(resolved, &st) == 0) { ++ if (S_ISDIR(st.st_mode)) ++ do_mark (fan_fd, resolved, false); ++ else ++ errx(EXIT_FAILURE, ++ "Not a directory: %s", option_dirs[i]); ++ } ++ else ++ err(EXIT_FAILURE, ++ "Cannot resolve directory: %s", option_dirs[i]); ++ } ++ return; ++ } ++ + FILE* mounts; + struct mntent* mount; + +@@ -644,7 +669,7 @@ setup_fanotify (int fan_fd) + static void + help (void) + { +- puts ("Usage: fatrace [options...] \n" ++ puts ("Usage: fatrace [options...] [--] [DIR...]\n" + "\n" + "Options:\n" + " -c, --current-mount Only record events on partition/mount of\n" +@@ -663,6 +688,10 @@ help (void) + " -j, --json Write events in JSONL format.\n" + " -P, --parents Include information about all parent processes.\n" + " -e, --exe Add executable path to events.\n" ++" -d DIR, --dir=DIR Show only events on files directly under this\n" ++" directory. NOT recursive. Can be specified\n" ++" multiple times. DIRs can also be specified at\n" ++" the end of the command line.\n" + " -h, --help Show help."); + } + +@@ -691,12 +720,13 @@ parse_args (int argc, char** argv) + {"json", no_argument, 0, 'j'}, + {"parents", no_argument, 0, 'P'}, + {"exe", no_argument, 0, 'e'}, ++ {"dir", required_argument, 0, 'd'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0 } + }; + + while (1) { +- c = getopt_long (argc, argv, "C:co:s:tup:f:jPeh", long_options, NULL); ++ c = getopt_long (argc, argv, "C:co:s:tup:f:jPed:h", long_options, NULL); + + if (c == -1) + break; +@@ -801,6 +831,13 @@ parse_args (int argc, char** argv) + option_exe = true; + break; + ++ case 'd': ++ if (option_dirs_len >= MAX_DIRS) ++ errx (EXIT_FAILURE, "Error: Too many --dir arguments" ++ " (maximum is %d).", MAX_DIRS); ++ option_dirs[option_dirs_len++] = optarg; ++ break; ++ + case 'h': + help (); + exit (EXIT_SUCCESS); +@@ -813,6 +850,15 @@ parse_args (int argc, char** argv) + errx (EXIT_FAILURE, "Internal error: unexpected option '%c'", c); + } + } ++ for (int i = optind; i < argc; i++) { ++ if (option_dirs_len >= MAX_DIRS) ++ errx (EXIT_FAILURE, "Error: Too many --dir and DIR arguments" ++ " (maximum is %d).", MAX_DIRS); ++ option_dirs[option_dirs_len++] = argv[i]; ++ } ++ if (option_current_mount && option_dirs_len > 0) ++ errx (EXIT_FAILURE, ++ "Error: -c,--current-mount and -d,--dir are mutually exclusive."); + } + + static void +diff --git a/tests/test.py b/tests/test.py +index 0dd904a..f86c0e1 100644 +--- a/tests/test.py ++++ b/tests/test.py +@@ -72,18 +72,30 @@ class FatraceRunner: + self.log_content = f.read() + self.log_dir.cleanup() + +- def assert_log(self, pattern: str) -> None: ++ def has_log(self, pattern: str) -> bool: + """Check if a regex pattern exists in the log content.""" + + assert self.log_content, "Need to call run() first" + +- if not re.search(pattern, self.log_content, re.MULTILINE): +- raise AssertionError(f"""Pattern not found in log: {pattern} +----- Log content ---- +-{self.log_content} +------------------""") ++ return bool(re.search(pattern, self.log_content, re.MULTILINE)) + +- def assert_json(self, condition_func: Callable[[dict], bool]) -> None: ++ def assert_log(self, pattern: str) -> None: ++ if self.has_log(pattern): ++ return ++ raise AssertionError(f"Pattern not found in log: {pattern}\n" ++ "---- Log content ----\n" ++ f"{self.log_content}\n" ++ "-----------------") ++ ++ def assert_not_log(self, pattern: str) -> None: ++ if not self.has_log(pattern): ++ return ++ raise AssertionError(f"Pattern found in log: {pattern}\n" ++ "---- Log content ----\n" ++ f"{self.log_content}\n" ++ "-----------------") ++ ++ def has_json(self, condition_func: Callable[[dict], bool]) -> bool: + """Check if any JSON line matches the condition function.""" + + assert self.log_content, "Need to call run() first" +@@ -94,16 +106,27 @@ class FatraceRunner: + entry = json.loads(line) + try: + if condition_func(entry): +- return ++ return True + except KeyError: + # Ignore entries that do not match the expected structure + pass ++ return False + +- raise AssertionError(f"""No JSON entry matched condition +----- Log content ---- +-{self.log_content} +------------------""") +- ++ def assert_json(self, condition_func: Callable[[dict], bool]) -> None: ++ if self.has_json(condition_func): ++ return ++ raise AssertionError("No JSON entry matched condition\n" ++ "---- Log content ----\n" ++ f"{self.log_content}\n" ++ "-----------------") ++ ++ def assert_not_json(self, condition_func: Callable[[dict], bool]) -> None: ++ if not self.has_json(condition_func): ++ return ++ raise AssertionError("At least one JSON entry matched condition\n" ++ "---- Log content ----\n" ++ f"{self.log_content}\n" ++ "-----------------") + + class FatraceTests(unittest.TestCase): + def setUp(self): +@@ -574,6 +597,64 @@ with open("{python_pid_file}", "w") as f: f.write(f"{{os.getpid()}}\\n") + "path" not in e + )) + ++ def test_dir(self): ++ yes1 = str(self.tmp_path / "yes-1") ++ yes2 = str(self.tmp_path / "yes-2") ++ no1 = str(self.tmp_path / "no-1") ++ ++ exe(["mkdir", yes1]) ++ exe(["mkdir", yes2]) ++ exe(["mkdir", no1]) ++ ++ f = FatraceRunner(["-s", "3", "-d", yes1, f"--dir={yes2}"]) ++ f_json = FatraceRunner(["-s", "3", "--json", "--", yes1, yes2]) ++ ++ slow_exe(["mkdir", f"{yes1}/subA"]) ++ slow_exe(["mkdir", f"{no1}/subB"]) ++ ++ slow_exe(["touch", f"{yes1}/yesC"]) ++ slow_exe(["touch", f"{yes1}/subA/noD"]) ++ slow_exe(["touch", f"{yes2}/yesE"]) ++ slow_exe(["touch", f"{no1}/noF"]) ++ slow_exe(["touch", f"{no1}/subB/noG"]) ++ ++ slow_exe(["mv", yes1, yes2]) ++ new_yes1 = str(self.tmp_path / "yes-2" / "yes-1") ++ slow_exe(["mv", no1, yes2]) ++ new_no1 = str(self.tmp_path / "yes-2" / "no-1") ++ ++ slow_exe(["touch", f"{new_yes1}/yesH"]) ++ slow_exe(["touch", f"{new_yes1}/subA/noI"]) ++ slow_exe(["touch", f"{new_no1}/noJ"]) ++ slow_exe(["touch", f"{new_no1}/subB/noK"]) ++ ++ f.finish() ++ f_json.finish() ++ ++ f.assert_log (rf"^mkdir\([0-9]*\): \+ +{re.escape(yes1)}") ++ f.assert_not_log(rf"^mkdir\([0-9]*\): \+ +{re.escape(no1)}") ++ f.assert_log (rf"^touch\([0-9]*\): C?WO? +{re.escape(yes1)}/yesC") ++ f.assert_not_log(rf"^touch\([0-9]*\): C?WO? +{re.escape(yes1)}/subA/noD") ++ f.assert_log (rf"^touch\([0-9]*\): C?WO? +{re.escape(yes2)}/yesE") ++ f.assert_not_log(rf"^touch\([0-9]*\): C?WO? +{re.escape(no1)}/noF") ++ f.assert_not_log(rf"^touch\([0-9]*\): C?WO? +{re.escape(no1)}/subB/noG") ++ f.assert_log (rf"^touch\([0-9]*\): C?WO? +{re.escape(new_yes1)}/yesH") ++ f.assert_not_log(rf"^touch\([0-9]*\): C?WO? +{re.escape(new_yes1)}/subA/noI") ++ f.assert_not_log(rf"^touch\([0-9]*\): C?WO? +{re.escape(new_no1)}/noJ") ++ f.assert_not_log(rf"^touch\([0-9]*\): C?WO? +{re.escape(new_no1)}/subB/noK") ++ ++ f_json.assert_json (lambda e: e["comm"] == "mkdir" and e["types"] == "+" and e["path"] == yes1) ++ f_json.assert_not_json(lambda e: e["comm"] == "mkdir" and e["types"] == "+" and e["path"] == no1) ++ f_json.assert_json (lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{yes1}/yesC") ++ f_json.assert_not_json(lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{yes1}/subA/noD") ++ f_json.assert_json (lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{yes2}/yesE") ++ f_json.assert_not_json(lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{no1}/noF") ++ f_json.assert_not_json(lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{no1}/subB/noG") ++ f_json.assert_json (lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{new_yes1}/yesH") ++ f_json.assert_not_json(lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{new_yes1}/subA/noI") ++ f_json.assert_not_json(lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{new_no1}/noJ") ++ f_json.assert_not_json(lambda e: e["comm"] == "touch" and "W" in e["types"] and e["path"] == f"{new_no1}/subB/noK") ++ + @unittest.skipIf("container" in os.environ, "Not supported in container environment") + @unittest.skipIf(os.path.exists("/sysroot/ostree"), "Test does not work on OSTree") + @unittest.skipIf(root_is_btrfs, "FANOTIFY does not work on btrfs, https://github.com/martinpitt/fatrace/issues/3") +-- +2.46.3 + |