1 |
|
2 |
|
3 |
|
4 | 'use strict';
|
5 |
|
6 | var net = require('net');
|
7 | var EE = require('events').EventEmitter;
|
8 | var util = require('util');
|
9 | var childProcess = require('child_process');
|
10 | var bser = require('bser');
|
11 |
|
12 |
|
13 | var unilateralTags = ['subscription', 'log'];
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | function 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 | }
|
31 | util.inherits(Client, EE);
|
32 |
|
33 | module.exports.Client = Client;
|
34 |
|
35 |
|
36 | Client.prototype.sendNextCommand = function() {
|
37 | if (this.currentCommand) {
|
38 |
|
39 | return;
|
40 | }
|
41 |
|
42 | this.currentCommand = this.commands.shift();
|
43 | if (!this.currentCommand) {
|
44 |
|
45 | return;
|
46 | }
|
47 |
|
48 | this.socket.write(bser.dumpToBuffer(this.currentCommand.cmd));
|
49 | }
|
50 |
|
51 | Client.prototype.cancelCommands = function(why) {
|
52 | var error = new Error(why);
|
53 |
|
54 |
|
55 |
|
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 |
|
65 | cmds.forEach(function(cmd) {
|
66 | cmd.cb(error);
|
67 | });
|
68 | }
|
69 |
|
70 | Client.prototype.connect = function() {
|
71 | var self = this;
|
72 |
|
73 | function makeSock(sockname) {
|
74 |
|
75 | self.bunser = new bser.BunserBuf();
|
76 |
|
77 | self.bunser.on('value', function(obj) {
|
78 |
|
79 |
|
80 |
|
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 |
|
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 |
|
134 |
|
135 |
|
136 |
|
137 | if (process.env.WATCHMAN_SOCK) {
|
138 | makeSock(process.env.WATCHMAN_SOCK);
|
139 | return;
|
140 | }
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | var args = ['--no-pretty', 'get-sockname'];
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 | var proc = null;
|
152 | var spawnFailed = false;
|
153 |
|
154 | function spawnError(error) {
|
155 | if (spawnFailed) {
|
156 |
|
157 |
|
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 |
|
219 | Client.prototype.command = function(args, done) {
|
220 | done = done || function() {};
|
221 |
|
222 |
|
223 | this.commands.push({cmd: args, cb: done});
|
224 |
|
225 |
|
226 | if (!this.socket) {
|
227 | if (!this.connecting) {
|
228 | this.connecting = true;
|
229 | this.connect();
|
230 | return;
|
231 | }
|
232 | return;
|
233 | }
|
234 |
|
235 |
|
236 | this.sendNextCommand();
|
237 | }
|
238 |
|
239 | var 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 |
|
249 | function 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;
|
259 | }
|
260 |
|
261 | function 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 |
|
269 | Client.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 |
|
287 | Client.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 |
|
301 |
|
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 |
|
315 | Client.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 | }
|