aboutsummaryrefslogtreecommitdiff
path: root/system/fatrace/patches/0003-Add-option-d-dir.patch
diff options
context:
space:
mode:
Diffstat (limited to 'system/fatrace/patches/0003-Add-option-d-dir.patch')
-rw-r--r--system/fatrace/patches/0003-Add-option-d-dir.patch320
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
+