diff --git a/.gitignore b/.gitignore
index 2ccbe46..32869d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
-/node_modules/
+node_modules
+experiments
+build
+bin/*-v8-*
diff --git a/.travis.yml b/.travis.yml
index 049285e..d599e98 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,16 @@
language: node_js
+sudo: false
+before_install:
+ # a hack lifted from https://github.com/redis/hiredis-node to fix iojs headers
+ - node --version | grep -q 'v0.8' && npm install -g npm@latest || true
node_js:
- '0.8'
- '0.10'
-before_script:
- - npm install -g grunt-cli
+ - "0.12"
+ - "iojs"
+install:
+ - npm install
+os:
+ - linux
+ - osx
+ - windows
diff --git a/AUTHORS b/AUTHORS
index a16dcb3..11924e9 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -8,3 +8,12 @@ Chris Chua (http://sirh.cc/)
Kael Zhang (http://kael.me)
Krasimir Tsonev (http://krasimirtsonev.com/blog)
brett-shwom
+Kai Groner
+zeripath
+Tim Schaub (http://tschaub.net/)
+Amjad Masad (http://amasad.github.com/)
+Eric Schoffstall (http://contra.io/)
+Eric O'Connor (http://oco.nnor.org/)
+Cheng Zhao (https://github.com/zcbenz)
+Kevin Sawicki (https://github.com/kevinsawicki)
+Nathan Sobo (https://github.com/nathansobo)
diff --git a/Gruntfile.js b/Gruntfile.js
index a2d3378..521b9a9 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,28 +1,15 @@
module.exports = function(grunt) {
'use strict';
+
+ grunt.option('stack', true);
+
grunt.initConfig({
- benchmark: {
- all: {
- src: ['benchmarks/*.js'],
- options: { times: 10 }
- }
- },
nodeunit: {
files: ['test/*_test.js'],
},
jshint: {
- options: {
- jshintrc: '.jshintrc'
- },
- gruntfile: {
- src: 'Gruntfile.js'
- },
- lib: {
- src: ['lib/**/*.js']
- },
- test: {
- src: ['test/**/*_test.js']
- },
+ options: { jshintrc: true },
+ all: ['Gruntfile.js', 'lib/**/*.js', 'test/*.js', 'benchmarks/*.js', '!lib/pathwatcher.js'],
},
});
@@ -32,7 +19,6 @@ module.exports = function(grunt) {
grunt.task.run('nodeunit');
});
- grunt.loadNpmTasks('grunt-benchmark');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-nodeunit');
grunt.registerTask('default', ['jshint', 'nodeunit']);
diff --git a/LICENSE-MIT b/LICENSE-MIT
index 8c1a833..6ca5f2c 100644
--- a/LICENSE-MIT
+++ b/LICENSE-MIT
@@ -1,4 +1,4 @@
-Copyright (c) 2013 Kyle Robinson Young
+Copyright (c) 2015 Kyle Robinson Young
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
diff --git a/README.md b/README.md
index 41c10e8..fb76fec 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,22 @@
-# gaze [](https://travis-ci.org/shama/gaze)
+# gaze [](https://travis-ci.org/shama/gaze) [](https://ci.appveyor.com/project/shama/gaze)
A globbing fs.watch wrapper built from the best parts of other fine watch libs.
Compatible with Node.js 0.10/0.8, Windows, OSX and Linux.

+## Features
+
+[](https://nodei.co/npm/gaze/)
+
+* Consistent events on OSX, Linux and Windows
+* Very fast start up and response time
+* High test coverage and well vetted
+* Uses native OS events but falls back to stat polling
+* Option to force stat polling with special file systems such as networked
+* Downloaded over 1M times a month
+* Used by [Grunt](http://gruntjs.com), [gulp](http://gulpjs.com), [Tower](http://tower.github.io/) and many others
+
## Usage
Install the module with: `npm install gaze` or place into your `package.json`
and run `npm install`.
@@ -18,7 +30,9 @@ gaze('**/*.js', function(err, watcher) {
// watcher === this
// Get all watched files
- console.log(this.watched());
+ this.watched(function(err, watched) {
+ console.log(watched);
+ });
// On file changed
this.on('changed', function(filepath) {
@@ -41,7 +55,9 @@ gaze('**/*.js', function(err, watcher) {
});
// Get watched files with relative paths
- console.log(this.relative());
+ this.relative(function(err, files) {
+ console.log(files);
+ });
});
// Also accepts an array of patterns
@@ -68,13 +84,28 @@ gaze.on('all', function(event, filepath) { });
### Errors
```javascript
-gaze('**/*', function() {
- this.on('error', function(err) {
- // Handle error here
- });
+gaze('**/*', function(error, watcher) {
+ if (error) {
+ // Handle error if it occurred while starting up
+ }
+});
+
+// Or with the alternative interface
+var gaze = new Gaze();
+gaze.on('error', function(error) {
+ // Handle error here
});
+gaze.add('**/*');
```
+#### `EMFILE` errors
+
+By default, gaze will use native OS events and then fallback to slower stat polling when an `EMFILE` error is reached. Gaze will still emit or return the error as the first argument of the ready callback for you to handle.
+
+It is recommended to advise your users to increase their file descriptor limits to utilize the faster native OS watching. Especially on OSX where the default descriptor limit is 256.
+
+In some cases, native OS events will not work. Such as with networked file systems or vagrant. It is recommended to set the option `mode: 'poll'` to always stat poll for those situations.
+
### Minimatch / Glob
See [isaacs's minimatch](https://github.com/isaacs/minimatch) for more
@@ -82,7 +113,7 @@ information on glob patterns.
## Documentation
-### gaze(patterns, [options], callback)
+### gaze([patterns, options, callback])
* `patterns` {String|Array} File patterns to be matched
* `options` {Object}
@@ -92,7 +123,7 @@ information on glob patterns.
### Class: gaze.Gaze
-Create a Gaze object by instanting the `gaze.Gaze` class.
+Create a Gaze object by instancing the `gaze.Gaze` class.
```javascript
var Gaze = require('gaze').Gaze;
@@ -105,6 +136,8 @@ var gaze = new Gaze(pattern, options, callback);
* `interval` {integer} Interval to pass to fs.watchFile
* `debounceDelay` {integer} Delay for events called in succession for the same
file/event
+ * `mode` {string} Force the watch mode. Either `'auto'` (default), `'watch'` (force native events), or `'poll'` (force stat polling).
+ * `cwd` {string} The current working directory to base file patterns from. Default is `process.cwd()`.
#### Events
@@ -126,26 +159,26 @@ var gaze = new Gaze(pattern, options, callback);
* `add(patterns, callback)` Adds file(s) patterns to be watched.
* `remove(filepath)` removes a file or directory from being watched. Does not
recurse directories.
-* `watched()` Returns the currently watched files.
-* `relative([dir, unixify])` Returns the currently watched files with relative paths.
+* `watched([callback])` Returns the currently watched files.
+ * `callback` {function} Calls with `function(err, files)`.
+* `relative([dir, unixify, callback])` Returns the currently watched files with relative paths.
* `dir` {string} Only return relative files for this directory.
* `unixify` {boolean} Return paths with `/` instead of `\\` if on Windows.
+ * `callback` {function} Calls with `function(err, files)`.
## FAQs
### Why Another `fs.watch` Wrapper?
I liked parts of other `fs.watch` wrappers but none had all the features I
-needed. This lib combines the features I needed from other fine watch libs:
-Speedy data behavior from
-[paulmillr's chokidar](https://github.com/paulmillr/chokidar), API interface
-from [mikeal's watch](https://github.com/mikeal/watch) and file globbing using
-[isaacs's glob](https://github.com/isaacs/node-glob) which is also used by
-[cowboy's Grunt](https://github.com/gruntjs/grunt).
-
-### How do I fix the error `EMFILE: Too many opened files.`?
-This is because of your system's max opened file limit. For OSX the default is
-very low (256). Increase your limit temporarily with `ulimit -n 10480`, the
-number being the new max limit.
+needed when this library was originally written. This lib once combined the features I needed from other fine watch libs
+but now has taken on a life of it's own (**gaze doesn't wrap `fs.watch` or `fs.watchFile` anymore**).
+
+Other great watch libraries to try are:
+
+* [paulmillr's chokidar](https://github.com/paulmillr/chokidar)
+* [mikeal's watch](https://github.com/mikeal/watch)
+* [github's pathwatcher](https://github.com/atom/node-pathwatcher)
+* [bevry's watchr](https://github.com/bevry/watchr)
## Contributing
In lieu of a formal styleguide, take care to maintain the existing coding style.
@@ -153,6 +186,11 @@ Add unit tests for any new or changed functionality. Lint and test your code
using [grunt](http://gruntjs.com/).
## Release History
+* 0.6.4 - Catch and emit error from readdir (@oconnore). Fix for 0 maxListeners. Use graceful-fs to avoid EMFILE errors in other places fs is used. Better method to determine if pathwatcher was built. Fix keeping process alive too much, only init pathwatcher if a file is being watched. Set min required to Windows Vista when building on Windows (@pvolok).
+* 0.6.3 - Add support for node v0.11
+* 0.6.2 - Fix argument error with watched(). Fix for erroneous added events on folders. Ignore msvs build error 4244.
+* 0.6.1 - Fix for absolute paths.
+* 0.6.0 - Uses native OS events (fork of pathwatcher) but can fall back to stat polling. Everything is async to avoid blocking, including `relative()` and `watched()`. Better error handling. Update to globule@0.2.0. No longer watches `cwd` by default. Added `mode` option. Better `EMFILE` message. Avoids `ENOENT` errors with symlinks. All constructor arguments are optional.
* 0.5.1 - Use setImmediate (process.nextTick for node v0.8) to defer ready/nomatch events (@amasad).
* 0.5.0 - Process is now kept alive while watching files. Emits a nomatch event when no files are matching.
* 0.4.3 - Track file additions in newly created folders (@brett-shwom).
@@ -176,5 +214,5 @@ using [grunt](http://gruntjs.com/).
* 0.1.0 - Initial release
## License
-Copyright (c) 2013 Kyle Robinson Young
+Copyright (c) 2015 Kyle Robinson Young
Licensed under the MIT license.
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..f3da96d
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,28 @@
+# appveyor file
+# http://www.appveyor.com/docs/appveyor-yml
+
+# branches to build
+branches:
+ # whitelist
+ only:
+ - master
+
+# build version format
+version: "{build}"
+
+# what combinations to test
+environment:
+ matrix:
+ - nodejs_version: 0.10
+
+# Get the latest stable version of Node 0.STABLE.latest
+install:
+ - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version)
+ - npm install
+
+build: off
+
+test_script:
+ - node --version
+ - npm --version
+ - cmd: npm test
diff --git a/benchmarks/benchmarker.js b/benchmarks/benchmarker.js
new file mode 100644
index 0000000..d0c701e
--- /dev/null
+++ b/benchmarks/benchmarker.js
@@ -0,0 +1,72 @@
+'use strict';
+
+var async = require('async');
+var fs = require('fs');
+var rimraf = require('rimraf');
+var path = require('path');
+var AsciiTable = require('ascii-table');
+var readline = require('readline');
+
+function Benchmarker(opts) {
+ if (!(this instanceof Benchmarker)) {
+ return new Benchmarker(opts);
+ }
+ opts = opts || {};
+ this.table = new AsciiTable(opts.name || 'benchmark');
+ this.tmpDir = opts.tmpDir || path.resolve(__dirname, 'tmp');
+ var max = opts.max || 2000;
+ var multiplesOf = opts.multiplesOf || 100;
+ this.fileNums = [];
+ for (var i = 0; i <= max / multiplesOf; i++) {
+ this.fileNums.push(i * multiplesOf);
+ }
+ this.startTime = 0;
+ this.files = [];
+}
+module.exports = Benchmarker;
+
+Benchmarker.prototype.log = function() {
+ readline.cursorTo(process.stdout, 0, 0);
+ readline.clearScreenDown(process.stdout);
+ this.table.addRow.apply(this.table, arguments);
+ console.log(this.table.toString());
+};
+
+Benchmarker.prototype.setup = function(num) {
+ this.teardown();
+ fs.mkdirSync(this.tmpDir);
+ this.files = [];
+ for (var i = 0; i <= num; i++) {
+ var file = path.join(this.tmpDir, 'test-' + i + '.txt');
+ fs.writeFileSync(file, String(i));
+ this.files.push(file);
+ }
+};
+
+Benchmarker.prototype.teardown = function() {
+ if (fs.existsSync(this.tmpDir)) {
+ rimraf.sync(this.tmpDir);
+ }
+};
+
+Benchmarker.prototype.run = function(fn, done) {
+ var self = this;
+ async.eachSeries(this.fileNums, function(num, next) {
+ self.setup(num);
+ fn(num, function() {
+ process.nextTick(next);
+ });
+ }, function() {
+ self.teardown();
+ done();
+ });
+};
+
+Benchmarker.prototype.start = function() {
+ this.startTime = process.hrtime();
+};
+
+Benchmarker.prototype.end = function(radix) {
+ var diff = process.hrtime(this.startTime);
+ return ((diff[0] * 1e9 + diff[1]) * 0.000001).toFixed(radix || 2).replace(/\d(?=(\d{3})+\.)/g, '$&,') + 'ms';
+};
diff --git a/benchmarks/changed.js b/benchmarks/changed.js
new file mode 100644
index 0000000..4a84d9d
--- /dev/null
+++ b/benchmarks/changed.js
@@ -0,0 +1,27 @@
+'use strict';
+
+var gaze = require('../');
+var path = require('path');
+var fs = require('fs');
+var Benchmarker = require('./benchmarker');
+
+var b = new Benchmarker({ name: path.basename(__filename) });
+b.table.setHeading('files', 'ms').setAlign(0, 2).setAlign(1, 2);
+b.run(function(num, done) {
+ gaze('**/*', {cwd: b.tmpDir, maxListeners:0}, function(err, watcher) {
+ if (err) {
+ console.error(err.code + ': ' + err.message);
+ return process.exit();
+ }
+ watcher.on('changed', function() {
+ b.log(num, b.end());
+ watcher.close();
+ });
+ watcher.on('end', done);
+ var randFile = path.join(b.tmpDir, 'test-' + Math.floor(Math.random() * num) + '.txt');
+ b.start();
+ fs.writeFileSync(randFile, '1234');
+ });
+}, function() {
+ process.exit();
+});
diff --git a/benchmarks/gaze100s.js b/benchmarks/gaze100s.js
deleted file mode 100644
index 1ada219..0000000
--- a/benchmarks/gaze100s.js
+++ /dev/null
@@ -1,46 +0,0 @@
-'use strict';
-
-var gaze = require('../lib/gaze');
-var grunt = require('grunt');
-var path = require('path');
-
-// Folder to watch
-var watchDir = path.resolve(__dirname, 'watch');
-
-// Helper for creating mock files
-function createFiles(num, dir) {
- for (var i = 0; i < num; i++) {
- grunt.file.write(path.join(dir, 'test-' + i + '.js'), 'var test = ' + i + ';');
- }
-}
-
-module.exports = {
- 'setUp': function(done) {
- // ensure that your `ulimit -n` is higher than amount of files
- if (grunt.file.exists(watchDir)) {
- grunt.file.delete(watchDir, {force:true});
- }
- createFiles(100, path.join(watchDir, 'one'));
- createFiles(100, path.join(watchDir, 'two'));
- createFiles(100, path.join(watchDir, 'three'));
- createFiles(100, path.join(watchDir, 'three', 'four'));
- createFiles(100, path.join(watchDir, 'three', 'four', 'five', 'six'));
- process.chdir(watchDir);
- done();
- },
- 'tearDown': function(done) {
- if (grunt.file.exists(watchDir)) {
- grunt.file.delete(watchDir, {force:true});
- }
- done();
- },
- changed: function(done) {
- gaze('**/*', {maxListeners:0}, function(err, watcher) {
- this.on('changed', done);
- setTimeout(function() {
- var rand = String(new Date().getTime()).replace(/[^\w]+/g, '');
- grunt.file.write(path.join(watchDir, 'one', 'test-99.js'), 'var test = "' + rand + '"');
- }, 100);
- });
- }
-};
\ No newline at end of file
diff --git a/benchmarks/relative.js b/benchmarks/relative.js
new file mode 100644
index 0000000..fdf49a5
--- /dev/null
+++ b/benchmarks/relative.js
@@ -0,0 +1,24 @@
+'use strict';
+
+var gaze = require('../');
+var path = require('path');
+var Benchmarker = require('./benchmarker');
+
+var b = new Benchmarker({ name: path.basename(__filename) });
+b.table.setHeading('files', 'ms').setAlign(0, 2).setAlign(1, 2);
+b.run(function(num, done) {
+ gaze('**/*', {cwd: b.tmpDir, maxListeners:0}, function(err, watcher) {
+ if (err) {
+ console.error(err.code + ': ' + err.message);
+ return process.exit();
+ }
+ b.start();
+ this.relative('.', function(err, files) {
+ b.log(num, b.end());
+ watcher.on('end', done);
+ watcher.close();
+ });
+ });
+}, function() {
+ process.exit();
+});
diff --git a/benchmarks/startup.js b/benchmarks/startup.js
index 2dd22c2..ddbe346 100644
--- a/benchmarks/startup.js
+++ b/benchmarks/startup.js
@@ -1,71 +1,22 @@
'use strict';
-var gaze = require('../lib/gaze');
-var async = require('async');
-var fs = require('fs');
-var rimraf = require('rimraf');
+var gaze = require('../');
var path = require('path');
-
-// Folder to watch
-var watchDir = path.resolve(__dirname, 'watch');
-var multiplesOf = 100;
-var max = 2000;
-var numFiles = [];
-for (var i = 0; i <= max/multiplesOf; i++) {
- numFiles.push(i*multiplesOf);
-}
-
-var modFile = path.join(watchDir, 'test-'+numFiles+'.txt');
-
-// Helper for creating mock files
-function createFiles(num, dir) {
- for (var i = 0; i <= num; i++) {
- fs.writeFileSync(path.join(dir, 'test-' + i + '.txt'), String(i));
- }
-}
-
-function teardown(){
- if (fs.existsSync(watchDir)) {
- rimraf.sync(watchDir);
- }
-}
-
-function setup(num){
- teardown();
- fs.mkdirSync(watchDir);
- createFiles(num, watchDir);
-}
-
-function measureStart(cb) {
- var start = Date.now();
- var blocked, ready, watcher;
- // workaround #77
- var check = function(){
- if (ready && blocked) {
- cb(ready, blocked, watcher);
+var Benchmarker = require('./benchmarker');
+
+var b = new Benchmarker({ name: path.basename(__filename) });
+b.table.setHeading('files', 'ms').setAlign(0, 2).setAlign(1, 2);
+b.run(function(num, done) {
+ b.start();
+ gaze('**/*', {cwd: b.tmpDir, maxListeners:0}, function(err, watcher) {
+ if (err) {
+ console.error(err.code + ': ' + err.message);
+ return process.exit();
}
- };
- gaze(watchDir+'/**/*', {maxListeners:0}, function(err) {
- ready = Date.now()-start;
- watcher = this;
- check();
- });
- blocked = Date.now()-start;
- check();
-}
-
-function bench(num, cb) {
- setup(num);
- measureStart(function(time, blocked, watcher){
- console.log(num, time);
+ b.log(num, b.end());
+ watcher.on('end', done);
watcher.close();
- cb();
});
-}
-
-console.log('numFiles startTime');
-async.eachSeries(numFiles, bench, function(){
- teardown();
- console.log('done!');
+}, function() {
process.exit();
-});
\ No newline at end of file
+});
diff --git a/binding.gyp b/binding.gyp
new file mode 100644
index 0000000..08ebb1f
--- /dev/null
+++ b/binding.gyp
@@ -0,0 +1,52 @@
+{
+ "targets": [
+ {
+ "target_name": "pathwatcher",
+ "sources": [
+ "src/main.cc",
+ "src/common.cc",
+ "src/common.h",
+ "src/handle_map.cc",
+ "src/handle_map.h",
+ "src/unsafe_persistent.h",
+ ],
+ "include_dirs": [
+ "src",
+ ' 1) {
+ return self.removeListener('error', defaultError);
+ }
+ nextback(function() {
+ done.call(self, error, self);
+ })();
+ }
+ this.on('error', defaultError);
+
+ // File watching mode to use when adding files to the platform
+ this._mode = opts.mode || 'auto';
+
// Default done callback
done = done || function() {};
@@ -63,7 +81,7 @@ function Gaze(patterns, opts, done) {
this._cached = Object.create(null);
// Set maxListeners
- if (this.options.maxListeners) {
+ if (this.options.maxListeners != null) {
this.setMaxListeners(this.options.maxListeners);
Gaze.super_.prototype.setMaxListeners(this.options.maxListeners);
delete this.options.maxListeners;
@@ -75,7 +93,16 @@ function Gaze(patterns, opts, done) {
}
// keep the process alive
- this._keepalive = setInterval(function() {}, 200);
+ this._keepalive = setInterval(platform.tick.bind(platform), opts.interval);
+
+ // Keep track of all instances created
+ this._instanceNum = instances.length;
+ instances.push(this);
+
+ // Keep track of safewriting and debounce timeouts
+ this._safewriting = null;
+ this._safewriteTimeout = null;
+ this._timeoutId = null;
return this;
}
@@ -90,12 +117,15 @@ module.exports.Gaze = Gaze;
// Override the emit function to emit `all` events
// and debounce on duplicate events per file
Gaze.prototype.emit = function() {
+ if (!this._emitEvents) {
+ return;
+ }
+
var self = this;
var args = arguments;
var e = args[0];
var filepath = args[1];
- var timeoutId;
// If not added/deleted/changed/renamed then just emit the event
if (e.slice(-2) !== 'ed') {
@@ -115,18 +145,48 @@ Gaze.prototype.emit = function() {
});
}
+ // Detect safewrite events, if file is deleted and then added/renamed, assume a safewrite happened
+ if (e === 'deleted' && this._safewriting == null) {
+ this._safewriting = filepath;
+ this._safewriteTimeout = setTimeout(function() {
+ // Just a normal delete, carry on
+ Gaze.super_.prototype.emit.apply(self, args);
+ Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
+ self._safewriting = null;
+ }, this.options.debounceDelay);
+ return this;
+ } else if ((e === 'added' || e === 'renamed') && this._safewriting === filepath) {
+ clearTimeout(this._safewriteTimeout);
+ this._safewriteTimeout = setTimeout(function() {
+ self._safewriting = null;
+ }, this.options.debounceDelay);
+ args[0] = e = 'changed';
+ } else if (e === 'deleted' && this._safewriting === filepath) {
+ return this;
+ }
+
// If cached doesnt exist, create a delay before running the next
// then emit the event
var cache = this._cached[filepath] || [];
+ var options = this.options;
+ var emitEvent = function (args, e) {
+ Gaze.super_.prototype.emit.apply(self, args);
+ Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
+ }
+
if (cache.indexOf(e) === -1) {
helper.objectPush(self._cached, filepath, e);
- clearTimeout(timeoutId);
- timeoutId = setTimeout(function() {
+ clearTimeout(this._timeoutId);
+ this._timeoutId = setTimeout(function() {
+ if (options.debounceLeading) {
+ emitEvent(args, e);
+ }
delete self._cached[filepath];
}, this.options.debounceDelay);
// Emit the event and `all` event
- Gaze.super_.prototype.emit.apply(self, args);
- Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
+ if (!this.options.debounceLeading) {
+ emitEvent(args, e);
+ }
}
// Detect if new folder added to trigger for matching files within folder
@@ -147,293 +207,186 @@ Gaze.prototype.emit = function() {
// Close watchers
Gaze.prototype.close = function(_reset) {
- var self = this;
- _reset = _reset === false ? false : true;
- Object.keys(self._watchers).forEach(function(file) {
- self._watchers[file].close();
- });
- self._watchers = Object.create(null);
- Object.keys(this._watched).forEach(function(dir) {
- self._unpollDir(dir);
- });
- if (_reset) {
- self._watched = Object.create(null);
- setTimeout(function() {
- self.emit('end');
- self.removeAllListeners();
- clearInterval(self._keepalive);
- }, delay + 100);
- }
- return self;
+ instances.splice(this._instanceNum, 1);
+ platform.closeAll();
+ this.emit('end');
+ this._emitEvents = false;
};
// Add file patterns to be watched
Gaze.prototype.add = function(files, done) {
+ var self = this;
if (typeof files === 'string') { files = [files]; }
this._patterns = helper.unique.apply(null, [this._patterns, files]);
files = globule.find(this._patterns, this.options);
- this._addToWatched(files);
- this.close(false);
- this._initWatched(done);
-};
-// Dont increment patterns and dont call done if nothing added
-Gaze.prototype._internalAdd = function(file, done) {
- var files = [];
- if (helper.isDir(file)) {
- files = [helper.markDir(file)].concat(globule.find(this._patterns, this.options));
- } else {
- if (globule.isMatch(this._patterns, file, this.options)) {
- files = [file];
- }
- }
- if (files.length > 0) {
- this._addToWatched(files);
- this.close(false);
- this._initWatched(done);
+ // If no matching files
+ if (files.length < 1) {
+ // Defer to emitting to give a chance to attach event handlers.
+ nextback(function() {
+ self.emit('ready', self);
+ if (done) {
+ done.call(self, null, self);
+ }
+ self.emit('nomatch');
+ })();
+ return;
}
-};
-// Remove file/dir from `watched`
-Gaze.prototype.remove = function(file) {
- var self = this;
- if (this._watched[file]) {
- // is dir, remove all files
- this._unpollDir(file);
- delete this._watched[file];
- } else {
- // is a file, find and remove
- Object.keys(this._watched).forEach(function(dir) {
- var index = self._watched[dir].indexOf(file);
- if (index !== -1) {
- self._unpollFile(file);
- self._watched[dir].splice(index, 1);
- return false;
+ // Set the platform mode before adding files
+ platform.mode = self._mode;
+
+ helper.forEachSeries(files, function(file, next) {
+ try {
+ var filepath = (!isAbsolute(file)) ? path.join(self.options.cwd, file) : file;
+ platform(filepath, self._trigger.bind(self));
+ } catch (err) {
+ self.emit('error', err);
+ }
+ next();
+ }, function() {
+ // A little delay here for backwards compatibility, lol
+ setTimeout(function() {
+ self.emit('ready', self);
+ if (done) {
+ done.call(self, null, self);
}
- });
- }
- if (this._watchers[file]) {
- this._watchers[file].close();
- }
- return this;
+ }, 10);
+ });
};
-// Return watched files
-Gaze.prototype.watched = function() {
- return this._watched;
+// Call when the platform has triggered
+Gaze.prototype._trigger = function(error, event, filepath, newFile) {
+ if (error) { return this.emit('error', error); }
+
+ // Set the platform mode before adding files
+ platform.mode = this._mode;
+
+ if (event === 'change' && helper.isDir(filepath)) {
+ this._wasAdded(filepath);
+ } else if (event === 'change') {
+ this._emitAll('changed', filepath);
+ } else if (event === 'delete') {
+ // Close out deleted filepaths (important to make safewrite detection work)
+ platform.close(filepath);
+ this._emitAll('deleted', filepath);
+ } else if (event === 'rename') {
+ // TODO: This occasionally throws, figure out why or use the old style rename detect
+ // The handle(26) returned by watching [filename] is the same with an already watched path([filename])
+ this._emitAll('renamed', newFile, filepath);
+ }
};
-// Returns `watched` files with relative paths to process.cwd()
-Gaze.prototype.relative = function(dir, unixify) {
+// If a folder received a change event, investigate
+Gaze.prototype._wasAdded = function(dir) {
var self = this;
- var relative = Object.create(null);
- var relDir, relFile, unixRelDir;
- var cwd = this.options.cwd || process.cwd();
- if (dir === '') { dir = '.'; }
- dir = helper.markDir(dir);
- unixify = unixify || false;
- Object.keys(this._watched).forEach(function(dir) {
- relDir = path.relative(cwd, dir) + path.sep;
- if (relDir === path.sep) { relDir = '.'; }
- unixRelDir = unixify ? helper.unixifyPathSep(relDir) : relDir;
- relative[unixRelDir] = self._watched[dir].map(function(file) {
- relFile = path.relative(path.join(cwd, relDir) || '', file || '');
- if (helper.isDir(file)) {
- relFile = helper.markDir(relFile);
+ try {
+ var dirstat = fs.lstatSync(dir);
+ } catch (err) {
+ self.emit('error', err);
+ return;
+ }
+ fs.readdir(dir, function(err, current) {
+ if (err) {
+ self.emit('error', err);
+ return;
+ }
+ helper.forEachSeries(current, function(file, next) {
+ var filepath = path.join(dir, file);
+ var stat;
+ try {
+ stat = fs.lstatSync(filepath);
}
- if (unixify) {
- relFile = helper.unixifyPathSep(relFile);
+ catch (err) {
+ if (err.code === 'ENOENT') return next();
+ throw err;
}
- return relFile;
+ if ((dirstat.mtime - stat.mtime) <= 0) {
+ var relpath = path.relative(self.options.cwd, filepath);
+ if (stat.isDirectory()) {
+ // If it was a dir, watch the dir and emit that it was added
+ platform(filepath, self._trigger.bind(self));
+ if (globule.isMatch(self._patterns, relpath, self.options)) {
+ self._emitAll('added', filepath);
+ }
+ self._wasAddedSub(filepath);
+ } else if (globule.isMatch(self._patterns, relpath, self.options)) {
+ // Otherwise if the file matches, emit added
+ platform(filepath, self._trigger.bind(self));
+ if (globule.isMatch(self._patterns, relpath, self.options)) {
+ self._emitAll('added', filepath);
+ }
+ }
+ }
+ next();
});
});
- if (dir && unixify) {
- dir = helper.unixifyPathSep(dir);
- }
- return dir ? relative[dir] || [] : relative;
};
-// Adds files and dirs to watched
-Gaze.prototype._addToWatched = function(files) {
- for (var i = 0; i < files.length; i++) {
- var file = files[i];
- var filepath = path.resolve(this.options.cwd, file);
-
- var dirname = (helper.isDir(file)) ? filepath : path.dirname(filepath);
- dirname = helper.markDir(dirname);
-
- // If a new dir is added
- if (helper.isDir(file) && !(filepath in this._watched)) {
- helper.objectPush(this._watched, filepath, []);
- }
-
- if (file.slice(-1) === '/') { filepath += path.sep; }
- helper.objectPush(this._watched, path.dirname(filepath) + path.sep, filepath);
-
- // add folders into the mix
- var readdir = fs.readdirSync(dirname);
- for (var j = 0; j < readdir.length; j++) {
- var dirfile = path.join(dirname, readdir[j]);
- if (fs.statSync(dirfile).isDirectory()) {
- helper.objectPush(this._watched, dirname, dirfile + path.sep);
- }
- }
- }
- return this;
-};
-
-Gaze.prototype._watchDir = function(dir, done) {
+// If a sub folder was added, investigate further
+// Such as with grunt.file.write('new_dir/tmp.js', '') as it will create the folder and file simultaneously
+Gaze.prototype._wasAddedSub = function(dir) {
var self = this;
- var timeoutId;
- try {
- this._watchers[dir] = fs.watch(dir, function(event) {
- // race condition. Let's give the fs a little time to settle down. so we
- // don't fire events on non existent files.
- clearTimeout(timeoutId);
- timeoutId = setTimeout(function() {
- // race condition. Ensure that this directory is still being watched
- // before continuing.
- if ((dir in self._watchers) && fs.existsSync(dir)) {
- done(null, dir);
+ fs.readdir(dir, function(err, current) {
+ helper.forEachSeries(current, function(file, next) {
+ var filepath = path.join(dir, file);
+ var relpath = path.relative(self.options.cwd, filepath);
+ try {
+ if (fs.lstatSync(filepath).isDirectory()) {
+ self._wasAdded(filepath);
+ } else if (globule.isMatch(self._patterns, relpath, self.options)) {
+ // Make sure to watch the newly added sub file
+ platform(filepath, self._trigger.bind(self));
+ self._emitAll('added', filepath);
}
- }, delay + 100);
+ } catch (err) {
+ self.emit('error', err);
+ }
+ next();
});
- } catch (err) {
- return this._handleError(err);
- }
- return this;
+ });
};
-Gaze.prototype._unpollFile = function(file) {
- if (this._pollers[file]) {
- fs.unwatchFile(file, this._pollers[file] );
- delete this._pollers[file];
- }
- return this;
+// Wrapper for emit to ensure we emit on all instances
+Gaze.prototype._emitAll = function() {
+ var args = Array.prototype.slice.call(arguments);
+ return this.emit.apply(this, args);
};
-Gaze.prototype._unpollDir = function(dir) {
- this._unpollFile(dir);
- for (var i = 0; i < this._watched[dir].length; i++) {
- this._unpollFile(this._watched[dir][i]);
- }
+// Remove file/dir from `watched`
+Gaze.prototype.remove = function(file) {
+ platform.close(file);
+ return this;
};
-Gaze.prototype._pollFile = function(file, done) {
- var opts = { persistent: true, interval: this.options.interval };
- if (!this._pollers[file]) {
- this._pollers[file] = function(curr, prev) {
- done(null, file);
- };
- try {
- fs.watchFile(file, opts, this._pollers[file]);
- } catch (err) {
- return this._handleError(err);
- }
- }
+// Return watched files
+Gaze.prototype.watched = function(done) {
+ done = nextback(done || function() {});
+ helper.flatToTree(platform.getWatchedPaths(), this.options.cwd, false, false, done);
return this;
};
-// Initialize the actual watch on `watched` files
-Gaze.prototype._initWatched = function(done) {
- var self = this;
- var cwd = this.options.cwd || process.cwd();
- var curWatched = Object.keys(self._watched);
-
- // if no matching files
- if (curWatched.length < 1) {
- // Defer to emitting to give a chance to attach event handlers.
- setImmediate(function () {
- self.emit('ready', self);
- if (done) { done.call(self, null, self); }
- self.emit('nomatch');
- });
- return;
+// Returns `watched` files with relative paths to cwd
+Gaze.prototype.relative = function(dir, unixify, done) {
+ if (typeof dir === 'function') {
+ done = dir;
+ dir = null;
+ unixify = false;
}
-
- helper.forEachSeries(curWatched, function(dir, next) {
- dir = dir || '';
- var files = self._watched[dir];
- // Triggered when a watched dir has an event
- self._watchDir(dir, function(event, dirpath) {
- var relDir = cwd === dir ? '.' : path.relative(cwd, dir);
- relDir = relDir || '';
-
- fs.readdir(dirpath, function(err, current) {
- if (err) { return self.emit('error', err); }
- if (!current) { return; }
-
- try {
- // append path.sep to directories so they match previous.
- current = current.map(function(curPath) {
- if (fs.existsSync(path.join(dir, curPath)) && fs.statSync(path.join(dir, curPath)).isDirectory()) {
- return curPath + path.sep;
- } else {
- return curPath;
- }
- });
- } catch (err) {
- // race condition-- sometimes the file no longer exists
- }
-
- // Get watched files for this dir
- var previous = self.relative(relDir);
-
- // If file was deleted
- previous.filter(function(file) {
- return current.indexOf(file) < 0;
- }).forEach(function(file) {
- if (!helper.isDir(file)) {
- var filepath = path.join(dir, file);
- self.remove(filepath);
- self.emit('deleted', filepath);
- }
- });
-
- // If file was added
- current.filter(function(file) {
- return previous.indexOf(file) < 0;
- }).forEach(function(file) {
- // Is it a matching pattern?
- var relFile = path.join(relDir, file);
- // Add to watch then emit event
- self._internalAdd(relFile, function() {
- self.emit('added', path.join(dir, file));
- });
- });
-
- });
- });
-
- // Watch for change/rename events on files
- files.forEach(function(file) {
- if (helper.isDir(file)) { return; }
- self._pollFile(file, function(err, filepath) {
- // Only emit changed if the file still exists
- // Prevents changed/deleted duplicate events
- if (fs.existsSync(filepath)) {
- self.emit('changed', filepath);
- }
- });
- });
-
- next();
- }, function() {
-
- // Return this instance of Gaze
- // delay before ready solves a lot of issues
- setTimeout(function() {
- self.emit('ready', self);
- if (done) { done.call(self, null, self); }
- }, delay + 100);
-
- });
-};
-
-// If an error, handle it here
-Gaze.prototype._handleError = function(err) {
- if (err.code === 'EMFILE') {
- return this.emit('error', new Error('EMFILE: Too many opened files.'));
+ if (typeof unixify === 'function') {
+ done = unixify;
+ unixify = false;
}
- return this.emit('error', err);
+ done = nextback(done || function() {});
+ helper.flatToTree(platform.getWatchedPaths(), this.options.cwd, true, unixify, function(err, relative) {
+ if (dir) {
+ if (unixify) {
+ dir = helper.unixifyPathSep(dir);
+ }
+ // Better guess what to return for backwards compatibility
+ return done(null, relative[dir] || relative[dir + (unixify ? '/' : path.sep)] || []);
+ }
+ return done(null, relative);
+ });
+ return this;
};
diff --git a/lib/gaze04.js b/lib/gaze04.js
new file mode 100644
index 0000000..64ae83e
--- /dev/null
+++ b/lib/gaze04.js
@@ -0,0 +1,467 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2013 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+// libs
+var util = require('util');
+var EE = require('events').EventEmitter;
+var fs = require('graceful-fs');
+var path = require('path');
+var globule = require('globule');
+var helper = require('./helper');
+var nextback = require('nextback');
+
+// globals
+var delay = 10;
+
+// `Gaze` EventEmitter object to return in the callback
+function Gaze(patterns, opts, done) {
+ var self = this;
+ EE.call(self);
+
+ // Optional arguments
+ if (typeof patterns === 'function') {
+ done = patterns;
+ patterns = null;
+ opts = {};
+ }
+ if (typeof opts === 'function') {
+ done = opts;
+ opts = {};
+ }
+
+ // Default options
+ opts = opts || {};
+ opts.mark = true;
+ opts.interval = opts.interval || 100;
+ opts.debounceDelay = opts.debounceDelay || 500;
+ opts.cwd = opts.cwd || process.cwd();
+ this.options = opts;
+
+ // Default error handler to prevent emit('error') throwing magically for us
+ this.on('error', function(error) {
+ if (self.listeners('error').length > 1) {
+ return self.removeListener('error', this);
+ }
+ nextback(function() {
+ done.call(self, error, self);
+ })();
+ });
+
+ // Default done callback
+ done = done || function() {};
+
+ // Remember our watched dir:files
+ this._watched = Object.create(null);
+
+ // Store watchers
+ this._watchers = Object.create(null);
+
+ // Store watchFile listeners
+ this._pollers = Object.create(null);
+
+ // Store patterns
+ this._patterns = [];
+
+ // Cached events for debouncing
+ this._cached = Object.create(null);
+
+ // Set maxListeners
+ if (this.options.maxListeners != null) {
+ this.setMaxListeners(this.options.maxListeners);
+ Gaze.super_.prototype.setMaxListeners(this.options.maxListeners);
+ delete this.options.maxListeners;
+ }
+
+ // Initialize the watch on files
+ if (patterns) {
+ this.add(patterns, done);
+ }
+
+ // keep the process alive
+ this._keepalive = setInterval(function() {}, 200);
+
+ return this;
+}
+util.inherits(Gaze, EE);
+
+// Main entry point. Start watching and call done when setup
+module.exports = function gaze(patterns, opts, done) {
+ return new Gaze(patterns, opts, done);
+};
+module.exports.Gaze = Gaze;
+
+// Override the emit function to emit `all` events
+// and debounce on duplicate events per file
+Gaze.prototype.emit = function() {
+ var self = this;
+ var args = arguments;
+
+ var e = args[0];
+ var filepath = args[1];
+ var timeoutId;
+
+ // If not added/deleted/changed/renamed then just emit the event
+ if (e.slice(-2) !== 'ed') {
+ Gaze.super_.prototype.emit.apply(self, args);
+ return this;
+ }
+
+ // Detect rename event, if added and previous deleted is in the cache
+ if (e === 'added') {
+ Object.keys(this._cached).forEach(function(oldFile) {
+ if (self._cached[oldFile].indexOf('deleted') !== -1) {
+ args[0] = e = 'renamed';
+ [].push.call(args, oldFile);
+ delete self._cached[oldFile];
+ return false;
+ }
+ });
+ }
+
+ // If cached doesnt exist, create a delay before running the next
+ // then emit the event
+ var cache = this._cached[filepath] || [];
+ if (cache.indexOf(e) === -1) {
+ helper.objectPush(self._cached, filepath, e);
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(function() {
+ delete self._cached[filepath];
+ }, this.options.debounceDelay);
+ // Emit the event and `all` event
+ Gaze.super_.prototype.emit.apply(self, args);
+ Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
+ }
+
+ // Detect if new folder added to trigger for matching files within folder
+ if (e === 'added') {
+ if (helper.isDir(filepath)) {
+ fs.readdirSync(filepath).map(function(file) {
+ return path.join(filepath, file);
+ }).filter(function(file) {
+ return globule.isMatch(self._patterns, file, self.options);
+ }).forEach(function(file) {
+ self.emit('added', file);
+ });
+ }
+ }
+
+ return this;
+};
+
+// Close watchers
+Gaze.prototype.close = function(_reset) {
+ var self = this;
+ _reset = _reset === false ? false : true;
+ Object.keys(self._watchers).forEach(function(file) {
+ self._watchers[file].close();
+ });
+ self._watchers = Object.create(null);
+ Object.keys(this._watched).forEach(function(dir) {
+ self._unpollDir(dir);
+ });
+ if (_reset) {
+ self._watched = Object.create(null);
+ setTimeout(function() {
+ self.emit('end');
+ self.removeAllListeners();
+ clearInterval(self._keepalive);
+ }, delay + 100);
+ }
+ return self;
+};
+
+// Add file patterns to be watched
+Gaze.prototype.add = function(files, done) {
+ if (typeof files === 'string') { files = [files]; }
+ this._patterns = helper.unique.apply(null, [this._patterns, files]);
+ files = globule.find(this._patterns, this.options);
+ this._addToWatched(files);
+ this.close(false);
+ this._initWatched(done);
+};
+
+// Dont increment patterns and dont call done if nothing added
+Gaze.prototype._internalAdd = function(file, done) {
+ var files = [];
+ if (helper.isDir(file)) {
+ files = [helper.markDir(file)].concat(globule.find(this._patterns, this.options));
+ } else {
+ if (globule.isMatch(this._patterns, file, this.options)) {
+ files = [file];
+ }
+ }
+ if (files.length > 0) {
+ this._addToWatched(files);
+ this.close(false);
+ this._initWatched(done);
+ }
+};
+
+// Remove file/dir from `watched`
+Gaze.prototype.remove = function(file) {
+ var self = this;
+ if (this._watched[file]) {
+ // is dir, remove all files
+ this._unpollDir(file);
+ delete this._watched[file];
+ } else {
+ // is a file, find and remove
+ Object.keys(this._watched).forEach(function(dir) {
+ var index = self._watched[dir].indexOf(file);
+ if (index !== -1) {
+ self._unpollFile(file);
+ self._watched[dir].splice(index, 1);
+ return false;
+ }
+ });
+ }
+ if (this._watchers[file]) {
+ this._watchers[file].close();
+ }
+ return this;
+};
+
+// Return watched files
+Gaze.prototype.watched = function(done) {
+ done(null, this._watched);
+ return this;
+};
+
+// Returns `watched` files with relative paths to process.cwd()
+Gaze.prototype.relative = function(dir, unixify, done) {
+ if (typeof dir === 'function') {
+ done = dir;
+ dir = null;
+ unixify = false;
+ }
+ if (typeof unixify === 'function') {
+ done = unixify;
+ unixify = false;
+ }
+ var self = this;
+ var relative = Object.create(null);
+ var relDir, relFile, unixRelDir;
+ var cwd = this.options.cwd || process.cwd();
+ if (dir === '') { dir = '.'; }
+ dir = helper.markDir(dir);
+ unixify = unixify || false;
+ Object.keys(this._watched).forEach(function(dir) {
+ relDir = path.relative(cwd, dir) + path.sep;
+ if (relDir === path.sep) { relDir = '.'; }
+ unixRelDir = unixify ? helper.unixifyPathSep(relDir) : relDir;
+ relative[unixRelDir] = self._watched[dir].map(function(file) {
+ relFile = path.relative(path.join(cwd, relDir) || '', file || '');
+ if (helper.isDir(file)) {
+ relFile = helper.markDir(relFile);
+ }
+ if (unixify) {
+ relFile = helper.unixifyPathSep(relFile);
+ }
+ return relFile;
+ });
+ });
+ if (dir && unixify) {
+ dir = helper.unixifyPathSep(dir);
+ }
+ var result = dir ? relative[dir] || [] : relative;
+ // For consistency. GH-74
+ if (result['.']) {
+ result['./'] = result['.'];
+ delete result['.'];
+ }
+ done(null, result);
+ return self;
+};
+
+// Adds files and dirs to watched
+Gaze.prototype._addToWatched = function(files) {
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ var filepath = path.resolve(this.options.cwd, file);
+
+ var dirname = (helper.isDir(file)) ? filepath : path.dirname(filepath);
+ dirname = helper.markDir(dirname);
+
+ // If a new dir is added
+ if (helper.isDir(file) && !(filepath in this._watched)) {
+ helper.objectPush(this._watched, filepath, []);
+ }
+
+ if (file.slice(-1) === '/') { filepath += path.sep; }
+ helper.objectPush(this._watched, path.dirname(filepath) + path.sep, filepath);
+
+ // add folders into the mix
+ var readdir = fs.readdirSync(dirname);
+ for (var j = 0; j < readdir.length; j++) {
+ var dirfile = path.join(dirname, readdir[j]);
+ if (fs.lstatSync(dirfile).isDirectory()) {
+ helper.objectPush(this._watched, dirname, dirfile + path.sep);
+ }
+ }
+ }
+ return this;
+};
+
+Gaze.prototype._watchDir = function(dir, done) {
+ var self = this;
+ var timeoutId;
+ // Dont even try watching the dir if it doesnt exist
+ if (!fs.existsSync(dir)) { return; }
+ try {
+ this._watchers[dir] = fs.watch(dir, function(event) {
+ // race condition. Let's give the fs a little time to settle down. so we
+ // don't fire events on non existent files.
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(function() {
+ // race condition. Ensure that this directory is still being watched
+ // before continuing.
+ if ((dir in self._watchers) && fs.existsSync(dir)) {
+ done(null, dir);
+ }
+ }, delay + 100);
+ });
+ } catch (err) {
+ return this._handleError(err);
+ }
+ return this;
+};
+
+Gaze.prototype._unpollFile = function(file) {
+ if (this._pollers[file]) {
+ fs.unwatchFile(file, this._pollers[file] );
+ delete this._pollers[file];
+ }
+ return this;
+};
+
+Gaze.prototype._unpollDir = function(dir) {
+ this._unpollFile(dir);
+ for (var i = 0; i < this._watched[dir].length; i++) {
+ this._unpollFile(this._watched[dir][i]);
+ }
+};
+
+Gaze.prototype._pollFile = function(file, done) {
+ var opts = { persistent: true, interval: this.options.interval };
+ if (!this._pollers[file]) {
+ this._pollers[file] = function(curr, prev) {
+ done(null, file);
+ };
+ try {
+ fs.watchFile(file, opts, this._pollers[file]);
+ } catch (err) {
+ return this._handleError(err);
+ }
+ }
+ return this;
+};
+
+// Initialize the actual watch on `watched` files
+Gaze.prototype._initWatched = function(done) {
+ var self = this;
+ var cwd = this.options.cwd || process.cwd();
+ var curWatched = Object.keys(self._watched);
+
+ // if no matching files
+ if (curWatched.length < 1) {
+ self.emit('ready', self);
+ if (done) { done.call(self, null, self); }
+ self.emit('nomatch');
+ return;
+ }
+
+ helper.forEachSeries(curWatched, function(dir, next) {
+ dir = dir || '';
+ var files = self._watched[dir];
+ // Triggered when a watched dir has an event
+ self._watchDir(dir, function(event, dirpath) {
+ var relDir = cwd === dir ? '.' : path.relative(cwd, dir);
+ relDir = relDir || '';
+
+ fs.readdir(dirpath, function(err, current) {
+ if (err) { return self.emit('error', err); }
+ if (!current) { return; }
+
+ try {
+ // append path.sep to directories so they match previous.
+ current = current.map(function(curPath) {
+ if (fs.existsSync(path.join(dir, curPath)) && fs.lstatSync(path.join(dir, curPath)).isDirectory()) {
+ return curPath + path.sep;
+ } else {
+ return curPath;
+ }
+ });
+ } catch (err) {
+ // race condition-- sometimes the file no longer exists
+ }
+
+ // Get watched files for this dir
+ self.relative(relDir, function(err, previous) {
+
+ // If file was deleted
+ previous.filter(function(file) {
+ return current.indexOf(file) < 0;
+ }).forEach(function(file) {
+ if (!helper.isDir(file)) {
+ var filepath = path.join(dir, file);
+ self.remove(filepath);
+ self.emit('deleted', filepath);
+ }
+ });
+
+ // If file was added
+ current.filter(function(file) {
+ return previous.indexOf(file) < 0;
+ }).forEach(function(file) {
+ // Is it a matching pattern?
+ var relFile = path.join(relDir, file);
+ // Add to watch then emit event
+ self._internalAdd(relFile, function() {
+ self.emit('added', path.join(dir, file));
+ });
+ });
+
+ });
+
+ });
+ });
+
+ // Watch for change/rename events on files
+ files.forEach(function(file) {
+ if (helper.isDir(file)) { return; }
+ self._pollFile(file, function(err, filepath) {
+ // Only emit changed if the file still exists
+ // Prevents changed/deleted duplicate events
+ if (fs.existsSync(filepath)) {
+ self.emit('changed', filepath);
+ }
+ });
+ });
+
+ next();
+ }, function() {
+
+ // Return this instance of Gaze
+ // delay before ready solves a lot of issues
+ setTimeout(function() {
+ self.emit('ready', self);
+ if (done) { done.call(self, null, self); }
+ }, delay + 100);
+
+ });
+};
+
+// If an error, handle it here
+Gaze.prototype._handleError = function(err) {
+ if (err.code === 'EMFILE') {
+ return this.emit('error', new Error('EMFILE: Too many opened files.'));
+ }
+ return this.emit('error', err);
+};
\ No newline at end of file
diff --git a/lib/helper.js b/lib/helper.js
index e1ccc80..39a600d 100644
--- a/lib/helper.js
+++ b/lib/helper.js
@@ -1,3 +1,11 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2015 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
'use strict';
var path = require('path');
@@ -6,7 +14,7 @@ var helper = module.exports = {};
// Returns boolean whether filepath is dir terminated
helper.isDir = function isDir(dir) {
if (typeof dir !== 'string') { return false; }
- return (dir.slice(-(path.sep.length)) === path.sep);
+ return (dir.slice(-(path.sep.length)) === path.sep) || (dir.slice(-1) === '/');
};
// Create a `key:[]` if doesnt exist on `obj` then push or concat the `val`
@@ -32,6 +40,49 @@ helper.unixifyPathSep = function unixifyPathSep(filepath) {
return (process.platform === 'win32') ? String(filepath).replace(/\\/g, '/') : filepath;
};
+// Converts a flat list of paths to the old style tree
+helper.flatToTree = function flatToTree(files, cwd, relative, unixify, done) {
+ cwd = helper.markDir(cwd);
+ var tree = Object.create(null);
+
+ helper.forEachSeries(files, function(filepath, next) {
+ var parent = path.dirname(filepath) + path.sep;
+
+ // If parent outside cwd, ignore
+ if (path.relative(cwd, parent) === '..') {
+ return next();
+ }
+
+ // If we want relative paths
+ if (relative === true) {
+ if (path.resolve(parent) === path.resolve(cwd)) {
+ parent = './';
+ } else {
+ parent = path.relative(cwd, parent) + path.sep;
+ }
+ filepath = path.relative(path.join(cwd, parent), filepath) + (helper.isDir(filepath) ? path.sep : '');
+ }
+
+ // If we want to transform paths to unix seps
+ if (unixify === true) {
+ filepath = helper.unixifyPathSep(filepath);
+ if (parent !== './') {
+ parent = helper.unixifyPathSep(parent);
+ }
+ }
+
+ if (!parent) { return next(); }
+
+ if (!Array.isArray(tree[parent])) {
+ tree[parent] = [];
+ }
+ tree[parent].push(filepath);
+ next();
+ }, function() {
+ done(null, tree);
+ });
+};
+
/**
* Lo-Dash 1.0.1
* Copyright 2012-2013 The Dojo Foundation
@@ -46,16 +97,20 @@ helper.unique = function unique() { var array = Array.prototype.concat.apply(Arr
* Available under MIT license
*/
helper.forEachSeries = function forEachSeries(arr, iterator, callback) {
- if (!arr.length) { return callback(); }
+ callback = callback || function () {};
+ if (!arr.length) {
+ return callback();
+ }
var completed = 0;
- var iterate = function() {
+ var iterate = function () {
iterator(arr[completed], function (err) {
if (err) {
callback(err);
- callback = function() {};
- } else {
+ callback = function () {};
+ }
+ else {
completed += 1;
- if (completed === arr.length) {
+ if (completed >= arr.length) {
callback(null);
} else {
iterate();
diff --git a/lib/pathwatcher.js b/lib/pathwatcher.js
new file mode 100644
index 0000000..0846437
--- /dev/null
+++ b/lib/pathwatcher.js
@@ -0,0 +1,201 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+var EE = require('events').EventEmitter;
+var fs = require('graceful-fs');
+var inherits = require('util').inherits;
+
+var path = require('path');
+var v8 = 'v8-' + /[0-9]+\.[0-9]+/.exec(process.versions.v8)[0];
+var pathwatcherPath = path.join(__dirname, '..', 'bin', process.platform + '-' + process.arch + '-' + v8, 'pathwatcher.node');
+
+var binding = require(pathwatcherPath);
+var handleWatchers = new binding.HandleMap;
+binding.setCallback(function(event, handle, filePath, oldFilePath) {
+ if (handleWatchers.has(handle)) {
+ return handleWatchers.get(handle).onEvent(event, filePath, oldFilePath);
+ }
+});
+
+function HandleWatcher(p) {
+ EE.call(this);
+ this.path = p;
+ this.start();
+}
+inherits(HandleWatcher, EE);
+
+HandleWatcher.prototype.onEvent = function(event, filePath, oldFilePath) {
+ var detectRename,
+ _this = this;
+ switch (event) {
+ case 'rename':
+ this.close();
+ detectRename = function() {
+ return fs.stat(_this.path, function(err) {
+ if (err) {
+ _this.path = filePath;
+ _this.start();
+ return _this.emit('change', 'rename', filePath);
+ } else {
+ _this.start();
+ return _this.emit('change', 'change', null);
+ }
+ });
+ };
+ return setTimeout(detectRename, 100);
+ case 'delete':
+ this.emit('change', 'delete', null);
+ return this.close();
+ case 'unknown':
+ throw new Error("Received unknown event for path: " + this.path);
+ break;
+ default:
+ return this.emit('change', event, filePath, oldFilePath);
+ }
+};
+
+HandleWatcher.prototype.start = function() {
+ var troubleWatcher;
+ this.handle = binding.watch(this.path);
+ if (handleWatchers.has(this.handle)) {
+ troubleWatcher = handleWatchers.get(this.handle);
+ troubleWatcher.close();
+ //console.error("The handle(" + this.handle + ") returned by watching " + this.path + " is the same with an already watched path(" + troubleWatcher.path + ")");
+ }
+ return handleWatchers.add(this.handle, this);
+};
+
+HandleWatcher.prototype.closeIfNoListener = function() {
+ if (this.listeners('change').length === 0) {
+ return this.close();
+ }
+};
+
+HandleWatcher.prototype.close = function() {
+ if (handleWatchers.has(this.handle)) {
+ binding.unwatch(this.handle);
+ return handleWatchers.remove(this.handle);
+ }
+};
+
+PathWatcher.prototype.isWatchingParent = false;
+
+PathWatcher.prototype.path = null;
+
+PathWatcher.prototype.handleWatcher = null;
+
+function PathWatcher(filePath, callback) {
+ EE.call(this);
+ var stats, watcher, _i, _len, _ref,
+ _this = this;
+ this.path = filePath;
+ if (process.platform === 'win32') {
+ stats = fs.statSync(filePath);
+ this.isWatchingParent = !stats.isDirectory();
+ }
+ if (this.isWatchingParent) {
+ filePath = path.dirname(filePath);
+ }
+ _ref = handleWatchers.values();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ watcher = _ref[_i];
+ if (watcher.path === filePath) {
+ this.handleWatcher = watcher;
+ break;
+ }
+ }
+ if (this.handleWatcher == null) {
+ this.handleWatcher = new HandleWatcher(filePath);
+ }
+ this.onChange = function(event, newFilePath, oldFilePath) {
+ switch (event) {
+ case 'rename':
+ case 'change':
+ case 'delete':
+ if (event === 'rename') {
+ _this.path = newFilePath;
+ }
+ if (typeof callback === 'function') {
+ callback.call(_this, event, newFilePath);
+ }
+ return _this.emit('change', event, newFilePath);
+ case 'child-rename':
+ if (_this.isWatchingParent) {
+ if (_this.path === oldFilePath) {
+ return _this.onChange('rename', newFilePath);
+ }
+ } else {
+ return _this.onChange('change', '');
+ }
+ break;
+ case 'child-delete':
+ if (_this.isWatchingParent) {
+ if (_this.path === newFilePath) {
+ return _this.onChange('delete', null);
+ }
+ } else {
+ return _this.onChange('change', '');
+ }
+ break;
+ case 'child-change':
+ if (_this.isWatchingParent && _this.path === newFilePath) {
+ return _this.onChange('change', '');
+ }
+ break;
+ case 'child-create':
+ if (!_this.isWatchingParent) {
+ return _this.onChange('change', '');
+ }
+ }
+ };
+ this.handleWatcher.on('change', this.onChange);
+}
+inherits(PathWatcher, EE);
+
+PathWatcher.prototype.close = function() {
+ this.handleWatcher.removeListener('change', this.onChange);
+ return this.handleWatcher.closeIfNoListener();
+};
+
+exports.watch = function(p, callback) {
+ return new PathWatcher(path.resolve(p), callback);
+};
+
+exports.closeAllWatchers = function() {
+ var watcher, _i, _len, _ref;
+ _ref = handleWatchers.values();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ watcher = _ref[_i];
+ watcher.close();
+ }
+ return handleWatchers.clear();
+};
+
+exports.getWatchedPaths = function() {
+ var paths, watcher, _i, _len, _ref;
+ paths = [];
+ _ref = handleWatchers.values();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ watcher = _ref[_i];
+ paths.push(watcher.path);
+ }
+ return paths;
+};
diff --git a/lib/platform.js b/lib/platform.js
new file mode 100644
index 0000000..e6f3870
--- /dev/null
+++ b/lib/platform.js
@@ -0,0 +1,214 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2015 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+var PathWatcher = null;
+var statpoll = require('./statpoll.js');
+var helper = require('./helper');
+var fs = require('graceful-fs');
+var path = require('path');
+
+// Define object that contains helper functions, jshint happy :)
+var platformUtils = {};
+
+// on purpose globals
+var watched = Object.create(null);
+var renameWaiting = null;
+var renameWaitingFile = null;
+var emitEvents = true;
+var noop = function() {};
+
+var platform = module.exports = function(file, cb) {
+ if (PathWatcher == null) {
+ PathWatcher = require('./pathwatcher');
+ }
+
+ // Ignore non-existent files
+ if (!fs.existsSync(file)) {
+ return;
+ }
+
+ emitEvents = true;
+
+ // Mark every folder
+ file = platformUtils.markDir(file);
+
+ // Also watch all folders, needed to catch change for detecting added files
+ if (!helper.isDir(file)) {
+ platform(path.dirname(file), cb);
+ }
+
+ // Already watched, just add cb to stack
+ if (watched[file]) {
+ watched[file].push(cb);
+ return false;
+ }
+
+ // Helper for when to use statpoll
+ function useStatPoll() {
+ statpoll(file, function(event) {
+ var filepath = file;
+ if (process.platform === 'linux') {
+ var go = platformUtils.linuxWorkarounds(event, filepath, cb);
+ if (go === false) { return; }
+ }
+ platformUtils.cbstack(null, event, filepath);
+ });
+ }
+
+ // Add callback to watched
+ watched[file] = [cb];
+
+ // By default try using native OS watchers
+ if (platform.mode === 'auto' || platform.mode === 'watch') {
+ // Delay adding files to watch
+ // Fixes the duplicate handle race condition when renaming files
+ // ie: (The handle(26) returned by watching [filename] is the same with an already watched path([filename]))
+ setTimeout(function() {
+ if (!fs.existsSync(file)) {
+ delete watched[file];
+ return;
+ }
+ // Workaround for lack of rename support on linux
+ if (process.platform === 'linux' && renameWaiting) {
+ clearTimeout(renameWaiting);
+ platformUtils.cbstack(null, 'rename', renameWaitingFile, file);
+ renameWaiting = renameWaitingFile = null;
+ return;
+ }
+ try {
+ watched[file].push(PathWatcher.watch(file, function(event, newFile) {
+ var filepath = file;
+ if (process.platform === 'linux') {
+ var go = platformUtils.linuxWorkarounds(event, filepath, cb);
+ if (go === false) { return; }
+ }
+ platformUtils.cbstack(null, event, filepath, newFile);
+ }));
+ } catch (error) {
+ // If we hit EMFILE, use stat poll
+ if (error.message.slice(0, 6) === 'EMFILE') { error.code = 'EMFILE'; }
+ if (error.code === 'EMFILE') {
+ // Only fallback to stat poll if not forced in watch mode
+ if (platform.mode !== 'watch') { useStatPoll(); }
+ // Format the error message for EMFILE a bit better
+ error.message = 'Too many open files.\nUnable to watch "' + file + '"\nusing native OS events so falling back to slower stat polling.\n';
+ }
+ platformUtils.cbstack(error);
+ }
+ }, 10);
+ } else {
+ useStatPoll();
+ }
+};
+
+platform.mode = 'auto';
+
+// Run the stat poller
+// NOTE: Run at minimum of 500ms to adequately capture change event
+// to folders when adding files
+platform.tick = statpoll.tick.bind(statpoll);
+
+// Close up a single watcher
+platform.close = function(filepath, cb) {
+ cb = cb || noop;
+ if (Array.isArray(watched[filepath])) {
+ try {
+ for (var i = 0; i < watched[filepath].length; i++) {
+ if (watched[filepath].hasOwnProperty('close')) {
+ watched[filepath].close();
+ }
+ }
+ delete watched[filepath];
+ } catch (error) {
+ return cb(error);
+ }
+ } else {
+ statpoll.close(filepath);
+ }
+ if (typeof cb === 'function') {
+ cb(null);
+ }
+};
+
+// Close up all watchers
+platform.closeAll = function() {
+ watched = Object.create(null);
+ statpoll.closeAll();
+ PathWatcher.closeAllWatchers();
+ emitEvents = false;
+};
+
+// Return all watched file paths
+platform.getWatchedPaths = function() {
+ return Object.keys(watched).concat(statpoll.getWatchedPaths());
+};
+
+// Helper for calling callbacks in a stack
+platformUtils.cbstack = function(err, event, filepath, newFile) {
+ if(!emitEvents) {
+ return;
+ }
+ if (watched[filepath]) {
+ helper.forEachSeries(watched[filepath], function(cb, next) {
+ if (typeof cb === 'function') {
+ cb(err, event, filepath, newFile);
+ }
+ next();
+ });
+ }
+};
+
+// Mark folders if not marked
+platformUtils.markDir = function(file) {
+ if (file.slice(-1) !== path.sep) {
+ if (fs.lstatSync(file).isDirectory()) {
+ file += path.sep;
+ }
+ }
+ return file;
+};
+
+// Workarounds for lack of rename support on linux and folders emit before files
+// https://github.com/atom/node-pathwatcher/commit/004a202dea89f4303cdef33912902ed5caf67b23
+var linuxQueue = Object.create(null);
+var linuxQueueInterval = null;
+platformUtils.linuxProcessQueue = function(cb) {
+ var len = Object.keys(linuxQueue).length;
+ if (len === 1) {
+ var key = Object.keys(linuxQueue).slice(0, 1)[0];
+ platformUtils.cbstack(null, key, linuxQueue[key]);
+ } else if (len > 1) {
+ if (linuxQueue['delete'] && linuxQueue['change']) {
+ renameWaitingFile = linuxQueue['delete'];
+ renameWaiting = setTimeout(function(filepath) {
+ platformUtils.cbstack(null, 'delete', filepath);
+ renameWaiting = renameWaitingFile = null;
+ }, 100, linuxQueue['delete']);
+ platformUtils.cbstack(null, 'change', linuxQueue['change']);
+ } else {
+ // TODO: This might not be needed
+ for (var i in linuxQueue) {
+ if (linuxQueue.hasOwnProperty(i)) {
+ platformUtils.cbstack(null, i, linuxQueue[i]);
+ }
+ }
+ }
+ }
+ linuxQueue = Object.create(null);
+};
+
+platformUtils.linuxWorkarounds = function(event, filepath, cb) {
+ linuxQueue[event] = filepath;
+ clearTimeout(linuxQueueInterval);
+ linuxQueueInterval = setTimeout(function() {
+ platformUtils.linuxProcessQueue(cb);
+ }, 100);
+ return false;
+};
diff --git a/lib/statpoll.js b/lib/statpoll.js
new file mode 100644
index 0000000..f88c66c
--- /dev/null
+++ b/lib/statpoll.js
@@ -0,0 +1,71 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2015 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+var fs = require('graceful-fs');
+var nextback = require('nextback');
+var helper = require('./helper');
+var polled = Object.create(null);
+var lastTick = Date.now();
+var running = false;
+
+var statpoll = module.exports = function(filepath, cb) {
+ if (!polled[filepath]) {
+ polled[filepath] = { stat: fs.lstatSync(filepath), cb: cb, last: null };
+ }
+};
+
+// Iterate over polled files
+statpoll.tick = function() {
+ var files = Object.keys(polled);
+ if (files.length < 1 || running === true) {
+ return;
+ }
+ running = true;
+ helper.forEachSeries(files, function(file, next) {
+ // If file deleted
+ if (!fs.existsSync(file)) {
+ polled[file].cb('delete', file);
+ delete polled[file];
+ return next();
+ }
+
+ var stat = fs.lstatSync(file);
+
+ // If file has changed
+ var diff = stat.mtime - polled[file].stat.mtime;
+ if (diff > 0) {
+ polled[file].cb('change', file);
+ }
+
+ // Set new last accessed time
+ polled[file].stat = stat;
+ next();
+ }, nextback(function() {
+ lastTick = Date.now();
+ running = false;
+ }));
+};
+
+// Close up a single watcher
+statpoll.close = nextback(function(file) {
+ delete polled[file];
+ running = false;
+});
+
+// Close up all watchers
+statpoll.closeAll = nextback(function() {
+ polled = Object.create(null);
+ running = false;
+});
+
+// Return all statpolled watched paths
+statpoll.getWatchedPaths = function() {
+ return Object.keys(polled);
+};
diff --git a/package.json b/package.json
index aed94be..ab3219f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "gaze",
"description": "A globbing fs.watch wrapper built from the best parts of other fine watch libs.",
- "version": "0.5.1",
+ "version": "0.6.4",
"homepage": "https://github.com/shama/gaze",
"author": {
"name": "Kyle Robinson Young",
@@ -14,37 +14,49 @@
"bugs": {
"url": "https://github.com/shama/gaze/issues"
},
- "licenses": [
- {
- "type": "MIT",
- "url": "https://github.com/shama/gaze/blob/master/LICENSE-MIT"
- }
- ],
- "main": "lib/gaze",
+ "license": "MIT",
+ "main": "index.js",
"engines": {
"node": ">= 0.8.0"
},
"scripts": {
- "test": "grunt nodeunit -v"
+ "test": "grunt nodeunit -v",
+ "install": "node build.js"
},
"dependencies": {
- "globule": "~0.1.0"
+ "absolute-path": "0.0.0",
+ "bindings": "~1.2.1",
+ "globule": "~0.2.0",
+ "graceful-fs": "~3.0.8",
+ "nan": "^1.8.4",
+ "nextback": "~0.1.0"
},
"devDependencies": {
- "grunt": "~0.4.1",
- "grunt-contrib-nodeunit": "~0.2.0",
- "grunt-contrib-jshint": "~0.6.0",
- "grunt-benchmark": "~0.2.0",
+ "grunt": "~0.4.5",
+ "grunt-contrib-nodeunit": "~0.4.1",
+ "grunt-contrib-jshint": "~0.11.2",
"grunt-cli": "~0.1.13",
- "async": "~0.2.10",
- "rimraf": "~2.2.6"
+ "async": "~1.2.1",
+ "rimraf": "~2.4.0",
+ "ascii-table": "0.0.8"
},
"keywords": [
"watch",
- "glob"
+ "watcher",
+ "watching",
+ "fs.watch",
+ "fswatcher",
+ "fs",
+ "glob",
+ "utility"
],
"files": [
+ "index.js",
"lib",
+ "src",
+ "bin",
+ "binding.gyp",
+ "AUTHORS",
"LICENSE-MIT"
]
}
diff --git a/src/common.cc b/src/common.cc
new file mode 100644
index 0000000..1a7054b
--- /dev/null
+++ b/src/common.cc
@@ -0,0 +1,171 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include "common.h"
+
+static uv_async_t g_async;
+static int g_watch_count;
+static uv_sem_t g_semaphore;
+static uv_thread_t g_thread;
+
+static EVENT_TYPE g_type;
+static WatcherHandle g_handle;
+static std::vector g_new_path;
+static std::vector g_old_path;
+static Persistent g_callback;
+
+static void CommonThread(void* handle) {
+ WaitForMainThread();
+ PlatformThread();
+}
+
+#if NODE_VERSION_AT_LEAST(0, 11, 13)
+static void MakeCallbackInMainThread(uv_async_t* handle) {
+#else
+static void MakeCallbackInMainThread(uv_async_t* handle, int status) {
+#endif
+ NanScope();
+
+ if (!g_callback.IsEmpty()) {
+ Handle type;
+ switch (g_type) {
+ case EVENT_CHANGE:
+ type = NanNew("change");
+ break;
+ case EVENT_DELETE:
+ type = NanNew("delete");
+ break;
+ case EVENT_RENAME:
+ type = NanNew("rename");
+ break;
+ case EVENT_CHILD_CREATE:
+ type = NanNew("child-create");
+ break;
+ case EVENT_CHILD_CHANGE:
+ type = NanNew("child-change");
+ break;
+ case EVENT_CHILD_DELETE:
+ type = NanNew("child-delete");
+ break;
+ case EVENT_CHILD_RENAME:
+ type = NanNew("child-rename");
+ break;
+ default:
+ type = NanNew("unknown");
+ return;
+ }
+
+ Handle argv[] = {
+ type,
+ WatcherHandleToV8Value(g_handle),
+ NanNew(std::string(g_new_path.begin(), g_new_path.end())),
+ NanNew(std::string(g_old_path.begin(), g_old_path.end())),
+ };
+ NanNew(g_callback)->Call(NanGetCurrentContext()->Global(), 4, argv);
+ }
+
+ WakeupNewThread();
+}
+
+static void SetRef(bool value) {
+ uv_handle_t* h = reinterpret_cast(&g_async);
+ if (value) {
+ uv_ref(h);
+ } else {
+ uv_unref(h);
+ }
+}
+
+void CommonInit() {
+ uv_sem_init(&g_semaphore, 0);
+ uv_async_init(uv_default_loop(), &g_async, MakeCallbackInMainThread);
+ // As long as any uv_ref'd uv_async_t handle remains active, the node
+ // process will never exit, so we must call uv_unref here (#47).
+ SetRef(false);
+ g_watch_count = 0;
+ uv_thread_create(&g_thread, &CommonThread, NULL);
+}
+
+void WaitForMainThread() {
+ uv_sem_wait(&g_semaphore);
+}
+
+void WakeupNewThread() {
+ uv_sem_post(&g_semaphore);
+}
+
+void PostEventAndWait(EVENT_TYPE type,
+ WatcherHandle handle,
+ const std::vector& new_path,
+ const std::vector& old_path) {
+ // FIXME should not pass args by settings globals.
+ g_type = type;
+ g_handle = handle;
+ g_new_path = new_path;
+ g_old_path = old_path;
+
+ uv_async_send(&g_async);
+ WaitForMainThread();
+}
+
+NAN_METHOD(SetCallback) {
+ NanScope();
+
+ if (!args[0]->IsFunction())
+ return NanThrowTypeError("Function required");
+
+ NanAssignPersistent(g_callback, Local::Cast(args[0]));
+ NanReturnUndefined();
+}
+
+NAN_METHOD(Watch) {
+ NanScope();
+
+ if (!args[0]->IsString())
+ return NanThrowTypeError("String required");
+
+ Handle path = args[0]->ToString();
+ WatcherHandle handle = PlatformWatch(*String::Utf8Value(path));
+ if (PlatformIsEMFILE(handle))
+ return NanThrowTypeError("EMFILE: Unable to watch path");
+ if (!PlatformIsHandleValid(handle))
+ return NanThrowTypeError("Unable to watch path");
+
+ if (g_watch_count++ == 0) {
+ SetRef(true);
+ }
+
+ NanReturnValue(WatcherHandleToV8Value(handle));
+}
+
+NAN_METHOD(Unwatch) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Handle type required");
+
+ PlatformUnwatch(V8ValueToWatcherHandle(args[0]));
+
+ if (--g_watch_count == 0) {
+ SetRef(false);
+ }
+
+ NanReturnUndefined();
+}
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..8a5601f
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,77 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#ifndef SRC_COMMON_H_
+#define SRC_COMMON_H_
+
+#include
+
+#include "nan.h"
+using namespace v8;
+
+#ifdef _WIN32
+// Platform-dependent definetion of handle.
+typedef HANDLE WatcherHandle;
+
+// Conversion between V8 value and WatcherHandle.
+Handle WatcherHandleToV8Value(WatcherHandle handle);
+WatcherHandle V8ValueToWatcherHandle(Handle value);
+bool IsV8ValueWatcherHandle(Handle value);
+#else
+// Correspoding definetions on OS X and Linux.
+typedef int32_t WatcherHandle;
+#define WatcherHandleToV8Value(h) NanNew(h)
+#define V8ValueToWatcherHandle(v) v->Int32Value()
+#define IsV8ValueWatcherHandle(v) v->IsInt32()
+#endif
+
+void PlatformInit();
+void PlatformThread();
+WatcherHandle PlatformWatch(const char* path);
+void PlatformUnwatch(WatcherHandle handle);
+bool PlatformIsHandleValid(WatcherHandle handle);
+bool PlatformIsEMFILE(WatcherHandle handle);
+
+enum EVENT_TYPE {
+ EVENT_NONE,
+ EVENT_CHANGE,
+ EVENT_RENAME,
+ EVENT_DELETE,
+ EVENT_CHILD_CHANGE,
+ EVENT_CHILD_RENAME,
+ EVENT_CHILD_DELETE,
+ EVENT_CHILD_CREATE,
+};
+
+void WaitForMainThread();
+void WakeupNewThread();
+void PostEventAndWait(EVENT_TYPE type,
+ WatcherHandle handle,
+ const std::vector& new_path,
+ const std::vector& old_path = std::vector());
+
+void CommonInit();
+
+NAN_METHOD(SetCallback);
+NAN_METHOD(Watch);
+NAN_METHOD(Unwatch);
+
+#endif // SRC_COMMON_H_
+
diff --git a/src/handle_map.cc b/src/handle_map.cc
new file mode 100644
index 0000000..f367f3e
--- /dev/null
+++ b/src/handle_map.cc
@@ -0,0 +1,158 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include "handle_map.h"
+
+#include
+
+HandleMap::HandleMap() {
+}
+
+HandleMap::~HandleMap() {
+ Clear();
+}
+
+bool HandleMap::Has(WatcherHandle key) const {
+ return map_.find(key) != map_.end();
+}
+
+bool HandleMap::Erase(WatcherHandle key) {
+ Map::iterator iter = map_.find(key);
+ if (iter == map_.end())
+ return false;
+
+ NanDisposeUnsafePersistent(iter->second);
+ map_.erase(iter);
+ return true;
+}
+
+void HandleMap::Clear() {
+ for (Map::iterator iter = map_.begin(); iter != map_.end(); ++iter)
+ NanDisposeUnsafePersistent(iter->second);
+ map_.clear();
+}
+
+// static
+NAN_METHOD(HandleMap::New) {
+ NanScope();
+ HandleMap* obj = new HandleMap();
+ obj->Wrap(args.This());
+ NanReturnUndefined();
+}
+
+// static
+NAN_METHOD(HandleMap::Add) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap(args.This());
+ WatcherHandle key = V8ValueToWatcherHandle(args[0]);
+ if (obj->Has(key))
+ return NanThrowError("Duplicate key");
+
+ NanAssignUnsafePersistent(obj->map_[key], args[1]);
+ NanReturnUndefined();
+}
+
+// static
+NAN_METHOD(HandleMap::Get) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap(args.This());
+ WatcherHandle key = V8ValueToWatcherHandle(args[0]);
+ if (!obj->Has(key))
+ return NanThrowError("Invalid key");
+
+ NanReturnValue(NanUnsafePersistentToLocal(obj->map_[key]));
+}
+
+// static
+NAN_METHOD(HandleMap::Has) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap(args.This());
+ NanReturnValue(NanNew(obj->Has(V8ValueToWatcherHandle(args[0]))));
+}
+
+// static
+NAN_METHOD(HandleMap::Values) {
+ NanScope();
+
+ HandleMap* obj = ObjectWrap::Unwrap(args.This());
+
+ int i = 0;
+ Handle keys = NanNew(obj->map_.size());
+ for (Map::const_iterator iter = obj->map_.begin();
+ iter != obj->map_.end();
+ ++iter, ++i)
+ keys->Set(i, NanUnsafePersistentToLocal(iter->second));
+
+ NanReturnValue(keys);
+}
+
+// static
+NAN_METHOD(HandleMap::Remove) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap(args.This());
+ if (!obj->Erase(V8ValueToWatcherHandle(args[0])))
+ return NanThrowError("Invalid key");
+
+ NanReturnUndefined();
+}
+
+// static
+NAN_METHOD(HandleMap::Clear) {
+ NanScope();
+
+ HandleMap* obj = ObjectWrap::Unwrap(args.This());
+ obj->Clear();
+
+ NanReturnUndefined();
+}
+
+// static
+void HandleMap::Initialize(Handle