UNPKG

8.9 kBJavaScriptView Raw
1/* Copyright 2014-present Facebook, Inc.
2 * Licensed under the Apache License, Version 2.0 */
3
4'use strict';
5
6var net = require('net');
7var EE = require('events').EventEmitter;
8var util = require('util');
9var childProcess = require('child_process');
10var bser = require('bser');
11
12// We'll emit the responses to these when they get sent down to us
13var unilateralTags = ['subscription', 'log'];
14
15/**
16 * @param options An object with the following optional keys:
17 * * 'watchmanBinaryPath' (string) Absolute path to the watchman binary.
18 * If not provided, the Client locates the binary using the PATH specified
19 * by the node child_process's default env.
20 */
21function Client(options) {
22 var self = this;
23 EE.call(this);
24
25 this.watchmanBinaryPath = 'watchman';
26 if (options && options.watchmanBinaryPath) {
27 this.watchmanBinaryPath = options.watchmanBinaryPath.trim();
28 };
29 this.commands = [];
30}
31util.inherits(Client, EE);
32
33module.exports.Client = Client;
34
35// Try to send the next queued command, if any
36Client.prototype.sendNextCommand = function() {
37 if (this.currentCommand) {
38 // There's a command pending response, don't send this new one yet
39 return;
40 }
41
42 this.currentCommand = this.commands.shift();
43 if (!this.currentCommand) {
44 // No further commands are queued
45 return;
46 }
47
48 this.socket.write(bser.dumpToBuffer(this.currentCommand.cmd));
49}
50
51Client.prototype.cancelCommands = function(why) {
52 var error = new Error(why);
53
54 // Steal all pending commands before we start cancellation, in
55 // case something decides to schedule more commands
56 var cmds = this.commands;
57 this.commands = [];
58
59 if (this.currentCommand) {
60 cmds.unshift(this.currentCommand);
61 this.currentCommand = null;
62 }
63
64 // Synthesize an error condition for any commands that were queued
65 cmds.forEach(function(cmd) {
66 cmd.cb(error);
67 });
68}
69
70Client.prototype.connect = function() {
71 var self = this;
72
73 function makeSock(sockname) {
74 // bunser will decode the watchman BSER protocol for us
75 self.bunser = new bser.BunserBuf();
76 // For each decoded line:
77 self.bunser.on('value', function(obj) {
78 // Figure out if this is a unliteral response or if it is the
79 // response portion of a request-response sequence. At the time
80 // of writing, there are only two possible unilateral responses.
81 var unilateral = false;
82 for (var i = 0; i < unilateralTags.length; i++) {
83 var tag = unilateralTags[i];
84 if (tag in obj) {
85 unilateral = tag;
86 }
87 }
88
89 if (unilateral) {
90 self.emit(unilateral, obj);
91 } else if (self.currentCommand) {
92 var cmd = self.currentCommand;
93 self.currentCommand = null;
94 if ('error' in obj) {
95 var error = new Error(obj.error);
96 error.watchmanResponse = obj;
97 cmd.cb(error);
98 } else {
99 cmd.cb(null, obj);
100 }
101 }
102
103 // See if we can dispatch the next queued command, if any
104 self.sendNextCommand();
105 });
106 self.bunser.on('error', function(err) {
107 self.emit('error', err);
108 });
109
110 self.socket = net.createConnection(sockname);
111 self.socket.on('connect', function() {
112 self.connecting = false;
113 self.emit('connect');
114 self.sendNextCommand();
115 });
116 self.socket.on('error', function(err) {
117 self.connecting = false;
118 self.emit('error', err);
119 });
120 self.socket.on('data', function(buf) {
121 if (self.bunser) {
122 self.bunser.append(buf);
123 }
124 });
125 self.socket.on('end', function() {
126 self.socket = null;
127 self.bunser = null;
128 self.cancelCommands('The watchman connection was closed');
129 self.emit('end');
130 });
131 }
132
133 // triggers will export the sock path to the environment.
134 // If we're invoked in such a way, we can simply pick up the
135 // definition from the environment and avoid having to fork off
136 // a process to figure it out
137 if (process.env.WATCHMAN_SOCK) {
138 makeSock(process.env.WATCHMAN_SOCK);
139 return;
140 }
141
142 // We need to ask the client binary where to find it.
143 // This will cause the service to start for us if it isn't
144 // already running.
145 var args = ['--no-pretty', 'get-sockname'];
146
147 // We use the more elaborate spawn rather than exec because there
148 // are some error cases on Windows where process spawning can hang.
149 // It is desirable to pipe stderr directly to stderr live so that
150 // we can discover the problem.
151 var proc = null;
152 var spawnFailed = false;
153
154 function spawnError(error) {
155 if (spawnFailed) {
156 // For ENOENT, proc 'close' will also trigger with a negative code,
157 // let's suppress that second error.
158 return;
159 }
160 spawnFailed = true;
161 if (error.errno === 'EACCES') {
162 error.message = 'The Watchman CLI is installed but cannot ' +
163 'be spawned because of a permission problem';
164 } else if (error.errno === 'ENOENT') {
165 error.message = 'Watchman was not found in PATH. See ' +
166 'https://facebook.github.io/watchman/docs/install.html ' +
167 'for installation instructions';
168 }
169 console.error('Watchman: ', error.message);
170 self.emit('error', error);
171 }
172
173 try {
174 proc = childProcess.spawn(this.watchmanBinaryPath, args, {
175 stdio: ['ignore', 'pipe', 'pipe']
176 });
177 } catch (error) {
178 spawnError(error);
179 return;
180 }
181
182 var stdout = [];
183 var stderr = [];
184 proc.stdout.on('data', function(data) {
185 stdout.push(data);
186 });
187 proc.stderr.on('data', function(data) {
188 data = data.toString('utf8');
189 stderr.push(data);
190 console.error(data);
191 });
192 proc.on('error', function(error) {
193 spawnError(error);
194 });
195
196 proc.on('close', function (code, signal) {
197 if (code !== 0) {
198 spawnError(new Error(
199 self.watchmanBinaryPath + ' ' + args.join(' ') +
200 ' returned with exit code=' + code + ', signal=' +
201 signal + ', stderr= ' + stderr.join('')));
202 return;
203 }
204 try {
205 var obj = JSON.parse(stdout.join(''));
206 if ('error' in obj) {
207 var error = new Error(obj.error);
208 error.watchmanResponse = obj;
209 self.emit('error', error);
210 return;
211 }
212 makeSock(obj.sockname);
213 } catch (e) {
214 self.emit('error', e);
215 }
216 });
217}
218
219Client.prototype.command = function(args, done) {
220 done = done || function() {};
221
222 // Queue up the command
223 this.commands.push({cmd: args, cb: done});
224
225 // Establish a connection if we don't already have one
226 if (!this.socket) {
227 if (!this.connecting) {
228 this.connecting = true;
229 this.connect();
230 return;
231 }
232 return;
233 }
234
235 // If we're already connected and idle, try sending the command immediately
236 this.sendNextCommand();
237}
238
239var cap_versions = {
240 "cmd-watch-del-all": "3.1.1",
241 "cmd-watch-project": "3.1",
242 "relative_root": "3.3",
243 "term-dirname": "3.1",
244 "term-idirname": "3.1",
245 "wildmatch": "3.7",
246}
247
248// Compares a vs b, returns < 0 if a < b, > 0 if b > b, 0 if a == b
249function vers_compare(a, b) {
250 a = a.split('.');
251 b = b.split('.');
252 for (var i = 0; i < 3; i++) {
253 var d = parseInt(a[i] || '0') - parseInt(b[i] || '0');
254 if (d != 0) {
255 return d;
256 }
257 }
258 return 0; // Equal
259}
260
261function have_cap(vers, name) {
262 if (name in cap_versions) {
263 return vers_compare(vers, cap_versions[name]) >= 0;
264 }
265 return false;
266}
267
268// This is a helper that we expose for testing purposes
269Client.prototype._synthesizeCapabilityCheck = function(
270 resp, optional, required) {
271 resp.capabilities = {}
272 var version = resp.version;
273 optional.forEach(function (name) {
274 resp.capabilities[name] = have_cap(version, name);
275 });
276 required.forEach(function (name) {
277 var have = have_cap(version, name);
278 resp.capabilities[name] = have;
279 if (!have) {
280 resp.error = 'client required capability `' + name +
281 '` is not supported by this server';
282 }
283 });
284 return resp;
285}
286
287Client.prototype.capabilityCheck = function(caps, done) {
288 var optional = caps.optional || [];
289 var required = caps.required || [];
290 var self = this;
291 this.command(['version', {
292 optional: optional,
293 required: required
294 }], function (error, resp) {
295 if (error) {
296 done(error);
297 return;
298 }
299 if (!('capabilities' in resp)) {
300 // Server doesn't support capabilities, so we need to
301 // synthesize the results based on the version
302 resp = self._synthesizeCapabilityCheck(resp, optional, required);
303 if (resp.error) {
304 error = new Error(resp.error);
305 error.watchmanResponse = resp;
306 done(error);
307 return;
308 }
309 }
310 done(null, resp);
311 });
312}
313
314// Close the connection to the service
315Client.prototype.end = function() {
316 this.cancelCommands('The client was ended');
317 if (this.socket) {
318 this.socket.end();
319 this.socket = null;
320 }
321 this.bunser = null;
322}