dwatch/source/app.d

401 lines
8 KiB
D
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

module app;
import dfanotify;
import fanotify;
import depoll;
import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE, O_CLOEXEC, AT_FDCWD;
import core.sys.posix.unistd : read, close;
import core.sys.posix.sys.stat : fstat, stat_t;
import core.stdc.errno : errno, EINTR;
import core.stdc.string : strerror;
import std.stdio : writeln, writefln, stderr;
import std.file : isDir, readText, exists, read;
import std.string : strip, splitLines, startsWith, toStringz, fromStringz, split;
import std.conv : to;
import std.exception : enforce;
import std.getopt : getopt, defaultGetoptPrinter;
import std.format : format;
import core.sys.linux.epoll : EPOLLIN, EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLL_CLOEXEC;
// Переписать
import core.sys.posix.sys.types : uid_t;
import core.sys.posix.pwd : passwd, getpwuid_r;
import core.stdc.stdlib : malloc, free;
import core.stdc.string : strlen;
/// ---- syscall + pidfd_open ----
extern (C) long syscall(long number, ...);
version (X86_64) enum __NR_pidfd_open = 434;
extern (C) int pidfd_open(int pid, uint flags)
{
return cast(int) syscall(__NR_pidfd_open, pid, flags);
}
/// ---- readlink по fd ----
extern (C) long readlink(const char* path, char* buf, size_t bufsiz);
string readlinkFdPath(int fd)
{
char[4_096] buf;
auto link = format("/proc/self/fd/%d", fd);
auto n = readlink(link.toStringz, buf.ptr, buf.length.to!int - 1);
if (n <= 0)
return "";
buf[n] = 0;
return buf.ptr.fromStringz.idup.strip;
}
/// ---- имя процесса и UIDы (ruid из status, uid из loginuid) ----
struct ProcIds
{
uint ruid;
uint uid;
} // uid = loginuid
string readProcComm(int pid)
{
auto p = format("/proc/%s/comm", pid);
string s;
try
{
s = readText(p);
}
catch (Exception e)
{
return "";
}
return s.strip;
}
uint readLoginUid(int pid)
{
auto p = format("/proc/%s/loginuid", pid);
try
{
auto s = readText(p).strip;
// loginuid может быть "-1" (unset). В этом случае вернём 4294967295 или 0 — выбери политику.
if (s.length && s[0] == '-')
return uint.max; // помечаем как "нет"
return s.to!uint;
}
catch (Exception e)
{
return uint.max; // нет файла или отказ — считаем не установленным
}
}
ProcIds readProcIds(int pid)
{
uint ruid = 0;
auto p = format("/proc/%s/status", pid);
string s;
try
{
s = readText(p);
}
catch (Exception e)
{
return ProcIds(0, readLoginUid(pid));
}
foreach (line; s.splitLines)
{
if (line.startsWith("Uid:"))
{
// Uid: RUID EUID SUID FSUID
auto parts = line.split;
if (parts.length >= 5)
{
ruid = parts[1].to!uint;
}
break;
}
}
return ProcIds(ruid, readLoginUid(pid));
}
/// ---- ключ карты соответствий: (pid, dev, ino) ----
struct DevIno
{
ulong dev;
ulong ino;
int pid;
bool opEquals(const DevIno rhs) const @safe nothrow
{
return dev == rhs.dev && ino == rhs.ino && pid == rhs.pid;
}
size_t toHash() const @safe nothrow
{
return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid);
}
}
bool getDevIno(int fd, out DevIno di, int pid)
{
stat_t st;
if (fstat(fd, &st) != 0)
return false;
di = DevIno(cast(ulong) st.st_dev, cast(ulong) st.st_ino, pid);
return true;
}
/// ---- сессия файла для конкретного PID ----
struct Session
{
string procName;
uint ruid; // из /proc/PID/status (Uid:)
uint uid; // LOGINUID из /proc/PID/loginuid
string filePathOpen; // путь, снятый на PERM
bool changed; // был CLOSE_WRITE
const(ubyte)[] fileBytesSource;
const(ubyte)[] fileBytesDiff;
}
__gshared Session[DevIno] gMap; // (pid,dev,ino) -> Session
__gshared int[int] gPidfdByPid; // pid -> pidfd
private string userName(uid_t uid)
{
enum BUF = 4096;
auto buf = cast(char*) malloc(BUF);
scope (exit)
if (buf)
free(buf);
passwd pwd;
passwd* outp;
auto rc = getpwuid_r(uid, &pwd, buf, BUF, &outp);
if (rc == 0 && outp !is null)
{
return pwd.pw_name[0 .. strlen(pwd.pw_name)].idup;
}
return "";
}
/// ---- журнал: proc, file, uid(loginuid), ruid ----
void logChange(string procName, string filePath, uint uid, uint ruid)
{
writefln(`proc="%s" file="%s" user=%s(%s) realUser=%s(%s)`,
procName, filePath, userName(uid), uid, userName(ruid), ruid);
}
/// ---- маска на КАТАЛОГ ----
ulong dirMask()
{
return FAN_EVENT_ON_CHILD |
FAN_OPEN_PERM | FAN_OPEN_EXEC_PERM | FAN_ACCESS_PERM |
FAN_OPEN | FAN_CLOSE_WRITE;
}
ubyte[] readAllFromFd(int fd)
{
import std.array : appender;
ubyte[16_384] buf; // 16 KiB буфер
auto output = appender!(ubyte[]);
for (;;)
{
auto n = read(fd, buf.ptr, buf.length);
if (n <= 0)
break;
output.put(buf[0 .. n]);
}
return output.data;
}
/// ---- обработчики ----
void onOpenPerm(FanotifyEvent ev, Epoll ep)
{
const pid = ev.pid;
auto procName = readProcComm(pid);
auto ids = readProcIds(pid);
DevIno key;
if (!getDevIno(ev.eventFd, key, pid))
{
ev.respond(FAN_ALLOW);
return;
}
auto path = readlinkFdPath(ev.eventFd);
const(ubyte)[] content;
try
{
if (path.length)
{
content = readAllFromFd(ev.eventFd);
}
}
catch (Exception e)
{
content = [];
}
if (auto ps = key in gMap)
{
// уже есть
}
else
{
gMap[key] = Session(procName, ids.ruid, ids.uid, path, false, content);
}
if (!(pid in gPidfdByPid))
{
int pfd = -1;
try
{
pfd = pidfd_open(pid, 0);
if (pfd < 0)
writeln("pidfd_open failed: ", strerror(errno).fromStringz);
}
catch (Exception e)
{
pfd = -1;
}
if (pfd >= 0)
{
try
{
ep.add(pfd, cast(uint) pid, EPOLLIN | EPOLLHUP | EPOLLRDHUP | EPOLLERR);
gPidfdByPid[pid] = pfd;
}
catch (Exception e)
{
}
}
}
ev.respond(FAN_ALLOW);
}
void onCloseWrite(FanotifyEvent ev)
{
DevIno key;
if (!getDevIno(ev.eventFd, key, ev.pid))
return;
if (auto ps = key in gMap)
{
ps.changed = true;
import sdiff;
const(ubyte)[] content;
try
{
if (ps.filePathOpen.length)
content = readAllFromFd(ev.eventFd);
}
catch (Exception e)
{
content = [];
}
auto dataSource = new MMFile(ps.fileBytesSource);
auto dataChange = new MMFile(content);
ps.fileBytesDiff = dataSource.diff(dataChange).slice();
}
}
void flushAndClearForPid(int pid, Epoll ep)
{
auto keys = gMap.keys.dup;
foreach (k; keys)
{
if (k.pid != pid)
continue;
auto s = gMap[k];
if (s.changed)
{
logChange(s.procName, s.filePathOpen, s.uid, s.ruid);
writeln(cast(string)s.fileBytesDiff);
}
gMap.remove(k);
}
if (auto p = pid in gPidfdByPid)
{
try
{
ep.remove(*p);
}
catch (Exception e)
{
}
close(*p);
gPidfdByPid.remove(pid);
}
}
/// ---- main ----
enum uint FAN_TAG = 0;
void main(string[] args)
{
string watchDir;
auto help = getopt(args, "dir|d", &watchDir);
if (help.helpWanted || watchDir.length == 0)
{
defaultGetoptPrinter("Usage: app -d <dir-to-watch>", help.options);
return;
}
enforce(isDir(watchDir), "Ожидается путь к КАТАЛОГУ (-d)");
enum initFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK;
auto fan = new Fanotify(initFlags, O_RDONLY | O_LARGEFILE | O_CLOEXEC);
try
{
fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, dirMask(), AT_FDCWD, watchDir);
}
catch (Exception e)
{
stderr.writefln("fanotify_mark failed: %s", e.msg);
return;
}
auto ep = new Epoll(EPOLL_CLOEXEC);
ep.add(fan.handle, FAN_TAG, EPOLLIN | EPOLLERR | EPOLLHUP);
writeln("watching dir: ", watchDir);
for (;;)
{
auto evs = ep.wait(256, -1);
if (evs.length == 0)
continue;
foreach (e; evs)
{
if (e.tag == FAN_TAG)
{
auto list = fan.readEvents(8_192);
foreach (fev; list)
{
if (fev.isOverflow)
{
writeln(
"FAN_Q_OVERFLOW — возможна потеря корреляции");
continue;
}
if (fev.isOpenPerm || fev.isAccessPerm || fev.isOpenExecPerm)
{
onOpenPerm(fev, ep);
continue;
}
if (fev.isCloseWrite)
{
onCloseWrite(fev);
continue;
}
}
}
else
{
auto pid = cast(int) e.tag;
flushAndClearForPid(pid, ep);
}
}
}
}