dwatch/source/app.d

358 lines
9.2 KiB
D
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module app;
import dfanotify; // твой модуль сверху
import depoll; // новый модуль для epoll
import event_actor;
import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE;
import core.stdc.errno : errno;
import core.stdc.string : strerror;
import std.string : fromStringz, toStringz, join, strip;
import std.conv : to;
import std.stdio : writeln, writefln;
import std.getopt;
import std.algorithm : canFind;
import core.sys.posix.unistd : readlink;
import core.stdc.stdint;
import std.exception;
import std.path : baseName;
import std.file : readText, exists, read;
/// Вернуть путь для fd из события PRE_CONTENT.
/// Может вернуть "(deleted)" или пустую строку, если объект без имени.
string pathFromEventFd(int fd)
{
// Конструируем путь в /proc/self/fd
auto linkPath = "/proc/self/fd/" ~ fd.to!string;
char[4096] buf;
// readlink не добавляет '\0'
auto n = readlink(linkPath.toStringz, buf.ptr, buf.length);
if (n < 0)
{
return "";
}
return cast(string) buf[0 .. n].idup;
}
/// Возвращает "имя процесса" для события fanotify.
/// Приоритет: /proc/<pid>/comm → имя ссылки /proc/<pid>/exe → первый токен из cmdline.
/// Если всё сломалось — "pid:<pid>".
string eventProcessName(int pid)
{
// 1) /proc/<pid>/comm — самое точное короткое имя
const commPath = "/proc/" ~ pid.to!string ~ "/comm";
try
{
if (exists(commPath))
{
auto s = readText(commPath).strip; // там обычно одна строка с \n
if (s.length)
return s;
}
}
catch (Exception e)
{
}
// 2) /proc/<pid>/exe — полный путь к исполняемому файлу
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;
}
}
catch (Exception e)
{
}
// 3) /proc/<pid>/cmdline — первый NUL-разделённый аргумент
try
{
const cmdPath = "/proc/" ~ pid.to!string ~ "/cmdline";
if (exists(cmdPath))
{
auto bytes = cast(const(ubyte)[]) read(cmdPath);
if (bytes.length)
{
// до первого \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;
}
// Удобная печать маски (для живых логов)
string maskToStr(uint64_t m)
{
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";
}
void main(string[] args)
{
string watchPath = "/tmp/scripts";
bool markMount = false;
getopt(args,
"path|p", &watchPath,
"mount", &markMount,
);
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;
try
{
pre = new Fanotify(preInitFlags, O_RDONLY | O_LARGEFILE);
}
catch (Exception e)
{
writeln("pre: ", e.msg);
}
// NOTIF: обычные уведомления (после факта), CLOSE_WRITE и т.п.
uint notifInitFlags =
FAN_CLASS_NOTIF
| FAN_CLOEXEC
| FAN_NONBLOCK
| FAN_REPORT_FID
| FAN_REPORT_DIR_FID
| FAN_REPORT_NAME;
try
{
notif = new Fanotify(notifInitFlags, O_RDONLY | O_LARGEFILE);
}
catch (Exception e)
{
writeln("notif", e.msg);
}
// ---------------------------
// 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)
{
auto tag = epEv.tag;
// Считываем пачку событий с соответствующего fd
FanotifyEvent[] events;
if (tag == TAG_PRE)
{
events = pre.readEvents(64 * 1024);
}
else if (tag == TAG_NOTIF)
{
events = notif.readEvents(64 * 1024);
}
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 - <name>" ...
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();
}
}
}
}