fb-watchman
Advanced tools
Comparing version 1.2.0 to 1.3.0
112
index.js
@@ -141,11 +141,36 @@ /* Copyright 2014-present Facebook, Inc. | ||
// already running. | ||
var watchmanCommand = this.watchmanBinaryPath + ' get-sockname'; | ||
childProcess.exec(watchmanCommand, | ||
function(error, stdout, stderr) { | ||
if (error) { | ||
self.emit('error', error); | ||
var args = ['--no-pretty', 'get-sockname']; | ||
// We use the more elaborate spawn rather than exec because there | ||
// are some error cases on Windows where process spawning can hang. | ||
// It is desirable to pipe stderr directly to stderr live so that | ||
// we can discover the problem. | ||
proc = childProcess.spawn(this.watchmanBinaryPath, args, { | ||
stdio: ['ignore', 'pipe', 'pipe'] | ||
}); | ||
var stdout = []; | ||
var stderr = []; | ||
proc.stdout.on('data', function(data) { | ||
stdout.push(data); | ||
}); | ||
proc.stderr.on('data', function(data) { | ||
data = data.toString('utf8'); | ||
stderr.push(data); | ||
console.log(data); | ||
}); | ||
proc.on('close', function (code) { | ||
proc.stderr.end(); | ||
proc.stdout.end(); | ||
if (code !== 0) { | ||
var why = this.watchmanBinaryPath + args.join(' ') + | ||
' returned with exit code ' + code + ' ' + | ||
stderr.join(''); | ||
console.log(why); | ||
self.emit('error', why); | ||
return; | ||
} | ||
try { | ||
var obj = JSON.parse(stdout); | ||
var obj = JSON.parse(stdout.join('')); | ||
if ('error' in obj) { | ||
@@ -184,2 +209,77 @@ error = new Error(obj.error); | ||
cap_versions = { | ||
"cmd-watch-del-all": "3.1.1", | ||
"cmd-watch-project": "3.1", | ||
"relative_root": "3.3", | ||
"term-dirname": "3.1", | ||
"term-idirname": "3.1", | ||
"wildmatch": "3.7", | ||
} | ||
// Compares a vs b, returns < 0 if a < b, > 0 if b > b, 0 if a == b | ||
function vers_compare(a, b) { | ||
a = a.split('.'); | ||
b = b.split('.'); | ||
for (i = 0; i < 3; i++) { | ||
d = parseInt(a[i] || '0') - parseInt(b[i] || '0'); | ||
if (d != 0) { | ||
return d; | ||
} | ||
} | ||
return 0; // Equal | ||
} | ||
function have_cap(vers, name) { | ||
if (name in cap_versions) { | ||
return vers_compare(vers, cap_versions[name]) >= 0; | ||
} | ||
return false; | ||
} | ||
// This is a helper that we expose for testing purposes | ||
Client.prototype._synthesizeCapabilityCheck = function( | ||
resp, optional, required) { | ||
resp.capabilities = {} | ||
version = resp.version; | ||
optional.forEach(function (name) { | ||
resp.capabilities[name] = have_cap(version, name); | ||
}); | ||
required.forEach(function (name) { | ||
var have = have_cap(version, name); | ||
resp.capabilities[name] = have; | ||
if (!have) { | ||
resp.error = 'client required capability `' + name + | ||
'` is not supported by this server'; | ||
} | ||
}); | ||
return resp; | ||
} | ||
Client.prototype.capabilityCheck = function(caps, done) { | ||
var optional = caps.optional || []; | ||
var required = caps.required || []; | ||
var self = this; | ||
this.command(['version', { | ||
optional: optional, | ||
required: required | ||
}], function (error, resp) { | ||
if (error) { | ||
done(error); | ||
return; | ||
} | ||
if (!('capabilities' in resp)) { | ||
// Server doesn't support capabilities, so we need to | ||
// synthesize the results based on the version | ||
resp = self._synthesizeCapabilityCheck(resp, optional, required); | ||
if (resp.error) { | ||
error = new Error(resp.error); | ||
error.watchmanResponse = resp; | ||
done(error); | ||
return; | ||
} | ||
} | ||
done(null, resp); | ||
}); | ||
} | ||
// Close the connection to the service | ||
@@ -186,0 +286,0 @@ Client.prototype.end = function() { |
{ | ||
"name": "fb-watchman", | ||
"version": "1.2.0", | ||
"version": "1.3.0", | ||
"description": "Bindings for the Watchman file watching service", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
296
README.md
@@ -14,3 +14,5 @@ # fb-watchman | ||
You should [install Watchman](https://facebook.github.io/watchman/docs/install.html) to make the most of this module. | ||
You should [install Watchman]( | ||
https://facebook.github.io/watchman/docs/install.html) to make the most of this | ||
module. | ||
@@ -31,292 +33,4 @@ Then simply: | ||
## Usage | ||
## How do I use it? | ||
``` | ||
var watchman = require('fb-watchman'); | ||
var client = new watchman.Client(); | ||
client.on('end', function() { | ||
// Called when the connection to watchman is terminated | ||
console.log('client ended'); | ||
}); | ||
client.on('error', function(error) { | ||
console.error('Error while talking to watchman: ', error); | ||
}); | ||
client.command(['version'], function(error, resp) { | ||
if (error) { | ||
console.error('Error getting version:', error); | ||
return; | ||
} | ||
console.log('Talking to watchman version', resp.version); | ||
}); | ||
// Example of the error case | ||
client.command(['invalid-command-never-will-work'], function(error, resp) { | ||
if (error) { | ||
console.error('failed to subscribe: ', error); | ||
return; | ||
} | ||
}); | ||
// Initiate a watch. You can repeatedly ask to watch the same dir without | ||
// error; Watchman will re-use an existing watch. | ||
client.command(['watch-project', process.cwd()], function(error, resp) { | ||
if (error) { | ||
console.error('Error initiating watch:', error); | ||
return; | ||
} | ||
// It is considered to be best practice to show any 'warning' or 'error' | ||
// information to the user, as it may suggest steps for remediation | ||
if ('warning' in resp) { | ||
console.log('warning: ', resp.warning); | ||
} | ||
// The default subscribe behavior is to deliver a list of all current files | ||
// when you first subscribe, so you don't need to walk the tree for yourself | ||
// on startup. If you don't want this behavior, you should issue a `clock` | ||
// command and use it to give a logical time constraint on the subscription. | ||
// See further below for an example of this. | ||
// watch-project may re-use an existing watch at a higher level in the | ||
// filesystem. It will tell us the relative path to the directory that | ||
// we expressed interest in, so we need to adjust for it in our results | ||
var path_prefix = ''; | ||
var root = resp['watch']; | ||
if ('relative_path' in resp) { | ||
path_prefix = resp['relative_path']; | ||
console.log('(re)using project watch at ', root, ', our dir is relative: ', | ||
path_prefix); | ||
} | ||
function get_relative_name(proj_rel) { | ||
if (path_prefix.length == 0) { | ||
return proj_rel; | ||
} | ||
if (proj_rel.substr(0, path_prefix.length) == path_prefix) { | ||
return proj_rel.substr(path_prefix.length + 1); | ||
} | ||
return null; | ||
} | ||
// Subscribe to notifications about .js files | ||
// https://facebook.github.io/watchman/docs/cmd/subscribe.html | ||
client.command(['subscribe', root, 'mysubscription', { | ||
// Match any .js file under process.cwd() | ||
// https://facebook.github.io/watchman/docs/file-query.html#expressions | ||
// Has more on the supported expression syntax | ||
expression: ["allof", | ||
["match", "*.js"], | ||
// focus on the relative path from the project to the path | ||
// of interest | ||
['dirname', path_prefix] | ||
], | ||
// Which fields we're interested in | ||
fields: ["name", "size", "exists", "type"] | ||
}], | ||
function(error, resp) { | ||
if (error) { | ||
// Probably an error in the subscription criteria | ||
console.error('failed to subscribe: ', error); | ||
return; | ||
} | ||
console.log('subscription ' + resp.subscribe + ' established'); | ||
} | ||
); | ||
// Subscription results are emitted via the subscription event. | ||
// Note that this emits for all subscriptions. If you have | ||
// subscriptions with different `fields` you will need to check | ||
// the subscription name and handle the differing data accordingly | ||
client.on('subscription', function(resp) { | ||
// Each entry in `resp.files` will have the fields you requested | ||
// in your subscription. The default is: | ||
// { name: 'example.js', | ||
// size: 1680, | ||
// new: true, | ||
// exists: true, | ||
// mode: 33188 } | ||
// | ||
// Names are relative to resp.root; join them together to | ||
// obtain a fully qualified path. | ||
// | ||
// `resp` looks like this in practice: | ||
// | ||
// { root: '/private/tmp/foo', | ||
// subscription: 'mysubscription', | ||
// files: [ { name: 'node_modules/fb-watchman/index.js', | ||
// size: 4768, | ||
// exists: true, | ||
// mode: 33188 } ] } | ||
console.log(resp.root, resp.subscription); | ||
for (var i in resp.files) { | ||
var f = resp.files[i]; | ||
// Fixup name for watch-project offset | ||
if (resp.subscription == 'mysubscription') { | ||
// we requested a set of fields in this subscription | ||
f.name = get_relative_name(f.name); | ||
} else { | ||
// the other subscription we set up returns only the name | ||
f = get_relative_name(f); | ||
} | ||
console.log(f); | ||
} | ||
}); | ||
// Here's an example of just subscribing for notifications after the | ||
// current point in time | ||
client.command(['clock', root], function(error, resp) { | ||
if (error) { | ||
console.error('Failed to query clock:', error); | ||
return; | ||
} | ||
client.command(['subscribe', root, 'sincesub', { | ||
expression: ['allof', | ||
["match", "*.js"], | ||
// focus on the relative path from the project to the path | ||
// of interest | ||
['dirname', path_prefix] | ||
], | ||
// Note: since we only request a single field, the `sincesub` subscription | ||
// response will just set files to an array of filenames, not an array of | ||
// objects with name properties | ||
// { root: '/private/tmp/foo', | ||
// subscription: 'sincesub', | ||
// files: [ 'node_modules/fb-watchman/index.js' ] } | ||
fields: ["name"], | ||
since: resp.clock // time constraint | ||
}], | ||
function(error, resp) { | ||
if (error) { | ||
console.error('failed to subscribe: ', error); | ||
return; | ||
} | ||
console.log('subscription ' + resp.subscribe + ' established'); | ||
} | ||
); | ||
}); | ||
}); | ||
``` | ||
## Methods | ||
### client.command(args [, done]) | ||
Sends a command to the watchman service. `args` is an array that specifies | ||
the command name and any optional arguments. The command is queued and | ||
dispatched asynchronously. You may queue multiple commands to the service; | ||
they will be dispatched in FIFO order once the client connection is established. | ||
The `done` parameter is a callback that will be passed (error, result) when the | ||
command completes. You may omit it if you are not interested in the result of | ||
the command. | ||
``` | ||
client.command(['watch-project', process.cwd()], function(error, resp) { | ||
if (error) { | ||
console.log('watch failed: ', error); | ||
return; | ||
} | ||
if ('warning' in resp) { | ||
console.log('warning: ', resp.warning); | ||
} | ||
if ('relative_path' in resp) { | ||
// We will need to remember and adjust for relative_path | ||
console.log('watching project ', resp.watch, ' relative path to cwd is ', | ||
resp.relative_path); | ||
} else { | ||
console.log('watching ', resp.watch); | ||
} | ||
}); | ||
``` | ||
If a field named `warning` is present in `resp`, the watchman service is trying | ||
to communicate an issue that the user should see and address. For example, if | ||
the system watch resources need adjustment, watchman will provide information | ||
about this and how to remediate the issue. It is suggested that tools that | ||
build on top of this library bubble the warning message up to the user. | ||
### client.end() | ||
Terminates the connection to the watchman service. Does not wait | ||
for any queued commands to send. | ||
## Events | ||
The following events are emitted by the watchman client object: | ||
### Event: 'connect' | ||
Emitted when the client successfully connects to the watchman service | ||
### Event: 'error' | ||
Emitted when the socket to the watchman service encounters an error. | ||
It may also be emitted prior to establishing a connection if we are unable | ||
to successfully execute the watchman CLI binary to determine how to talk | ||
to the server process. | ||
It is passed a variable that encapsulates the error. | ||
### Event: 'end' | ||
Emitted when the socket to the watchman service is closed | ||
### Event: 'log' | ||
Emitted in response to a unilateral `log` PDU from the watchman service. | ||
To enable these, you need to send a `log-level` command to the service: | ||
``` | ||
// This is very verbose, you probably don't want to do this | ||
client.command(['log-level', 'debug']); | ||
client.on('log', function(info) { | ||
console.log(info); | ||
}); | ||
``` | ||
### Event: 'subscription' | ||
Emitted in response to a unilateral `subscription` PDU from the watchman | ||
service. To enable these, you need to send a `subscribe` command to the service: | ||
``` | ||
// Subscribe to notifications about .js files | ||
client.command(['subscribe', process.cwd(), 'mysubscription', { | ||
expression: ["match", "*.js"] | ||
}], | ||
function(error, resp) { | ||
if (error) { | ||
// Probably an error in the subscription criteria | ||
console.log('failed to subscribe: ', error); | ||
return; | ||
} | ||
console.log('subscription ' + resp.subscribe + ' established'); | ||
} | ||
); | ||
// Subscription results are emitted via the subscription event. | ||
// Note that watchman will deliver a list of all current files | ||
// when you first subscribe, so you don't need to walk the tree | ||
// for yourself on startup | ||
client.on('subscription', function(resp) { | ||
console.log(resp.root, resp.subscription, resp.files); | ||
}); | ||
``` | ||
To cancel a subscription, use the `unsubscribe` command and pass in the name of | ||
the subscription you want to cancel: | ||
``` | ||
client.command(['unsubscribe', process.cwd(), 'mysubscription']); | ||
``` | ||
Note that subscriptions names are scoped to your connection to the watchman | ||
service; multiple different clients can use the same subscription name without | ||
fear of colliding. | ||
[Read the NodeJS watchman documentation](https://facebook.github.io/watchman/docs/nodejs.html) |
783
25351
35