add linux file watcher and test runner script

This commit is contained in:
Matthew 2025-08-23 15:47:00 +10:00
parent 475c1cf8d6
commit 8dc6ca9a09
3 changed files with 305 additions and 5 deletions

View File

@ -1,14 +1,15 @@
module dlib.platform; module dlib.platform;
import dlib.aliases; import dlib.aliases;
import dlib.alloc;
import dlib.util;
import includes; import includes;
import std.stdio; import std.stdio;
import core.memory; import core.memory;
import core.thread.osthread; import core.thread.osthread;
import core.time; import core.time;
@nogc:
const WINDOW_EDGE_BUFFER = 50; const WINDOW_EDGE_BUFFER = 50;
enum Input enum Input
@ -45,6 +46,11 @@ version(linux)
{ {
import core.sys.posix.dlfcn; import core.sys.posix.dlfcn;
import core.sys.posix.sys.mman; import core.sys.posix.sys.mman;
import core.sys.linux.sys.inotify;
import core.sys.linux.fcntl;
import core.sys.posix.unistd;
import core.stdc.string : strlen;
struct InputEvent struct InputEvent
{ {
@ -616,6 +622,291 @@ MemFree(void* ptr, u64 size)
assert(munmap(ptr, size) == 0, "MemFree failure"); assert(munmap(ptr, size) == 0, "MemFree failure");
} }
struct Watcher
{
Arena arena;
u8[] buffer;
WatcherH handle;
WatchH dir_handle;
u8[] watched_dir;
bool blocking;
}
alias WatcherH = int;
alias WatchH = int;
enum WatchType
{
None,
Access = IN_ACCESS,
Metadata = IN_ATTRIB,
Create = IN_CREATE,
Delete = IN_DELETE,
Modify = IN_MODIFY,
Moved = IN_MOVED_FROM | IN_MOVED_TO,
}
alias WT = WatchType;
Watcher
WatchDirectory(string dir, WatchType type, bool blocking = false)
{
assert(dir.length > 0);
Watcher watcher = {
arena: CreateArena(MB(4)),
buffer: AllocArray!(u8)(MB(1)),
blocking: blocking,
watched_dir: (cast(u8*)dir.ptr)[0 .. dir.length],
};
watcher.handle = inotify_init();
assert(watcher.dir_handle >= 0, "WatchDirectory failure: unable to initialize");
if (!blocking)
{
fcntl(watcher.handle, F_SETFL, fcntl(watcher.handle, F_GETFL) | O_NONBLOCK);
}
watcher.dir_handle = inotify_add_watch(watcher.handle, dir.ptr, IN_CREATE|IN_DELETE|IN_DELETE_SELF|IN_MODIFY|IN_MOVE_SELF|IN_MOVED_FROM|IN_MOVED_TO|IN_ATTRIB|IN_EXCL_UNLINK);
return watcher;
}
WatchEvent[]
ViewChanges(Watcher* watcher)
{
assert(watcher.handle >= 0 && watcher.dir_handle >= 0, "ViewChanges failure: handles are not valid");
Reset(&watcher.arena);
WatchEvent[] events;
i64 length = read(watcher.handle, watcher.buffer.ptr, watcher.buffer.length);
if (length > 0)
{
i64 count = 0;
i64 i = 0;
while(i < length)
{
inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i));
count += 1;
assert(event.wd == watcher.dir_handle);
i += inotify_event.sizeof + event.len;
}
if (count > 0)
{
struct Moved
{
u32 cookie;
i64 to;
i64 from;
}
Moved[] moved = AllocArray!(Moved)(&watcher.arena, (count/2)+1);
i64 m_count = 0;
events = AllocArray!(WatchEvent)(&watcher.arena, count);
count = 0;
i = 0;
while (i < length)
{
inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i));
if (event.len > 0)
{
u8[] file_name = (cast(u8*)event.name)[0 .. strlen(event.name.ptr)];
if (event.mask & IN_MOVED_FROM || event.mask & IN_MOVED_TO)
{
bool from = (event.mask & IN_MOVED_FROM) > 0;
Moved* m;
foreach(j; 0 .. m_count)
{
if (moved[j].cookie == event.cookie)
{
m = moved.ptr + j;
}
}
if (m != null)
{
if (from && m.to >= 0)
{
events[m.to].names[0] = file_name;
if (watcher.watched_dir == file_name)
{
events[m.to].type &= ~(WET.File | WET.Dir);
events[m.to].type |= WET.Self;
}
}
else if (!from && m.from >= 0)
{
events[m.from].names[1] = file_name;
}
}
else
{
WatchEvent* ev = events.ptr + count;
ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File;
ev.type |= WET.Moved;
moved[m_count].cookie = event.cookie;
if (from)
{
ev.names[0] = file_name;
moved[m_count].from = count;
moved[m_count].to = -1;
if (watcher.watched_dir == file_name)
{
ev.type &= ~(WET.File | WET.Dir);
ev.type |= WET.Self;
}
}
else
{
ev.names[1] = file_name;
moved[m_count].to = count;
moved[m_count].from = -1;
}
count += 1;
m_count += 1;
}
}
else
{
WatchEvent* ev = events.ptr + count;
ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File;
ev.names[0] = file_name;
if (ev.names[0] == watcher.watched_dir)
{
ev.type = WET.Self;
}
SetEventType(ev, event.mask);
count += 1;
}
}
else
{
WatchEvent* ev = events.ptr + count;
ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File;
SetEventType(ev, event.mask);
count += 1;
}
i += inotify_event.sizeof + event.len;
}
}
}
return events;
}
void
SetEventType(WatchEvent* ev, u32 mask)
{
if (mask & IN_ACCESS)
{
ev.type |= WET.Accessed;
}
else if (mask & IN_ATTRIB)
{
ev.type |= WET.Metadata;
}
else if (mask & IN_CREATE)
{
ev.type |= WET.Created;
}
else if (mask & IN_MODIFY)
{
ev.type |= WET.Modified;
}
else if (mask & IN_DELETE)
{
ev.type |= WET.Deleted;
}
}
unittest
{
import std.stdio;
import std.file : Remove = remove;
Watcher fw = WatchDirectory("./", WT.Create | WT.Delete | WT.Moved | WT.Modify | WT.Access | WT.Metadata | WT.Access | WT.Metadata);
auto f = File("test_file.txt", "wb");
f.write("test");
f.sync();
f.close();
Remove("./test_file.txt");
WatchEvent[] events = ViewChanges(&fw);
assert(events.length == 3);
assert(events[0].type == WET.FileCreated);
assert(events[0].names[0] == r"test_file.txt");
assert(events[1].type == WET.FileModified);
assert(events[1].names[0] == r"test_file.txt");
assert(events[2].type == WET.FileDeleted);
assert(events[2].names[0] == r"test_file.txt");
}
}
enum WatchEventType
{
None = 0x0000,
File = 0x0001,
Dir = 0x0002,
Self = 0x0004,
Created = 0x0008,
Modified = 0x0010,
Deleted = 0x0020,
Moved = 0x0040,
Metadata = 0x0080,
Accessed = 0x0100,
FileCreated = WET.File | WET.Created,
FileModified = WET.File | WET.Modified,
FileDeleted = WET.File | WET.Deleted,
FileMoved = WET.File | WET.Moved,
FileMetadata = WET.File | WET.Metadata,
FileAccessed = WET.File | WET.Accessed,
DirCreated = WET.Dir | WET.Created,
DirModified = WET.Dir | WET.Modified,
DirDeleted = WET.Dir | WET.Deleted,
DirMoved = WET.Dir | WET.Moved,
DirMetadata = WET.Dir | WET.Metadata,
DirAccessed = WET.Dir | WET.Accessed,
SelfCreated = WET.Self | WET.Created,
SelfModified = WET.Self | WET.Modified,
SelfDeleted = WET.Self | WET.Deleted,
SelfMoved = WET.Self | WET.Moved,
SelfMetadata = WET.Self | WET.Metadata,
SelfAccessed = WET.Self | WET.Accessed,
}
alias WET = WatchEventType;
struct WatchEvent
{
WatchEventType type;
u8[][2] names;
} }
version(Windows) version(Windows)

9
test.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
name="Test_Runner"
ldc2 platform.d aliases.d math.d util.d alloc.d external/xxhash/xxhash.d -P-I/usr/include/freetype2 -L-lfreetype --main --unittest --of=$name
rm $name.o
./$name
rm $name

6
util.d
View File

@ -72,19 +72,19 @@ Log(char* str)
writeln(str); writeln(str);
} }
u64 @nogc u64
KB(u64 v) KB(u64 v)
{ {
return v * 1024; return v * 1024;
}; };
u64 @nogc u64
MB(u64 v) MB(u64 v)
{ {
return KB(v) * 1024; return KB(v) * 1024;
}; };
u64 @nogc u64
GB(u64 v) GB(u64 v)
{ {
return MB(v) * 1024; return MB(v) * 1024;