index.js 8.9 KB

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