This commit is contained in:
Alexander Zhirov 2025-08-22 22:09:19 +03:00
commit d14c1d37ba
Signed by: alexander
GPG key ID: C8D8BE544A27C511
10 changed files with 386 additions and 0 deletions

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
.dub
docs.json
__dummy.html
docs/
/dwatch
dwatch.so
dwatch.dylib
dwatch.dll
dwatch.a
dwatch.lib
dwatch-test-*
*.exe
*.pdb
*.o
*.obj
*.lst
bin

39
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,39 @@
{
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "code-d",
"request": "launch",
"dubBuild": true,
"name": "Build & Debug DUB project",
"cwd": "${command:dubWorkingDirectory}",
"program": "bin/${command:dubTarget}",
"args": [
"/tmp/test"
]
},
{
"name": "Debug D Program with sudo-gdb",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/dwatch",
"args": [], // Аргументы командной строки для программы, если нужны
"stopAtEntry": false, // Остановить на входе в main
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/sudo-gdb", // Путь к вашему скрипту
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.detectIndentation": false
}

23
LICENSE Normal file
View file

@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# dwatch
Эксперементальная утилита для мониторинга файлов на основе библиотеки [fanotify](https://man7.org/linux/man-pages/man7/fanotify.7.html).
[Отдельный низкоуровневый модуль](https://git.zhirov.kz/dlang/fanotify) `fanotify` для D.

17
dub.json Normal file
View file

@ -0,0 +1,17 @@
{
"authors": [
"Alexander Zhirov"
],
"copyright": "Copyright © 2025, Alexander Zhirov",
"description": "Monitoring files using fanotify",
"license": "BSL-1.0",
"name": "dwatch",
"targetPath": "bin",
"targetType": "executable",
"dependencies": {
"fanotify": {
"repository": "git+https://git.zhirov.kz/dlang/fanotify.git",
"version": "97edc0d795c93ef773ff60d260951e5ff6ae6215"
}
}
}

5
dub.selections.json Normal file
View file

@ -0,0 +1,5 @@
{
"fileVersion": 1,
"versions": {
}
}

4
dub.settings.json Normal file
View file

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

71
source/app.d Normal file
View file

@ -0,0 +1,71 @@
// Модуль app.d
// Это основное приложение, демонстрирующее использование обёртки fanotify_wrapper.
// Инициализирует fanotify, маркирует директорию /tmp/scripts для мониторинга событий (открытие, модификация и т.д.),
// затем в бесконечном цикле читает события и выводит информацию о них в консоль.
// Импорты: fanotify_wrapper - обёртка, std.stdio для вывода, std.file для readLink (хотя здесь не используется из-за режима),
// std.format для форматирования строк, core.sys.posix для констант.
import fanotify_wrapper; // Импорт обёртки для fanotify.
import std.stdio; // Импорт для writeln, writefln (вывод в консоль).
import std.file : readLink; // Импорт readLink для чтения симлинков (не используется здесь, но оставлено для возможного расширения).
import std.format : format; // Импорт format для форматирования строк (хотя здесь используется writefln напрямую).
import core.sys.posix.fcntl : AT_FDCWD; // Импорт AT_FDCWD для текущей директории.
import core.sys.posix.unistd : close; // Импорт close (не используется здесь, но для возможного расширения с fd).
// Функция main: точка входа приложения.
void main()
{
// Инициализация объекта Fanotify с флагами:
// FAN_CLASS_NOTIF - режим уведомлений (без контроля доступа),
// FAN_CLOEXEC - закрытие дескриптора при exec,
// FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME - отчёт с FID (идентификатор файла) вместо fd, плюс имя файла.
// Это позволяет получать имя без реального fd (fd будет FAN_NOFD).
auto fan = new Fanotify(
FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME);
// Определение маски событий: битовая OR флагов для мониторинга.
// FAN_OPEN - открытие, FAN_MODIFY - модификация, FAN_CLOSE - закрытие (включает WRITE и NOWRITE),
// FAN_CREATE - создание, FAN_DELETE - удаление, FAN_EVENT_ON_CHILD - события в поддиректориях.
auto eventMask = FAN_OPEN | FAN_MODIFY | FAN_CLOSE | FAN_CREATE | FAN_DELETE | FAN_EVENT_ON_CHILD;
// Маркировка директории /tmp/scripts:
// FAN_MARK_ADD - добавить марку, FAN_MARK_ONLYDIR - только для директории (ошибка, если не директория).
// AT_FDCWD - базовая директория текущая, путь "/tmp/scripts".
fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, eventMask, AT_FDCWD, "/tmp/scripts");
writeln("Мониторинг запущен для /tmp/scripts..."); // Вывод сообщения о старте мониторинга.
// Бесконечный цикл: постоянно читает события и обрабатывает их.
while (true)
{
auto events = fan.readEvents(); // Чтение событий (блокирующее, ждёт до появления событий).
foreach (ref e; events) // Цикл по каждому событию в массиве.
{
// Определение пути: если name извлечено (из FAN_REPORT_NAME), использовать его; иначе "unknown".
// name - относительное имя файла/директории относительно маркированной.
string path = e.name.length ? e.name : "unknown"; // Используем извлечённое имя (относительное)
// Комментарий: в режиме с FAN_REPORT_FID fd = FAN_NOFD, так что нельзя использовать readLink("/proc/self/fd/" ~ to!string(e.eventFd)) для полного пути.
// fd теперь FAN_NOFD, так что пропускаем readLink/close
// Вывод общей информации о событии: маска в hex, PID, путь/имя.
writefln("Событие: mask=0x%x, pid=%d, name/path=%s", e.mask, e.pid, path);
// Проверки и вывод конкретных типов событий с использованием методов структуры.
if (e.isOpen) // Если открытие.
writeln(" - Открытие файла");
if (e.isModify) // Если модификация.
writeln(" - Модификация файла");
if (e.isCloseWrite) // Если закрытие после записи.
writeln(" - Закрытие после записи");
if (e.isCloseNoWrite) // Если закрытие без записи.
writeln(" - Закрытие без записи");
if (e.isCreate) // Если создание.
writeln(" - Создание файла/директории");
if (e.isDelete) // Если удаление.
writeln(" - Удаление файла/директории");
}
}
}

200
source/fanotify_wrapper.d Normal file
View file

@ -0,0 +1,200 @@
// Модуль fanotify_wrapper.d
// Этот модуль предоставляет обёртку вокруг API fanotify для упрощения мониторинга событий файловой системы в Linux.
// Fanotify позволяет получать уведомления о действиях с файлами, такими как открытие, модификация, создание и удаление.
// Обёртка включает структуру для событий, класс для управления дескриптором и методы для инициализации, маркировки и чтения событий.
// Импорты: public import fanotify - предполагается, что это низкоуровневый модуль с определениями из <linux/fanotify.h>.
// Другие импорты из core.sys.posix для системных вызовов (read, close и т.д.), std.exception для обработки ошибок,
// std.string и std.conv для работы со строками, core.stdc.errno для errno и strerror для детальных сообщений об ошибках,
// core.stdc.stdint для типов вроде uint64_t.
module fanotify_wrapper;
public import fanotify; // Импорт низкоуровневых определений fanotify (структуры, константы, функции вроде fanotify_init, fanotify_mark).
import core.sys.posix.unistd : read, close, ssize_t; // Импорт функций для чтения (read), закрытия (close) дескрипторов и типа ssize_t для возвращаемых значений.
import core.sys.posix.fcntl : O_RDONLY, O_RDWR, O_LARGEFILE, AT_FDCWD; // Импорт флагов для open (O_RDONLY - только чтение, O_LARGEFILE - поддержка больших файлов) и AT_FDCWD для текущей директории.
import std.exception : enforce; // Импорт enforce для проверки условий и бросания исключений при ошибках.
import std.string : toStringz, fromStringz; // Импорт функций для конвертации строк D в C-строки (toStringz) и обратно (fromStringz).
import std.conv : to; // Импорт to для конвертации типов (например, int в string).
import core.stdc.errno : errno; // Импорт errno для получения кода последней ошибки.
import core.stdc.string : strerror; // Импорт strerror для получения строкового описания ошибки по errno.
import core.stdc.stdint; // Импорт стандартных целочисленных типов (uint64_t и т.п.).
// Структура FanotifyEvent: представляет одно событие fanotify.
// Расширена по сравнению с базовой fanotify_event_metadata: добавлено поле name для извлечённого имени файла (если используются флаги FAN_REPORT_NAME).
// Также добавлены свойства для доступа к ключевым полям и методы для проверки конкретных типов событий.
// Это упрощает работу с событиями, делая код более читаемым (вместо прямого доступа к meta.mask и т.д.).
struct FanotifyEvent
{
fanotify_event_metadata meta; // Базовая структура метаданных события из fanotify (содержит mask, fd, pid, event_len и т.д.).
string name; // Извлечённое имя файла или директории (относительное имя, если FAN_REPORT_NAME включено; парсится из дополнительной информации в буфере).
// Свойство mask: возвращает маску событий (битовая маска, где каждый бит соответствует типу события, например FAN_OPEN).
@property uint64_t mask() const
{
return meta.mask; // Просто возвращает значение из meta; const гарантирует, что структура не модифицируется.
}
// Свойство eventFd: возвращает дескриптор файла события (fd). В режиме FAN_REPORT_FID это FAN_NOFD (-1), иначе реальный fd.
@property int eventFd() const
{
return meta.fd; // Доступ к fd из meta; полезно, если нужно работать с файлом напрямую (но в этом коде используется режим без fd).
}
// Свойство pid: возвращает PID процесса, который вызвал событие.
@property int pid() const
{
return meta.pid; // Доступ к pid из meta; помогает идентифицировать, какой процесс взаимодействовал с файлом.
}
// Метод isOpen: проверяет, включает ли маска событие открытия файла (FAN_OPEN).
// Использует битовую операцию & для проверки наличия бита FAN_OPEN в mask.
bool isOpen() const
{
return (mask & FAN_OPEN) != 0; // Если бит установлен, возвращает true; это стандартный способ работы с битoвыми масками.
}
// Метод isModify: проверяет событие модификации файла (FAN_MODIFY, например, запись в файл).
bool isModify() const
{
return (mask & FAN_MODIFY) != 0; // Аналогично, проверка бита FAN_MODIFY.
}
// Метод isCloseWrite: проверяет закрытие файла после записи (FAN_CLOSE_WRITE).
bool isCloseWrite() const
{
return (mask & FAN_CLOSE_WRITE) != 0; // Проверка бита для закрытия с модификацией.
}
// Метод isCloseNoWrite: проверяет закрытие файла без записи (FAN_CLOSE_NOWRITE, например, после чтения).
bool isCloseNoWrite() const
{
return (mask & FAN_CLOSE_NOWRITE) != 0; // Проверка бита для закрытия без модификации.
}
// Метод isAccess: проверяет событие доступа (FAN_ACCESS, например, чтение).
bool isAccess() const
{
return (mask & FAN_ACCESS) != 0; // Проверка бита FAN_ACCESS.
}
// Метод isCreate: проверяет создание файла или директории (FAN_CREATE).
bool isCreate() const
{
return (mask & FAN_CREATE) != 0; // Проверка бита для создания.
}
// Метод isDelete: проверяет удаление файла или директории (FAN_DELETE).
bool isDelete() const
{
return (mask & FAN_DELETE) != 0; // Проверка бита для удаления.
}
}
// Класс Fanotify: основной класс для работы с fanotify.
// Управляет дескриптором (fd), инициализирует его, маркирует пути для мониторинга, читает события и закрывает дескриптор.
// Расширен для парсинга дополнительной информации (FID, NAME) в методе readEvents.
// Использует RAII: дескриптор закрывается в деструкторе автоматически.
class Fanotify
{
private int fd = -1; // Приватный дескриптор fanotify; инициализирован -1 (недействительный), чтобы избежать использования до инициализации.
// Конструктор: инициализирует fanotify с заданными флагами.
// initFlags: флаги для fanotify_init (например, FAN_CLASS_NOTIF для уведомлений без контроля доступа).
// eventFFlags: флаги для событий (по умолчанию O_RDONLY | O_LARGEFILE для чтения больших файлов).
this(uint initFlags, uint eventFFlags = O_RDONLY | O_LARGEFILE)
{
fd = fanotify_init(initFlags, eventFFlags); // Вызов системной функции fanotify_init для создания дескриптора.
enforce(fd >= 0, "Ошибка инициализации fanotify: " ~ to!string(fd)); // Проверка: если fd < 0, бросить исключение с сообщением; enforce упрощает обработку ошибок.
}
// Деструктор: автоматически вызывается при уничтожении объекта.
~this()
{
if (fd >= 0) // Проверка, валиден ли fd (чтобы избежать закрытия -1).
{
close(fd); // Закрытие дескриптора через системный вызов close; освобождает ресурсы.
fd = -1; // Установка в -1 для безопасности (хотя объект уничтожается).
}
}
// Метод mark: маркирует путь (файл или директорию) для мониторинга.
// markFlags: флаги маркировки (например, FAN_MARK_ADD для добавления, FAN_MARK_ONLYDIR для только директорий).
// eventMask: маска событий, которые нужно мониторить (битовая маска, например FAN_OPEN | FAN_MODIFY).
// dirFd: дескриптор директории (по умолчанию AT_FDCWD - текущая).
// path: путь к маркируемому объекту (null для текущей директории).
void mark(uint markFlags, uint64_t eventMask, int dirFd = AT_FDCWD, string path = null)
{
const(char)* cPath = path ? path.toStringz() : null; // Конвертация пути в C-строку (toStringz добавляет null-терминатор); если path null, то null.
int res = fanotify_mark(fd, markFlags, eventMask, dirFd, cPath); // Вызов системной функции fanotify_mark для добавления марки.
if (res == -1) // Проверка на ошибку (-1 означает неудачу).
{
// Сборка детального сообщения об ошибке: включает res, errno и описание от strerror.
string errMsg = "Ошибка маркировки fanotify: " ~ to!string(
res) ~ " (errno: " ~ to!string(errno) ~ ", " ~ strerror(errno)
.fromStringz.to!string ~ ")";
throw new Exception(errMsg); // Бросить исключение для прерывания выполнения при ошибке.
}
}
// Метод readEvents: читает события из дескриптора fanotify.
// bufferSize: размер буфера для чтения (по умолчанию 4096 байт - размер страницы памяти, достаточно для нескольких событий).
// Возвращает массив FanotifyEvent; расширен для парсинга дополнительной информации (FID, DFID, NAME).
FanotifyEvent[] readEvents(size_t bufferSize = 4096)
{
ubyte[] buffer = new ubyte[bufferSize]; // Выделение буфера unsigned byte[] для сырых данных (fanotify возвращает байты).
ssize_t len = read(fd, buffer.ptr, buffer.length); // Чтение данных из fd через системный вызов read; len - количество прочитанных байт (блокирующий вызов, ждёт событий).
if (len <= 0) // Если ничего не прочитано или ошибка, вернуть пустой массив.
{
return []; // Нет событий или ошибка (не бросаем исключение, чтобы цикл мог продолжаться).
}
FanotifyEvent[] events; // Массив для собранных событий.
size_t offset = 0; // Смещение в буфере для парсинга (события идут подряд).
while (offset + FAN_EVENT_METADATA_LEN <= len) // Цикл по буферу: пока хватает места для минимальной структуры metadata.
{
auto meta = cast(fanotify_event_metadata*)(buffer.ptr + offset); // Кастинг байтов в структуру metadata (unsafe, но стандартно для C-API).
if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > len) // Проверка валидности: длина события должна быть >= минимальной и не выходить за буфер.
{
break; // Если некорректно, прервать цикл (защита от повреждённых данных).
}
FanotifyEvent ev = FanotifyEvent(*meta); // Создание структуры события на основе meta (копирует данные).
// Парсинг дополнительной информации (info blocks): если флаги включают FAN_REPORT_FID/NAME, в буфере после meta идут блоки с FID, NAME и т.д.
size_t infoOffset = offset + fanotify_event_metadata.sizeof; // Смещение после meta.
while (infoOffset < offset + meta.event_len) // Цикл по блокам info внутри события.
{
auto hdr = cast(fanotify_event_info_header*)(buffer.ptr + infoOffset); // Кастинг в заголовок info (содержит type и len).
if (hdr.len == 0 || infoOffset + hdr.len > offset + meta.event_len) // Проверка валидности блока.
{
break; // Если некорректно, прервать.
}
if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME) // Если тип - DFID + NAME (директория FID + имя).
{
// Расчёт смещения: пропускаем hdr, fsid (filesystem ID), затем file_handle.
size_t fidOffset = infoOffset + fanotify_event_info_header.sizeof + __kernel_fsid_t
.sizeof;
auto handle = cast(file_handle*)(buffer.ptr + fidOffset); // Кастинг в структуру file_handle (содержит handle_bytes - размер handle).
size_t handleEnd = fidOffset + file_handle.sizeof + handle.handle_bytes; // Конец handle в буфере.
if (handleEnd < offset + meta.event_len) // Проверка, что за handle есть место для имени.
{
// Извлечение имени: null-terminated C-строка после handle; конвертируем в string D.
ev.name = (cast(char*)(buffer.ptr + handleEnd)).fromStringz.to!string;
}
}
infoOffset += hdr.len; // Переход к следующему блоку info.
}
events ~= ev; // Добавление parsed события в массив.
offset += meta.event_len; // Переход к следующему событию в буфере.
}
return events; // Возврат массива событий.
}
// Свойство handle: возвращает дескриптор fd для низкоуровневого доступа (если нужно, например, для select или других вызовов).
@property int handle() const
{
return fd; // Просто геттер.
}
}