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

Commit 28f646c

Browse files
committed
Add support for watch options and windows recursive watch
1 parent e9312f3 commit 28f646c

File tree

10 files changed

+119
-26
lines changed

10 files changed

+119
-26
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,25 @@ npm install pathwatcher
2020
PathWatcher = require 'pathwatcher'
2121
```
2222

23-
### PathWatcher.watch(filename, [listener])
23+
### PathWatcher.watch(filename, [options], [listener])
2424

2525
Watch for changes on `filename`, where `filename` is either a file or a
2626
directory. The returned object is a `PathWatcher`.
2727

28+
The options argument is a javascript object:
29+
`{ recursive: true }` will allow pathWatcher to watch a Windows directory
30+
recursively for any changes to files or directories under it. This option has
31+
no effect on non-Windows operating systems.
32+
2833
The listener callback gets two arguments `(event, path)`. `event` can be `rename`,
2934
`delete` or `change`, and `path` is the path of the file which triggered the
3035
event.
3136

3237
For directories, the `change` event is emitted when a file or directory under
33-
the watched directory got created or deleted. And the `PathWatcher.watch` is
34-
not recursive, so changes of subdirectories under the watched directory would
35-
not be detected.
38+
the watched directory got created or deleted. And if the `PathWatcher.watch` is
39+
not recursive i.e. non-Windows operating systems or if the option has not
40+
been enabled on Windows, changes of subdirectories under the watched directory
41+
would not be detected.
3642

3743
### PathWatcher.close()
3844

spec/pathwatcher-spec.coffee

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ temp.track()
77

88
describe 'PathWatcher', ->
99
tempDir = temp.mkdirSync('node-pathwatcher-directory')
10+
baseDir = path.dirname(tempDir)
1011
tempFile = path.join(tempDir, 'file')
1112

1213
beforeEach ->
@@ -165,3 +166,55 @@ describe 'PathWatcher', ->
165166
fs.unlinkSync(nested3)
166167
fs.rmdirSync(nested2)
167168
fs.rmdirSync(nested1)
169+
170+
describe 'when a file is added under a recursively watched directory #win32', ->
171+
it 'fires the callback with the event type and file path', ->
172+
eventType = null
173+
eventPath = null
174+
newFile = path.join(tempDir, 'newfile')
175+
176+
watcher = pathWatcher.watch baseDir, { recursive: true }, (type, path) ->
177+
eventType = type
178+
eventPath = path
179+
180+
fs.writeFileSync(newFile, 'added')
181+
182+
waitsFor -> eventType?
183+
runs ->
184+
expect(eventType).toBe 'change'
185+
expect(eventPath).toBe newFile
186+
187+
describe 'when a file is renamed under a recursively watched directory #win32', ->
188+
it 'fires the callback with the event type and file path', ->
189+
eventType = null
190+
eventPath = null
191+
newFile = path.join(tempDir, 'newfile')
192+
renamedFile = path.join(tempDir, 'renamedfile')
193+
194+
watcher = pathWatcher.watch baseDir, { recursive: true }, (type, path) ->
195+
eventType = type
196+
eventPath = path
197+
198+
fs.renameSync(newFile, renamedFile)
199+
200+
waitsFor -> eventType?
201+
runs ->
202+
expect(eventType).toBe 'change'
203+
expect(eventPath).toBe renamedFile
204+
205+
describe 'when a file is removed under a recursively watched directory #win32', ->
206+
it 'fires the callback with the event type and file path', ->
207+
eventType = null
208+
eventPath = null
209+
renamedFile = path.join(tempDir, 'renamedfile')
210+
211+
watcher = pathWatcher.watch baseDir, { recursive: true }, (type, path) ->
212+
eventType = type
213+
eventPath = path
214+
215+
fs.unlinkSync(renamedFile)
216+
217+
waitsFor -> eventType?
218+
runs ->
219+
expect(eventType).toBe 'change'
220+
expect(eventPath).toBe renamedFile

src/common.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,11 @@ NAN_METHOD(Watch) {
122122
return Nan::ThrowTypeError("String required");
123123

124124
Handle<String> path = info[0]->ToString();
125-
WatcherHandle handle = PlatformWatch(*String::Utf8Value(path));
125+
unsigned int flags = 0;
126+
127+
if (info[1]->IsTrue())
128+
flags |= FLAG_RECURSIVE;
129+
WatcherHandle handle = PlatformWatch(*String::Utf8Value(path), flags);
126130
if (!PlatformIsHandleValid(handle)) {
127131
int error_number = PlatformInvalidHandleToErrorNumber(handle);
128132
v8::Local<v8::Value> err =

src/common.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ typedef int32_t WatcherHandle;
2424

2525
void PlatformInit();
2626
void PlatformThread();
27-
WatcherHandle PlatformWatch(const char* path);
27+
WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint);
2828
void PlatformUnwatch(WatcherHandle handle);
2929
bool PlatformIsHandleValid(WatcherHandle handle);
3030
int PlatformInvalidHandleToErrorNumber(WatcherHandle handle);
@@ -40,6 +40,10 @@ enum EVENT_TYPE {
4040
EVENT_CHILD_CREATE,
4141
};
4242

43+
enum WATCH_FLAGS {
44+
FLAG_RECURSIVE = 1
45+
};
46+
4347
void WaitForMainThread();
4448
void WakeupNewThread();
4549
void PostEventAndWait(EVENT_TYPE type,

src/directory.coffee

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,11 @@ class Directory
307307
###
308308

309309
subscribeToNativeChangeEvents: ->
310-
@watchSubscription ?= PathWatcher.watch @path, (eventType) =>
311-
if eventType is 'change'
312-
@emit 'contents-changed' if Grim.includeDeprecatedAPIs
313-
@emitter.emit 'did-change'
310+
if not PathWatcher.isWatchedByParent @path
311+
@watchSubscription ?= PathWatcher.watch @path, (eventType) =>
312+
if eventType is 'change'
313+
@emit 'contents-changed' if Grim.includeDeprecatedAPIs
314+
@emitter.emit 'did-change'
314315

315316
unsubscribeFromNativeChangeEvents: ->
316317
if @watchSubscription?

src/file.coffee

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,9 @@ class File
471471
@emitter.emit 'did-delete'
472472

473473
subscribeToNativeChangeEvents: ->
474-
@watchSubscription ?= PathWatcher.watch @path, (args...) =>
475-
@handleNativeChangeEvent(args...)
474+
if not PathWatcher.isWatchedByParent @path
475+
@watchSubscription ?= PathWatcher.watch @path, (args...) =>
476+
@handleNativeChangeEvent(args...)
476477

477478
unsubscribeFromNativeChangeEvents: ->
478479
if @watchSubscription?

src/main.coffee

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ binding.setCallback (event, handle, filePath, oldFilePath) ->
1010
handleWatchers.get(handle).onEvent(event, filePath, oldFilePath) if handleWatchers.has(handle)
1111

1212
class HandleWatcher extends EventEmitter
13-
constructor: (@path) ->
13+
constructor: (@path, @options) ->
1414
@setMaxListeners(Infinity)
1515
@start()
1616

@@ -46,7 +46,7 @@ class HandleWatcher extends EventEmitter
4646
@emit('change', event, filePath, oldFilePath)
4747

4848
start: ->
49-
@handle = binding.watch(@path)
49+
@handle = binding.watch(@path, @options.recursive)
5050
if handleWatchers.has(@handle)
5151
troubleWatcher = handleWatchers.get(@handle)
5252
troubleWatcher.close()
@@ -66,21 +66,25 @@ class PathWatcher extends EventEmitter
6666
path: null
6767
handleWatcher: null
6868

69-
constructor: (filePath, callback) ->
69+
constructor: (filePath, options, callback) ->
7070
@path = filePath
71+
@isRecursive = false
7172

7273
# On Windows watching a file is emulated by watching its parent folder.
7374
if process.platform is 'win32'
7475
stats = fs.statSync(filePath)
7576
@isWatchingParent = not stats.isDirectory()
77+
@isRecursive = not @isWatchingParent and options.recursive or false
78+
else
79+
options.recursive = false
7680

7781
filePath = path.dirname(filePath) if @isWatchingParent
7882
for watcher in handleWatchers.values()
7983
if watcher.path is filePath
8084
@handleWatcher = watcher
8185
break
8286

83-
@handleWatcher ?= new HandleWatcher(filePath)
87+
@handleWatcher ?= new HandleWatcher(filePath, options)
8488

8589
@onChange = (event, newFilePath, oldFilePath) =>
8690
switch event
@@ -91,27 +95,38 @@ class PathWatcher extends EventEmitter
9195
when 'child-rename'
9296
if @isWatchingParent
9397
@onChange('rename', newFilePath) if @path is oldFilePath
98+
else if @isRecursive
99+
@onChange('change', newFilePath)
94100
else
95101
@onChange('change', '')
96102
when 'child-delete'
97103
if @isWatchingParent
98104
@onChange('delete', null) if @path is newFilePath
105+
else if @isRecursive
106+
@onChange('change', newFilePath)
99107
else
100108
@onChange('change', '')
101109
when 'child-change'
102110
@onChange('change', '') if @isWatchingParent and @path is newFilePath
103111
when 'child-create'
104-
@onChange('change', '') unless @isWatchingParent
112+
if @isRecursive
113+
@onChange('change', newFilePath)
114+
else
115+
@onChange('change', '') unless @isWatchingParent
105116

106117
@handleWatcher.on('change', @onChange)
107118

108119
close: ->
109120
@handleWatcher.removeListener('change', @onChange)
110121
@handleWatcher.closeIfNoListener()
111122

112-
exports.watch = (path, callback) ->
123+
exports.watch = (path, options, callback) ->
113124
path = require('path').resolve(path)
114-
new PathWatcher(path, callback)
125+
options = options or {}
126+
if (typeof options is 'function')
127+
callback = options
128+
options = {}
129+
new PathWatcher(path, options, callback)
115130

116131
exports.closeAllWatchers = ->
117132
watcher.close() for watcher in handleWatchers.values()
@@ -122,5 +137,12 @@ exports.getWatchedPaths = ->
122137
paths.push(watcher.path) for watcher in handleWatchers.values()
123138
paths
124139

140+
exports.isWatchedByParent = (path) ->
141+
for watcher in handleWatchers.values()
142+
watcherPath = watcher.path
143+
if path.startsWith(watcherPath) and watcherPath.options.recursive
144+
return true
145+
false
146+
125147
exports.File = require './file'
126148
exports.Directory = require './directory'

src/pathwatcher_linux.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ void PlatformThread() {
6262
}
6363
}
6464

65-
WatcherHandle PlatformWatch(const char* path) {
65+
WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint) {
6666
if (g_inotify == -1) {
6767
return -g_init_errno;
6868
}

src/pathwatcher_unix.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ void PlatformThread() {
7272
}
7373
}
7474

75-
WatcherHandle PlatformWatch(const char* path) {
75+
WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint) {
7676
if (g_kqueue == -1) {
7777
return -g_init_errno;
7878
}

src/pathwatcher_win.cc

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ struct ScopedLocker {
2929
};
3030

3131
struct HandleWrapper {
32-
HandleWrapper(WatcherHandle handle, const char* path_str)
32+
HandleWrapper(WatcherHandle handle, const char* path_str, unsigned int flags_uint)
3333
: dir_handle(handle),
3434
path(strlen(path_str)),
35-
canceled(false) {
35+
canceled(false),
36+
flags(flags_uint) {
3637
memset(&overlapped, 0, sizeof(overlapped));
3738
overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
3839
g_events.push_back(overlapped.hEvent);
@@ -61,6 +62,7 @@ struct HandleWrapper {
6162
WatcherHandle dir_handle;
6263
std::vector<char> path;
6364
bool canceled;
65+
unsigned int flags;
6466
OVERLAPPED overlapped;
6567
char buffer[kDirectoryWatcherBufferSize];
6668

@@ -82,7 +84,7 @@ static bool QueueReaddirchanges(HandleWrapper* handle) {
8284
return ReadDirectoryChangesW(handle->dir_handle,
8385
handle->buffer,
8486
kDirectoryWatcherBufferSize,
85-
FALSE,
87+
(handle->flags & FLAG_RECURSIVE) ? TRUE : FALSE,
8688
FILE_NOTIFY_CHANGE_FILE_NAME |
8789
FILE_NOTIFY_CHANGE_DIR_NAME |
8890
FILE_NOTIFY_CHANGE_ATTRIBUTES |
@@ -246,7 +248,7 @@ void PlatformThread() {
246248
}
247249
}
248250

249-
WatcherHandle PlatformWatch(const char* path) {
251+
WatcherHandle PlatformWatch(const char* path, unsigned int flags_uint) {
250252
wchar_t wpath[MAX_PATH] = { 0 };
251253
MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH);
252254

@@ -272,7 +274,7 @@ WatcherHandle PlatformWatch(const char* path) {
272274
std::unique_ptr<HandleWrapper> handle;
273275
{
274276
ScopedLocker locker(g_handle_wrap_map_mutex);
275-
handle.reset(new HandleWrapper(dir_handle, path));
277+
handle.reset(new HandleWrapper(dir_handle, path, flags_uint));
276278
}
277279

278280
if (!QueueReaddirchanges(handle.get())) {

0 commit comments

Comments
 (0)