Node Monitor v0.6.5

Show:

File: lib/probes/ReplProbe.js

// ReplProbe.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 - this runs server-side only
  var Monitor = root.Monitor || require('../Monitor'),
      _ = Monitor._,
      Probe = Monitor.Probe,
      REPL = require('repl'),
      Stream = require('stream'),
      util = require('util'),
      events = require('events'),
      ChildProcess = require('child_process');

  // Statics
  var CONSOLE_PROMPT = '> ';
  var NEW_REPL = (typeof REPL.disableColors === 'undefined');

  /**
  * A probe based Read-Execute-Print-Loop console for node.js processes
  *
  * @class ReplProbe
  * @extends Probe
  * @constructor
  * @param initParams {Object} Probe initialization parameters
  *     @param initParams.uniqueInstance - Usually specified to obtain a unique REPL probe instance
  * @param model {Object} Monitor data model elements
  *     @param model.output {String} Last (current) REPL output line
  *     @param model.sequence {Integer} Increasing sequence number - to enforce unique line output
  */
  var ReplProbe = Monitor.ReplProbe = Probe.extend({

    probeClass: 'Repl',
    description: 'A socket.io based Read-Execute-Print-Loop console for node.js processes.',
    defaults: {
      // This assures output events are sent, even if the
      // data is the same as the prior output.
      sequence: 0,
      output: ''
    },

    initialize: function(attributes, options){
      var t = this;
      Probe.prototype.initialize.apply(t, arguments);

      // Don't send change events before connected
      process.nextTick(function(){
        t.stream = new ReplStream(t);
        if (NEW_REPL) {
          t.repl = require('repl').start({
            prompt: CONSOLE_PROMPT,
            input: t.stream,
            output: t.stream
          });
        } else {
          t.repl = REPL.start(CONSOLE_PROMPT, t.stream);
        }
        t.htmlConsole = new HtmlConsole(t);
        t.shellCmd = null;
        t.repl.context.console = t.htmlConsole;
      });
    },

    /**
    * Send output to the terminal
    *
    * This forces the change event even if the last output is the same
    * as this output.
    *
    * @protected
    * @method output
    * @param str {String} String to output to the repl console
    */
    _output: function(str) {
      var t = this;
      t.set({
        output: str,
        sequence: t.get('sequence') + 1
      });
    },

    /**
    * Release any resources consumed by this probe.
    *
    * Stop the REPL console.  Consoles live 1-1 with a UI counterpart, so stop
    * requests exit the underlying repl console.  If the probe is re-started it
    * will get a new repl stream and console.
    *
    * @method release
    */
    release: function(){
      var t = this;
      t.stream = null;
      t.repl = null;
    },

    /**
    * Process an autocomplete request from the client
    *
    * @method autocomplete
    * @param {Object} params Named parameters
    * @param {Function(error, returnParams)} callback Callback function
    */
    autocomplete_control: function(params, callback) {
      var t = this;
      if (typeof(params) !== 'string' || params.length < 1) {
        callback("Autocomplete paramter must be a nonzero string");
      }

      // Forward to the completion mechanism if it can be completed
      if (params.substr(-1).match(/([0-9])|([a-z])|([A-Z])|([_])/)) {
        t.repl.complete(params, callback);
      } else {
        // Return a no-op autocomplete
        callback(null, [[],'']);
      }
    },

    /**
    * Handle user input from the console line
    *
    * @method input
    * @param {Object} params Named parameters
    * @param {Function(error, returnParams)} callback Callback function
    */
    input_control: function(params, callback) {
      var t = this;
      if (params === '.break' && t.shellCmd) {
        t.shellCmd.kill();
      }
      if (NEW_REPL) {
        t.stream.emit('data', params + "\n");
      } else {
        t.stream.emit('data', params);
      }
      return callback(null);
    },

    /**
    * Execute a shell command
    *
    * @method sh
    * @param {Object} params Named parameters
    * @param {Function(error, returnParams)} callback Callback function
    */
    sh_control: function(params, callback) {
      var t = this;
      return callback(null, t._runShellCmd(params));
    },

    /**
    * Run a shell command and emit the output to the browser.
    *
    * @private
    * @method _runShellCmd
    * @param {String} command - The shell command to invoke
    */
    _runShellCmd: function(command) {
      var t = this;
      t.shellCmd = ChildProcess.exec(command, function(err, stdout, stderr) {
        if (err) {
          var outstr = 'exit';
          if (err.code) {
            outstr += ' (' + err.code + ')';
          }
          if (err.signal) {
            outstr += ' ' + err.signal;
          }
          t._output(outstr);
          return null;
        }
        if (stdout.length) {
          t._output(stdout);
        }
        if (stderr.length) {
          t._output(stderr);
        }
        t.shellCmd = null;
        t._output(CONSOLE_PROMPT);
      });
      return null;
    }

  });

  // Define an internal stream class for the probe
  var ReplStream = function(probe){
    var t = this;
    t.probe = probe;
    events.EventEmitter.call(t);
    if (t.setEncoding) {
      t.setEncoding('utf8');
    }
  };
  util.inherits(ReplStream, events.EventEmitter);
  // util.inherits(ReplStream, require('stream'));
  ReplStream.prototype.readable = true;
  ReplStream.prototype.writable = true;
  ['pause','resume','destroySoon','pipe', 'end']
    .forEach(function(fnName){
      ReplStream.prototype[fnName] = function(){
        console.log("REPL Stream function unexpected: " + fnName);
      };
    });
  ['resume']
    .forEach(function(fnName){
      ReplStream.prototype[fnName] = function(){
        // Handled
      };
    });
  ReplStream.prototype.write = function(data) {
    var t = this;
    t.probe._output(data);
  };
  ReplStream.prototype.destroy = function(data) {
    var t = this;
  console.log("REPL stream destroy " + t.probe.get('id'));
    t.probe.stop();
  };

  // Define format if it's not in util.
  var formatRegExp = /%[sdj]/g;
  var format = util.format || function (f) {
    if (typeof f !== 'string') {
      var objects = [];
      for (var i = 0; i < arguments.length; i++) {
        objects.push(util.inspect(arguments[i]));
      }
      return objects.join(' ');
    }
    var j = 1;
    var args = arguments;
    var str = String(f).replace(formatRegExp, function(x) {
      switch (x) {
        case '%s': return String(args[j++]);
        case '%d': return Number(args[j++]);
        case '%j': return JSON.stringify(args[j++]);
        default:
          return x;
      }
    });
    for (var len = args.length, x = args[j]; j < len; x = args[++j]) {
      if (x === null || typeof x !== 'object') {
        str += ' ' + x;
      } else {
        str += ' ' + util.inspect(x);
      }
    }
    return str;
  };

  // Re-define the console so it goes to the HTML window
  var HtmlConsole = function(probe){
    this.probe = probe;
  };
  HtmlConsole.prototype.log = function(msg) {
    this.probe._output(format.apply(this, arguments));
  };
  HtmlConsole.prototype.info = HtmlConsole.prototype.log;
  HtmlConsole.prototype.warn = HtmlConsole.prototype.log;
  HtmlConsole.prototype.error = HtmlConsole.prototype.log;
  HtmlConsole.prototype.dir = function(object) {
    this.probe._output(util.inspect(object));
  };
  var times = {};
  HtmlConsole.prototype.time = function(label) {
    times[label] = Date.now();
  };
  HtmlConsole.prototype.timeEnd = function(label) {
    var duration = Date.now() - times[label];
    this.log('%s: %dms', label, duration);
  };

}(this));