eventcore/source/eventcore/drivers/winapi/watchers.d
Sönke Ludwig 496e99c3b4 Make the API robust against using invalid handles. Fixes #105.
Introduces a "validationCounter" field for all handle types that gets incremented (at least) whenever an OS file descriptor/handle gets invalidated or re-allocated. This way, an old eventcore handle to a reused OS handle can always be distinguished from the current one to avoid interference.
2020-05-11 17:05:53 +02:00

237 lines
7.3 KiB
D

module eventcore.drivers.winapi.watchers;
version (Windows):
import eventcore.driver;
import eventcore.drivers.winapi.core;
import eventcore.drivers.winapi.driver : WinAPIEventDriver; // FIXME: this is an ugly dependency
import eventcore.internal.win32;
import std.experimental.allocator.mallocator : Mallocator;
import std.experimental.allocator : dispose, makeArray;
final class WinAPIEventDriverWatchers : EventDriverWatchers {
@safe: /*@nogc:*/ nothrow:
private {
WinAPIEventDriverCore m_core;
}
this(WinAPIEventDriverCore core)
@nogc {
m_core = core;
}
override WatcherID watchDirectory(string path, bool recursive, FileChangesCallback callback)
{
import std.utf : toUTF16z;
auto handle = () @trusted {
scope (failure) assert(false);
return CreateFileW(path.toUTF16z, FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
null, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
null);
} ();
if (handle == INVALID_HANDLE_VALUE)
return WatcherID.invalid;
auto slot = m_core.setupSlot!WatcherSlot(handle);
slot.directory = path;
slot.recursive = recursive;
slot.callback = callback;
slot.overlapped.driver = m_core;
slot.buffer = () @trusted {
try return Mallocator.instance.makeArray!ubyte(16384);
catch (Exception e) assert(false, "Failed to allocate directory watcher buffer.");
} ();
auto id = WatcherID(cast(size_t)handle, m_core.m_handles[handle].validationCounter);
if (!triggerRead(handle, *slot)) {
releaseRef(id);
return WatcherID.invalid;
}
// keep alive as long as the overlapped I/O operation is pending
addRef(id);
m_core.addWaiter();
return id;
}
override bool isValid(WatcherID handle)
const {
if (auto ph = idToHandle(handle) in m_core.m_handles)
return ph.validationCounter == handle.validationCounter;
return false;
}
override void addRef(WatcherID descriptor)
{
if (!isValid(descriptor)) return;
m_core.m_handles[idToHandle(descriptor)].addRef();
}
override bool releaseRef(WatcherID descriptor)
{
if (!isValid(descriptor)) return true;
return doReleaseRef(idToHandle(descriptor));
}
protected override void* rawUserData(WatcherID descriptor, size_t size, DataInitializer initialize, DataInitializer destroy)
@system {
if (!isValid(descriptor)) return null;
return m_core.rawUserDataImpl(idToHandle(descriptor), size, initialize, destroy);
}
private static bool doReleaseRef(HANDLE handle)
{
auto core = WinAPIEventDriver.threadInstance.core;
auto slot = () @trusted { return &core.m_handles[handle]; } ();
if (!slot.releaseRef(() nothrow {
CloseHandle(handle);
() @trusted {
try Mallocator.instance.dispose(slot.watcher.buffer);
catch (Exception e) assert(false, "Freeing directory watcher buffer failed.");
} ();
slot.watcher.buffer = null;
core.discardEvents(&slot.watcher.overlapped);
core.freeSlot(handle);
}))
{
return false;
}
// If only one reference left, then this is the reference created for
// the current wait operation. Simply cancel the I/O to let the
// completion callback
if (slot.refCount == 1) {
() @trusted { CancelIoEx(handle, &slot.watcher.overlapped.overlapped); } ();
slot.watcher.callback = null;
core.removeWaiter();
}
return true;
}
private static nothrow
void onIOCompleted(DWORD dwError, DWORD cbTransferred, OVERLAPPED_CORE* overlapped)
{
import std.algorithm.iteration : map;
import std.conv : to;
import std.file : isDir;
import std.path : dirName, baseName, buildPath;
auto handle = overlapped.hEvent; // *file* handle
auto gslot = () @trusted { return &WinAPIEventDriver.threadInstance.core.m_handles[handle]; } ();
auto slot = () @trusted { return &gslot.watcher(); } ();
auto id = WatcherID(cast(size_t)handle, gslot.validationCounter);
if (dwError != 0 || gslot.refCount == 1) {
// FIXME: error must be propagated to the caller (except for ABORTED
// errors)
//logWarn("Failed to read directory changes: %s", dwError);
doReleaseRef(handle);
return;
}
if (!slot.callback) return;
// NOTE: cbTransferred can be 0 if the buffer overflowed
ubyte[] result = slot.buffer[0 .. cbTransferred];
while (result.length) {
assert(result.length >= FILE_NOTIFY_INFORMATION._FileName.offsetof);
auto fni = () @trusted { return cast(FILE_NOTIFY_INFORMATION*)result.ptr; } ();
auto fulllen = () @trusted { try return cast(ubyte*)&fni.FileName[fni.FileNameLength/2] - result.ptr; catch (Exception e) return size_t.max; } ();
if (fni.NextEntryOffset > result.length || fulllen > (fni.NextEntryOffset ? fni.NextEntryOffset : result.length)) {
import std.stdio : stderr;
() @trusted {
try stderr.writefln("ERROR: Invalid directory watcher event received: %s", *fni);
catch (Exception e) {}
} ();
break;
}
result = result[fni.NextEntryOffset .. $];
FileChange ch;
switch (fni.Action) {
default: ch.kind = FileChangeKind.modified; break;
case 0x1: ch.kind = FileChangeKind.added; break;
case 0x2: ch.kind = FileChangeKind.removed; break;
case 0x3: ch.kind = FileChangeKind.modified; break;
case 0x4: ch.kind = FileChangeKind.removed; break;
case 0x5: ch.kind = FileChangeKind.added; break;
}
ch.baseDirectory = slot.directory;
string path;
try {
() @trusted {
path = fni.FileName[0 .. fni.FileNameLength/2].map!(ch => dchar(ch)).to!string;
} ();
} catch (Exception e) {
import std.stdio : stderr;
// NOTE: sometimes corrupted strings and invalid UTF-16
// surrogate pairs occur here, until the cause of this is
// found, the best alternative is to ignore those changes
() @trusted {
try stderr.writefln("Invalid path in directory change: %(%02X %)", cast(ushort[])fni.FileName[0 .. fni.FileNameLength/2]);
catch (Exception e) assert(false, e.msg);
} ();
if (fni.NextEntryOffset > 0) continue;
else break;
}
auto fullpath = buildPath(slot.directory, path);
ch.directory = dirName(path);
if (ch.directory == ".") ch.directory = "";
ch.name = baseName(path);
try ch.isDirectory = isDir(fullpath);
catch (Exception e) {} // FIXME: can happen if the base path is relative and the CWD has changed
if (ch.kind != FileChangeKind.modified || !ch.isDirectory)
slot.callback(id, ch);
if (fni.NextEntryOffset == 0 || !slot.callback) break;
}
if (slot.callback)
triggerRead(handle, *slot);
else if (gslot.refCount == 1)
doReleaseRef(handle);
}
private static bool triggerRead(HANDLE handle, ref WatcherSlot slot)
{
enum UINT notifications = FILE_NOTIFY_CHANGE_FILE_NAME|
FILE_NOTIFY_CHANGE_DIR_NAME|FILE_NOTIFY_CHANGE_SIZE|
FILE_NOTIFY_CHANGE_LAST_WRITE;
slot.overlapped.Internal = 0;
slot.overlapped.InternalHigh = 0;
slot.overlapped.Offset = 0;
slot.overlapped.OffsetHigh = 0;
slot.overlapped.hEvent = handle;
BOOL ret;
auto handler = &overlappedIOHandler!onIOCompleted;
() @trusted {
ret = ReadDirectoryChangesW(handle, slot.buffer.ptr, cast(DWORD)slot.buffer.length, slot.recursive,
notifications, null, &slot.overlapped.overlapped, handler);
} ();
if (!ret) {
//logError("Failed to read directory changes in '%s'", m_path);
return false;
}
return true;
}
static private HANDLE idToHandle(WatcherID id) @trusted @nogc { return cast(HANDLE)cast(size_t)id.value; }
}