1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | 'use strict';
|
9 |
|
10 | var net = require('net');
|
11 | var EE = require('events').EventEmitter;
|
12 | var util = require('util');
|
13 | var childProcess = require('child_process');
|
14 | var bser = require('bser');
|
15 |
|
16 |
|
17 | var unilateralTags = ['subscription', 'log'];
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | function Client(options) {
|
26 | var self = this;
|
27 | EE.call(this);
|
28 |
|
29 | this.watchmanBinaryPath = 'watchman';
|
30 | if (options && options.watchmanBinaryPath) {
|
31 | this.watchmanBinaryPath = options.watchmanBinaryPath.trim();
|
32 | };
|
33 | this.commands = [];
|
34 | }
|
35 | util.inherits(Client, EE);
|
36 |
|
37 | module.exports.Client = Client;
|
38 |
|
39 |
|
40 | Client.prototype.sendNextCommand = function() {
|
41 | if (this.currentCommand) {
|
42 |
|
43 | return;
|
44 | }
|
45 |
|
46 | this.currentCommand = this.commands.shift();
|
47 | if (!this.currentCommand) {
|
48 |
|
49 | return;
|
50 | }
|
51 |
|
52 | this.socket.write(bser.dumpToBuffer(this.currentCommand.cmd));
|
53 | }
|
54 |
|
55 | Client.prototype.cancelCommands = function(why) {
|
56 | var error = new Error(why);
|
57 |
|
58 |
|
59 |
|
60 | var cmds = this.commands;
|
61 | this.commands = [];
|
62 |
|
63 | if (this.currentCommand) {
|
64 | cmds.unshift(this.currentCommand);
|
65 | this.currentCommand = null;
|
66 | }
|
67 |
|
68 |
|
69 | cmds.forEach(function(cmd) {
|
70 | cmd.cb(error);
|
71 | });
|
72 | }
|
73 |
|
74 | Client.prototype.connect = function() {
|
75 | var self = this;
|
76 |
|
77 | function makeSock(sockname) {
|
78 |
|
79 | self.bunser = new bser.BunserBuf();
|
80 |
|
81 | self.bunser.on('value', function(obj) {
|
82 |
|
83 |
|
84 |
|
85 | var unilateral = false;
|
86 | for (var i = 0; i < unilateralTags.length; i++) {
|
87 | var tag = unilateralTags[i];
|
88 | if (tag in obj) {
|
89 | unilateral = tag;
|
90 | }
|
91 | }
|
92 |
|
93 | if (unilateral) {
|
94 | self.emit(unilateral, obj);
|
95 | } else if (self.currentCommand) {
|
96 | var cmd = self.currentCommand;
|
97 | self.currentCommand = null;
|
98 | if ('error' in obj) {
|
99 | var error = new Error(obj.error);
|
100 | error.watchmanResponse = obj;
|
101 | cmd.cb(error);
|
102 | } else {
|
103 | cmd.cb(null, obj);
|
104 | }
|
105 | }
|
106 |
|
107 |
|
108 | self.sendNextCommand();
|
109 | });
|
110 | self.bunser.on('error', function(err) {
|
111 | self.emit('error', err);
|
112 | });
|
113 |
|
114 | self.socket = net.createConnection(sockname);
|
115 | self.socket.on('connect', function() {
|
116 | self.connecting = false;
|
117 | self.emit('connect');
|
118 | self.sendNextCommand();
|
119 | });
|
120 | self.socket.on('error', function(err) {
|
121 | self.connecting = false;
|
122 | self.emit('error', err);
|
123 | });
|
124 | self.socket.on('data', function(buf) {
|
125 | if (self.bunser) {
|
126 | self.bunser.append(buf);
|
127 | }
|
128 | });
|
129 | self.socket.on('end', function() {
|
130 | self.socket = null;
|
131 | self.bunser = null;
|
132 | self.cancelCommands('The watchman connection was closed');
|
133 | self.emit('end');
|
134 | });
|
135 | }
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | if (process.env.WATCHMAN_SOCK) {
|
142 | makeSock(process.env.WATCHMAN_SOCK);
|
143 | return;
|
144 | }
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | var args = ['--no-pretty', 'get-sockname'];
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | var proc = null;
|
156 | var spawnFailed = false;
|
157 |
|
158 | function spawnError(error) {
|
159 | if (spawnFailed) {
|
160 |
|
161 |
|
162 | return;
|
163 | }
|
164 | spawnFailed = true;
|
165 | if (error.code === 'EACCES' || error.errno === 'EACCES') {
|
166 | error.message = 'The Watchman CLI is installed but cannot ' +
|
167 | 'be spawned because of a permission problem';
|
168 | } else if (error.code === 'ENOENT' || error.errno === 'ENOENT') {
|
169 | error.message = 'Watchman was not found in PATH. See ' +
|
170 | 'https://facebook.github.io/watchman/docs/install.html ' +
|
171 | 'for installation instructions';
|
172 | }
|
173 | console.error('Watchman: ', error.message);
|
174 | self.emit('error', error);
|
175 | }
|
176 |
|
177 | try {
|
178 | proc = childProcess.spawn(this.watchmanBinaryPath, args, {
|
179 | stdio: ['ignore', 'pipe', 'pipe'],
|
180 | windowsHide: true
|
181 | });
|
182 | } catch (error) {
|
183 | spawnError(error);
|
184 | return;
|
185 | }
|
186 |
|
187 | var stdout = [];
|
188 | var stderr = [];
|
189 | proc.stdout.on('data', function(data) {
|
190 | stdout.push(data);
|
191 | });
|
192 | proc.stderr.on('data', function(data) {
|
193 | data = data.toString('utf8');
|
194 | stderr.push(data);
|
195 | console.error(data);
|
196 | });
|
197 | proc.on('error', function(error) {
|
198 | spawnError(error);
|
199 | });
|
200 |
|
201 | proc.on('close', function (code, signal) {
|
202 | if (code !== 0) {
|
203 | spawnError(new Error(
|
204 | self.watchmanBinaryPath + ' ' + args.join(' ') +
|
205 | ' returned with exit code=' + code + ', signal=' +
|
206 | signal + ', stderr= ' + stderr.join('')));
|
207 | return;
|
208 | }
|
209 | try {
|
210 | var obj = JSON.parse(stdout.join(''));
|
211 | if ('error' in obj) {
|
212 | var error = new Error(obj.error);
|
213 | error.watchmanResponse = obj;
|
214 | self.emit('error', error);
|
215 | return;
|
216 | }
|
217 | makeSock(obj.sockname);
|
218 | } catch (e) {
|
219 | self.emit('error', e);
|
220 | }
|
221 | });
|
222 | }
|
223 |
|
224 | Client.prototype.command = function(args, done) {
|
225 | done = done || function() {};
|
226 |
|
227 |
|
228 | this.commands.push({cmd: args, cb: done});
|
229 |
|
230 |
|
231 | if (!this.socket) {
|
232 | if (!this.connecting) {
|
233 | this.connecting = true;
|
234 | this.connect();
|
235 | return;
|
236 | }
|
237 | return;
|
238 | }
|
239 |
|
240 |
|
241 | this.sendNextCommand();
|
242 | }
|
243 |
|
244 | var cap_versions = {
|
245 | "cmd-watch-del-all": "3.1.1",
|
246 | "cmd-watch-project": "3.1",
|
247 | "relative_root": "3.3",
|
248 | "term-dirname": "3.1",
|
249 | "term-idirname": "3.1",
|
250 | "wildmatch": "3.7",
|
251 | }
|
252 |
|
253 |
|
254 | function vers_compare(a, b) {
|
255 | a = a.split('.');
|
256 | b = b.split('.');
|
257 | for (var i = 0; i < 3; i++) {
|
258 | var d = parseInt(a[i] || '0') - parseInt(b[i] || '0');
|
259 | if (d != 0) {
|
260 | return d;
|
261 | }
|
262 | }
|
263 | return 0;
|
264 | }
|
265 |
|
266 | function have_cap(vers, name) {
|
267 | if (name in cap_versions) {
|
268 | return vers_compare(vers, cap_versions[name]) >= 0;
|
269 | }
|
270 | return false;
|
271 | }
|
272 |
|
273 |
|
274 | Client.prototype._synthesizeCapabilityCheck = function(
|
275 | resp, optional, required) {
|
276 | resp.capabilities = {}
|
277 | var version = resp.version;
|
278 | optional.forEach(function (name) {
|
279 | resp.capabilities[name] = have_cap(version, name);
|
280 | });
|
281 | required.forEach(function (name) {
|
282 | var have = have_cap(version, name);
|
283 | resp.capabilities[name] = have;
|
284 | if (!have) {
|
285 | resp.error = 'client required capability `' + name +
|
286 | '` is not supported by this server';
|
287 | }
|
288 | });
|
289 | return resp;
|
290 | }
|
291 |
|
292 | Client.prototype.capabilityCheck = function(caps, done) {
|
293 | var optional = caps.optional || [];
|
294 | var required = caps.required || [];
|
295 | var self = this;
|
296 | this.command(['version', {
|
297 | optional: optional,
|
298 | required: required
|
299 | }], function (error, resp) {
|
300 | if (error) {
|
301 | done(error);
|
302 | return;
|
303 | }
|
304 | if (!('capabilities' in resp)) {
|
305 |
|
306 |
|
307 | resp = self._synthesizeCapabilityCheck(resp, optional, required);
|
308 | if (resp.error) {
|
309 | error = new Error(resp.error);
|
310 | error.watchmanResponse = resp;
|
311 | done(error);
|
312 | return;
|
313 | }
|
314 | }
|
315 | done(null, resp);
|
316 | });
|
317 | }
|
318 |
|
319 |
|
320 | Client.prototype.end = function() {
|
321 | this.cancelCommands('The client was ended');
|
322 | if (this.socket) {
|
323 | this.socket.end();
|
324 | this.socket = null;
|
325 | }
|
326 | this.bunser = null;
|
327 | }
|