Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Add support for watch options and windows recursive watch #115

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
53 changes: 53 additions & 0 deletions spec/pathwatcher-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ temp.track()

describe 'PathWatcher', ->
tempDir = temp.mkdirSync('node-pathwatcher-directory')
baseDir = path.dirname(tempDir)
tempFile = path.join(tempDir, 'file')

beforeEach ->
Expand Down Expand Up @@ -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
6 changes: 5 additions & 1 deletion src/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ NAN_METHOD(Watch) {
return Nan::ThrowTypeError("String required");

Handle<String> 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<v8::Value> err =
Expand Down
6 changes: 5 additions & 1 deletion src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions src/directory.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
5 changes: 3 additions & 2 deletions src/file.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
36 changes: 29 additions & 7 deletions src/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand All @@ -66,21 +66,25 @@ 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()
if watcher.path is filePath
@handleWatcher = watcher
break

@handleWatcher ?= new HandleWatcher(filePath)
@handleWatcher ?= new HandleWatcher(filePath, options)

@onChange = (event, newFilePath, oldFilePath) =>
switch event
Expand All @@ -91,27 +95,38 @@ 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)

close: ->
@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()
Expand All @@ -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'
2 changes: 1 addition & 1 deletion src/pathwatcher_linux.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/pathwatcher_unix.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
12 changes: 7 additions & 5 deletions src/pathwatcher_win.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -61,6 +62,7 @@ struct HandleWrapper {
WatcherHandle dir_handle;
std::vector<char> path;
bool canceled;
unsigned int flags;
OVERLAPPED overlapped;
char buffer[kDirectoryWatcherBufferSize];

Expand All @@ -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 |
Expand Down Expand Up @@ -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);

Expand All @@ -272,7 +274,7 @@ WatcherHandle PlatformWatch(const char* path) {
std::unique_ptr<HandleWrapper> 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())) {
Expand Down