Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@bmeck
Created April 18, 2014 17:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmeck/7f5bf6b0672e9d9aa1a4 to your computer and use it in GitHub Desktop.
Save bmeck/7f5bf6b0672e9d9aa1a4 to your computer and use it in GitHub Desktop.
var EventEmitter = require('events').EventEmitter;
var readline = require('readline');
var tty = require('tty');
function DebuggerConnection(conn, toolname) {
EventEmitter.call(this);
this.seq = 1;
this.conn = conn;
this.toolname = toolname || "node";
this.outstanding = {};
this.state = DebuggerConnection.READING_HEADER;
this._parser_tmp_buffer = null;
this._parser_tmp_prev_header = null;
this._response = null;
this.on('message', this.onmessage);
this.conn.on('readable', this.onreadable.bind(this));
return this;
}
require('util').inherits(DebuggerConnection, EventEmitter);
DebuggerConnection.ERROR = 0;
DebuggerConnection.READING_HEADER = 2;
DebuggerConnection.READING_CONTENT = 4;
DebuggerConnection.prototype.onmessage = function (msg) {
if (msg.body) {
var body = msg.body;
var fn = this.outstanding[body.request_seq]
if (fn) {
if (body.success) {
fn(null, msg);
}
else {
fn(new Error(body.message, msg));
}
}
}
}
DebuggerConnection.prototype.onreadable = function () {
var unread = this.conn.read();
if (unread) this._read(unread);
}
DebuggerConnection.prototype._read = function (buff) {
var state = this.state;
if (state === DebuggerConnection.ERROR) {
self.conn.removeListener('readable', this.onreadable);
return;
}
var remaining_buff = this._parser_tmp_buffer ?
Buffer.concat([this._parser_tmp_buffer, buff]) :
buff;
while (true) {
if (this.state === DebuggerConnection.READING_HEADER) {
var line_and_buff = this._readLine(remaining_buff);
if (!line_and_buff) {
break;
}
else {
var line = String(line_and_buff.line);
remaining_buff = line_and_buff.remaining;
if (line === '') {
this.state = DebuggerConnection.READING_CONTENT;
}
else if (/^\s/.test(line)) {
this._response[this._parser_tmp_prev_header] += line.replace(/^\s+/,' ').replace(/\s+$/, '');
}
else {
var index = line.indexOf(':');
var name = line.substr(0, index).trim().toLowerCase();
var value = line.substr(index+1).trim();
this.setupResponse();
this._response.headers[name] = value;
}
}
}
else if (this.state === DebuggerConnection.READING_CONTENT) {
var content_and_buff = this._readNumberOfBytes(remaining_buff, +this._response.headers['content-length']);
if (!content_and_buff) {
break;
}
else {
this.state = DebuggerConnection.READING_HEADER;
var content = content_and_buff.content;
remaining_buff = content_and_buff.remaining;
this.setupResponse();
if (content.length > 0) {
try {
var msg = JSON.parse(String(content));
}
catch (e) {
this.emit('error', e);
break;
}
this._response.body = msg;
}
this.emit('message', this._response);
this._response = null;
}
}
}
this._parser_tmp_buffer = remaining_buff;
}
DebuggerConnection.prototype.setupResponse = function () {
this._response = this._response || {headers:{},body:null};
}
DebuggerConnection.prototype._readLine = function (buff) {
for (var i = 0; i < buff.length; i++) {
if (buff[i] === 0x0A) {
var end = i;
if (end > 0 && buff[end-1] === 0x0D) end--;
return {
line: buff.slice(0, end),
remaining: buff.slice(i+1, buff.length)
}
}
}
return null;
}
DebuggerConnection.prototype._readNumberOfBytes = function (buff, length) {
if (buff.length >= length) {
return {
content: buff.slice(0, length),
remaining: buff.slice(length, buff.length)
}
}
return null;
}
DebuggerConnection.prototype.send = function (headers, msg, cb) {
var conn = this.conn;
if (headers) for (var k in headers) {
var v = headers[k];
if (Array.isArray(v)) {
v.forEach(function (v) {
conn.write(k + ': ' + v + '\r\n');
});
}
else {
conn.write(k + ': ' + v + '\r\n');
}
}
var obj = {};
for (var k in msg) obj[k] = msg[k];
obj.seq = this.seq++;
obj.type = 'request';
this.outstanding[obj.seq] = cb;
var str = JSON.stringify(obj);
conn.write('Tool: '+this.toolname.replace(/[\r\n\v\f\x85\u2028\u2029]/g,'')+'\r\n');
conn.write('Content-Length: '+str.length+'\r\n\r\n');
conn.write(str);
}
function DebuggerCommandLine(debugger_conn, options) {
debugger_conn.on('message', function (msg) {
//console.log(msg)
})
this.debuggerConn = debugger_conn;
this.readlineInterface = readline.createInterface({
input: options.input,
output: options.output,
completer: this.autoComplete.bind(this),
terminal: options.input instanceof tty.ReadStream
});
this.readlineInterface.prompt(false);
var self = this;
this._delegate = null;
this.readlineInterface.on('line', function (line) {
if (self._delegate) {
self._delegate(line);
return;
}
self.parse(line, function (err) {
if (err) {
self.readlineInterface.output.write(err.message+'\n');
}
self.readlineInterface.prompt(false);
});
});
return this;
}
DebuggerCommandLine.prototype.commands = {
backtrace: {
completer: function (preCursor, callback) {
if (!preCursor) {
callback(null, [['full'], preCursor]);
return;
}
var options = [];
if (preCursor.length <= 'full'.length && 'full'.slice(0, preCursor.length) == preCursor) {
options.push('full')
}
callback(null, [options, preCursor]);
},
handler: function (dbg_cli, line, callback) {
if (line) line = line.trim();
var args = {};
if (line === 'full') {
args.fromFrame = 0;
args.toFrame = 1024 * 1024;
}
var msg = {command:'backtrace', arguments: args};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
if (msg.body.body.frames) msg.body.body.frames.forEach(function (frame) {
dbg_cli.readlineInterface.output.write(frame.text+'\n');
});
else {
var description = 'no JS frames were found. JS engine is ';
if (!msg.running) {
// could be due to GC
// could be due to breakpoint
description += 'paused\n'
}
else {
// does not mean JS code is currently running
description += 'running\n'
}
dbg_cli.readlineInterface.output.write(description);
}
callback(null);
});
}
},
"break": {
handler: function (dbg_cli, line, callback) {
var msg = {command:'suspend'};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
callback(null);
});
}
},
"continue": {
handler: function (dbg_cli, line, callback) {
var msg = {command:'continue'};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
callback(null);
});
}
},
"finish": {
handler: function (dbg_cli, line, callback) {
var msg = {command:'continue',arguments:{stepaction:'out',stepcount:1}};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
callback(null);
});
}
},
"step": {
handler: function (dbg_cli, line, callback) {
var msg = {command:'continue',arguments:{stepaction:'min',stepcount:1}};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
callback(null);
});
}
},
"help": {
handler: function (debug_cli, line, callback) {
debug_cli.readlineInterface.output.write('COMMANDS: ' + Object.keys(debug_cli.commands).sort().join(', ') + '\n');
}
},
"next": {
handler: function (dbg_cli, line, callback) {
var msg = {command:'continue',arguments:{stepaction:'next',stepcount:1}};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
callback(null);
});
}
},
"print": {
handler: function (dbg_cli, line, callback) {
var msg = {command:'evaluate',arguments:{expression:line}};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
if (err) {
callback(err);
return;
}
dbg_cli.showValue(msg.body.body);
callback(null);
});
}
},
"where": {
handler: function (dbg_cli, line, callback) {
var msg = {command:'backtrace',arguments:{fromFrame:0,toFrame:1}};
dbg_cli.debuggerConn.send(null, msg, function (err, msg) {
if (msg.body.body.frames) msg.body.body.frames.forEach(function (frame) {
dbg_cli.readlineInterface.output.write(frame.text+'\n');
});
else {
var description = 'no JS frames were found. JS engine is ';
if (!msg.running) {
// could be due to GC
// could be due to breakpoint
description += 'paused\n'
}
else {
// does not mean JS code is currently running
description += 'running\n'
}
dbg_cli.readlineInterface.output.write(description);
}
callback(null);
});
}
},
'define': {
handler: function (dbg_cli, line, callback) {
line = line || '';
var name = line.trim();
if (!name) {
callback(new Error('Command must have a name'));
}
if (/\s/.test(name)) {
callback(new Error('Commands cannot contain whitespace'));
return;
}
if (dbg_cli.commands[name]) {
callback(new Error('Command already exists'));
return;
}
var lines = [];
dbg_cli.delegate(function _(line) {
line = line || '';
line = line.trim();
if (line !== 'end') {
lines.push(line);
}
else {
dbg_cli.undelegate(_);
dbg_cli.commands[name] = {
handler: function (fn_dbg_cli, fn_line, fn_callback) {
var i = 0;
var done = false;
function next() {
if (done) {
return;
}
if (i >= lines.length) {
done = true;
fn_callback(null);
return;
}
var torun = lines[i];
i++;
dbg_cli.parse(torun, function (err) {
if (err) {
done = true;
fn_callback(err);
return;
}
next();
});
}
next();
}
};
}
dbg_cli.readlineInterface.prompt(false);
});
dbg_cli.readlineInterface.prompt(false);
}
}
};
;(function(commands) {
commands.bt = commands.backtrace;
commands.c = commands["continue"];
commands.n = commands.next;
commands.p = commands.print;
commands.s = commands.step;
commands.br = commands["break"];
})(DebuggerCommandLine.prototype.commands);
// for when something needs to take over the readline
DebuggerCommandLine.prototype.delegate = function (handler) {
if (this._delegate) {
throw new Error('Already delegating');
}
this._delegate = handler;
}
DebuggerCommandLine.prototype.undelegate = function (handler) {
if (this._delegate === handler) {
this._delegate = null;
}
}
DebuggerCommandLine.prototype.autoComplete = function (preCursor, callback) {
var command_and_remaining = preCursor.match(/^(\S*)(?:\s+([\s\S]*))?$/);
if (!command_and_remaining) {
callback(null, [[], preCursor]);
return;
}
var command = command_and_remaining[1];
var remaining = command_and_remaining[2];
// still typing a command?
if (command.length === preCursor.length) {
callback(null, [Object.keys(this.commands).filter(function (definedCommand) {
return command === definedCommand.slice(0, command.length);
}), preCursor]);
return;
}
if (!this.commands[command]) {
callback(null, [[], preCursor]);
return;
}
var completer = this.commands[command].completer;
if (completer) completer(remaining, callback);
else callback(null, [[], preCursor]);
}
DebuggerCommandLine.prototype.parse = function (line, callback) {
var command_and_remaining = line.match(/^(\S*)(?:\s+([\s\S]+))?$/);
if (!command_and_remaining) {
this.readlineInterface.output.write('Unable to parse command\n');
callback(null);
return;
}
var command = command_and_remaining[1];
var remaining = command_and_remaining[2];
if (!this.commands[command]) {
this.readlineInterface.output.write('Unable to find command '+JSON.stringify(command)+'\n');
callback(null);
return;
}
this.commands[command].handler(this, remaining, callback);
}
DebuggerCommandLine.prototype.showValue = function (value) {
var output = this.readlineInterface.output;
if (value.type === 'object'){
output.write('[' + value.type + ' ' + value.className + ']'+(value.name || value.inferredName || '')+' {\n');
output.write(value.properties.map(function (prop) {
return ' ' + prop.name
}).join('\n')+'\n');
output.write('}\n');
}
else if (value.type === 'function') {
output.write(value.source+'\n');
}
else {
output.write(JSON.stringify(value.value)+'\n');
}
}
var dbg_conn = new DebuggerConnection(require('net').connect(5858));
new DebuggerCommandLine(dbg_conn, {input: process.stdin, output: process.stdout});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment