You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							322 lines
						
					
					
						
							8.7 KiB
						
					
					
				
			
		
		
	
	
							322 lines
						
					
					
						
							8.7 KiB
						
					
					
				/* Copyright 2014-present Facebook, Inc. | 
						|
 * Licensed under the Apache License, Version 2.0 */ | 
						|
 | 
						|
'use strict'; | 
						|
 | 
						|
var net = require('net'); | 
						|
var EE = require('events').EventEmitter; | 
						|
var util = require('util'); | 
						|
var childProcess = require('child_process'); | 
						|
var bser = require('bser'); | 
						|
 | 
						|
// We'll emit the responses to these when they get sent down to us | 
						|
var unilateralTags = ['subscription', 'log']; | 
						|
 | 
						|
/** | 
						|
 * @param options An object with the following optional keys: | 
						|
 *   * 'watchmanBinaryPath' (string) Absolute path to the watchman binary. | 
						|
 *     If not provided, the Client locates the binary using the PATH specified | 
						|
 *     by the node child_process's default env. | 
						|
 */ | 
						|
function Client(options) { | 
						|
  var self = this; | 
						|
  EE.call(this); | 
						|
 | 
						|
  this.watchmanBinaryPath = 'watchman'; | 
						|
  if (options && options.watchmanBinaryPath) { | 
						|
    this.watchmanBinaryPath = options.watchmanBinaryPath.trim(); | 
						|
  }; | 
						|
  this.commands = []; | 
						|
} | 
						|
util.inherits(Client, EE); | 
						|
 | 
						|
module.exports.Client = Client; | 
						|
 | 
						|
// Try to send the next queued command, if any | 
						|
Client.prototype.sendNextCommand = function() { | 
						|
  if (this.currentCommand) { | 
						|
    // There's a command pending response, don't send this new one yet | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  this.currentCommand = this.commands.shift(); | 
						|
  if (!this.currentCommand) { | 
						|
    // No further commands are queued | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  this.socket.write(bser.dumpToBuffer(this.currentCommand.cmd)); | 
						|
} | 
						|
 | 
						|
Client.prototype.cancelCommands = function(why) { | 
						|
  var error = new Error(why); | 
						|
 | 
						|
  // Steal all pending commands before we start cancellation, in | 
						|
  // case something decides to schedule more commands | 
						|
  var cmds = this.commands; | 
						|
  this.commands = []; | 
						|
 | 
						|
  if (this.currentCommand) { | 
						|
    cmds.unshift(this.currentCommand); | 
						|
    this.currentCommand = null; | 
						|
  } | 
						|
 | 
						|
  // Synthesize an error condition for any commands that were queued | 
						|
  cmds.forEach(function(cmd) { | 
						|
    cmd.cb(error); | 
						|
  }); | 
						|
} | 
						|
 | 
						|
Client.prototype.connect = function() { | 
						|
  var self = this; | 
						|
 | 
						|
  function makeSock(sockname) { | 
						|
    // bunser will decode the watchman BSER protocol for us | 
						|
    self.bunser = new bser.BunserBuf(); | 
						|
    // For each decoded line: | 
						|
    self.bunser.on('value', function(obj) { | 
						|
      // Figure out if this is a unliteral response or if it is the | 
						|
      // response portion of a request-response sequence.  At the time | 
						|
      // of writing, there are only two possible unilateral responses. | 
						|
      var unilateral = false; | 
						|
      for (var i = 0; i < unilateralTags.length; i++) { | 
						|
        var tag = unilateralTags[i]; | 
						|
        if (tag in obj) { | 
						|
          unilateral = tag; | 
						|
        } | 
						|
      } | 
						|
 | 
						|
      if (unilateral) { | 
						|
        self.emit(unilateral, obj); | 
						|
      } else if (self.currentCommand) { | 
						|
        var cmd = self.currentCommand; | 
						|
        self.currentCommand = null; | 
						|
        if ('error' in obj) { | 
						|
          var error = new Error(obj.error); | 
						|
          error.watchmanResponse = obj; | 
						|
          cmd.cb(error); | 
						|
        } else { | 
						|
          cmd.cb(null, obj); | 
						|
        } | 
						|
      } | 
						|
 | 
						|
      // See if we can dispatch the next queued command, if any | 
						|
      self.sendNextCommand(); | 
						|
    }); | 
						|
    self.bunser.on('error', function(err) { | 
						|
      self.emit('error', err); | 
						|
    }); | 
						|
 | 
						|
    self.socket = net.createConnection(sockname); | 
						|
    self.socket.on('connect', function() { | 
						|
      self.connecting = false; | 
						|
      self.emit('connect'); | 
						|
      self.sendNextCommand(); | 
						|
    }); | 
						|
    self.socket.on('error', function(err) { | 
						|
      self.connecting = false; | 
						|
      self.emit('error', err); | 
						|
    }); | 
						|
    self.socket.on('data', function(buf) { | 
						|
      if (self.bunser) { | 
						|
        self.bunser.append(buf); | 
						|
      } | 
						|
    }); | 
						|
    self.socket.on('end', function() { | 
						|
      self.socket = null; | 
						|
      self.bunser = null; | 
						|
      self.cancelCommands('The watchman connection was closed'); | 
						|
      self.emit('end'); | 
						|
    }); | 
						|
  } | 
						|
 | 
						|
  // triggers will export the sock path to the environment. | 
						|
  // If we're invoked in such a way, we can simply pick up the | 
						|
  // definition from the environment and avoid having to fork off | 
						|
  // a process to figure it out | 
						|
  if (process.env.WATCHMAN_SOCK) { | 
						|
    makeSock(process.env.WATCHMAN_SOCK); | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  // We need to ask the client binary where to find it. | 
						|
  // This will cause the service to start for us if it isn't | 
						|
  // already running. | 
						|
  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. | 
						|
  var proc = null; | 
						|
  var spawnFailed = false; | 
						|
 | 
						|
  function spawnError(error) { | 
						|
    if (spawnFailed) { | 
						|
      // For ENOENT, proc 'close' will also trigger with a negative code, | 
						|
      // let's suppress that second error. | 
						|
      return; | 
						|
    } | 
						|
    spawnFailed = true; | 
						|
    if (error.errno === 'EACCES') { | 
						|
      error.message = 'The Watchman CLI is installed but cannot ' + | 
						|
                      'be spawned because of a permission problem'; | 
						|
    } else if (error.errno === 'ENOENT') { | 
						|
      error.message = 'Watchman was not found in PATH.  See ' + | 
						|
          'https://facebook.github.io/watchman/docs/install.html ' + | 
						|
          'for installation instructions'; | 
						|
    } | 
						|
    console.error('Watchman: ', error.message); | 
						|
    self.emit('error', error); | 
						|
  } | 
						|
 | 
						|
  try { | 
						|
    proc = childProcess.spawn(this.watchmanBinaryPath, args, { | 
						|
      stdio: ['ignore', 'pipe', 'pipe'] | 
						|
    }); | 
						|
  } catch (error) { | 
						|
    spawnError(error); | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  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.error(data); | 
						|
  }); | 
						|
  proc.on('error', function(error) { | 
						|
    spawnError(error); | 
						|
  }); | 
						|
 | 
						|
  proc.on('close', function (code, signal) { | 
						|
    if (code !== 0) { | 
						|
      spawnError(new Error( | 
						|
          self.watchmanBinaryPath + ' ' + args.join(' ') + | 
						|
          ' returned with exit code=' + code + ', signal=' + | 
						|
          signal + ', stderr= ' + stderr.join(''))); | 
						|
      return; | 
						|
    } | 
						|
    try { | 
						|
      var obj = JSON.parse(stdout.join('')); | 
						|
      if ('error' in obj) { | 
						|
        var error = new Error(obj.error); | 
						|
        error.watchmanResponse = obj; | 
						|
        self.emit('error', error); | 
						|
        return; | 
						|
      } | 
						|
      makeSock(obj.sockname); | 
						|
    } catch (e) { | 
						|
      self.emit('error', e); | 
						|
    } | 
						|
  }); | 
						|
} | 
						|
 | 
						|
Client.prototype.command = function(args, done) { | 
						|
  done = done || function() {}; | 
						|
 | 
						|
  // Queue up the command | 
						|
  this.commands.push({cmd: args, cb: done}); | 
						|
 | 
						|
  // Establish a connection if we don't already have one | 
						|
  if (!this.socket) { | 
						|
    if (!this.connecting) { | 
						|
      this.connecting = true; | 
						|
      this.connect(); | 
						|
      return; | 
						|
    } | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  // If we're already connected and idle, try sending the command immediately | 
						|
  this.sendNextCommand(); | 
						|
} | 
						|
 | 
						|
var 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 (var i = 0; i < 3; i++) { | 
						|
    var 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 = {} | 
						|
  var 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 | 
						|
Client.prototype.end = function() { | 
						|
  this.cancelCommands('The client was ended'); | 
						|
  if (this.socket) { | 
						|
    this.socket.end(); | 
						|
    this.socket = null; | 
						|
  } | 
						|
  this.bunser = null; | 
						|
}
 | 
						|
 |