Skip to content

Instantly share code, notes, and snippets.

Created June 1, 2014 23:52
Show Gist options
  • Save anonymous/549d1e4975a744b49cac to your computer and use it in GitHub Desktop.
Save anonymous/549d1e4975a744b49cac to your computer and use it in GitHub Desktop.
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