-
-
Save flaviut/a9a6771726db3443347c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import winlean, os, asyncdispatch, tables, sets, strutils | |
type | |
AlignedBuffer = tuple[base, start: pointer] | |
FileEvent* = enum | |
feFileCreated | |
feFileRemoved | |
feFileModified | |
feNameChangedNew | |
feNameChangedOld | |
FileEventCb* = proc ( | |
fileName: string, | |
eventKind: FileEvent, | |
bufferOverflowed: bool | |
): PFuture[void] | |
WinCharBuffer {.unchecked.} = ptr array[1, TWinChar] | |
const | |
allFileEvents* = {FileEvent.low .. FileEvent.high} | |
proc getPath(h: THandle, initSize = 80): string = | |
var | |
lastSize = initSize | |
buffer = cast[WinCharBuffer](alloc0(initSize * sizeOf(TWinChar))) | |
while true: | |
let bufSize = GetFinalPathNameByHandle(h, buffer, initSize.Dword, 0.Dword) | |
if bufSize == 0: | |
osError(osLastError()) | |
elif bufSize > lastSize: | |
buffer = resize(buffer, bufSize * sizeOf(TWinChar)) | |
lastSize = bufSize | |
continue | |
else: | |
break | |
result = $cast[WideCString](buffer) | |
dealloc(buffer) | |
proc openDirHandle(path: string, followSymlink=true): TAsyncFD = | |
let accessFlags = (fileShareDelete or fileShareRead or fileShareWrite) | |
var modeFlags = (fileFlagBackupSemantics or fileFlagOverlapped) | |
if not followSymlink: | |
modeFlags = modeFlags or fileFlagOpenReparsePoint | |
when useWinUnicode: | |
result = createFileW(newWideCString(path), fileListDirectory, accessFlags, | |
nil, openExisting, modeFlags, 0).TAsyncFD | |
else: | |
result = createFileA(path, fileListDirectory, accessFlags, | |
nil, openExisting, modeFlags, 0).TAsyncFD | |
if result == invalidHandleValue.TAsyncFD: | |
osError(osLastError()) | |
proc openHandle(path: string, followSymlink=true): THandle = | |
var flags = FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL | |
if not followSymlink: | |
flags = flags or FILE_FLAG_OPEN_REPARSE_POINT | |
when useWinUnicode: | |
result = createFileW( | |
newWideCString(path), 0'i32, | |
FILE_SHARE_DELETE or FILE_SHARE_READ or FILE_SHARE_WRITE, | |
nil, OPEN_EXISTING, flags, 0 | |
) | |
else: | |
result = createFileA( | |
path, 0'i32, | |
FILE_SHARE_DELETE or FILE_SHARE_READ or FILE_SHARE_WRITE, | |
nil, OPEN_EXISTING, flags, 0 | |
) | |
if result == invalidHandleValue: | |
osError(osLastError()) | |
proc allocAligned(size, alignment: int): AlignedBuffer = | |
## Allocate a buffer of `size` bytes, aligned to an address that is a | |
## multiple of the given `alignment`. Note that this buffer must be freed | |
## manually! | |
assert((alignment and (alignment - 1)) == 0) # Power of 2? | |
var address = alloc0(size+alignment) | |
if (cast[int](address) and (alignment - 1)) == 0: | |
(address, address) | |
else: | |
let offset = alignment - (cast[int](address) and (alignment - 1)) | |
(address, cast[pointer](cast[int](address) + offset)) | |
proc toFileEvent(action: dword): FileEvent = | |
case action | |
of FILE_ACTION_ADDED: | |
result = feFileCreated | |
of FILE_ACTION_REMOVED: | |
result = feFileRemoved | |
of FILE_ACTION_MODIFIED: | |
result = feFileModified | |
of FILE_ACTION_RENAMED_OLD_NAME: | |
result = feNameChangedNew | |
of FILE_ACTION_RENAMED_NEW_NAME: | |
result = feNameChangedOld | |
else: | |
raise newException(EInvalidValue, "Invalid file action: " & $action) | |
proc toDword(actions: set[FileEvent]): dword = | |
for a in actions: | |
case a | |
of feFileCreated: | |
result = result or FILE_ACTION_ADDED | |
of feFileRemoved: | |
result = result or FILE_ACTION_REMOVED | |
of feFileModified: | |
result = result or FILE_ACTION_MODIFIED | |
of feNameChangedNew: | |
result = result or FILE_ACTION_RENAMED_OLD_NAME | |
of feNameChangedOld: | |
result = result or FILE_ACTION_RENAMED_NEW_NAME | |
proc cancelWatch*(handle: TAsyncFD) = | |
if cancelIo(handle.THandle) == false.WinBool: | |
osError(osLastError()) | |
proc watchPathImpl(handle: TAsyncFD, callBack: FileEventCb, filterFlags: dword, | |
bufferLen: int, watchSubdir: bool): TAsyncFD = | |
## Raw implementation that the watchDir/watchFile procedures use. | |
# Note: Eventually, the file handle will need to be closed, the buffer | |
# freed, and the overlapped structure manually decremented. | |
let | |
bufferSize = bufferLen * sizeof(FileNotifyInformation) | |
buffer = allocAligned(bufferSize, 32) | |
ol = PCustomOverlapped() | |
proc callReadChanges: WinBool = | |
result = ReadDirectoryChangesW( | |
handle.THandle, | |
buffer.start, | |
bufferSize.int32, | |
watchSubdir.WinBool, | |
filterFlags, | |
cast[ptr dword](nil), | |
cast[POverlapped](ol), | |
cast[LPOVERLAPPED_COMPLETION_ROUTINE](nil) | |
) | |
proc cleanupReadChanges = | |
discard handle.THandle.closeHandle() | |
getGlobalDispatcher().handles.excl(handle) | |
buffer.base.dealloc | |
GC_unref(ol) | |
proc rawEventCb(sock: TAsyncFD, bytesCount: DWord, errcode: TOSErrorCode) = | |
## Raw callback for the asyncdispatch machinary to call. | |
var | |
data = cast[ptr FileNotifyInformation](buffer.start) | |
if errcode == ERROR_OPERATION_ABORTED.TOSErrorCode: | |
cleanupReadChanges() | |
while true: | |
let | |
offset = data.NextEntryOffset | |
action = data.Action.toFileEvent() | |
nameLength = data.FileNameLength | |
fileName = bufferToString(data.FileName, nameLength div 2) | |
discard callBack(fileName, action, false) | |
if offset == 0: | |
break | |
data = cast[ptr FileNotifyInformation](cast[int](data) + offset) | |
if callReadChanges() == WinBool(false): | |
let error = osLastError() | |
cleanupReadChanges() | |
osError(error) | |
ol.data = TCompletionData( | |
sock: handle, | |
cb: rawEventCb | |
) | |
handle.register() | |
if callReadChanges() == WinBool(false): | |
let error = osLastError() | |
cleanupReadChanges() | |
osError(error) | |
result = handle | |
proc watchPathImpl(path: string, callBack: FileEventCb, filterFlags: dword, | |
bufferLen: int, watchSubdir: bool): TAsyncFD = | |
result = watchPathImpl(openDirHandle(path), callBack, filterFlags, | |
bufferLen, watchSubdir) | |
proc watchDirectory*(path: string, callback: FileEventCb, | |
filter = allFileEvents, bufferLen = 40, | |
watchSubdirs = false): TAsyncFD = | |
if existsDir(path): | |
result = watchPathImpl(path, callback, filter.toDword, bufferLen, | |
watchSubdirs) | |
else: | |
raise newException(EInvalidValue, path & " is not a directory") | |
proc watchFile*(path: string, callback: FileEventCb, filter = allFileEvents, | |
bufferLen = 40, watchSubdirs = false): TAsyncFD = | |
var | |
parentPath = path.expandFileName.parentDir | |
targetName = extractFileName(path) | |
wasRenamed = false | |
newName: string | |
parentHandle = openDirHandle(parentPath) | |
fileHandle = openHandle(path) | |
proc filterLayer(givenName: string, eventKind: FileEvent, | |
overflowed: bool): PFuture[void] = | |
template reSync = | |
var newPath = getPath(fileHandle) | |
newPath = newPath[4..(newPath.high)] | |
echo(newPath) | |
let | |
newParentPath = newPath.parentDir | |
targetName = extractFileName(newPath) | |
if newParentPath != parentPath: | |
# We have to re-sync the path, by stopping change notifications on | |
# the old directory, and re-activating them on the new directory | |
cancelWatch(parentHandle) | |
parentPath = newParentPath | |
discard watchPathImpl(parentPath, filterLayer, filter.toDword, | |
bufferLen, watchSubdirs) | |
echo("Returning") | |
return result | |
var namesAreEqual: bool | |
if FileSystemCaseSensitive: | |
namesAreEqual = (givenName == targetName) | |
else: | |
namesAreEqual = (givenName.toLower == targetName.toLower) | |
if namesAreEqual: | |
result = callback(givenName, eventKind, overflowed) | |
reSync() | |
if existsFile(path): | |
result = watchPathImpl(parentPath, filterLayer, filter.toDword, bufferLen, watchSubdirs) | |
else: | |
raise newException(EInvalidValue, path & " is not a file") | |
proc echoBack(name: string, event: FileEvent, overflowed: bool): PFuture[void] = | |
name.echo | |
event.echo | |
overflowed.echo | |
when isMainModule: | |
let handle = watchFile(r".\tests5.nim", echoBack) | |
runForever() | |
## | |
## Handle, | |
## addWatch | |
## Add a file/directory watch to the file monitor | |
## removeWatch | |
## Remove a file/directory watch to the file monitor | |
## isWatching ? | |
## Determine if the path is being watched |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment