Обычный режим работы
This commit is contained in:
parent
aa214dcf96
commit
62be8600c6
4 changed files with 308 additions and 636 deletions
483
source/app.d
483
source/app.d
|
|
@ -1,182 +1,200 @@
|
|||
module app;
|
||||
public import fanotify;
|
||||
|
||||
import dfanotify;
|
||||
import fanotify;
|
||||
import depoll;
|
||||
|
||||
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.sys.posix.unistd : read, write, close, ssize_t;
|
||||
import core.sys.posix.fcntl : O_RDONLY, O_RDWR, O_LARGEFILE, AT_FDCWD, O_CLOEXEC;
|
||||
import std.exception : enforce, collectException;
|
||||
import std.string : toStringz, fromStringz;
|
||||
import std.conv : to;
|
||||
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 core.stdc.stdint;
|
||||
import std.stdio : writeln;
|
||||
import std.format : format;
|
||||
import core.sys.linux.epoll : EPOLLIN, EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLL_CLOEXEC;
|
||||
import std.file : readLink, FileException;
|
||||
|
||||
// Переписать
|
||||
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)
|
||||
{
|
||||
return cast(int) syscall(__NR_pidfd_open, pid, flags);
|
||||
}
|
||||
|
||||
/// ---- 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;
|
||||
string link = "/proc/self/fd/%d".format(fd);
|
||||
string path;
|
||||
// Заглушка для FileException
|
||||
collectException!FileException(path = link.readLink());
|
||||
return path;
|
||||
}
|
||||
|
||||
/// ---- имя процесса и UIDы (ruid из status, uid из loginuid) ----
|
||||
struct ProcIds
|
||||
{
|
||||
uint ruid;
|
||||
uint uid;
|
||||
} // uid = loginuid
|
||||
struct FanotifyMetadataRange {
|
||||
private fanotify_event_metadata* current; // Current position in buffer.
|
||||
private size_t remainingLen; // Remaining bytes in buffer from current.
|
||||
|
||||
string readProcComm(int pid)
|
||||
{
|
||||
auto p = format("/proc/%s/comm", pid);
|
||||
string s;
|
||||
try
|
||||
{
|
||||
s = readText(p);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return s.strip;
|
||||
// Private helper functions (now non-static const methods inside the class):
|
||||
private @nogc nothrow @trusted
|
||||
bool FAN_EVENT_OK(const(fanotify_event_metadata)* meta, size_t len) const {
|
||||
enum long BASE = cast(long) FAN_EVENT_METADATA_LEN;
|
||||
const long L = cast(long) len;
|
||||
const long EL = cast(long) meta.event_len;
|
||||
return (L >= BASE) && (EL >= BASE) && (EL <= L);
|
||||
}
|
||||
|
||||
private @nogc nothrow @trusted
|
||||
fanotify_event_metadata* FAN_EVENT_NEXT(fanotify_event_metadata* meta, ref size_t len) const {
|
||||
const uint step = meta.event_len;
|
||||
len -= step;
|
||||
return cast(fanotify_event_metadata*)((cast(ubyte*) meta) + step);
|
||||
}
|
||||
|
||||
// Constructor: Takes the buffer slice and read length.
|
||||
this(ubyte[] buffer, size_t len) @nogc nothrow @trusted {
|
||||
if (len < FAN_EVENT_METADATA_LEN) {
|
||||
remainingLen = 0; // Empty if too small.
|
||||
return;
|
||||
}
|
||||
current = cast(fanotify_event_metadata*) buffer.ptr;
|
||||
remainingLen = len;
|
||||
}
|
||||
|
||||
// Range primitives:
|
||||
@nogc nothrow @trusted
|
||||
bool empty() const {
|
||||
return !FAN_EVENT_OK(current, remainingLen);
|
||||
}
|
||||
|
||||
@nogc nothrow @trusted
|
||||
ref const(fanotify_event_metadata) front() const {
|
||||
assert(!empty, "Range is empty");
|
||||
return *current; // Returns by const ref to avoid copies.
|
||||
}
|
||||
|
||||
@nogc nothrow @trusted
|
||||
void popFront() {
|
||||
assert(!empty, "Range is empty");
|
||||
current = FAN_EVENT_NEXT(current, remainingLen);
|
||||
}
|
||||
}
|
||||
|
||||
uint readLoginUid(int pid)
|
||||
// Класс для представления события fanotify (ООП-стиль, с методами для проверки и обработки)
|
||||
struct FanotifyEvent
|
||||
{
|
||||
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; // нет файла или отказ — считаем не установленным
|
||||
}
|
||||
}
|
||||
private fanotify_event_metadata _meta;
|
||||
private int _fanFd;
|
||||
|
||||
ProcIds readProcIds(int pid)
|
||||
{
|
||||
uint ruid = 0;
|
||||
auto p = format("/proc/%s/status", pid);
|
||||
string s;
|
||||
try
|
||||
// Деструктор: автоматически закрывает fd события, если он валиден (RAII)
|
||||
// ~this()
|
||||
// {
|
||||
// if (_meta.fd >= 0 && _meta.fd != FAN_NOFD)
|
||||
// {
|
||||
// close(_meta.fd);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Геттеры (value types)
|
||||
@property uint64_t mask() const { return _meta.mask; }
|
||||
@property int eventFd() const { return _meta.fd; }
|
||||
@property int pid() const { return _meta.pid; }
|
||||
|
||||
// Методы проверки событий
|
||||
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; }
|
||||
|
||||
// Метод для отправки response (для permission-событий), закрывает fd автоматически после
|
||||
void respond(uint response)
|
||||
{
|
||||
s = readText(p);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return ProcIds(0, readLoginUid(pid));
|
||||
}
|
||||
foreach (line; s.splitLines)
|
||||
{
|
||||
if (line.startsWith("Uid:"))
|
||||
if (eventFd < 0 || eventFd == FAN_NOFD)
|
||||
{
|
||||
// Uid: RUID EUID SUID FSUID
|
||||
auto parts = line.split;
|
||||
if (parts.length >= 5)
|
||||
{
|
||||
ruid = parts[1].to!uint;
|
||||
}
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
return ProcIds(ruid, readLoginUid(pid));
|
||||
}
|
||||
|
||||
/// ---- ключ карты соответствий: (pid, dev, ino) ----
|
||||
struct DevIno
|
||||
{
|
||||
ulong dev;
|
||||
ulong ino;
|
||||
int pid;
|
||||
bool opEquals(const DevIno rhs) const @safe nothrow
|
||||
// Метод для добавления/удаления/модификации меток (управление событиями)
|
||||
void mark(uint markFlags, uint64_t eventMask, int dirFd = AT_FDCWD, string path = null)
|
||||
{
|
||||
return dev == rhs.dev && ino == rhs.ino && pid == rhs.pid;
|
||||
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);
|
||||
}
|
||||
|
||||
size_t toHash() const @safe nothrow
|
||||
// Метод для чтения событий (возвращает массив объектов событий)
|
||||
FanotifyEvent[] readEvents(size_t bufferSize = 4096)
|
||||
{
|
||||
return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid);
|
||||
size_t sz = bufferSize;
|
||||
if (sz < FAN_EVENT_METADATA_LEN) sz = FAN_EVENT_METADATA_LEN;
|
||||
|
||||
ubyte[] buffer = new ubyte[sz];
|
||||
|
||||
ssize_t len;
|
||||
while (true)
|
||||
{
|
||||
len = read(_fd, buffer.ptr, buffer.length);
|
||||
if (len < 0 && errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
if (len <= 0) return [];
|
||||
|
||||
auto range = FanotifyMetadataRange(buffer, cast(size_t) len);
|
||||
|
||||
import std.array : Appender;
|
||||
auto events = Appender!(FanotifyEvent[])();
|
||||
// FanotifyEvent[] events;
|
||||
foreach (ref const meta; range) {
|
||||
events ~= FanotifyEvent(meta, _fd);
|
||||
}
|
||||
|
||||
return events.data;
|
||||
}
|
||||
|
||||
// Геттер для fd (если нужно внешне, но лучше использовать методы)
|
||||
@property int handle() const
|
||||
{
|
||||
return _fd;
|
||||
}
|
||||
}
|
||||
|
||||
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 |
|
||||
|
|
@ -184,163 +202,32 @@ ulong dirMask()
|
|||
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 <dir-to-watch>", 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);
|
||||
// fan.mark(FAN_MARK_ADD | FAN_MARK_FILESYSTEM, dirMask(), AT_FDCWD, "/home");
|
||||
fan.mark(FAN_MARK_ADD | FAN_MARK_FILESYSTEM, dirMask(), AT_FDCWD, "/tmp");
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto evs = ep.wait(256, -1);
|
||||
if (evs.length == 0)
|
||||
continue;
|
||||
|
||||
foreach (e; evs)
|
||||
auto list = fan.readEvents(8_192);
|
||||
foreach (fev; list)
|
||||
{
|
||||
if (e.tag == FAN_TAG)
|
||||
if (fev.isOverflow)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
if (fev.isOpenPerm || fev.isAccessPerm || fev.isOpenExecPerm)
|
||||
{
|
||||
auto pid = cast(int) e.tag;
|
||||
flushAndClearForPid(pid, ep);
|
||||
writeln(fev.eventFd.readlinkFdPath);
|
||||
fev.respond(FAN_ALLOW);
|
||||
continue;
|
||||
}
|
||||
if (fev.isCloseWrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue