var extend = require('util')._extend; var EventEmitter = require('events').EventEmitter, inherits = require('util').inherits, DebugConnection = require('./debugger.js'); function createFailingConnection(reason) { return { connected: false, isRunning: false, request: function(command, args, callback) { callback({ message: new ErrorNotConnected(reason) }); }, close: function() { } }; } /** * @constructor * @param {number} debuggerPort */ function DebuggerClient(debuggerPort) { this._conn = createFailingConnection('node-inspector server was restarted'); this._port = debuggerPort; } inherits(DebuggerClient, EventEmitter); Object.defineProperties(DebuggerClient.prototype, { /** @type {boolean} */ isRunning: { get: function() { return this._conn.isRunning; } }, isConnected: { get: function() { return this._conn.connected; } } }); DebuggerClient.prototype.connect = function() { this._conn = DebugConnection.attachDebugger(this._port); this._conn. on('connect', this._onConnectionOpen.bind(this)). on('error', this.emit.bind(this, 'error')). on('close', this._onConnectionClose.bind(this)); this._registerDebuggerEventHandlers('break', 'afterCompile', 'exception'); }; /** * @param {...string} eventNames */ DebuggerClient.prototype._registerDebuggerEventHandlers = function(eventNames) { for (var i in arguments) { var name = arguments[i]; this._conn.on(name, this._emitDebuggerEvent.bind(this, name)); } }; DebuggerClient.prototype._onConnectionOpen = function() { //We need to update isRunning state before we continue with debugging. //Send the dummy requestso that we can read the state from the response. this.request('version', {}, function(error, result) { this.emit('connect'); }.bind(this)); }; /** * @param {string} reason */ DebuggerClient.prototype._onConnectionClose = function(reason) { this._conn = createFailingConnection(reason); this.emit('close', reason); }; /** * @param {string} name * @param {Object} message */ DebuggerClient.prototype._emitDebuggerEvent = function(name, message) { this.emit(name, message.body); }; /** * @param {string} command * @param {!Object} args * @param {function(error, response, refs)} callback */ DebuggerClient.prototype.request = function(command, args, callback) { if (typeof callback !== 'function') { callback = function(error) { if (!error) return; console.log('Warning: ignored V8 debugger error. %s', error); }; } // Note: we must not add args object if it was not sent. // E.g. resume (V8 request 'continue') does no work // correctly when args are empty instead of undefined if (args && args.maxStringLength == null) args.maxStringLength = 10000; this._conn.request(command, { arguments: args }, function(response) { var refsLookup; if (!response.success) callback(response.message); else { refsLookup = {}; if (response.refs) response.refs.forEach(function(r) { refsLookup[r.handle] = r; }); callback(null, response.body, refsLookup); } }); }; /** */ DebuggerClient.prototype.close = function() { this._conn.close(); }; /** * @param {number} breakpointId * @param {function(error, response, refs)} done */ DebuggerClient.prototype.clearBreakpoint = function(breakpointId, done) { this.request( 'clearbreakpoint', { breakpoint: breakpointId }, done ); }; /** * @param {string} expression * @param {function(error, response)} done */ DebuggerClient.prototype.evaluateGlobal = function(expression, done) { // Note: we can't simply evaluate JSON.stringify(`expression`) // because V8 debugger protocol truncates returned value to 80 characters // The workaround is to split the serialized value into multiple pieces, // each piece 80 characters long, send an array over the wire, // and reconstruct the value back here var code = 'JSON.stringify(' + expression + ').match(/.{1,80}/g).slice()'; this.request( 'evaluate', { expression: code, global: true }, function _handleEvaluateResponse(err, result, refs) { if (err) return done(err); if (result.type != 'object' && result.className != 'Array') { return done( new Error( 'Evaluate returned unexpected result:' + ' type: ' + result.type + ' className: ' + result.className ) ); } var fullJsonString = result.properties .filter(function isArrayIndex(p) { return /^\d+$/.test(p.name);}) .map(function resolvePropertyValue(p) { return refs[p.ref].value; }) .join(''); try { done(null, JSON.parse(fullJsonString)); } catch (e) { console.error('evaluateGlobal "%s" failed', expression); console.error(e.stack); console.error('--json-begin--\n%s--json-end--', fullJsonString); done(e); } } ); }; /** * @param {number} id * @param {function(Object, string?)} callback */ DebuggerClient.prototype.getScriptSourceById = function(id, callback) { this.request( 'scripts', { includeSource: true, types: 4, ids: [id] }, function handleScriptSourceResponse(err, result) { if (err) return callback(err); // Some modules gets unloaded (?) after they are parsed, // e.g. node_modules/express/node_modules/methods/index.js // V8 request 'scripts' returns an empty result in such case var source = result.length > 0 ? result[0].source : undefined; callback(null, source); } ); }; /** * @param {string} message * @constructor */ function ErrorNotConnected(message) { Error.call(this); this.name = ErrorNotConnected.name; this.message = message; } inherits(ErrorNotConnected, Error); exports.DebuggerClient = DebuggerClient; exports.ErrorNotConnected = ErrorNotConnected;