Compare commits

...

2 commits

15 changed files with 1196 additions and 43 deletions

View file

@ -1,23 +1,20 @@
{
"database": "/var/lib/dwatch/data.db",
"rules": {
"tracking": [],
"ignore": []
"tracking": ["*.txt"]
},
"watch": [
{
"path": "/path/to/watch-1",
"child": true,
"path": "/tmp/test1",
"rules": {
"tracking": [],
"tracking": ["test"],
"ignore": []
}
},
{
"path": "/path/to/watch-2",
"child": false,
"path": "/tmp/test2",
"rules": {
"tracking": [],
"tracking": ["*"],
"ignore": []
}
}

159
doc/depoll.md Normal file
View file

@ -0,0 +1,159 @@
# depoll.d
Модуль предоставляет тонкую обёртку над `epoll(7)` для мультиплексирования событий на файловых дескрипторах в Linux. Класс `Epoll` инкапсулирует жизненный цикл epoll-дескриптора, регистрацию/снятие отслеживаемых FD и ожидание событий. Структура `Event` — компактный переносимый результат ожидания: «метка источника» (`tag`) и флаги события (`events`).
# Данные и типы
```d
struct Event { uint tag; uint events; }
```
* `tag` — произвольная метка, которую код вызывающей стороны кладёт в `epoll_event.data.u32` при добавлении FD. Удобно для маршрутизации: по `tag` сразу понятно, кто источник (например: 0 для fanotify, PID для pidfd, индекс в таблице и т. п.).
* `events` — битовая маска флагов `EPOLL*`, полученная от ядра (например, `EPOLLIN`, `EPOLLERR`, `EPOLLHUP`).
# Класс `Epoll`: устройство и причины решений
## Поле инициализации
```d
private int _epfd = -1;
```
* Хранит epoll-дескриптор. Значение `-1` сигнализирует «не создан/закрыт», что упрощает защитные проверки.
## Конструктор
```d
this(int flags = EPOLL_CLOEXEC)
{
_epfd = epoll_create1(flags);
if (auto e = collectException!ErrnoException(
errnoEnforce(_epfd >= 0, "epoll_create1 failed:")))
{
throw new DWException(e.msg);
}
}
```
* Вызов `epoll_create1(flags)` создаёт epoll-инстанс.
* Значение по умолчанию `EPOLL_CLOEXEC` закрывает дескриптор при `exec*()`; это снижает риск утечек дескрипторов в дочерние процессы — хорошая практика для демонов/долгоживущих сервисов.
* `errnoEnforce(cond, msg)` проверяет условие и, если оно ложно, выбрасывает `ErrnoException` с прикреплённым `errno` и человекочитаемым системным текстом ошибки. Это компактнее и безопаснее, чем ручной `strerror(errno)`.
* `collectException!ErrnoException(...)` перехватывает возможное исключение и возвращает его объект (или `null`, если исключения не было). Далее исключение преобразуется в доменное `DWException`. Так достигается единообразие типа ошибок с сохранением информативного сообщения.
**Почему именно так:**
— Использование `EPOLL_CLOEXEC` по умолчанию предотвращает нежелательную наследуемость дескриптора.
— Связка `errnoEnforce + collectException` даёт лаконичную проверку ошибок системных вызовов с корректным текстом и при этом позволяет конвертировать тип исключения на уровне API.
## Деструктор
```d
~this()
{
if (_epfd >= 0) {
close(_epfd);
_epfd = -1;
}
}
```
* Закрывает epoll-дескриптор, когда объект покидает область жизни. Это RAII-подход.
## Регистрация FD: `add`
```d
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) {
rc = epoll_ctl(_epfd, EPOLL_CTL_MOD, fd, &ev);
}
if (auto e = collectException!ErrnoException(
errnoEnforce(rc == 0, "epoll_ctl ADD/MOD:")))
{
throw new DWException(e.msg);
}
}
```
* Формируется `epoll_event`: в `events` указывается интересующая маска (по умолчанию — чтение, ошибка, отвал/конец), в `data.u32` кладётся пользовательский `tag`.
* `EPOLL_CTL_ADD` регистрирует дескриптор. Если он уже в наборе (ядро вернуло `EEXIST`), корректный путь — обновление записи через `EPOLL_CTL_MOD`. Это штатное поведение epoll: один FD может быть добавлен в данный epoll-инстанс лишь однажды.
* Проверка результата оборачивается в `errnoEnforce` и конвертируется в `DWException` при ошибках.
**Почему именно так:**
— Явное добавление `EPOLLERR|EPOLLHUP` в маску делает обработку аварий/закрытий предсказуемой.
— Шаблон `ADD → если EEXIST то MOD` — общепринятая практика для идемпотентных добавлений/обновлений маски.
## Снятие регистрации: `remove`
```d
void remove(int fd)
{
auto rc = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, null);
if (auto e = collectException!ErrnoException(
errnoEnforce(rc == 0, "epoll_ctl DEL:")))
{
throw new DWException(e.msg);
}
}
```
* Явно удаляет FD из набора. Перед закрытием FD это полезно для снижения редких гонок с повторным использованием номера FD.
## Ожидание событий: `wait`
```d
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;
}
```
* Выделяет буфер `epoll_event[]` на `maxevents`. Значение должно быть > 0; иначе ядро вернёт `EINVAL`.
* Вызов `epoll_wait` повторяется в случае `EINTR`. Это стандартный шаблон: если ожидание прервано сигналом, оно возобновляется.
* Если `n == 0` — сработал таймаут; возвращается пустой массив.
* Результат проецируется в массив `Event` с сохранением `tag` и `events`.
**Почему именно так:**
— Повтор на `EINTR` предотвращает «ложные» выходы из ожидания при приходе сигналов (например, `SIGCHLD`).
— Возврат массива `Event` удобен семантически.
## Свойство `handle`
```d
@property int handle() const { return _epfd; }
```
* Предоставляет прямой доступ к epoll-дескриптору на случай, если требуется системная операция вне обёртки.
# Семантика флагов и поведение epoll
* Маска по умолчанию `EPOLLIN | EPOLLERR | EPOLLHUP` покрывает наиболее частые сценарии: готовность к чтению/сигнал о наличии события, ошибка источника, закрытие соединения/канала.
* Триггерность по умолчанию — **уровневая** (level-triggered). При необходимости можно расширить интерфейс так, чтобы разрешить `EPOLLET` (edge-triggered) или `EPOLLONESHOT`; эти режимы требуют более строгой логики чтения «до `EAGAIN`».
# Обработка ошибок: паттерн и последствия
* `errnoEnforce` формирует информативное сообщение, включающее пользовательский префикс и строку системы по `errno`.
* `collectException!ErrnoException(...)` локально перехватывает исключение и даёт возможность перевести его в доменный `DWException`.

332
doc/dfanotify.md Normal file
View file

@ -0,0 +1,332 @@
# dfanotify.d
Код описывает два уровня абстракции поверх Linux fanotify:
* `Fanotify` — объект верхнего уровня, владеющий «контрольным» дескриптором fanotify, выставляющий операции инициализации (`fanotify_init`), маркировки (`fanotify_mark`) и чтения «сырой» очереди событий (`read`).
* `FanotifyEvent` — объект-представление одного события, полученного из очереди: хранит метаданные ядра о событии, строковое имя (если оно пришло), ссылку на контрольный дескриптор для отправки ответа по permission-событиям и управляет временем жизни «пер-событийного» дескриптора.
Такой раздельный подход соответствует модели fanotify: есть один управляющий FD (для чтения событий и отправки ответов), и у некоторых событий есть собственный FD объекта файловой системы, которым нужно владеть и закрывать.
---
# Класс `FanotifyEvent`
## Поля
```d
fanotify_event_metadata _meta; // метаданные события, включая mask, pid, fd, длину записи
string _name; // имя объекта (если ядро прислало в инфоблоках)
int _fanFd; // контрольный fanotify FD для записи ответа
```
* `_meta` — точная копия заголовка `fanotify_event_metadata`, полученного из ядра. Ключевые поля:
* `mask` — битовая маска типа события и атрибутов;
* `fd` — файловый дескриптор, ассоциированный с событием, либо `FAN_NOFD`;
* `pid` — инициатор (PID процесса);
* `event_len` — фактическая длина блока события в байтах.
* `_name` — человекочитаемое имя пути, если оно присутствовало в одном из инфоблоков `*_DFID_NAME`.
* `_fanFd` — дескриптор, через который отправляется `fanotify_response` при обработке permission-событий.
## Конструктор
```d
this(fanotify_event_metadata meta, string name, int fanFd)
{
_meta = meta; // копирование метаданных по значению
_name = name; // копирование строки имени
_fanFd = fanFd; // сохранение контрольного FD для ответа
}
```
* Параметры копируются, что задаёт value-семантику: объект события становится самостоятельным носителем нужных данных и не зависит от внешнего буфера чтения.
## Деструктор (RAII управления event FD)
```d
~this()
{
if (_meta.fd >= 0 && _meta.fd != FAN_NOFD)
{
close(_meta.fd);
_meta.fd = -1;
}
}
```
* Событие владеет своим `_meta.fd` и закрывает его при разрушении. Это соответствует требованию освобождать полученные от ядра дескрипторы, чтобы избежать утечек.
* Присваивание `-1` устраняет риск повторного закрытия.
## Свойства-геттеры
```d
@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; }
```
* Предоставляют доступ к основным полям метаданных и имени. Значение `eventFd` становится `-1` после вызова `respond()` или деструктора.
## Предикаты по типам событий
```d
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; }
```
* Каждая функция проверяет соответствующий бит `mask` и возвращает булево значение.
* Биты `*_PERM` обозначают permission-события: ядро ожидает ответа, прежде чем продолжить операцию пользовательского процесса.
* `FAN_Q_OVERFLOW` сигнализирует, что очередь переполнилась и часть событий могла быть утеряна.
* `FAN_FS_ERROR` сообщает о произошедших ошибках файловой системы.
## Пост-обработка события
```d
void postProcess() { /* по умолчанию — пусто */ }
```
* Виртуальная точка расширения для сценариев, где требуется дополнительная логика после первичной обработки (например, запись в журнал или сбор метрик).
## Ответ по permission-событию
```d
void respond(uint response)
{
if (eventFd < 0 || eventFd == FAN_NOFD)
return;
fanotify_response resp;
resp.fd = eventFd;
resp.response = response;
ssize_t res = write(_fanFd, &resp, fanotify_response.sizeof);
if (auto e = collectException!ErrnoException(
errnoEnforce(res == fanotify_response.sizeof, "fanotify response write failed:")))
{
throw new DWException(e.msg);
}
close(_meta.fd);
_meta.fd = -1;
}
```
* Проверка наличия валидного `event fd` исключает попытку отвечать по событиям без дескриптора.
* Формируется структура `fanotify_response` с двумя полями: `fd` события и код ответа `response` (например, разрешить/запретить).
* Системный вызов `write` записывает ответ в контрольный дескриптор `_fanFd`. Конструкция
`collectException!ErrnoException(errnoEnforce(...))` интерпретирует результат:
* `errnoEnforce` проверяет условие успешной записи ожидаемого размера и, при нарушении, создаёт `ErrnoException` с сохранённым `errno`;
* `collectException` возвращает объект исключения (или `null`), что позволяет конвертировать системное исключение в доменное `DWException` с тем же сообщением.
* После успешной записи ответа `event fd` закрывается немедленно; деструктор остаётся страховкой от забытых закрытий.
---
# Класс `Fanotify`
## Поле
```d
private int _fd = -1;
```
* Контрольный дескриптор fanotify, через который читаются события и отправляются ответы.
## Конструктор и деструктор
```d
this(uint initFlags, uint eventFFlags = O_RDONLY | O_LARGEFILE)
{
_fd = fanotify_init(initFlags, eventFFlags);
if (auto e = collectException!ErrnoException(errnoEnforce(_fd >= 0, "fanotify_init failed:")))
throw new DWException(e.msg);
}
~this()
{
if (_fd >= 0)
{
close(_fd);
_fd = -1;
}
}
```
* `fanotify_init(initFlags, eventFFlags)` создаёт и настраивает интерфейс:
* `initFlags` определяют класс и режим генерации событий (например, отчёт FID/имени, класс очереди и т. п.);
* `eventFFlags` задают флаги для дескрипторов объектов, которые будут приходить в событиях (например, доступ «только на чтение» и поддержка больших файлов).
* Проверка успешности выполняется через тот же шаблон `errnoEnforce``ErrnoException` → конвертация в `DWException`.
* Деструктор закрывает контрольный дескриптор, когда объект покидает область жизни.
## Маркирование объектов/деревьев
```d
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);
if (auto e = collectException!ErrnoException(errnoEnforce(res == 0, "fanotify_mark failed:")))
throw new DWException(e.msg);
}
```
* Обёртка над `fanotify_mark(2)` управляет источниками событий:
* `markFlags` определяют операцию (добавить/удалить/очистить) и область (файл, директория, только директории, вся ФС и т. д.);
* `eventMask` задаёт интересующие типы событий для отмеченной цели;
* `dirFd`/`path` указывают объект маркировки: путь как строка или путь, заданный относительно дескриптора каталога.
* Результат проверяется тем же механизмом ошибок, что и в конструкторе.
## Чтение и разбор очереди событий
```d
FanotifyEvent[] readEvents(size_t bufferSize = 4096)
```
Ход выполнения:
1. **Подготовка буфера:**
* Выбирается размер `sz` не меньше `FAN_EVENT_METADATA_LEN`;
* Выделяется массив `ubyte[] buffer` нужной длины.
2. **Чтение из контрольного FD:**
```d
ssize_t len;
while (true)
{
len = read(_fd, buffer.ptr, buffer.length);
if (len < 0 && errno == EINTR) continue;
break;
}
if (len <= 0)
{
if (len < 0 && errno == EAGAIN) return [];
return [];
}
```
* При `EINTR` чтение повторяется;
* Нулевой или отрицательный результат интерпретируется как отсутствие готовых данных либо условие «попробовать позже»; в этом случае возвращается пустой массив.
3. **Парсинг последовательности событий в буфере:**
* Переменная `offset` указывает на начало очередной записи;
* Выполняется цикл, пока в буфере остаётся минимум место под заголовок `fanotify_event_metadata`.
4. **Чтение метаданных и валидация границ:**
```d
auto meta = *(cast(fanotify_event_metadata*)(buffer.ptr + offset));
if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > cast(size_t)len)
break;
```
* Проверка `event_len` гарантирует корректность и целостность записи в рамках прочитанного блока.
5. **Извлечение имени (если присутствует):**
```d
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 || hdr.len < fanotify_event_info_header.sizeof || 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;
}
```
* Внутри записи события может находиться несколько «инфоблоков», начинающихся с `fanotify_event_info_header` и имеющих длину `hdr.len`.
* Для типов `*_DFID_NAME` после заголовка и `__kernel_fsid_t` идёт `file_handle` переменной длины (`handle_bytes`), за которым, при наличии, располагается NUL-терминированная строка имени.
* Проверки длины (`hdr.len`, `handleEnd <= ...`) обеспечивают корректные границы доступа в рамках `meta.event_len`.
6. **Создание объектов событий и продвижение по буферу:**
```d
auto ev = new FanotifyEvent(meta, name, _fd);
events ~= ev;
offset += meta.event_len;
```
* На каждую запись создаётся `FanotifyEvent` с копиями метаданных и имени, а также с контрольным дескриптором, необходимым для возможного ответа.
7. **Возврат массива событий:**
* После разбора всего буфера возвращается массив созданных объектов.
## Пост-обработка набора событий
```d
void postProcessEvents(FanotifyEvent[] events)
{
foreach (ev; events)
ev.postProcess();
}
```
* Вызывает переопределяемый `postProcess()` для каждого элемента массива, реализуя единообразный проход по пачке событий.
## Свойство доступа к контрольному FD
```d
@property int handle() const { return _fd; }
```
* Предоставляет доступ к внутреннему дескриптору, если это требуется операциям более низкого уровня вне класса.
---
# Логика обработки ошибок
В местах, где выполняются системные вызовы (`fanotify_init`, `fanotify_mark`, запись ответа), используется единая связка:
* `errnoEnforce(condition, "text")` — проверяет условие успеха и, при нарушении, формирует `ErrnoException` с сохранённым `errno` и системным сообщением на основе текущего кода ошибки;
* `collectException!ErrnoException(...)` — перехватывает исключение, возвращая его объект либо `null`;
* в случае ошибки создаётся и выбрасывается `DWException` с сообщением из перехваченного исключения.
Такой шаблон стандартизирует формат сообщений об ошибках и обеспечивает корректную привязку к коду `errno` в момент сбоя.
---
# Жизненный цикл данных
1. `Fanotify` инициализирует интерфейс (`fanotify_init`) и маркирует интересующие объекты/деревья (`fanotify_mark`) с заданными масками событий.
2. При готовности данных из ядра вызывается `readEvents`, который:
* читает пачку событий в буфер;
* последовательно извлекает записи по полю `event_len`;
* по необходимости извлекает имя;
* создаёт объекты `FanotifyEvent`, владеющие собственными `event fd`.
3. Пользовательский код проверяет типы через `is*`-методы и, при необходимости, отвечает на permission-события через `respond`.
4. При разрушении `FanotifyEvent` закрывает свой `event fd`; при разрушении `Fanotify` закрывается контрольный `_fd`.

View file

@ -7,15 +7,24 @@
"license": "BSL-1.0",
"name": "dwatch",
"libs": [
"xdiff"
"xdiff",
"wildmatch"
],
"dependencies": {
"commandr": "~>1.1.0",
"fanotify": "~>0.1.0",
"sdiff": {
"repository": "git+https://git.zhirov.kz/dlang/sdiff.git",
"version": "~0.1.0"
},
"commandr": "~>1.1.0"
"wildmatch": {
"repository": "git+https://git.zhirov.kz/dwatch/wildmatch.git",
"version": "779980828708cafdd3db3ae57face10338b14f33"
}
},
"targetPath": "bin",
"targetType": "executable"
}
"targetType": "executable",
"postBuildCommands": [
"ln -sf dwatch bin/dwatchd"
]
}

View file

@ -2,7 +2,9 @@
"fileVersion": 1,
"versions": {
"commandr": "1.1.0",
"fanotify": "0.1.0",
"sdiff": {"version":"~0.1.0","repository":"git+https://git.zhirov.kz/dlang/sdiff.git"},
"wildmatch": {"version":"779980828708cafdd3db3ae57face10338b14f33","repository":"git+https://git.zhirov.kz/dwatch/wildmatch.git"},
"xdiff": {"version":"e2396bc172eba813cdcd1a96c494e35d687f576a","repository":"git+https://git.zhirov.kz/dlang/xdiff.git"}
}
}

4
dub.settings.json Normal file
View file

@ -0,0 +1,4 @@
{
"defaultArchitecture": "x86_64",
"defaultCompiler": "ldc2"
}

View file

@ -3,24 +3,36 @@ import dwatch;
import commandr;
import core.stdc.stdlib : EXIT_SUCCESS, EXIT_FAILURE;
import std.file : exists, isFile;
import std.path : baseName;
private immutable string programName = "dwatch";
int main(string[] args)
{
Program program = new Program(programName, dwatchVersion)
string currentName = args[0].baseName;
string daemonName = programName ~ 'd';
bool isDaemon;
Program program = new Program(currentName, dwatchVersion)
.add(new Option("c", "config", "Путь к файлу конфигурации")
.optional
.validateEachWith(
opt => opt.exists && opt.isFile,
"необходимо указать путь к файлу JSON"
)
)
.add(new Flag("d", "daemon", "Режим демона")
.name("daemon")
.optional
);
if (currentName == daemonName)
{
isDaemon = true;
program.add(new Flag("g", "global", "Глобальный режим")
.name("global")
.optional
).summary("Демон мониторинга изменений в текстовых файлах");
} else {
program.summary("CLI интерфейс для просмотра изменений в текстовых файлах");
}
ProgramArgs argumets = program.parse(args);
string configFile = argumets.option("config", "config.json");
@ -35,8 +47,8 @@ int main(string[] args)
}
try {
if (argumets.flag("daemon")) {
DWDaemon daemon = new DWDaemon(config);
if (isDaemon) {
DWDaemon daemon = new DWDaemon(config, argumets.flag("global"));
daemon.run();
} else {
DWCLI cli = new DWCLI(config);

View file

@ -1,15 +1,173 @@
module dwatch.daemon.core;
import wildmatch;
import dwatch.lib;
import dwatch.daemon.depoll;
import dwatch.daemon.dfanotify;
import std.stdio : writeln, writefln;
import core.stdc.stdint : uint64_t;
import core.thread : Thread;
import std.conv : to;
import core.sys.linux.epoll : EPOLLIN, EPOLLONESHOT;
import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE, O_CLOEXEC, AT_FDCWD;
import fanotify : FAN_CLASS_PRE_CONTENT, FAN_CLOEXEC, FAN_NONBLOCK, FAN_ALLOW, FAN_OPEN, FAN_CLOSE_WRITE,
FAN_MARK_ADD, FAN_MARK_ONLYDIR, FAN_MARK_FILESYSTEM, FAN_OPEN_PERM, FAN_ACCESS_PERM, FAN_MARK_MOUNT,
FAN_OPEN_EXEC_PERM, FAN_EVENT_ON_CHILD;
struct FanotifyWatch
{
Fanotify fanotify;
Queue!Event queue;
string path;
string[] rules;
}
private uint INIT_FLAGS = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK;
private uint EVENT_FLAGS = O_RDONLY | O_LARGEFILE | O_CLOEXEC;
private uint BASE_FLAGS = FAN_MARK_ADD;
private enum uint64_t PERM_MASK = FAN_OPEN_PERM | FAN_ACCESS_PERM | FAN_OPEN_EXEC_PERM |
FAN_OPEN | FAN_CLOSE_WRITE;
final class DWDaemon {
private:
Epoll _ep;
FanotifyWatch[] _fanotifyWatch;
Thread[] _workers;
class FanotifyWorker {
private:
Epoll* ep;
FanotifyWatch* fw;
size_t idx;
public:
this(Epoll* ep, FanotifyWatch* fw, size_t idx) {
this.ep = ep;
// this(FanotifyWatch* fw, size_t idx) {
this.fw = fw;
this.idx = idx;
}
void run()
{
// writeln("worker bound to idx=", idx);
Event e;
while(true) {
fw.queue.dequeue(e);
while(true) {
// writeln("Текущий воркер - ", e.tag);
auto events = fw.fanotify.readEvents(8_192);
if (events.length == 0) break;
foreach (ev; events) {
if (ev.isOverflow)
{
writeln("Что-то непонятное: ", e.tag);
continue;
}
if (ev.isOpenPerm || ev.isAccessPerm || ev.isOpenExecPerm)
{
writeln("Разрешение доступа: ", e.tag);
ev.respond(FAN_ALLOW);
continue;
}
if (ev.isCloseWrite)
{
writeln("Запись и закрытие: ", e.tag);
continue;
}
}
}
// Повторное добавление - модификация события.
ep.add(fw.fanotify.handle, idx.to!uint, EPOLLIN | EPOLLONESHOT);
}
}
}
public:
this(const DWConfig config) {
this(const DWConfig config, bool global) {
uint baseFlags;
uint64_t permMask = PERM_MASK;
if (global) {
writeln("ВНИМАНИЕ! Демон запущен в глобальном режиме!");
baseFlags = global ? BASE_FLAGS | FAN_MARK_FILESYSTEM : BASE_FLAGS | FAN_MARK_ONLYDIR;
permMask |= FAN_EVENT_ON_CHILD;
}
// Чтение основных правил
string[] rulesMain = config.rules.get.dup;
foreach (const ref DWWatch watch; config.watch)
{
FanotifyWatch fw;
fw.path = watch.path;
fw.rules ~= rulesMain;
fw.rules ~= watch.rules.get;
try
{
fw.fanotify = new Fanotify(INIT_FLAGS, EVENT_FLAGS);
fw.fanotify.mark(baseFlags, permMask, AT_FDCWD, watch.path);
}
catch (Exception e)
{
throw new DWException(e.msg);
}
fw.queue = new Queue!Event();
_fanotifyWatch ~= fw;
}
try
{
_ep = new Epoll();
}
catch (Exception e)
{
throw new DWException(e.msg);
}
foreach (i, ref fw; _fanotifyWatch)
{
try
{
_ep.add(fw.fanotify.handle, i.to!uint, EPOLLIN | EPOLLONESHOT);
// _ep.add(fw.fanotify.handle, i.to!uint);
}
catch (Exception e)
{
throw new DWException(e.msg);
}
}
_workers.length = _fanotifyWatch.length;
}
void run() {
// Сколько watch-еров, столько и потоков, который каждый обрабатывает из очереди только свои события
foreach (i; 0 .. _fanotifyWatch.length) {
auto fanotifyWorker = new FanotifyWorker(&_ep, &_fanotifyWatch[i], i);
// auto fanotifyWorker = new FanotifyWorker(&_fanotifyWatch[i], i);
_workers[i] = new Thread(&fanotifyWorker.run);
_workers[i].name = "fanotify-" ~ i.to!string;
_workers[i].start();
}
while (true)
{
foreach (epEv; _ep.wait())
{
if (epEv.tag < _fanotifyWatch.length) {
if (epEv.events & EPOLLIN) {
_fanotifyWatch[epEv.tag].queue.enqueue(epEv);
}
// _fanotifyWatch[epEv.tag].queue.enqueue(epEv);
}
}
}
}
}

View file

@ -0,0 +1,96 @@
module dwatch.daemon.depoll;
import core.sys.linux.epoll : epoll_create1, epoll_ctl, epoll_wait, epoll_event,
EPOLLIN, EPOLLERR, EPOLLHUP, EPOLL_CTL_ADD, EPOLL_CTL_DEL, EPOLL_CTL_MOD, EPOLL_CLOEXEC;
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 : errnoEnforce, ErrnoException, collectException;
import dwatch.lib;
struct Event
{
uint tag;
uint events;
}
class Epoll
{
private int _epfd = -1;
this(int flags = EPOLL_CLOEXEC)
{
_epfd = epoll_create1(flags);
if (auto e = collectException!ErrnoException(errnoEnforce(_epfd >= 0, "epoll_create1 failed:")))
{
throw new DWException(e.msg);
}
}
~this()
{
if (_epfd >= 0)
{
close(_epfd);
_epfd = -1;
}
}
/// Добавление дескриптора с произвольным tag
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);
}
if (auto e = collectException!ErrnoException(errnoEnforce(rc == 0, "epoll_ctl ADD/MOD:")))
{
throw new DWException(e.msg);
}
}
void remove(int fd)
{
auto rc = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, null);
if (auto e = collectException!ErrnoException(errnoEnforce(rc == 0, "epoll_ctl DEL:")))
{
throw new DWException(e.msg);
}
}
/// Возвращает события; пустой массив — если таймаут/прерывание
Event[] wait(int maxevents = 16, int timeout = -1)
{
epoll_event[] evs = new epoll_event[maxevents];
int n;
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;
}
}

View file

@ -0,0 +1,272 @@
module dwatch.daemon.dfanotify;
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 : errnoEnforce, ErrnoException, collectException;
import std.string : toStringz, fromStringz;
import std.conv : to;
import core.stdc.errno : errno, EINTR, EAGAIN;
import core.stdc.string : strerror;
import core.stdc.stdint : uint64_t;
import dwatch.lib;
// Класс для представления события fanotify (ООП-стиль, с методами для проверки и обработки)
class FanotifyEvent
{
private:
fanotify_event_metadata _meta;
string _name;
int _fanFd; // Ссылка на fanotify fd для отправки response (копируется при создании)
public:
// Конструктор (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);
if (auto e = collectException!ErrnoException(
errnoEnforce(res == fanotify_response.sizeof, "fanotify response write failed:")))
{
throw new DWException(e.msg);
}
// Закрываем 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);
if (auto e = collectException!ErrnoException(errnoEnforce(_fd >= 0, "fanotify_init failed:")))
{
throw new DWException(e.msg);
}
}
// Деструктор: автоматически закрывает 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);
if (auto e = collectException!ErrnoException(errnoEnforce(res == 0, "fanotify_mark failed:")))
{
throw new DWException(e.msg);
}
}
// Метод для чтения событий (возвращает массив объектов событий)
FanotifyEvent[] readEvents(size_t bufferSize = 4096)
{
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)
{
if (len < 0 && errno == EAGAIN) return [];
return [];
}
FanotifyEvent[] events;
size_t offset = 0;
while (offset + FAN_EVENT_METADATA_LEN <= cast(size_t)len)
{
auto meta = *(cast(fanotify_event_metadata*)(buffer.ptr + offset));
if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > cast(size_t)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 || hdr.len < fanotify_event_info_header.sizeof || 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;
}
void postProcessEvents(FanotifyEvent[] events)
{
foreach (ev; events)
{
ev.postProcess();
}
}
@property int handle() const
{
return _fd;
}
}

View file

@ -2,6 +2,11 @@ module dwatch.lib.dwconfig;
import std.file : readText;
import std.json : parseJSON, JSONValue, JSONType;
import std.container : RedBlackTree;
import std.string : split, empty;
import std.conv : to;
import std.algorithm : each, filter;
import std.array : array;
import dwatch.lib.dwexception;
import dwatch.lib.jget;
@ -9,24 +14,62 @@ import dwatch.lib.jget;
final class DWRules
{
private:
string[] _tracking;
string[] _ignore;
string[] _rules;
string[] generatePattern(string path) {
string[] rules;
string[] parts = path.split("/").filter!(p => !p.empty).array;
if (parts.length == 0) return rules;
rules ~= "/*";
rules ~= "!/" ~ parts[0];
rules ~= "/" ~ parts[0] ~ "/*";
if (parts.length > 1) {
string currentPath = "/" ~ parts[0];
foreach (i; 1 .. parts.length) {
currentPath ~= "/" ~ parts[i];
rules ~= "!" ~ currentPath;
if (i < parts.length.to!int - 1) {
rules ~= currentPath ~ "/*";
}
}
}
return rules;
}
string[] generate(string[] tracking) {
string[] rules;
auto tempRules = new RedBlackTree!string;
tracking.each!(
track => rules ~= generatePattern(track)
);
rules.each!((rule) {
if (rule in tempRules) return;
tempRules.insert(rule);
_rules ~= rule;
});
return rules;
}
public:
this(const JSONValue rulesJson)
{
_tracking = rulesJson.jget!(string[])("tracking", JSONType.ARRAY);
_ignore = rulesJson.jget!(string[])("ignore", JSONType.ARRAY);
auto tracking = rulesJson.jgetOptional!(string[])("tracking", JSONType.ARRAY);
auto ignore = rulesJson.jgetOptional!(string[])("ignore", JSONType.ARRAY);
_rules ~= generate(tracking);
_rules ~= ignore;
}
@property const(string)[] tracking() const
@property const(string)[] get() const
{
return _tracking;
}
@property const(string)[] ignore() const
{
return _ignore;
return _rules;
}
}
@ -34,15 +77,13 @@ final class DWWatch
{
private:
string _path;
bool _child;
DWRules _rules;
public:
this(const JSONValue watchJson)
{
_path = watchJson.jget!string("path", JSONType.STRING);
_child = watchJson.jget!bool("child");
auto rj = watchJson.jget!JSONValue("rules", JSONType.OBJECT);
auto rj = watchJson.jgetOptional!JSONValue("rules", JSONType.OBJECT);
_rules = new DWRules(rj);
}
@ -51,11 +92,6 @@ public:
return _path;
}
@property bool child() const
{
return _child;
}
@property const(DWRules) rules() const
{
return _rules;
@ -85,10 +121,15 @@ public:
}
_database = rootJson.jget!string("database", JSONType.STRING);
auto rj = rootJson.jget!JSONValue("rules", JSONType.OBJECT);
auto rj = rootJson.jgetOptional!JSONValue("rules", JSONType.OBJECT);
_rules = new DWRules(rj);
foreach (node; rootJson.jget!(JSONValue[])("watch", JSONType.ARRAY))
auto watchNodes = rootJson.jget!(JSONValue[])("watch", JSONType.ARRAY);
if (watchNodes.length == 0)
{
throw new DWException("`watch` должен содержать хотя бы один элемент");
}
foreach (node; watchNodes)
{
_watch ~= new DWWatch(node);
}

View file

@ -113,3 +113,10 @@ T jget(T)(in JSONValue obj, string key, JSONType expect = JSONType.NULL)
static assert(0, "jget!T: неподдерживаемый тип " ~ T.stringof);
}
}
T jgetOptional(T)(in JSONValue obj, string key, JSONType expect = JSONType.NULL, lazy T def = T.init)
{
if (obj.type != JSONType.OBJECT) return def;
if (!(key in obj.object)) return def;
return jget!T(obj, key, expect);
}

View file

@ -2,3 +2,4 @@ module dwatch.lib;
public import dwatch.lib.dwconfig;
public import dwatch.lib.dwexception;
public import dwatch.lib.queue;

63
source/dwatch/lib/queue.d Normal file
View file

@ -0,0 +1,63 @@
module dwatch.lib.queue;
import core.sync.mutex : Mutex;
import core.sync.condition : Condition;
class Queue(T)
{
private T[] elements;
private Mutex mutex;
private Condition cond;
private bool done;
this()
{
mutex = new Mutex();
cond = new Condition(mutex);
done = false;
}
void enqueue(T item)
{
synchronized (mutex)
{
elements ~= item;
cond.notify();
}
}
bool dequeue(out T item)
{
synchronized (mutex)
{
while (elements.length == 0 && !done)
{
cond.wait();
}
if (elements.length == 0 && done)
{
return false;
}
item = elements[0];
elements = elements[1 .. $];
return true;
}
}
void finish()
{
synchronized (mutex)
{
done = true;
cond.notify();
}
}
bool isEmpty()
{
synchronized (mutex)
{
return elements.length == 0;
}
}
}

View file

@ -1,3 +1,3 @@
module dwatch.version_;
enum dwatchVersion = "0.0.1";
enum dwatchVersion = "0.0.2";