Node Monitor v0.6.5

Show:

File: lib/Server.js

// Server.js (c) 2010-2014 Loren West and other contributors
// May be freely distributed under the MIT license.
// For further details and documentation:
// http://lorenwest.github.com/node-monitor
(function(root){

  // Module loading
  var Monitor = root.Monitor || require('./Monitor'),
      Config = Monitor.Config, _ = Monitor._, Backbone = Monitor.Backbone,
      log = Monitor.getLogger('Server'),
      stat = Monitor.getStatLogger('Server'),
      Connection = Monitor.Connection,
      Http = Monitor.commonJS ? require('http') : null,
      SocketIO = root.io || require('socket.io');

  /**
  * A server for accepting inbound connections from remote monitors
  *
  * Servers are created when a process wants to expose probe data to remote
  * monitors.  Example:
  *
  *     // Accept remote monitors
  *     var server = new Monitor.Server();
  *     server.start();
  *
  * An instance of this class represents a listening server accepting inbound
  * connections.  As inbound connections are detected, a new
  * <a href="Connection.html">Connection</a> object is created to manage
  * traffic on that connection.
  *
  * Security:  Make sure the port range specified in Monitor.Config (starting
  * at 42000) is not exposed outside your internal network.  If you desire a
  * different security model, create your secure server and pass it to the
  * constructor.
  *
  * @class Server
  * @extends Backbone.Model
  * @constructor
  * @param model - Initial data model.  Can be a JS object or another Model.
  *     @param model.gateway {Boolean} - Allow incoming monitors to use me as a gateway (default false)
  *     @param model.server {HttpServer} - The listening node.js server.  Constructed by this class, or specified if a custom server is desired.
  *     @param model.port {Integer} - The connected port.  This is set upon start() if the server isn't specified on construction.
  */
  var Server = Monitor.Server = Backbone.Model.extend({

    initialize: function(params) {
      var t = this;
      t.isListening = false;
      t.connections = new Connection.List();
    },

    /**
    * Start accepting monitor connections
    *
    * This method starts listening for incoming monitor connections on the
    * server.
    *
    * If the server was specified during object creation, this binds the
    * socket.io service to the server.
    *
    * If the server was not specified during object creation, this will create
    * a server on the first available monitor port.
    *
    * @method start
    * @param options {Object} - Start options. OPTIONAL
    *     @param options.port {Integer} - Port to attempt listening on if server isn't specified.  Default: 42000
    *     @param options.attempt {Integer} - Attempt number for internal recursion detection.  Default: 1
    * @param callback {Function(error)} - Called when the server is accepting connections.
    */
    /**
    * The server has started
    *
    * This event is fired when the server has determined the port to accept
    * connections on, and has successfully configured the server to start
    * accepting new monitor connections.
    *
    * @event start
    */
    /**
    * A client error has been detected
    *
    * This event is fired if an error has been detected in the underlying
    * transport.  It may indicate message loss, and may result in a
    * subsequent stop event if the connection cannot be restored.
    *
    * @event error
    */
    start: function(options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = null;
      }
      options = options || {};
      callback = callback || function(){};
      var t = this, server = t.get('server'), error,
          startTime = Date.now(),
          port = options.port || Config.Monitor.serviceBasePort,
          attempt = options.attempt || 1,
          allowExternalConnections = Config.Monitor.allowExternalConnections;

      // Recursion detection.  Only scan for so many ports
      if (attempt > Config.Monitor.portsToScan) {
        error = {err:'connect:failure', msg: 'no ports available'};
        log.error('start', error);
        return callback(error);
      }

      // Bind to an existing server, or create a new server
      if (server) {
        t.bindEvents(callback);
      } else {
        server = Http.createServer();

        // Try next port if a server is listening on this port
        server.on('error', function(err) {
          if (err.code === 'EADDRINUSE') {
            // Error if the requested port is in use
            if (t.get('port')) {
              log.error('portInUse',{host:host, port:port});
              return callback({err:'portInUse'});
            }
            // Try the next port
            log.info('portInUse',{host:host, port:port});
            return t.start({port:port + 1, attempt:attempt + 1}, callback);
          }
          // Unknown error
          callback(err);
        });

        // Allow connections from INADDR_ANY or LOCALHOST only
        var host = allowExternalConnections ? '0.0.0.0' : '127.0.0.1';

        // Start listening, callback on success
        server.listen(port, host, function(){

          // Set a default NODE_APP_INSTANCE based on the available server port
          if (!process.env.NODE_APP_INSTANCE)  {
            process.env.NODE_APP_INSTANCE = '' + (port - Config.Monitor.serviceBasePort + 1);
          }

          // Record the server & port, and bind incoming events
          t.set({server: server, port: port});
          t.bindEvents(callback);
          log.info('listening', {
            appName: Config.Monitor.appName,
            NODE_APP_INSTANCE: process.env.NODE_APP_INSTANCE,
            listeningOn: host,
            port: port
          });
        });
      }
    },

    /**
    * Bind incoming socket events to the server
    *
    * This method binds to the socket events and attaches the socket.io
    * server.  It is called when the connection starts listening.
    *
    * @protected
    * @method bindEvents
    * @param callback {Function(error)} - Called when all events are bound
    */
    bindEvents: function(callback) {

      // Detect server errors
      var t = this, server = t.get('server');
      server.on('clientError', function(err){
        log.error('bindEvents', 'clientError detected on server', err);
        t.trigger('error', err);
      });
      server.on('close', function(err){
        server.hasEmittedClose = true;
        log.info('bindEvents.serverClose', 'Server has closed', err);
        t.stop();
      });

      // Start up the socket.io server.
      var socketIoParams = {
        log: false
      };
      t.socketServer = SocketIO.listen(server, socketIoParams);
      t.socketServer.sockets.on('connection', function (socket) {
        var connection = Monitor.getRouter().addConnection({
          socket: socket, gateway: t.get('gateway')
        });
        t.connections.add(connection);
        var onDisconnect = function(reason) {
          t.connections.remove(connection);
          Monitor.getRouter().removeConnection(connection);
          connection.off('disconnect', onDisconnect);
          log.info('client.disconnect', 'Disconnected client socket');
        };
        connection.on('disconnect', onDisconnect);
        log.info('client.connect', 'Connected client socket');
      });

      // Notify that we've started
      t.isListening = true;
      if (callback) {callback(null);}
      t.trigger('start');
    },

    /**
    * Stop processing inbound monitor traffic
    *
    * This method stops accepting new inbound monitor connections, and closes
    * all existing monitor connections associated with the server.
    *
    * @method stop
    * @param callback {Function(error)} - Called when the server has stopped
    */
    /**
    * The server has stopped
    *
    * This event is fired after the server has stopped accepting inbound
    * connections, and has closed all existing connections and released
    * associated resources.
    *
    * @event stop
    */
    stop: function(callback) {
      var t = this, server = t.get('server'), router = Monitor.getRouter();
      callback = callback || function(){};

      // Call the callback, but don't stop more than once.
      if (!t.isListening) {
        return callback();
      }

      // Release resources
      t.connections.each(router.removeConnection, router);
      t.connections.reset();

      // Shut down the server
      t.isListening = false;
      server.close();

      // Send notices
      t.trigger('stop');
      return callback();
    }
  });

  /**
  * Constructor for a list of Server objects
  *
  *     var myList = new Server.List(initialElements);
  *
  * @static
  * @method List
  * @param [items] {Array} Initial list items.  These can be raw JS objects or Server data model objects.
  * @return {Backbone.Collection} Collection of Server data model objects
  */
  Server.List = Backbone.Collection.extend({model: Server});

}(this));