diff --git a/.vscode/launch.json b/.vscode/launch.json index 028b890..35d0dcd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/bin/dwatch", - "args": [], // Аргументы командной строки для программы, если нужны + "args": ["-d", "/tmp/scripts"], // Аргументы командной строки для программы, если нужны "stopAtEntry": false, // Остановить на входе в main "cwd": "${workspaceFolder}", "environment": [], diff --git a/dub.json b/dub.json index 4cf8d3a..60ff96a 100644 --- a/dub.json +++ b/dub.json @@ -9,9 +9,6 @@ "targetPath": "bin", "targetType": "executable", "dependencies": { - "fanotify": { - "repository": "git+https://git.zhirov.kz/dlang/fanotify.git", - "version": "97edc0d795c93ef773ff60d260951e5ff6ae6215" - } + "fanotify": "~>0.1.0" } } \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json index 322586b..3de4f99 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -1,5 +1,6 @@ { "fileVersion": 1, "versions": { + "fanotify": "0.1.0" } } diff --git a/scheme.md b/scheme.md new file mode 100644 index 0000000..99e5fb7 --- /dev/null +++ b/scheme.md @@ -0,0 +1,74 @@ +```mermaid +sequenceDiagram +%%{init: { 'theme': 'default', 'themeVariables': { + 'primaryColor': '#ff0000', + 'nodeTextColor': '#ffffff', + 'edgeLabelBackground': '#f0f0f0' +}}}%% + autonumber + participant Watcher as Демон-наблюдатель + participant Kernel as Ядро (fanotify) + participant Editor as Процесс-редактор (PID X) + participant FS as Файловая система (целевой каталог/файл) + participant Ep as epoll + participant Pfd as pidfd(PID X) + participant Map as Карта соответствий (в памяти) + participant ChLog as Журнал изменений + + Note over Watcher: Инициализация + Watcher->>Kernel: fanotify_init(PRE_CONTENT|NONBLOCK|CLOEXEC, O_RDONLY|O_CLOEXEC|O_LARGEFILE) + Watcher->>Kernel: fanotify_mark(ADD|ONLYDIR, EVENT_ON_CHILD | OPEN[_EXEC]_PERM | OPEN | CLOSE_WRITE, watchDir) + Watcher->>Ep: add(fanotify_fd, tag=FAN) + + Note over Editor: Запрос доступа к файлу + Editor->>FS: open(target, O_RDONLY/O_RDWR/EXEC) + FS-->>Kernel: системный вызов доступа + Kernel-->>Watcher: PERM-событие (+fd, mask, pid) + + Note over Watcher: Сбор контекста + Watcher->>FS: fstat(fd) → (inode, dev) + FS-->>Watcher: inode, dev + Watcher->>Watcher: readlink(/proc/self/fd/FD) → file_path_open + Watcher->>Watcher: read /proc/PID/comm → proc_name + Watcher->>Watcher: read /proc/PID/status(Uid:) → RUID + Watcher->>Watcher: read /proc/PID/loginuid → UID(loginuid) + + %% Кладём связку + Watcher->>Map: PUT key=(PID,inode,dev), val={proc_name, RUID, UID, file_path_open, changed=false} + Note over Map: Добавлена связка key=(PID,inode,dev) + + %% pidfd и epoll + Watcher->>Pfd: pidfd_open(PID, flags=0) + Pfd-->>Watcher: pidfd + Watcher->>Ep: add(pidfd, tag=PID, events=IN|HUP|RDHUP|ERR) + + %% Разрешаем доступ + Watcher->>Kernel: fanotify_response{fd, FAN_ALLOW} + Watcher->>FS: close(fd) + + Note over Editor: Работа с файлом + Editor->>FS: write(...) + Editor->>FS: close() + Kernel-->>Watcher: CLOSE_WRITE (+fd) + + Note over Watcher: Фиксация изменения + Watcher->>FS: fstat(fd) → (inode, dev) + FS-->>Watcher: inode, dev + Watcher->>Map: GET key=(PID,inode,dev) + Map-->>Watcher: {…} + Watcher->>Map: SET changed=true + Watcher->>FS: close(fd) + + Note over Editor: Завершение процесса + Editor->>Editor: exit() + Pfd-->>Ep: EPOLLIN/HUP (процесс завершён) + Ep-->>Watcher: событие(tag=PID) + + %% Пишем журнал и чистим + Watcher->>Map: GET_ALL where pid=PID + Map-->>Watcher: список записей + Watcher->>ChLog: APPEND для changed=true → {proc=proc_name, file=file_path_open, uid=UID(loginuid), ruid=RUID} + Watcher->>Ep: remove(pidfd); Watcher->>Pfd: close(pidfd) + Watcher->>Map: DELETE_ALL where pid=PID + Note over Map: Удалены все связки для PID +``` diff --git a/source/app.d b/source/app.d index 2cc78c9..0d80ce7 100644 --- a/source/app.d +++ b/source/app.d @@ -1,71 +1,347 @@ -// Модуль app.d -// Это основное приложение, демонстрирующее использование обёртки fanotify_wrapper. -// Инициализирует fanotify, маркирует директорию /tmp/scripts для мониторинга событий (открытие, модификация и т.д.), -// затем в бесконечном цикле читает события и выводит информацию о них в консоль. -// Импорты: fanotify_wrapper - обёртка, std.stdio для вывода, std.file для readLink (хотя здесь не используется из-за режима), -// std.format для форматирования строк, core.sys.posix для констант. +module app; -import fanotify_wrapper; // Импорт обёртки для fanotify. +import dfanotify; +import fanotify; +import depoll; -import std.stdio; // Импорт для writeln, writefln (вывод в консоль). -import std.file : readLink; // Импорт readLink для чтения симлинков (не используется здесь, но оставлено для возможного расширения). -import std.format : format; // Импорт format для форматирования строк (хотя здесь используется writefln напрямую). -import core.sys.posix.fcntl : AT_FDCWD; // Импорт AT_FDCWD для текущей директории. -import core.sys.posix.unistd : close; // Импорт close (не используется здесь, но для возможного расширения с fd). +import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE, O_CLOEXEC, AT_FDCWD; +import core.sys.posix.unistd : read, close; +import core.sys.posix.sys.stat : fstat, stat_t; +import core.stdc.errno : errno, EINTR; +import core.stdc.string : strerror; +import std.stdio : writeln, writefln, stderr; +import std.file : isDir, readText, exists; +import std.string : strip, splitLines, startsWith, toStringz, fromStringz, split; +import std.conv : to; +import std.exception : enforce; +import std.getopt : getopt, defaultGetoptPrinter; +import std.format : format; +import core.sys.linux.epoll : EPOLLIN, EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLL_CLOEXEC; -// Функция main: точка входа приложения. -void main() +// Переписать +import core.sys.posix.sys.types : uid_t; +import core.sys.posix.pwd : passwd, getpwuid_r; +import core.stdc.stdlib : malloc, free; +import core.stdc.string : strlen; + +/// ---- syscall + pidfd_open ---- +extern (C) long syscall(long number, ...); +version (X86_64) enum __NR_pidfd_open = 434; +extern (C) int pidfd_open(int pid, uint flags) { - // Инициализация объекта Fanotify с флагами: - // FAN_CLASS_NOTIF - режим уведомлений (без контроля доступа), - // FAN_CLOEXEC - закрытие дескриптора при exec, - // FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME - отчёт с FID (идентификатор файла) вместо fd, плюс имя файла. - // Это позволяет получать имя без реального fd (fd будет FAN_NOFD). - auto fan = new Fanotify( - FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME); + return cast(int) syscall(__NR_pidfd_open, pid, flags); +} - // Определение маски событий: битовая OR флагов для мониторинга. - // FAN_OPEN - открытие, FAN_MODIFY - модификация, FAN_CLOSE - закрытие (включает WRITE и NOWRITE), - // FAN_CREATE - создание, FAN_DELETE - удаление, FAN_EVENT_ON_CHILD - события в поддиректориях. - auto eventMask = FAN_OPEN | FAN_MODIFY | FAN_CLOSE | FAN_CREATE | FAN_DELETE | FAN_EVENT_ON_CHILD; +/// ---- readlink по fd ---- +extern (C) long readlink(const char* path, char* buf, size_t bufsiz); +string readlinkFdPath(int fd) +{ + char[4_096] buf; + auto link = format("/proc/self/fd/%d", fd); + auto n = readlink(link.toStringz, buf.ptr, buf.length.to!int - 1); + if (n <= 0) + return ""; + buf[n] = 0; + return buf.ptr.fromStringz.idup.strip; +} - // Маркировка директории /tmp/scripts: - // FAN_MARK_ADD - добавить марку, FAN_MARK_ONLYDIR - только для директории (ошибка, если не директория). - // AT_FDCWD - базовая директория текущая, путь "/tmp/scripts". - fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, eventMask, AT_FDCWD, "/tmp/scripts"); +/// ---- имя процесса и UIDы (ruid из status, uid из loginuid) ---- +struct ProcIds +{ + uint ruid; + uint uid; +} // uid = loginuid - writeln("Мониторинг запущен для /tmp/scripts..."); // Вывод сообщения о старте мониторинга. - - // Бесконечный цикл: постоянно читает события и обрабатывает их. - while (true) +string readProcComm(int pid) +{ + auto p = format("/proc/%s/comm", pid); + string s; + try { - auto events = fan.readEvents(); // Чтение событий (блокирующее, ждёт до появления событий). + s = readText(p); + } + catch (Exception e) + { + return ""; + } + return s.strip; +} - foreach (ref e; events) // Цикл по каждому событию в массиве. +uint readLoginUid(int pid) +{ + auto p = format("/proc/%s/loginuid", pid); + try + { + auto s = readText(p).strip; + // loginuid может быть "-1" (unset). В этом случае вернём 4294967295 или 0 — выбери политику. + if (s.length && s[0] == '-') + return uint.max; // помечаем как "нет" + return s.to!uint; + } + catch (Exception e) + { + return uint.max; // нет файла или отказ — считаем не установленным + } +} + +ProcIds readProcIds(int pid) +{ + uint ruid = 0; + auto p = format("/proc/%s/status", pid); + string s; + try + { + s = readText(p); + } + catch (Exception e) + { + return ProcIds(0, readLoginUid(pid)); + } + foreach (line; s.splitLines) + { + if (line.startsWith("Uid:")) { - // Определение пути: если name извлечено (из FAN_REPORT_NAME), использовать его; иначе "unknown". - // name - относительное имя файла/директории относительно маркированной. - string path = e.name.length ? e.name : "unknown"; // Используем извлечённое имя (относительное) + // Uid: RUID EUID SUID FSUID + auto parts = line.split; + if (parts.length >= 5) + { + ruid = parts[1].to!uint; + } + break; + } + } + return ProcIds(ruid, readLoginUid(pid)); +} - // Комментарий: в режиме с FAN_REPORT_FID fd = FAN_NOFD, так что нельзя использовать readLink("/proc/self/fd/" ~ to!string(e.eventFd)) для полного пути. - // fd теперь FAN_NOFD, так что пропускаем readLink/close +/// ---- ключ карты соответствий: (pid, dev, ino) ---- +struct DevIno +{ + ulong dev; + ulong ino; + int pid; + bool opEquals(const DevIno rhs) const @safe nothrow + { + return dev == rhs.dev && ino == rhs.ino && pid == rhs.pid; + } - // Вывод общей информации о событии: маска в hex, PID, путь/имя. - writefln("Событие: mask=0x%x, pid=%d, name/path=%s", e.mask, e.pid, path); + size_t toHash() const @safe nothrow + { + return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid); + } +} - // Проверки и вывод конкретных типов событий с использованием методов структуры. - if (e.isOpen) // Если открытие. - writeln(" - Открытие файла"); - if (e.isModify) // Если модификация. - writeln(" - Модификация файла"); - if (e.isCloseWrite) // Если закрытие после записи. - writeln(" - Закрытие после записи"); - if (e.isCloseNoWrite) // Если закрытие без записи. - writeln(" - Закрытие без записи"); - if (e.isCreate) // Если создание. - writeln(" - Создание файла/директории"); - if (e.isDelete) // Если удаление. - writeln(" - Удаление файла/директории"); +bool getDevIno(int fd, out DevIno di, int pid) +{ + stat_t st; + if (fstat(fd, &st) != 0) + return false; + di = DevIno(cast(ulong) st.st_dev, cast(ulong) st.st_ino, pid); + return true; +} + +/// ---- сессия файла для конкретного PID ---- +struct Session +{ + string procName; + uint ruid; // из /proc/PID/status (Uid:) + uint uid; // LOGINUID из /proc/PID/loginuid + string filePathOpen; // путь, снятый на PERM + bool changed; // был CLOSE_WRITE +} + +__gshared Session[DevIno] gMap; // (pid,dev,ino) -> Session +__gshared int[int] gPidfdByPid; // pid -> pidfd + +private string userName(uid_t uid) +{ + enum BUF = 4096; + auto buf = cast(char*) malloc(BUF); + scope (exit) + if (buf) + free(buf); + passwd pwd; + passwd* outp; + auto rc = getpwuid_r(uid, &pwd, buf, BUF, &outp); + if (rc == 0 && outp !is null) + { + return pwd.pw_name[0 .. strlen(pwd.pw_name)].idup; + } + return ""; +} + +/// ---- журнал: proc, file, uid(loginuid), ruid ---- +void logChange(string procName, string filePath, uint uid, uint ruid) +{ + writefln(`proc="%s" file="%s" user=%s(%s) realUser=%s(%s)`, + procName, filePath, userName(uid), uid, userName(ruid), ruid); +} + +/// ---- маска на КАТАЛОГ ---- +ulong dirMask() +{ + return FAN_EVENT_ON_CHILD | + FAN_OPEN_PERM | FAN_OPEN_EXEC_PERM | FAN_ACCESS_PERM | + FAN_OPEN | FAN_CLOSE_WRITE; +} + +/// ---- обработчики ---- +void onOpenPerm(FanotifyEvent ev, Epoll ep) +{ + const pid = ev.pid; + auto procName = readProcComm(pid); + auto ids = readProcIds(pid); // <-- ruid + loginuid + + DevIno key; + if (!getDevIno(ev.eventFd, key, pid)) + { + ev.respond(FAN_ALLOW); + return; + } + + auto path = readlinkFdPath(ev.eventFd); + + if (auto ps = key in gMap) + { + // уже есть + } + else + { + gMap[key] = Session(procName, ids.ruid, ids.uid, path, false); + } + + if (!(pid in gPidfdByPid)) + { + int pfd = -1; + try + { + pfd = pidfd_open(pid, 0); // на твоей системе так ок + if (pfd < 0) + writeln("pidfd_open failed: ", strerror(errno).fromStringz); + } + catch (Exception e) + { + pfd = -1; + } + if (pfd >= 0) + { + try + { + ep.add(pfd, cast(uint) pid, EPOLLIN | EPOLLHUP | EPOLLRDHUP | EPOLLERR); + gPidfdByPid[pid] = pfd; + } + catch (Exception e) + { + } + } + } + + ev.respond(FAN_ALLOW); +} + +void onCloseWrite(FanotifyEvent ev) +{ + DevIno key; + if (!getDevIno(ev.eventFd, key, ev.pid)) + return; + if (auto ps = key in gMap) + ps.changed = true; +} + +void flushAndClearForPid(int pid, Epoll ep) +{ + auto keys = gMap.keys.dup; + foreach (k; keys) + { + if (k.pid != pid) + continue; + auto s = gMap[k]; + if (s.changed) + logChange(s.procName, s.filePathOpen, s.uid, s.ruid); + gMap.remove(k); + } + if (auto p = pid in gPidfdByPid) + { + try + { + ep.remove(*p); + } + catch (Exception e) + { + } + close(*p); + gPidfdByPid.remove(pid); + } +} + +/// ---- main ---- +enum uint FAN_TAG = 0; + +void main(string[] args) +{ + string watchDir; + + auto help = getopt(args, "dir|d", &watchDir); + if (help.helpWanted || watchDir.length == 0) + { + defaultGetoptPrinter("Usage: app -d ", help.options); + return; + } + enforce(isDir(watchDir), "Ожидается путь к КАТАЛОГУ (-d)"); + + enum initFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK; + auto fan = new Fanotify(initFlags, O_RDONLY | O_LARGEFILE | O_CLOEXEC); + + try + { + fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, dirMask(), AT_FDCWD, watchDir); + } + catch (Exception e) + { + stderr.writefln("fanotify_mark failed: %s", e.msg); + return; + } + + auto ep = new Epoll(EPOLL_CLOEXEC); + ep.add(fan.handle, FAN_TAG, EPOLLIN | EPOLLERR | EPOLLHUP); + + writeln("watching dir: ", watchDir); + + for (;;) + { + auto evs = ep.wait(256, -1); + if (evs.length == 0) + continue; + + foreach (e; evs) + { + if (e.tag == FAN_TAG) + { + auto list = fan.readEvents(8_192); + foreach (fev; list) + { + if (fev.isOverflow) + { + writeln( + "FAN_Q_OVERFLOW — возможна потеря корреляции"); + continue; + } + if (fev.isOpenPerm || fev.isAccessPerm || fev.isOpenExecPerm) + { + onOpenPerm(fev, ep); + continue; + } + if (fev.isCloseWrite) + { + onCloseWrite(fev); + continue; + } + } + } + else + { + auto pid = cast(int) e.tag; + flushAndClearForPid(pid, ep); + } } } } diff --git a/source/depoll.d b/source/depoll.d new file mode 100644 index 0000000..c1dc9c9 --- /dev/null +++ b/source/depoll.d @@ -0,0 +1,86 @@ +module depoll; + +import core.sys.linux.epoll; +import core.sys.posix.unistd : close; +import core.stdc.errno : errno, EINTR, EEXIST; +import core.stdc.string : strerror; +import std.string : fromStringz; +import std.exception : enforce; + +class Epoll +{ + private int epfd_ = -1; + + this(int flags = 0) + { + epfd_ = epoll_create1(flags); + enforce(epfd_ >= 0, "epoll_create1: " ~ strerror(errno).fromStringz.idup); + } + + ~this() + { + if (epfd_ >= 0) + { + close(epfd_); + epfd_ = -1; + } + } + + /// Добавление дескриптора с произвольным tag (например, tag=0 для fanotify, tag=pid для pidfd) + void add(int fd, uint tag, uint events = EPOLLIN | EPOLLERR | EPOLLHUP) + { + epoll_event ev; + ev.events = events; + ev.data.u32 = tag; + auto rc = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev); + if (rc != 0 && errno == EEXIST) + { + // Если уже добавлен — делаем MOD + rc = epoll_ctl(epfd_, EPOLL_CTL_MOD, fd, &ev); + } + enforce(rc == 0, "epoll_ctl ADD/MOD: " ~ strerror(errno).fromStringz.idup); + } + + /// Опционально: явное удаление + void remove(int fd) + { + auto rc = epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null); + enforce(rc == 0, "epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup); + } + + struct Event + { + uint tag; // то самое, что передавали в add() + uint events; // EPOLL* биты + } + + /// Возвращает события; пустой массив — если таймаут/прерывание + Event[] wait(int maxevents = 16, int timeout = -1) + { + epoll_event[] evs = new epoll_event[maxevents]; + int n; + // Терпим EINTR и повторяем + while (true) + { + n = epoll_wait(epfd_, evs.ptr, maxevents, timeout); + if (n < 0 && errno == EINTR) + continue; + break; + } + if (n <= 0) + return []; + + Event[] res; + res.reserve(n); + foreach (i; 0 .. n) + { + res ~= Event(evs[i].data.u32, evs[i].events); + } + return res; + } + + @property int handle() const + { + return epfd_; + } +} diff --git a/source/dfanotify.d b/source/dfanotify.d new file mode 100644 index 0000000..b4d4d30 --- /dev/null +++ b/source/dfanotify.d @@ -0,0 +1,252 @@ +module dfanotify; + +public import fanotify; + +import core.sys.posix.unistd : read, write, close, ssize_t; +import core.sys.posix.fcntl : O_RDONLY, O_RDWR, O_LARGEFILE, AT_FDCWD; +import std.exception : enforce; +import std.string : toStringz, fromStringz; +import std.conv : to; +import core.stdc.errno : errno; +import core.stdc.string : strerror; +import core.stdc.stdint; + +// Класс для представления события fanotify (ООП-стиль, с методами для проверки и обработки) +class FanotifyEvent +{ + private fanotify_event_metadata meta_; + private string name_; + private int fanFd_; // Ссылка на fanotify fd для отправки response (копируется при создании) + + // Конструктор (value semantics, копирует данные) + this(fanotify_event_metadata meta, string name, int fanFd) + { + meta_ = meta; + name_ = name; + fanFd_ = fanFd; + } + + // Деструктор: автоматически закрывает fd события, если он валиден (RAII) + ~this() + { + if (meta_.fd >= 0 && meta_.fd != FAN_NOFD) + { + close(meta_.fd); + meta_.fd = -1; // Избегаем повторного закрытия + } + } + + // Геттеры (value types) + @property uint64_t mask() const + { + return meta_.mask; + } + + @property int eventFd() const + { + return meta_.fd; + } + + @property int pid() const + { + return meta_.pid; + } + + @property string name() const + { + return name_; + } + + // Методы проверки событий (без ref) + bool isOpen() const + { + return (mask & FAN_OPEN) != 0; + } + + bool isModify() const + { + return (mask & FAN_MODIFY) != 0; + } + + bool isCloseWrite() const + { + return (mask & FAN_CLOSE_WRITE) != 0; + } + + bool isCloseNoWrite() const + { + return (mask & FAN_CLOSE_NOWRITE) != 0; + } + + bool isAccess() const + { + return (mask & FAN_ACCESS) != 0; + } + + bool isMoved() const + { + return (mask & FAN_MOVED_FROM) != 0; + } + + bool isCreate() const + { + return (mask & FAN_CREATE) != 0; + } + + bool isDelete() const + { + return (mask & FAN_DELETE) != 0; + } + + bool isOpenPerm() const + { + return (mask & FAN_OPEN_PERM) != 0; + } + + bool isAccessPerm() const + { + return (mask & FAN_ACCESS_PERM) != 0; + } + + bool isOpenExecPerm() const + { + return (mask & FAN_OPEN_EXEC_PERM) != 0; + } + + bool isOverflow() const + { + return (mask & FAN_Q_OVERFLOW) != 0; + } + + bool isFsError() const + { + return (mask & FAN_FS_ERROR) != 0; + } + + // Метод для постобработки события (виртуальный, можно override для кастомной логики) + void postProcess() + { + // По умолчанию ничего, но можно добавить логику, например, логирование + } + + // Метод для отправки response (для permission-событий), закрывает fd автоматически после + void respond(uint response) + { + if (eventFd < 0 || eventFd == FAN_NOFD) + { + return; // Нет fd для response + } + + fanotify_response resp; + resp.fd = eventFd; + resp.response = response; + + ssize_t res = write(fanFd_, &resp, fanotify_response.sizeof); + enforce(res == fanotify_response.sizeof, "Ошибка записи response: " ~ strerror(errno) + .fromStringz.to!string); + + // Закрываем fd сразу после response (не ждем деструктора, но деструктор на всякий случай) + close(meta_.fd); + meta_.fd = -1; + } +} + +// Основной ООП-класс для управления fanotify +class Fanotify +{ + private int fd_ = -1; + + // Конструктор: инициализация с флагами + this(uint initFlags, uint eventFFlags = O_RDONLY | O_LARGEFILE) + { + fd_ = fanotify_init(initFlags, eventFFlags); + enforce(fd_ >= 0, "Ошибка инициализации fanotify: " ~ strerror(errno) + .fromStringz.to!string); + } + + // Деструктор: автоматически закрывает fanotify fd + ~this() + { + if (fd_ >= 0) + { + close(fd_); + fd_ = -1; + } + } + + // Метод для добавления/удаления/модификации меток (управление событиями) + void mark(uint markFlags, uint64_t eventMask, int dirFd = AT_FDCWD, string path = null) + { + const(char)* cPath = path ? path.toStringz() : null; + int res = fanotify_mark(fd_, markFlags, eventMask, dirFd, cPath); + enforce(res == 0, "Ошибка маркировки fanotify: " ~ strerror(errno) + .fromStringz.to!string); + } + + // Метод для чтения событий (возвращает массив объектов событий) + FanotifyEvent[] readEvents(size_t bufferSize = 4096) + { + ubyte[] buffer = new ubyte[bufferSize]; + ssize_t len = read(fd_, buffer.ptr, buffer.length); + if (len <= 0) + { + return []; + } + + FanotifyEvent[] events; + size_t offset = 0; + while (offset + FAN_EVENT_METADATA_LEN <= len) + { + auto meta = *(cast(fanotify_event_metadata*)(buffer.ptr + offset)); + if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > len) + { + break; + } + + string name; + size_t infoOffset = offset + fanotify_event_metadata.sizeof; + while (infoOffset < offset + meta.event_len) + { + auto hdr = *(cast(fanotify_event_info_header*)(buffer.ptr + infoOffset)); + if (hdr.len == 0 || infoOffset + hdr.len > offset + meta.event_len) + { + break; + } + + if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME || + hdr.info_type == FAN_EVENT_INFO_TYPE_OLD_DFID_NAME || + hdr.info_type == FAN_EVENT_INFO_TYPE_NEW_DFID_NAME) + { + size_t fidOffset = infoOffset + fanotify_event_info_header.sizeof + __kernel_fsid_t.sizeof; + auto handle = *(cast(file_handle*)(buffer.ptr + fidOffset)); + size_t handleEnd = fidOffset + file_handle.sizeof + handle.handle_bytes; + if (handleEnd < offset + meta.event_len) + { + name = (cast(char*)(buffer.ptr + handleEnd)).fromStringz.to!string; + } + } + infoOffset += hdr.len; + } + + auto ev = new FanotifyEvent(meta, name, fd_); + events ~= ev; + offset += meta.event_len; + } + return events; + } + + // Метод для постобработки всех событий (вызывает postProcess на каждом) + void postProcessEvents(FanotifyEvent[] events) + { + foreach (ev; events) + { + ev.postProcess(); + } + } + + // Геттер для fd (если нужно внешне, но лучше использовать методы) + @property int handle() const + { + return fd_; + } +} diff --git a/source/fanotify_wrapper.d b/source/fanotify_wrapper.d deleted file mode 100644 index 067373f..0000000 --- a/source/fanotify_wrapper.d +++ /dev/null @@ -1,200 +0,0 @@ -// Модуль fanotify_wrapper.d -// Этот модуль предоставляет обёртку вокруг API fanotify для упрощения мониторинга событий файловой системы в Linux. -// Fanotify позволяет получать уведомления о действиях с файлами, такими как открытие, модификация, создание и удаление. -// Обёртка включает структуру для событий, класс для управления дескриптором и методы для инициализации, маркировки и чтения событий. -// Импорты: public import fanotify - предполагается, что это низкоуровневый модуль с определениями из . -// Другие импорты из core.sys.posix для системных вызовов (read, close и т.д.), std.exception для обработки ошибок, -// std.string и std.conv для работы со строками, core.stdc.errno для errno и strerror для детальных сообщений об ошибках, -// core.stdc.stdint для типов вроде uint64_t. - -module fanotify_wrapper; - -public import fanotify; // Импорт низкоуровневых определений fanotify (структуры, константы, функции вроде fanotify_init, fanotify_mark). - -import core.sys.posix.unistd : read, close, ssize_t; // Импорт функций для чтения (read), закрытия (close) дескрипторов и типа ssize_t для возвращаемых значений. -import core.sys.posix.fcntl : O_RDONLY, O_RDWR, O_LARGEFILE, AT_FDCWD; // Импорт флагов для open (O_RDONLY - только чтение, O_LARGEFILE - поддержка больших файлов) и AT_FDCWD для текущей директории. -import std.exception : enforce; // Импорт enforce для проверки условий и бросания исключений при ошибках. -import std.string : toStringz, fromStringz; // Импорт функций для конвертации строк D в C-строки (toStringz) и обратно (fromStringz). -import std.conv : to; // Импорт to для конвертации типов (например, int в string). -import core.stdc.errno : errno; // Импорт errno для получения кода последней ошибки. -import core.stdc.string : strerror; // Импорт strerror для получения строкового описания ошибки по errno. -import core.stdc.stdint; // Импорт стандартных целочисленных типов (uint64_t и т.п.). - -// Структура FanotifyEvent: представляет одно событие fanotify. -// Расширена по сравнению с базовой fanotify_event_metadata: добавлено поле name для извлечённого имени файла (если используются флаги FAN_REPORT_NAME). -// Также добавлены свойства для доступа к ключевым полям и методы для проверки конкретных типов событий. -// Это упрощает работу с событиями, делая код более читаемым (вместо прямого доступа к meta.mask и т.д.). -struct FanotifyEvent -{ - fanotify_event_metadata meta; // Базовая структура метаданных события из fanotify (содержит mask, fd, pid, event_len и т.д.). - string name; // Извлечённое имя файла или директории (относительное имя, если FAN_REPORT_NAME включено; парсится из дополнительной информации в буфере). - - // Свойство mask: возвращает маску событий (битовая маска, где каждый бит соответствует типу события, например FAN_OPEN). - @property uint64_t mask() const - { - return meta.mask; // Просто возвращает значение из meta; const гарантирует, что структура не модифицируется. - } - - // Свойство eventFd: возвращает дескриптор файла события (fd). В режиме FAN_REPORT_FID это FAN_NOFD (-1), иначе реальный fd. - @property int eventFd() const - { - return meta.fd; // Доступ к fd из meta; полезно, если нужно работать с файлом напрямую (но в этом коде используется режим без fd). - } - - // Свойство pid: возвращает PID процесса, который вызвал событие. - @property int pid() const - { - return meta.pid; // Доступ к pid из meta; помогает идентифицировать, какой процесс взаимодействовал с файлом. - } - - // Метод isOpen: проверяет, включает ли маска событие открытия файла (FAN_OPEN). - // Использует битовую операцию & для проверки наличия бита FAN_OPEN в mask. - bool isOpen() const - { - return (mask & FAN_OPEN) != 0; // Если бит установлен, возвращает true; это стандартный способ работы с битoвыми масками. - } - - // Метод isModify: проверяет событие модификации файла (FAN_MODIFY, например, запись в файл). - bool isModify() const - { - return (mask & FAN_MODIFY) != 0; // Аналогично, проверка бита FAN_MODIFY. - } - - // Метод isCloseWrite: проверяет закрытие файла после записи (FAN_CLOSE_WRITE). - bool isCloseWrite() const - { - return (mask & FAN_CLOSE_WRITE) != 0; // Проверка бита для закрытия с модификацией. - } - - // Метод isCloseNoWrite: проверяет закрытие файла без записи (FAN_CLOSE_NOWRITE, например, после чтения). - bool isCloseNoWrite() const - { - return (mask & FAN_CLOSE_NOWRITE) != 0; // Проверка бита для закрытия без модификации. - } - - // Метод isAccess: проверяет событие доступа (FAN_ACCESS, например, чтение). - bool isAccess() const - { - return (mask & FAN_ACCESS) != 0; // Проверка бита FAN_ACCESS. - } - - // Метод isCreate: проверяет создание файла или директории (FAN_CREATE). - bool isCreate() const - { - return (mask & FAN_CREATE) != 0; // Проверка бита для создания. - } - - // Метод isDelete: проверяет удаление файла или директории (FAN_DELETE). - bool isDelete() const - { - return (mask & FAN_DELETE) != 0; // Проверка бита для удаления. - } -} - -// Класс Fanotify: основной класс для работы с fanotify. -// Управляет дескриптором (fd), инициализирует его, маркирует пути для мониторинга, читает события и закрывает дескриптор. -// Расширен для парсинга дополнительной информации (FID, NAME) в методе readEvents. -// Использует RAII: дескриптор закрывается в деструкторе автоматически. -class Fanotify -{ - private int fd = -1; // Приватный дескриптор fanotify; инициализирован -1 (недействительный), чтобы избежать использования до инициализации. - - // Конструктор: инициализирует fanotify с заданными флагами. - // initFlags: флаги для fanotify_init (например, FAN_CLASS_NOTIF для уведомлений без контроля доступа). - // eventFFlags: флаги для событий (по умолчанию O_RDONLY | O_LARGEFILE для чтения больших файлов). - this(uint initFlags, uint eventFFlags = O_RDONLY | O_LARGEFILE) - { - fd = fanotify_init(initFlags, eventFFlags); // Вызов системной функции fanotify_init для создания дескриптора. - enforce(fd >= 0, "Ошибка инициализации fanotify: " ~ to!string(fd)); // Проверка: если fd < 0, бросить исключение с сообщением; enforce упрощает обработку ошибок. - } - - // Деструктор: автоматически вызывается при уничтожении объекта. - ~this() - { - if (fd >= 0) // Проверка, валиден ли fd (чтобы избежать закрытия -1). - { - close(fd); // Закрытие дескриптора через системный вызов close; освобождает ресурсы. - fd = -1; // Установка в -1 для безопасности (хотя объект уничтожается). - } - } - - // Метод mark: маркирует путь (файл или директорию) для мониторинга. - // markFlags: флаги маркировки (например, FAN_MARK_ADD для добавления, FAN_MARK_ONLYDIR для только директорий). - // eventMask: маска событий, которые нужно мониторить (битовая маска, например FAN_OPEN | FAN_MODIFY). - // dirFd: дескриптор директории (по умолчанию AT_FDCWD - текущая). - // path: путь к маркируемому объекту (null для текущей директории). - void mark(uint markFlags, uint64_t eventMask, int dirFd = AT_FDCWD, string path = null) - { - const(char)* cPath = path ? path.toStringz() : null; // Конвертация пути в C-строку (toStringz добавляет null-терминатор); если path null, то null. - int res = fanotify_mark(fd, markFlags, eventMask, dirFd, cPath); // Вызов системной функции fanotify_mark для добавления марки. - if (res == -1) // Проверка на ошибку (-1 означает неудачу). - { - // Сборка детального сообщения об ошибке: включает res, errno и описание от strerror. - string errMsg = "Ошибка маркировки fanotify: " ~ to!string( - res) ~ " (errno: " ~ to!string(errno) ~ ", " ~ strerror(errno) - .fromStringz.to!string ~ ")"; - throw new Exception(errMsg); // Бросить исключение для прерывания выполнения при ошибке. - } - } - - // Метод readEvents: читает события из дескриптора fanotify. - // bufferSize: размер буфера для чтения (по умолчанию 4096 байт - размер страницы памяти, достаточно для нескольких событий). - // Возвращает массив FanotifyEvent; расширен для парсинга дополнительной информации (FID, DFID, NAME). - FanotifyEvent[] readEvents(size_t bufferSize = 4096) - { - ubyte[] buffer = new ubyte[bufferSize]; // Выделение буфера unsigned byte[] для сырых данных (fanotify возвращает байты). - ssize_t len = read(fd, buffer.ptr, buffer.length); // Чтение данных из fd через системный вызов read; len - количество прочитанных байт (блокирующий вызов, ждёт событий). - if (len <= 0) // Если ничего не прочитано или ошибка, вернуть пустой массив. - { - return []; // Нет событий или ошибка (не бросаем исключение, чтобы цикл мог продолжаться). - } - - FanotifyEvent[] events; // Массив для собранных событий. - size_t offset = 0; // Смещение в буфере для парсинга (события идут подряд). - while (offset + FAN_EVENT_METADATA_LEN <= len) // Цикл по буферу: пока хватает места для минимальной структуры metadata. - { - auto meta = cast(fanotify_event_metadata*)(buffer.ptr + offset); // Кастинг байтов в структуру metadata (unsafe, но стандартно для C-API). - if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > len) // Проверка валидности: длина события должна быть >= минимальной и не выходить за буфер. - { - break; // Если некорректно, прервать цикл (защита от повреждённых данных). - } - - FanotifyEvent ev = FanotifyEvent(*meta); // Создание структуры события на основе meta (копирует данные). - - // Парсинг дополнительной информации (info blocks): если флаги включают FAN_REPORT_FID/NAME, в буфере после meta идут блоки с FID, NAME и т.д. - size_t infoOffset = offset + fanotify_event_metadata.sizeof; // Смещение после meta. - while (infoOffset < offset + meta.event_len) // Цикл по блокам info внутри события. - { - auto hdr = cast(fanotify_event_info_header*)(buffer.ptr + infoOffset); // Кастинг в заголовок info (содержит type и len). - if (hdr.len == 0 || infoOffset + hdr.len > offset + meta.event_len) // Проверка валидности блока. - { - break; // Если некорректно, прервать. - } - - if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME) // Если тип - DFID + NAME (директория FID + имя). - { - // Расчёт смещения: пропускаем hdr, fsid (filesystem ID), затем file_handle. - size_t fidOffset = infoOffset + fanotify_event_info_header.sizeof + __kernel_fsid_t - .sizeof; - auto handle = cast(file_handle*)(buffer.ptr + fidOffset); // Кастинг в структуру file_handle (содержит handle_bytes - размер handle). - size_t handleEnd = fidOffset + file_handle.sizeof + handle.handle_bytes; // Конец handle в буфере. - if (handleEnd < offset + meta.event_len) // Проверка, что за handle есть место для имени. - { - // Извлечение имени: null-terminated C-строка после handle; конвертируем в string D. - ev.name = (cast(char*)(buffer.ptr + handleEnd)).fromStringz.to!string; - } - } - infoOffset += hdr.len; // Переход к следующему блоку info. - } - events ~= ev; // Добавление parsed события в массив. - offset += meta.event_len; // Переход к следующему событию в буфере. - } - return events; // Возврат массива событий. - } - - // Свойство handle: возвращает дескриптор fd для низкоуровневого доступа (если нужно, например, для select или других вызовов). - @property int handle() const - { - return fd; // Просто геттер. - } -}