Compare commits
2 commits
752de72563
...
3f664f06ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f664f06ff | |||
| 8e085fb21f |
15 changed files with 1196 additions and 43 deletions
13
config.json
13
config.json
|
|
@ -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
159
doc/depoll.md
Normal 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
332
doc/dfanotify.md
Normal 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`.
|
||||
15
dub.json
15
dub.json
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
4
dub.settings.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"defaultArchitecture": "x86_64",
|
||||
"defaultCompiler": "ldc2"
|
||||
}
|
||||
26
source/app.d
26
source/app.d
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
public:
|
||||
this(const DWConfig config) {
|
||||
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, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
96
source/dwatch/daemon/depoll.d
Normal file
96
source/dwatch/daemon/depoll.d
Normal 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;
|
||||
}
|
||||
}
|
||||
272
source/dwatch/daemon/dfanotify.d
Normal file
272
source/dwatch/daemon/dfanotify.d
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
63
source/dwatch/lib/queue.d
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
module dwatch.version_;
|
||||
|
||||
enum dwatchVersion = "0.0.1";
|
||||
enum dwatchVersion = "0.0.2";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue