From 3bf9250ea2f8329288b707afa29f4fcf534b7d1c Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 30 Aug 2025 20:32:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B5=D1=85=D0=B0=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20?= =?UTF-8?q?=D0=B2=20=D1=84=D0=B0=D0=B9=D0=BB=20(=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=B6?= =?UTF-8?q?=D1=83=D1=80=D0=BD=D0=B0=D0=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 2 +- scheme.md | 73 ++++++ source/app.d | 577 +++++++++++++++++++++---------------------- source/depoll.d | 37 ++- source/event_actor.d | 139 ----------- 5 files changed, 382 insertions(+), 446 deletions(-) create mode 100644 scheme.md delete mode 100644 source/event_actor.d 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/scheme.md b/scheme.md new file mode 100644 index 0000000..a47f883 --- /dev/null +++ b/scheme.md @@ -0,0 +1,73 @@ +```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,status} → proc_name, RUID, UID(EUID) + + %% КЛАДЁМ СВЯЗКУ + 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 (pid завершён) + 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, 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 5a85ec0..0d80ce7 100644 --- a/source/app.d +++ b/source/app.d @@ -1,357 +1,346 @@ module app; -import dfanotify; // твой модуль сверху -import depoll; // новый модуль для epoll -import event_actor; +import dfanotify; +import fanotify; +import depoll; -import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE; -import core.stdc.errno : errno; +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.string : fromStringz, toStringz, join, strip; +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.stdio : writeln, writefln; -import std.getopt; -import std.algorithm : canFind; -import core.sys.posix.unistd : readlink; +import std.exception : enforce; +import std.getopt : getopt, defaultGetoptPrinter; +import std.format : format; +import core.sys.linux.epoll : EPOLLIN, EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLL_CLOEXEC; -import core.stdc.stdint; -import std.exception; +// Переписать +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; -import std.path : baseName; - -import std.file : readText, exists, read; - -/// Вернуть путь для fd из события PRE_CONTENT. -/// Может вернуть "(deleted)" или пустую строку, если объект без имени. -string pathFromEventFd(int fd) +/// ---- 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) { - // Конструируем путь в /proc/self/fd - auto linkPath = "/proc/self/fd/" ~ fd.to!string; - char[4096] buf; + return cast(int) syscall(__NR_pidfd_open, pid, flags); +} - // readlink не добавляет '\0' - auto n = readlink(linkPath.toStringz, buf.ptr, buf.length); - if (n < 0) +/// ---- 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; +} + +/// ---- имя процесса и UIDы (ruid из status, uid из loginuid) ---- +struct ProcIds +{ + uint ruid; + uint uid; +} // uid = loginuid + +string readProcComm(int pid) +{ + auto p = format("/proc/%s/comm", pid); + string s; + try + { + s = readText(p); + } + catch (Exception e) { return ""; } - return cast(string) buf[0 .. n].idup; + return s.strip; } -/// Возвращает "имя процесса" для события fanotify. -/// Приоритет: /proc//comm → имя ссылки /proc//exe → первый токен из cmdline. -/// Если всё сломалось — "pid:". -string eventProcessName(int pid) +uint readLoginUid(int pid) { - // 1) /proc//comm — самое точное короткое имя - const commPath = "/proc/" ~ pid.to!string ~ "/comm"; + auto p = format("/proc/%s/loginuid", pid); try { - if (exists(commPath)) - { - auto s = readText(commPath).strip; // там обычно одна строка с \n - if (s.length) - return s; - } + 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; // нет файла или отказ — считаем не установленным } +} - // 2) /proc//exe — полный путь к исполняемому файлу +ProcIds readProcIds(int pid) +{ + uint ruid = 0; + auto p = format("/proc/%s/status", pid); + string s; try { - char[4096] buf; - auto link = "/proc/" ~ pid.to!string ~ "/exe"; - auto n = readlink(link.toStringz, buf.ptr, buf.length); - if (n > 0) - { - auto exePath = cast(string) buf[0 .. n].idup; - auto name = baseName(exePath); - if (name.length) - return name; - } + s = readText(p); } catch (Exception e) { + return ProcIds(0, readLoginUid(pid)); } - - // 3) /proc//cmdline — первый NUL-разделённый аргумент - try + foreach (line; s.splitLines) { - const cmdPath = "/proc/" ~ pid.to!string ~ "/cmdline"; - if (exists(cmdPath)) + if (line.startsWith("Uid:")) { - auto bytes = cast(const(ubyte)[]) read(cmdPath); - if (bytes.length) + // Uid: RUID EUID SUID FSUID + auto parts = line.split; + if (parts.length >= 5) + { + ruid = parts[1].to!uint; + } + break; + } + } + return ProcIds(ruid, readLoginUid(pid)); +} + +/// ---- ключ карты соответствий: (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; + } + + size_t toHash() const @safe nothrow + { + return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid); + } +} + +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) { - // до первого \0 - size_t i = 0; - for (; i < bytes.length && bytes[i] != 0; ++i) - { - } - auto first = cast(string) bytes[0 .. i].idup; - if (first.length) - { - auto name = baseName(first); - if (name.length) - return name; - } } } } - catch (Exception e) - { - } - // fallback - return "pid:" ~ pid.to!string; + ev.respond(FAN_ALLOW); } -// Удобная печать маски (для живых логов) -string maskToStr(uint64_t m) +void onCloseWrite(FanotifyEvent ev) { - string[] parts; - if (m & FAN_OPEN) - parts ~= "OPEN"; - if (m & FAN_ACCESS) - parts ~= "ACCESS"; - if (m & FAN_MODIFY) - parts ~= "MODIFY"; - if (m & FAN_CLOSE_WRITE) - parts ~= "CLOSE_WRITE"; - if (m & FAN_CLOSE_NOWRITE) - parts ~= "CLOSE_NOWRITE"; - if (m & FAN_CREATE) - parts ~= "CREATE"; - if (m & FAN_DELETE) - parts ~= "DELETE"; - if (m & FAN_MOVED_FROM) - parts ~= "MOVED_FROM"; - if (m & FAN_MOVED_TO) - parts ~= "MOVED_TO"; - if (m & FAN_OPEN_PERM) - parts ~= "OPEN_PERM"; - if (m & FAN_ACCESS_PERM) - parts ~= "ACCESS_PERM"; - if (m & FAN_OPEN_EXEC) - parts ~= "OPEN_EXEC"; - if (m & FAN_OPEN_EXEC_PERM) - parts ~= "OPEN_EXEC_PERM"; - if (m & FAN_Q_OVERFLOW) - parts ~= "Q_OVERFLOW"; - if (m & FAN_FS_ERROR) - parts ~= "FS_ERROR"; - return parts.length ? parts.join("|") : "0"; + 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 watchPath = "/tmp/scripts"; - bool markMount = false; + string watchDir; - getopt(args, - "path|p", &watchPath, - "mount", &markMount, - ); + auto help = getopt(args, "dir|d", &watchDir); + if (help.helpWanted || watchDir.length == 0) + { + defaultGetoptPrinter("Usage: app -d ", help.options); + return; + } + enforce(isDir(watchDir), "Ожидается путь к КАТАЛОГУ (-d)"); - writefln("[*] Watch: %s (mode: %s)", watchPath, markMount ? "MOUNT" : "DIR"); - - Fanotify pre, notif; - - // --------------------------- - // 1) Создаём две группы - // --------------------------- - - // PRE_CONTENT: решаем "до доступа" - // Важно: для permission-событий обязательно включить REPORT_* для имени - // и DFID (иначе имя не извлечёшь). - uint preInitFlags = - FAN_CLASS_PRE_CONTENT - | FAN_CLOEXEC - | FAN_NONBLOCK; + enum initFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK; + auto fan = new Fanotify(initFlags, O_RDONLY | O_LARGEFILE | O_CLOEXEC); try { - pre = new Fanotify(preInitFlags, O_RDONLY | O_LARGEFILE); + fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, dirMask(), AT_FDCWD, watchDir); } catch (Exception e) { - writeln("pre: ", e.msg); + stderr.writefln("fanotify_mark failed: %s", e.msg); + return; } - // NOTIF: обычные уведомления (после факта), CLOSE_WRITE и т.п. - uint notifInitFlags = - FAN_CLASS_NOTIF - | FAN_CLOEXEC - | FAN_NONBLOCK - | FAN_REPORT_FID - | FAN_REPORT_DIR_FID - | FAN_REPORT_NAME; + auto ep = new Epoll(EPOLL_CLOEXEC); + ep.add(fan.handle, FAN_TAG, EPOLLIN | EPOLLERR | EPOLLHUP); - try + writeln("watching dir: ", watchDir); + + for (;;) { - notif = new Fanotify(notifInitFlags, O_RDONLY | O_LARGEFILE); - } - catch (Exception e) - { - writeln("notif", e.msg); - } + auto evs = ep.wait(256, -1); + if (evs.length == 0) + continue; - // --------------------------- - // 2) Ставим метки - // --------------------------- - - // Что именно хотим ловить - enum uint64_t PERM_MASK = - FAN_OPEN_PERM - | FAN_ACCESS_PERM - | FAN_OPEN_EXEC_PERM; - - enum uint64_t NOTIF_MASK = - FAN_OPEN - | FAN_OPEN_EXEC - | FAN_MODIFY - | FAN_CLOSE_WRITE - | FAN_CLOSE_NOWRITE - | FAN_CREATE - | FAN_DELETE - | FAN_MOVED_FROM - | FAN_MOVED_TO; - - // Важно: для слежения за потомками каталога добавляем FAN_EVENT_ON_CHILD. - auto preMask = PERM_MASK | FAN_EVENT_ON_CHILD; - auto notifMask = NOTIF_MASK | FAN_EVENT_ON_CHILD; - - uint baseFlags = FAN_MARK_ADD /*| FAN_MARK_ONLYDIR*/ ; - - // Если нужно «видеть всё на файловой системе/монтировании», используй MOUNT: - // (на многих сценариях это предпочтительно, иначе будет только каталог + его прямые дети) - if (markMount) - { - baseFlags |= FAN_MARK_MOUNT; - } - - // Метки на обе группы - pre.mark(baseFlags, preMask, /*dirFd*/ 0, watchPath); - notif.mark(baseFlags, notifMask, /*dirFd*/ 0, watchPath); - - // --------------------------- - // 3) Готовим epoll (теперь через ООП-класс) - // --------------------------- - - Epoll ep = new Epoll(); - - enum uint TAG_PRE = 1; - enum uint TAG_NOTIF = 2; - - ep.add(pre.handle, TAG_PRE); - ep.add(notif.handle, TAG_NOTIF); - - // --------------------------- - // 4) Главный цикл - // --------------------------- - - while (true) - { - auto epEvents = ep.wait(); - - foreach (epEv; epEvents) + foreach (e; evs) { - auto tag = epEv.tag; - - // Считываем пачку событий с соответствующего fd - FanotifyEvent[] events; - if (tag == TAG_PRE) + if (e.tag == FAN_TAG) { - events = pre.readEvents(64 * 1024); - } - else if (tag == TAG_NOTIF) - { - events = notif.readEvents(64 * 1024); + 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 { - continue; - } - - foreach (ev; events) - { - auto pname = eventProcessName(ev.pid); - // writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask)); - writefln("[%s] pid = %s proc = %s mask = %s name = %s", - (tag == TAG_PRE) ? "PRE" : "NOTIF", - ev.pid, pname, - maskToStr(ev.mask), - ev.name.length ? ev.name : "<неизвестно>" - ); - auto actor = eventActorFromPid(ev.pid); - writeln("user=", actor.user, " uid=", actor.uid, - " ruid=", actor.ruid, "(", actor.ruser, ")", - " euid=", actor.euid, "(", actor.euser, ")"); - - if (tag == TAG_PRE && (ev.isOpenPerm || ev.isAccessPerm)) - { - string path = pathFromEventFd(ev.eventFd); - writeln("Попытка доступа к: ", path.length ? path - : "<неизвестно>"); - // ev.respond(FAN_ALLOW); // или FAN_DENY по своей политике - baseName(path) == "test" ? ev.respond(FAN_DENY) : ev.respond(FAN_ALLOW); - } - - // Лог - // writefln("[%s] pid=%s mask=%s name=%s", - // (tag == TAG_PRE) ? "PRE" : "NOTIF", - // ev.pid.to!string, - // maskToStr(ev.mask), - // ev.name.length ? ev.name : "(unknown)"); - - // --- ВЕТКА РЕШЕНИЯ ДЛЯ PERM-СОБЫТИЙ --- - // if (tag == TAG_PRE && - // (ev.isOpenPerm || ev.isAccessPerm || ev.isOpenExecPerm)) - // { - // // Пример «политики»: запрещать выполнение из временных директорий - // // (поменяй на свои условия) - // bool deny = false; - - // // простейший сэмпл-правил - // auto nm = ev.name; - // if (nm.length) - // { - // if (nm.canFind("/tmp/") && ev.isOpenExecPerm) - // { - // deny = true; - // } - // } - - // // Отправляем решение ядру - // ev.respond(deny ? FAN_DENY : FAN_ALLOW); - - // writefln(" -> %s", deny ? "DENY" : "ALLOW"); - // } - - // --- ПОСТ-ФАКТУМ УВЕДОМЛЕНИЯ --- - if (tag == TAG_NOTIF) - { - // Реакции в реальном времени - if (ev.isCloseWrite) - { - // Напр., дернуть диф/бэкап/индексацию - // snag --config ... create --comment "CLOSE_WRITE - " ... - writefln(" -> HANDLE CLOSE_WRITE for: %s", ev.name); - } - else if (ev.isMoved) - { - writefln(" -> HANDLE MOVED for: %s", ev.name); - } - else if (ev.isModify) - { - writefln(" -> HANDLE MODIFY for: %s", ev.name); - } - } - - // Кастомный postProcess если надо - ev.postProcess(); + auto pid = cast(int) e.tag; + flushAndClearForPid(pid, ep); } } } diff --git a/source/depoll.d b/source/depoll.d index 8b85272..c1dc9c9 100644 --- a/source/depoll.d +++ b/source/depoll.d @@ -2,7 +2,7 @@ module depoll; import core.sys.linux.epoll; import core.sys.posix.unistd : close; -import core.stdc.errno : errno, EINTR; +import core.stdc.errno : errno, EINTR, EEXIST; import core.stdc.string : strerror; import std.string : fromStringz; import std.exception : enforce; @@ -26,39 +26,52 @@ class Epoll } } + /// Добавление дескриптора с произвольным 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; - enforce(epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev) == 0, - "epoll_ctl ADD: " ~ strerror(errno).fromStringz.idup); + 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); } - // Опционально: метод для удаления fd + /// Опционально: явное удаление void remove(int fd) { - enforce(epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null) == 0, - "epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup); + auto rc = epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null); + enforce(rc == 0, "epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup); } struct Event { - uint tag; - uint events; + uint tag; // то самое, что передавали в add() + uint events; // EPOLL* биты } + /// Возвращает события; пустой массив — если таймаут/прерывание Event[] wait(int maxevents = 16, int timeout = -1) { epoll_event[] evs = new epoll_event[maxevents]; - int n = epoll_wait(epfd_, evs.ptr, maxevents, timeout); - if (n <= 0) + int n; + // Терпим EINTR и повторяем + while (true) { - // Игнорируем EINTR и другие (как в оригинале: continue) - return []; + 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); diff --git a/source/event_actor.d b/source/event_actor.d deleted file mode 100644 index 5ac27de..0000000 --- a/source/event_actor.d +++ /dev/null @@ -1,139 +0,0 @@ -module event_actor; - -import core.sys.posix.pwd : passwd, getpwuid_r; -import core.sys.posix.sys.types : uid_t; -import core.stdc.stdlib : malloc, free; -import core.stdc.string : strlen; -import std.file : readText, exists; -import std.string : splitLines, strip; -import std.conv : to; - -// для токенизации по пробельным -import std.ascii : isWhite; -import std.algorithm.iteration : splitter; -import std.array : array; - -struct EventActor -{ - uid_t uid; // выбранный UID (loginuid если есть, иначе ruid) - string user; // имя пользователя для uid (может быть пустым) - uid_t ruid, euid, suid, fsuid; - string ruser, euser, suser, fsuser; - bool hasLoginuid; - uid_t loginuid; - string loginuser; -} - -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 ""; -} - -/// Парсит строку вида: "Uid:\t\t\t\t" -private bool parseUidLine(string line, out uid_t r, out uid_t e, out uid_t s, out uid_t f) -{ - // разобьём по любым пробельным символам - auto toks = line.splitter!isWhite.array; // ["Uid:", "1000", "1000", "1000", "1000"] - if (toks.length >= 5 && toks[0] == "Uid:") - { - try - { - r = toks[1].to!uid_t; - e = toks[2].to!uid_t; - s = toks[3].to!uid_t; - f = toks[4].to!uid_t; - return true; - } - catch (Exception e) - { - } - } - return false; -} - -/// Главная функция: кто инициировал событие (по PID из fanotify_event_metadata) -EventActor eventActorFromPid(int pid) -{ - EventActor a; - if (pid <= 0) - return a; - - const base = "/proc/" ~ pid.to!string ~ "/"; - const statusPath = base ~ "status"; - - if (exists(statusPath)) - { - try - { - auto txt = readText(statusPath); - uid_t r = 0, e = 0, s = 0, f = 0; - foreach (line; txt.splitLines()) - { - // ищем строку "Uid:" - if (line.length >= 4 && line[0 .. 4] == "Uid:") - { - if (parseUidLine(line, r, e, s, f)) - { - a.ruid = r; - a.euid = e; - a.suid = s; - a.fsuid = f; - a.ruser = userName(r); - a.euser = userName(e); - a.suser = userName(s); - a.fsuser = userName(f); - } - break; - } - } - } - catch (Exception e) - { - } - } - - // loginuid — владелец сессии (если audit включён) - const loginPath = base ~ "loginuid"; - if (exists(loginPath)) - { - try - { - auto s = readText(loginPath).strip; - if (s.length) - { - a.loginuid = s.to!uid_t; - a.loginuser = userName(a.loginuid); - a.hasLoginuid = true; - } - } - catch (Exception e) - { - } - } - - // выбираем «итогового» пользователя - if (a.hasLoginuid) - { - a.uid = a.loginuid; - a.user = a.loginuser.length ? a.loginuser : ("uid:" ~ a.loginuid.to!string); - } - else - { - a.uid = a.ruid; - a.user = a.ruser.length ? a.ruser : ("uid:" ~ a.ruid.to!string); - } - - return a; -}