From 28f646c4704791a05dca3b9ab58ad7ff6d3bd0a8 Mon Sep 17 00:00:00 2001 From: Gary Johnson Date: Tue, 20 Dec 2016 11:20:09 +0000 Subject: [PATCH] Add support for watch options and windows recursive watch --- README.md | 14 +++++++--- spec/pathwatcher-spec.coffee | 53 ++++++++++++++++++++++++++++++++++++ src/common.cc | 6 +++- src/common.h | 6 +++- src/directory.coffee | 9 +++--- src/file.coffee | 5 ++-- src/main.coffee | 36 +++++++++++++++++++----- src/pathwatcher_linux.cc | 2 +- src/pathwatcher_unix.cc | 2 +- src/pathwatcher_win.cc | 12 ++++---- 10 files changed, 119 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 01bcf3c..d99a64b 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,25 @@ npm install pathwatcher PathWatcher = require 'pathwatcher' ``` -### PathWatcher.watch(filename, [listener]) +### PathWatcher.watch(filename, [options], [listener]) Watch for changes on `filename`, where `filename` is either a file or a directory. The returned object is a `PathWatcher`. +The options argument is a javascript object: +`{ recursive: true }` will allow pathWatcher to watch a Windows directory +recursively for any changes to files or directories under it. This option has +no effect on non-Windows operating systems. + The listener callback gets two arguments `(event, path)`. `event` can be `rename`, `delete` or `change`, and `path` is the path of the file which triggered the event. For directories, the `change` event is emitted when a file or directory under -the watched directory got created or deleted. And the `PathWatcher.watch` is -not recursive, so changes of subdirectories under the watched directory would -not be detected. +the watched directory got created or deleted. And if the `PathWatcher.watch` is +not recursive i.e. non-Windows operating systems or if the option has not +been enabled on Windows, changes of subdirectories under the watched directory +would not be detected. ### PathWatcher.close() diff --git a/spec/pathwatcher-spec.coffee b/spec/pathwatcher-spec.coffee index f57c758..0e7ecc1 100644 --- a/spec/pathwatcher-spec.coffee +++ b/spec/pathwatcher-spec.coffee @@ -7,6 +7,7 @@ temp.track() describe 'PathWatcher', -> tempDir = temp.mkdirSync('node-pathwatcher-directory') + baseDir = path.dirname(tempDir) tempFile = path.join(tempDir, 'file') beforeEach -> @@ -165,3 +166,55 @@ describe 'PathWatcher', -> fs.unlinkSync(nested3) fs.rmdirSync(nested2) fs.rmdirSync(nested1) + + describe 'when a file is added under a recursively watched directory #win32', -> + it 'fires the callback with the event type and file path', -> + eventType = null + eventPath = null + newFile = path.join(tempDir, 'newfile') + + watcher = pathWatcher.watch baseDir, { recursive: true }, (type, path) -> + eventType = type + eventPath = path + + fs.writeFileSync(newFile, 'added') + + waitsFor -> eventType? + runs -> + expect(eventType).toBe 'change' + expect(eventPath).toBe newFile + + describe 'when a file is renamed under a recursively watched directory #win32', -> + it 'fires the callback with the event type and file path', -> + eventType = null + eventPath = null + newFile = path.join(tempDir, 'newfile') + renamedFile = path.join(tempDir, 'renamedfile') + + watcher = pathWatcher.watch baseDir, { recursive: true }, (type, path) -> + eventType = type + eventPath = path + + fs.renameSync(newFile, renamedFile) + + waitsFor -> eventType? + runs -> + expect(eventType).toBe 'change' + expect(eventPath).toBe renamedFile + + describe 'when a file is removed under a recursively watched directory #win32', -> + it 'fires the callback with the event type and file path', -> + eventType = null + eventPath = null + renamedFile = path.join(tempDir, 'renamedfile') + + watcher = pathWatcher.watch baseDir, { recursive: true }, (type, path) -> + eventType = type + eventPath = path + + fs.unlinkSync(renamedFile) + + waitsFor -> eventType? + runs -> + expect(eventType).toBe 'change' + expect(eventPath).toBe renamedFile diff --git a/src/common.cc b/src/common.cc index 563c943..49ce689 100644 --- a/src/common.cc +++ b/src/common.cc @@ -122,7 +122,11 @@ NAN_METHOD(Watch) { return Nan::ThrowTypeError("String required"); Handle path = info[0]->ToString(); - WatcherHandle handle = PlatformWatch(*String::Utf8Value(path)); + unsigned int flags = 0; + + if (info[1]->IsTrue()) + flags |= FLAG_RECURSIVE; + WatcherHandle handle = PlatformWatch(*String::Utf8Value(path), flags); if (!PlatformIsHandleValid(handle)) { int error_number = PlatformInvalidHandleToErrorNumber(handle); v8::Local err = diff --git a/src/common.h b/src/common.h index a667800..733f080 100644 --- a/src/common.h +++ b/src/common.h @@ -24,7 +24,7 @@ typedef int32_t WatcherHandle; void PlatformInit(); void PlatformThread(); -WatcherHandle PlatformWatch(const char* path); +WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint); void PlatformUnwatch(WatcherHandle handle); bool PlatformIsHandleValid(WatcherHandle handle); int PlatformInvalidHandleToErrorNumber(WatcherHandle handle); @@ -40,6 +40,10 @@ enum EVENT_TYPE { EVENT_CHILD_CREATE, }; +enum WATCH_FLAGS { + FLAG_RECURSIVE = 1 +}; + void WaitForMainThread(); void WakeupNewThread(); void PostEventAndWait(EVENT_TYPE type, diff --git a/src/directory.coffee b/src/directory.coffee index cffac6a..8f7e2d3 100644 --- a/src/directory.coffee +++ b/src/directory.coffee @@ -307,10 +307,11 @@ class Directory ### subscribeToNativeChangeEvents: -> - @watchSubscription ?= PathWatcher.watch @path, (eventType) => - if eventType is 'change' - @emit 'contents-changed' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-change' + if not PathWatcher.isWatchedByParent @path + @watchSubscription ?= PathWatcher.watch @path, (eventType) => + if eventType is 'change' + @emit 'contents-changed' if Grim.includeDeprecatedAPIs + @emitter.emit 'did-change' unsubscribeFromNativeChangeEvents: -> if @watchSubscription? diff --git a/src/file.coffee b/src/file.coffee index ed5b909..fa10a8a 100644 --- a/src/file.coffee +++ b/src/file.coffee @@ -471,8 +471,9 @@ class File @emitter.emit 'did-delete' subscribeToNativeChangeEvents: -> - @watchSubscription ?= PathWatcher.watch @path, (args...) => - @handleNativeChangeEvent(args...) + if not PathWatcher.isWatchedByParent @path + @watchSubscription ?= PathWatcher.watch @path, (args...) => + @handleNativeChangeEvent(args...) unsubscribeFromNativeChangeEvents: -> if @watchSubscription? diff --git a/src/main.coffee b/src/main.coffee index 107b2c0..17ff1a9 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -10,7 +10,7 @@ binding.setCallback (event, handle, filePath, oldFilePath) -> handleWatchers.get(handle).onEvent(event, filePath, oldFilePath) if handleWatchers.has(handle) class HandleWatcher extends EventEmitter - constructor: (@path) -> + constructor: (@path, @options) -> @setMaxListeners(Infinity) @start() @@ -46,7 +46,7 @@ class HandleWatcher extends EventEmitter @emit('change', event, filePath, oldFilePath) start: -> - @handle = binding.watch(@path) + @handle = binding.watch(@path, @options.recursive) if handleWatchers.has(@handle) troubleWatcher = handleWatchers.get(@handle) troubleWatcher.close() @@ -66,13 +66,17 @@ class PathWatcher extends EventEmitter path: null handleWatcher: null - constructor: (filePath, callback) -> + constructor: (filePath, options, callback) -> @path = filePath + @isRecursive = false # On Windows watching a file is emulated by watching its parent folder. if process.platform is 'win32' stats = fs.statSync(filePath) @isWatchingParent = not stats.isDirectory() + @isRecursive = not @isWatchingParent and options.recursive or false + else + options.recursive = false filePath = path.dirname(filePath) if @isWatchingParent for watcher in handleWatchers.values() @@ -80,7 +84,7 @@ class PathWatcher extends EventEmitter @handleWatcher = watcher break - @handleWatcher ?= new HandleWatcher(filePath) + @handleWatcher ?= new HandleWatcher(filePath, options) @onChange = (event, newFilePath, oldFilePath) => switch event @@ -91,17 +95,24 @@ class PathWatcher extends EventEmitter when 'child-rename' if @isWatchingParent @onChange('rename', newFilePath) if @path is oldFilePath + else if @isRecursive + @onChange('change', newFilePath) else @onChange('change', '') when 'child-delete' if @isWatchingParent @onChange('delete', null) if @path is newFilePath + else if @isRecursive + @onChange('change', newFilePath) else @onChange('change', '') when 'child-change' @onChange('change', '') if @isWatchingParent and @path is newFilePath when 'child-create' - @onChange('change', '') unless @isWatchingParent + if @isRecursive + @onChange('change', newFilePath) + else + @onChange('change', '') unless @isWatchingParent @handleWatcher.on('change', @onChange) @@ -109,9 +120,13 @@ class PathWatcher extends EventEmitter @handleWatcher.removeListener('change', @onChange) @handleWatcher.closeIfNoListener() -exports.watch = (path, callback) -> +exports.watch = (path, options, callback) -> path = require('path').resolve(path) - new PathWatcher(path, callback) + options = options or {} + if (typeof options is 'function') + callback = options + options = {} + new PathWatcher(path, options, callback) exports.closeAllWatchers = -> watcher.close() for watcher in handleWatchers.values() @@ -122,5 +137,12 @@ exports.getWatchedPaths = -> paths.push(watcher.path) for watcher in handleWatchers.values() paths +exports.isWatchedByParent = (path) -> + for watcher in handleWatchers.values() + watcherPath = watcher.path + if path.startsWith(watcherPath) and watcherPath.options.recursive + return true + false + exports.File = require './file' exports.Directory = require './directory' diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index 13e6a0b..1ea2491 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -62,7 +62,7 @@ void PlatformThread() { } } -WatcherHandle PlatformWatch(const char* path) { +WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint) { if (g_inotify == -1) { return -g_init_errno; } diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc index d88865e..8d35909 100644 --- a/src/pathwatcher_unix.cc +++ b/src/pathwatcher_unix.cc @@ -72,7 +72,7 @@ void PlatformThread() { } } -WatcherHandle PlatformWatch(const char* path) { +WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint) { if (g_kqueue == -1) { return -g_init_errno; } diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index b1a879c..ff13630 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -29,10 +29,11 @@ struct ScopedLocker { }; struct HandleWrapper { - HandleWrapper(WatcherHandle handle, const char* path_str) + HandleWrapper(WatcherHandle handle, const char* path_str, unsigned int flags_uint) : dir_handle(handle), path(strlen(path_str)), - canceled(false) { + canceled(false), + flags(flags_uint) { memset(&overlapped, 0, sizeof(overlapped)); overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); g_events.push_back(overlapped.hEvent); @@ -61,6 +62,7 @@ struct HandleWrapper { WatcherHandle dir_handle; std::vector path; bool canceled; + unsigned int flags; OVERLAPPED overlapped; char buffer[kDirectoryWatcherBufferSize]; @@ -82,7 +84,7 @@ static bool QueueReaddirchanges(HandleWrapper* handle) { return ReadDirectoryChangesW(handle->dir_handle, handle->buffer, kDirectoryWatcherBufferSize, - FALSE, + (handle->flags & FLAG_RECURSIVE) ? TRUE : FALSE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | @@ -246,7 +248,7 @@ void PlatformThread() { } } -WatcherHandle PlatformWatch(const char* path) { +WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint) { wchar_t wpath[MAX_PATH] = { 0 }; MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH); @@ -272,7 +274,7 @@ WatcherHandle PlatformWatch(const char* path) { std::unique_ptr handle; { ScopedLocker locker(g_handle_wrap_map_mutex); - handle.reset(new HandleWrapper(dir_handle, path)); + handle.reset(new HandleWrapper(dir_handle, path, flags_uint)); } if (!QueueReaddirchanges(handle.get())) {