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

Commit a8be58d

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

File tree

10 files changed

+129
-33
lines changed

10 files changed

+129
-33
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: 63 additions & 7 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 ->
@@ -51,18 +52,21 @@ describe 'PathWatcher', ->
5152

5253
describe 'when a watched path is renamed #darwin #win32', ->
5354
it 'fires the callback with the event type and new path and watches the new path', ->
54-
eventType = null
55-
eventPath = null
55+
eventType = []
56+
eventPath = []
5657
watcher = pathWatcher.watch tempFile, (type, path) ->
57-
eventType = type
58-
eventPath = path
58+
eventType.push(type)
59+
eventPath.push(path)
5960

6061
tempRenamed = path.join(tempDir, 'renamed')
6162
fs.renameSync(tempFile, tempRenamed)
62-
waitsFor -> eventType?
63+
waitsFor -> eventType[0]?
6364
runs ->
64-
expect(eventType).toBe 'rename'
65-
expect(fs.realpathSync(eventPath)).toBe fs.realpathSync(tempRenamed)
65+
expect(eventType[0]).toBe 'rename'
66+
expect(fs.realpathSync(eventPath[0])).toBe fs.realpathSync(tempRenamed)
67+
if process.platform is 'win32'
68+
expect(eventType[1]).toBe 'change'
69+
expect(eventPath[1]).toBe ''
6670
expect(pathWatcher.getWatchedPaths()).toEqual [watcher.handleWatcher.path]
6771

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