node-inspector/lib/DebuggerClient.js

230 строки
5.8 KiB
JavaScript

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;