diff --git a/README.md b/README.md
index 0ceb15e53..5054adbc6 100644
--- a/README.md
+++ b/README.md
@@ -95,3 +95,7 @@ For more details on installation, usage, configuration, extension development, a
alt="extension settings"
/>
+
+# TEMP NOTES
+
+sudo setcap cap_dac_read_search,cap_sys_admin=+ep ./vicinae
diff --git a/tempnotes.md b/tempnotes.md
new file mode 100644
index 000000000..d5c80e194
--- /dev/null
+++ b/tempnotes.md
@@ -0,0 +1,9 @@
+# TEMP NOTES
+
+sudo setcap cap_dac_read_search,cap_sys_admin=+ep ./vicinae
+
+## TODO
+
+fs:
+Since it marks the filesystem, ensure that it only uses mark_filesystem_mount when the folder is a mount point?
+note/warning about bftrs
diff --git a/vicinae/CMakeLists.txt b/vicinae/CMakeLists.txt
index 61c0b4c42..9851ca179 100644
--- a/vicinae/CMakeLists.txt
+++ b/vicinae/CMakeLists.txt
@@ -9,6 +9,10 @@ find_package(LibXml2 REQUIRED)
list(APPEND LIBS Qt6::Widgets Qt6::Sql Qt6::Network Qt6::Svg Qt6::DBus Qt6::Concurrent ${CMARK_LIBRARY} ${CMARK_EXT_LIBRARY} protobuf::libprotobuf minizip OpenSSL::Crypto wayland-client xdgpp qt6keychain LibXml2::LibXml2)
+if (UNIX AND NOT APPLE)
+ list(APPEND LIBS cap)
+endif()
+
set(WLR_CLIP_BIN ${CMAKE_BINARY_DIR}/wlr-clip/wlr-clip${CMAKE_EXECUTABLE_SUFFIX})
set(ASSET_PATH ${CMAKE_CURRENT_SOURCE_DIR}/assets)
set(EXTRA_PATH ${CMAKE_SOURCE_DIR}/extra)
@@ -787,3 +791,4 @@ protobuf_generate(
)
install(TARGETS ${TARGET})
+install(CODE "execute_process(COMMAND setcap cap_sys_admin,cap_dac_read_search+ep \${CMAKE_INSTALL_PREFIX}/bin/${TARGET})")
diff --git a/vicinae/include/watcher.hpp b/vicinae/include/watcher.hpp
index ef4a0d88e..c85a5b191 100644
--- a/vicinae/include/watcher.hpp
+++ b/vicinae/include/watcher.hpp
@@ -12,6 +12,7 @@
#include
#include
#include
+#include
namespace wtr {
inline namespace watcher {
@@ -944,7 +945,10 @@ template inline auto walkdir_do(char const *const path, Fn const &f)
#include
#include
+#include
#include
+#include
+#include
#include
#include
#include
@@ -954,6 +958,20 @@ template inline auto walkdir_do(char const *const path, Fn const &f)
namespace detail::wtr::watcher::adapter::fanotify {
+#if KERNEL_VERSION(4, 20, 0) <= LINUX_VERSION_CODE
+#define WATCHER_HAVE_FAN_MARK_FILESYSTEM 1
+#else
+#define WATCHER_HAVE_FAN_MARK_FILESYSTEM 0
+#endif
+
+inline auto get_watcher_mode_name() -> const char * {
+#if WATCHER_HAVE_FAN_MARK_FILESYSTEM
+ return "FAN_MARK_FILESYSTEM (filesystem-level, requires root)";
+#else
+ return "FAN_MARK_ADD (directory-level recursive)";
+#endif
+}
+
/* We request post-event reporting, non-blocking
IO and unlimited marks for fanotify. We need
sudo mode for the unlimited marks. If we were
@@ -990,6 +1008,7 @@ struct ke_fa_ev {
| FAN_EVENT_ON_CHILD;
int fd = -1;
+ bool using_filesystem_mode = false;
alignas(fanotify_event_metadata) char buf[buf_len]{0};
};
@@ -1002,6 +1021,65 @@ struct sysres {
adapter::ep ep{};
};
+inline auto get_mount_point = [](char const *path) -> std::optional {
+ std::ifstream mounts("/proc/self/mountinfo");
+ if (!mounts) return std::nullopt;
+
+ char real_path[PATH_MAX];
+ if (!realpath(path, real_path)) return std::nullopt;
+
+ std::string line;
+ std::filesystem::path best_mount;
+ size_t best_len = 0;
+
+ while (std::getline(mounts, line)) {
+ std::istringstream iss(line);
+ std::string mount_id, parent_id, dev_id, root, mount_point;
+ iss >> mount_id >> parent_id >> dev_id >> root >> mount_point;
+
+ if (std::string(real_path).starts_with(mount_point)) {
+ if (mount_point.length() > best_len) {
+ best_mount = mount_point;
+ best_len = mount_point.length();
+ }
+ }
+ }
+
+ return best_len > 0 ? std::optional(best_mount) : std::nullopt;
+};
+
+inline auto do_mark_recursive = [](char const *const dirpath, int fa_fd, auto const &cb) -> result {
+ auto e = result::w_sys_not_watched;
+ char real[PATH_MAX];
+ int anonymous_wd = realpath(dirpath, real) && is_dir(real)
+ ? fanotify_mark(fa_fd, FAN_MARK_ADD, ke_fa_ev::recv_flags, AT_FDCWD, real)
+ : -1;
+ if (anonymous_wd == 0)
+ return result::complete;
+ else
+ return send_msg(e, dirpath, cb), e;
+};
+
+inline auto do_mark_filesystem = [](char const *const dirpath, int fa_fd, auto const &cb) -> result {
+#if WATCHER_HAVE_FAN_MARK_FILESYSTEM
+ auto e = result::w_sys_not_watched;
+ char real[PATH_MAX];
+
+ if (!realpath(dirpath, real) || !is_dir(real)) return result::w_sys_not_watched;
+
+ int anonymous_wd =
+ fanotify_mark(fa_fd, FAN_MARK_FILESYSTEM | FAN_MARK_ADD, ke_fa_ev::recv_flags, AT_FDCWD, real);
+
+ if (anonymous_wd == 0) return result::complete;
+
+ if (errno == EINVAL || errno == EXDEV) { return do_mark_recursive(dirpath, fa_fd, cb); }
+
+ return send_msg(e, dirpath, cb), e;
+#else
+ return do_mark_recursive(dirpath, fa_fd, cb);
+#endif
+};
+
inline auto do_mark = [](char const *const dirpath, int fa_fd, auto const &cb) -> result {
auto e = result::w_sys_not_watched;
char real[PATH_MAX];
@@ -1022,18 +1100,48 @@ inline auto do_mark = [](char const *const dirpath, int fa_fd, auto const &cb) -
inline auto make_sysres = [](char const *const base_path, auto const &cb, semabin const &living) -> sysres {
int fa_fd = fanotify_init(ke_fa_ev::init_flags, ke_fa_ev::init_io_flags);
if (fa_fd < 1) return sysres{.ok = result::e_sys_api_fanotify, .il = living};
- walkdir_do(base_path, [&](auto dir) { do_mark(dir, fa_fd, cb); });
+
+ bool using_fs_mode = false;
+ auto mark_result = do_mark_filesystem(base_path, fa_fd, cb);
+ if (mark_result == result::complete)
+ using_fs_mode = true;
+ else
+ walkdir_do(base_path, [&](auto dir) { do_mark(dir, fa_fd, cb); });
+
auto ep = make_ep(fa_fd, living.fd);
if (ep.fd < 1) return close(fa_fd), sysres{.ok = result::e_sys_api_epoll, .il = living};
+
return sysres{
.ok = result::pending,
- .ke{.fd = fa_fd},
+ .ke{.fd = fa_fd, .using_filesystem_mode = using_fs_mode},
.il = living,
.ep = ep,
};
};
-/* Parses a full path from an event's metadata.
+/* Parses path from file descriptor (used in filesystem mode without FID reporting) */
+inline auto pathof_fd(int fd, int *ec) -> std::string {
+ if (fd <= 0) {
+ *ec = -EBADF;
+ return {};
+ }
+
+ char path_buf[PATH_MAX];
+ char fd_path[32];
+ snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd);
+ ssize_t len = readlink(fd_path, path_buf, sizeof(path_buf) - 1);
+
+ if (len == -1) {
+ *ec = -errno;
+ return {};
+ }
+
+ path_buf[len] = '\0';
+ return {path_buf};
+}
+
+inline auto pathof_dfid_name(fanotify_event_metadata const *const mtd, int *ec) -> std::string {
+ /*
The shenanigans we do here depend on this event being
`FAN_EVENT_INFO_TYPE_DFID_NAME`. The kernel passes us
some info about the directory and the directory entry
@@ -1063,7 +1171,6 @@ inline auto make_sysres = [](char const *const base_path, auto const &cb, semabi
character string to the event's directory entry
after the file handle to the directory.
Confusing, right? */
-inline auto pathof(fanotify_event_metadata const *const mtd, int *ec) -> std::string {
constexpr size_t path_ulim = PATH_MAX - sizeof('\0');
constexpr int ofl = O_RDONLY | O_CLOEXEC | O_PATH;
auto dir_info = (fanotify_event_info_fid *)(mtd + 1);
@@ -1080,6 +1187,10 @@ inline auto pathof(fanotify_event_metadata const *const mtd, int *ec) -> std::st
snprintf(fs_ev_pidpath, sizeof(fs_ev_pidpath), "/proc/self/fd/%d", fd);
file_name_offset = readlink(fs_ev_pidpath, path_buf, path_ulim);
close(fd);
+ if (file_name_offset < 0) {
+ *ec = -errno;
+ return {};
+ }
path_buf[file_name_offset] = 0;
/* File name ("Directory entry")
If we wrote the directory name before here, we
@@ -1094,6 +1205,47 @@ inline auto pathof(fanotify_event_metadata const *const mtd, int *ec) -> std::st
return {path_buf};
}
+inline auto pathof_fid(fanotify_event_metadata const *const mtd, int *ec) -> std::string {
+ constexpr size_t path_ulim = PATH_MAX - sizeof('\0');
+ constexpr int ofl = O_RDONLY | O_CLOEXEC | O_PATH;
+ auto fid_info = (fanotify_event_info_fid *)(mtd + 1);
+ auto fh = (file_handle *)(fid_info->handle);
+ char path_buf[PATH_MAX] = {0};
+ int fd = open_by_handle_at(AT_FDCWD, fh, ofl);
+ if (fd <= 0) {
+ *ec = -errno;
+ return {};
+ }
+ char fs_ev_pidpath[32] = {0};
+ snprintf(fs_ev_pidpath, sizeof(fs_ev_pidpath), "/proc/self/fd/%d", fd);
+ ssize_t len = readlink(fs_ev_pidpath, path_buf, path_ulim);
+ close(fd);
+ if (len < 0) {
+ *ec = -errno;
+ return {};
+ }
+ path_buf[len] = 0;
+ return {path_buf};
+}
+
+inline auto pathof(fanotify_event_metadata const *const mtd, int *ec) -> std::string {
+ auto info = (fanotify_event_info_header *)(mtd + 1);
+
+ if (!info) {
+ *ec = 1;
+ return {};
+ }
+
+ if (info->info_type == FAN_EVENT_INFO_TYPE_FID || info->info_type == FAN_EVENT_INFO_TYPE_DFID) {
+ return pathof_fid(mtd, ec);
+ } else if (info->info_type == FAN_EVENT_INFO_TYPE_DFID_NAME) {
+ return pathof_dfid_name(mtd, ec);
+ } else {
+ *ec = 1;
+ return {};
+ }
+}
+
inline auto peek(fanotify_event_metadata const *const m, size_t read_len) -> fanotify_event_metadata const * {
if (m) {
auto ev_len = m->event_len;
@@ -1189,19 +1341,23 @@ inline auto do_ev_recv = [](auto const &cb, sysres &sr) -> result {
return result::w_sys_bad_fd;
else if (mtd->mask & FAN_Q_OVERFLOW)
return result::w_sys_q_overflow;
- else if (!ev_has_dirname(mtd))
+ else if (!ev_has_dirname(mtd)) // still needed?
return result::w_sys_bad_meta;
else {
int ec = 0;
auto r = parse_ev(mtd, read_len, &ec);
- if (ec < 0) return result::w_sys_bad_fd;
- if (is_newdir(r.ev))
- walkdir_do(r.ev.path_name.c_str(), [&](auto dir) {
- do_mark(dir, sr.ke.fd, cb);
- cb({dir, r.ev.effect_type, r.ev.path_type});
- });
- else if (ec == 0)
- cb(r.ev);
+ std::string path = r.ev.path_name.c_str();
+
+ // we prevent an infinite loop of database file changes triggering events
+ auto is_db_file = [](const std::string &p) {
+ return p.ends_with("file-indexer.db") || p.ends_with("file-indexer.db-wal");
+ };
+
+ if (!r.ev.path_name.empty() and !is_db_file(path)) {
+ if (ec < 0) return result::w_sys_bad_fd;
+
+ if (ec == 0) cb(r.ev);
+ }
mtd = r.next;
read_len -= r.this_len;
}
@@ -1583,9 +1739,21 @@ inline auto do_ev_recv = [](auto const &cb, sysres &sr) -> result {
#endif
#include
+#include
namespace detail::wtr::watcher::adapter {
+inline bool has_cap_rights() {
+ cap_t caps = cap_get_proc();
+ if (!caps) return false;
+ cap_flag_value_t cap_val;
+ bool has_cap = cap_get_flag(caps, CAP_SYS_ADMIN, CAP_EFFECTIVE, &cap_val) == 0 && cap_val == CAP_SET;
+ has_cap =
+ has_cap && cap_get_flag(caps, CAP_DAC_READ_SEARCH, CAP_PERMITTED, &cap_val) == 0 && cap_val == CAP_SET;
+ cap_free(caps);
+ return has_cap;
+}
+
inline auto watch = [](auto const &path, auto const &cb, auto const &living) -> bool {
auto platform_watch = [&](auto make_sysres, auto do_ev_recv) -> result {
auto sr = make_sysres(path.c_str(), cb, living);
@@ -1637,7 +1805,8 @@ inline auto watch = [](auto const &path, auto const &cb, auto const &living) ->
*/
auto try_fanotify = [&]() {
#if (KERNEL_VERSION(5, 9, 0) <= LINUX_VERSION_CODE) && !__ANDROID_API__
- if (geteuid() == 0) return platform_watch(fanotify::make_sysres, fanotify::do_ev_recv);
+ if (geteuid() == 0 || has_cap_rights())
+ return platform_watch(fanotify::make_sysres, fanotify::do_ev_recv);
#endif
return result::e_sys_api_fanotify;
};
diff --git a/vicinae/src/services/files-service/file-indexer/watcher-scanner.cpp b/vicinae/src/services/files-service/file-indexer/watcher-scanner.cpp
index f693971e6..9ed4a0469 100644
--- a/vicinae/src/services/files-service/file-indexer/watcher-scanner.cpp
+++ b/vicinae/src/services/files-service/file-indexer/watcher-scanner.cpp
@@ -11,7 +11,7 @@ void WatcherScanner::handleMessage(const wtr::event &ev) {
case 's':
if (err_case("s/self/live@")) {
- qInfo() << "Creating inotify watchers in" << scan.path.c_str();
+ qInfo() << "Starting watcher for" << scan.path.c_str();
start(scan);
return;
}
@@ -20,8 +20,9 @@ void WatcherScanner::handleMessage(const wtr::event &ev) {
// TODO
if (err_case("w/sys/not_watched@")) {
qCritical()
- << "Ran out of inotify watchers.\n"
- << " Please increase /proc/sys/fs/inotify/max_user_watches, or set it parmanently:\n"
+ << "Failed to create filesystem watcher.\n"
+ << " If using FAN_MARK_FILESYSTEM: ensure you have CAP_SYS_ADMIN capability (run as root)\n"
+ << " If using inotify: increase /proc/sys/fs/inotify/max_user_watches\n"
<< " `echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`";
fail();
return;