mirror of
https://github.com/nodejs/node.git
synced 2025-05-05 12:04:25 +00:00

Many of the util.is*() methods used to check data types simply compare against a single value or the result of typeof. This commit replaces calls to these methods with equivalent checks. This commit does not touch calls to the more complex methods (isRegExp(), isDate(), etc.). Fixes: https://github.com/iojs/io.js/issues/607 PR-URL: https://github.com/iojs/io.js/pull/647 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
1730 lines
39 KiB
JavaScript
1730 lines
39 KiB
JavaScript
'use strict';
|
|
|
|
const util = require('util');
|
|
const path = require('path');
|
|
const net = require('net');
|
|
const vm = require('vm');
|
|
const Module = require('module');
|
|
const repl = Module.requireRepl();
|
|
const inherits = util.inherits;
|
|
const assert = require('assert');
|
|
const spawn = require('child_process').spawn;
|
|
|
|
exports.start = function(argv, stdin, stdout) {
|
|
argv || (argv = process.argv.slice(2));
|
|
|
|
if (argv.length < 1) {
|
|
console.error('Usage: iojs debug script.js');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Setup input/output streams
|
|
stdin = stdin || process.stdin;
|
|
stdout = stdout || process.stdout;
|
|
|
|
var args = ['--debug-brk'].concat(argv),
|
|
interface_ = new Interface(stdin, stdout, args);
|
|
|
|
stdin.resume();
|
|
|
|
process.on('uncaughtException', function(e) {
|
|
console.error("There was an internal error in Node's debugger. " +
|
|
'Please report this bug.');
|
|
console.error(e.message);
|
|
console.error(e.stack);
|
|
if (interface_.child) interface_.child.kill();
|
|
process.exit(1);
|
|
});
|
|
};
|
|
|
|
exports.port = 5858;
|
|
|
|
|
|
//
|
|
// Parser/Serializer for V8 debugger protocol
|
|
// http://code.google.com/p/v8/wiki/DebuggerProtocol
|
|
//
|
|
// Usage:
|
|
// p = new Protocol();
|
|
//
|
|
// p.onResponse = function(res) {
|
|
// // do stuff with response from V8
|
|
// };
|
|
//
|
|
// socket.setEncoding('utf8');
|
|
// socket.on('data', function(s) {
|
|
// // Pass strings into the protocol
|
|
// p.execute(s);
|
|
// });
|
|
//
|
|
//
|
|
function Protocol() {
|
|
this._newRes();
|
|
}
|
|
exports.Protocol = Protocol;
|
|
|
|
|
|
Protocol.prototype._newRes = function(raw) {
|
|
this.res = { raw: raw || '', headers: {} };
|
|
this.state = 'headers';
|
|
this.reqSeq = 1;
|
|
this.execute('');
|
|
};
|
|
|
|
|
|
Protocol.prototype.execute = function(d) {
|
|
var res = this.res;
|
|
res.raw += d;
|
|
|
|
switch (this.state) {
|
|
case 'headers':
|
|
var endHeaderIndex = res.raw.indexOf('\r\n\r\n');
|
|
|
|
if (endHeaderIndex < 0) break;
|
|
|
|
var rawHeader = res.raw.slice(0, endHeaderIndex);
|
|
var endHeaderByteIndex = Buffer.byteLength(rawHeader, 'utf8');
|
|
var lines = rawHeader.split('\r\n');
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var kv = lines[i].split(/: +/);
|
|
res.headers[kv[0]] = kv[1];
|
|
}
|
|
|
|
this.contentLength = +res.headers['Content-Length'];
|
|
this.bodyStartByteIndex = endHeaderByteIndex + 4;
|
|
|
|
this.state = 'body';
|
|
|
|
var len = Buffer.byteLength(res.raw, 'utf8');
|
|
if (len - this.bodyStartByteIndex < this.contentLength) {
|
|
break;
|
|
}
|
|
// pass thru
|
|
case 'body':
|
|
var resRawByteLength = Buffer.byteLength(res.raw, 'utf8');
|
|
|
|
if (resRawByteLength - this.bodyStartByteIndex >= this.contentLength) {
|
|
var buf = new Buffer(resRawByteLength);
|
|
buf.write(res.raw, 0, resRawByteLength, 'utf8');
|
|
res.body =
|
|
buf.slice(this.bodyStartByteIndex,
|
|
this.bodyStartByteIndex +
|
|
this.contentLength).toString('utf8');
|
|
// JSON parse body?
|
|
res.body = res.body.length ? JSON.parse(res.body) : {};
|
|
|
|
// Done!
|
|
this.onResponse(res);
|
|
|
|
this._newRes(buf.slice(this.bodyStartByteIndex +
|
|
this.contentLength).toString('utf8'));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Error('Unknown state');
|
|
break;
|
|
}
|
|
};
|
|
|
|
|
|
Protocol.prototype.serialize = function(req) {
|
|
req.type = 'request';
|
|
req.seq = this.reqSeq++;
|
|
var json = JSON.stringify(req);
|
|
return 'Content-Length: ' + Buffer.byteLength(json, 'utf8') +
|
|
'\r\n\r\n' + json;
|
|
};
|
|
|
|
|
|
const NO_FRAME = -1;
|
|
|
|
function Client() {
|
|
net.Stream.call(this);
|
|
var protocol = this.protocol = new Protocol(this);
|
|
this._reqCallbacks = [];
|
|
var socket = this;
|
|
|
|
this.currentFrame = NO_FRAME;
|
|
this.currentSourceLine = -1;
|
|
this.handles = {};
|
|
this.scripts = {};
|
|
this.breakpoints = [];
|
|
|
|
// Note that 'Protocol' requires strings instead of Buffers.
|
|
socket.setEncoding('utf8');
|
|
socket.on('data', function(d) {
|
|
protocol.execute(d);
|
|
});
|
|
|
|
protocol.onResponse = this._onResponse.bind(this);
|
|
}
|
|
inherits(Client, net.Stream);
|
|
exports.Client = Client;
|
|
|
|
|
|
Client.prototype._addHandle = function(desc) {
|
|
if (desc === null || typeof desc !== 'object' ||
|
|
typeof desc.handle !== 'number') {
|
|
return;
|
|
}
|
|
|
|
this.handles[desc.handle] = desc;
|
|
|
|
if (desc.type == 'script') {
|
|
this._addScript(desc);
|
|
}
|
|
};
|
|
|
|
|
|
const natives = process.binding('natives');
|
|
|
|
|
|
Client.prototype._addScript = function(desc) {
|
|
this.scripts[desc.id] = desc;
|
|
if (desc.name) {
|
|
desc.isNative = (desc.name.replace('.js', '') in natives) ||
|
|
desc.name == 'node.js';
|
|
}
|
|
};
|
|
|
|
|
|
Client.prototype._removeScript = function(desc) {
|
|
this.scripts[desc.id] = undefined;
|
|
};
|
|
|
|
|
|
Client.prototype._onResponse = function(res) {
|
|
var cb,
|
|
index = -1;
|
|
|
|
this._reqCallbacks.some(function(fn, i) {
|
|
if (fn.request_seq == res.body.request_seq) {
|
|
cb = fn;
|
|
index = i;
|
|
return true;
|
|
}
|
|
});
|
|
|
|
var self = this;
|
|
var handled = false;
|
|
|
|
if (res.headers.Type == 'connect') {
|
|
// Request a list of scripts for our own storage.
|
|
self.reqScripts();
|
|
self.emit('ready');
|
|
handled = true;
|
|
|
|
} else if (res.body && res.body.event == 'break') {
|
|
this.emit('break', res.body);
|
|
handled = true;
|
|
|
|
} else if (res.body && res.body.event == 'exception') {
|
|
this.emit('exception', res.body);
|
|
handled = true;
|
|
|
|
} else if (res.body && res.body.event == 'afterCompile') {
|
|
this._addHandle(res.body.body.script);
|
|
handled = true;
|
|
|
|
} else if (res.body && res.body.event == 'scriptCollected') {
|
|
// ???
|
|
this._removeScript(res.body.body.script);
|
|
handled = true;
|
|
|
|
} else if (res.body && res.body.event === 'compileError') {
|
|
// This event is not used anywhere right now, perhaps somewhere in the
|
|
// future?
|
|
handled = true;
|
|
}
|
|
|
|
if (cb) {
|
|
this._reqCallbacks.splice(index, 1);
|
|
handled = true;
|
|
|
|
var err = res.success === false && (res.message || true) ||
|
|
res.body.success === false && (res.body.message || true);
|
|
cb(err, res.body && res.body.body || res.body, res);
|
|
}
|
|
|
|
if (!handled) this.emit('unhandledResponse', res.body);
|
|
};
|
|
|
|
|
|
Client.prototype.req = function(req, cb) {
|
|
this.write(this.protocol.serialize(req));
|
|
cb.request_seq = req.seq;
|
|
this._reqCallbacks.push(cb);
|
|
};
|
|
|
|
|
|
Client.prototype.reqVersion = function(cb) {
|
|
cb = cb || function() {};
|
|
this.req({ command: 'version' } , function(err, body, res) {
|
|
if (err) return cb(err);
|
|
cb(null, res.body.body.V8Version, res.body.running);
|
|
});
|
|
};
|
|
|
|
|
|
Client.prototype.reqLookup = function(refs, cb) {
|
|
var self = this;
|
|
|
|
// TODO: We have a cache of handle's we've already seen in this.handles
|
|
// This can be used if we're careful.
|
|
var req = {
|
|
command: 'lookup',
|
|
arguments: {
|
|
handles: refs
|
|
}
|
|
};
|
|
|
|
cb = cb || function() {};
|
|
this.req(req, function(err, res) {
|
|
if (err) return cb(err);
|
|
for (var ref in res) {
|
|
if (res[ref] !== null && typeof res[ref] === 'object') {
|
|
self._addHandle(res[ref]);
|
|
}
|
|
}
|
|
|
|
cb(null, res);
|
|
});
|
|
};
|
|
|
|
Client.prototype.reqScopes = function(cb) {
|
|
var self = this,
|
|
req = {
|
|
command: 'scopes',
|
|
arguments: {}
|
|
};
|
|
|
|
cb = cb || function() {};
|
|
this.req(req, function(err, res) {
|
|
if (err) return cb(err);
|
|
var refs = res.scopes.map(function(scope) {
|
|
return scope.object.ref;
|
|
});
|
|
|
|
self.reqLookup(refs, function(err, res) {
|
|
if (err) return cb(err);
|
|
|
|
var globals = Object.keys(res).map(function(key) {
|
|
return res[key].properties.map(function(prop) {
|
|
return prop.name;
|
|
});
|
|
});
|
|
|
|
cb(null, globals.reverse());
|
|
});
|
|
});
|
|
};
|
|
|
|
// This is like reqEval, except it will look up the expression in each of the
|
|
// scopes associated with the current frame.
|
|
Client.prototype.reqEval = function(expression, cb) {
|
|
var self = this;
|
|
|
|
if (this.currentFrame == NO_FRAME) {
|
|
// Only need to eval in global scope.
|
|
this.reqFrameEval(expression, NO_FRAME, cb);
|
|
return;
|
|
}
|
|
|
|
cb = cb || function() {};
|
|
// Otherwise we need to get the current frame to see which scopes it has.
|
|
this.reqBacktrace(function(err, bt) {
|
|
if (err || !bt.frames) {
|
|
// ??
|
|
return cb(null, {});
|
|
}
|
|
|
|
var frame = bt.frames[self.currentFrame];
|
|
|
|
var evalFrames = frame.scopes.map(function(s) {
|
|
if (!s) return;
|
|
var x = bt.frames[s.index];
|
|
if (!x) return;
|
|
return x.index;
|
|
});
|
|
|
|
self._reqFramesEval(expression, evalFrames, cb);
|
|
});
|
|
};
|
|
|
|
|
|
// Finds the first scope in the array in which the expression evals.
|
|
Client.prototype._reqFramesEval = function(expression, evalFrames, cb) {
|
|
if (evalFrames.length == 0) {
|
|
// Just eval in global scope.
|
|
this.reqFrameEval(expression, NO_FRAME, cb);
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
var i = evalFrames.shift();
|
|
|
|
cb = cb || function() {};
|
|
this.reqFrameEval(expression, i, function(err, res) {
|
|
if (!err) return cb(null, res);
|
|
self._reqFramesEval(expression, evalFrames, cb);
|
|
});
|
|
};
|
|
|
|
|
|
Client.prototype.reqFrameEval = function(expression, frame, cb) {
|
|
var self = this;
|
|
var req = {
|
|
command: 'evaluate',
|
|
arguments: { expression: expression }
|
|
};
|
|
|
|
if (frame == NO_FRAME) {
|
|
req.arguments.global = true;
|
|
} else {
|
|
req.arguments.frame = frame;
|
|
}
|
|
|
|
cb = cb || function() {};
|
|
this.req(req, function(err, res) {
|
|
if (!err) self._addHandle(res);
|
|
cb(err, res);
|
|
});
|
|
};
|
|
|
|
|
|
// reqBacktrace(cb)
|
|
// TODO: from, to, bottom
|
|
Client.prototype.reqBacktrace = function(cb) {
|
|
this.req({ command: 'backtrace', arguments: { inlineRefs: true } } , cb);
|
|
};
|
|
|
|
|
|
// reqSetExceptionBreak(type, cb)
|
|
// TODO: from, to, bottom
|
|
Client.prototype.reqSetExceptionBreak = function(type, cb) {
|
|
this.req({
|
|
command: 'setexceptionbreak',
|
|
arguments: { type: type, enabled: true }
|
|
}, cb);
|
|
};
|
|
|
|
|
|
// Returns an array of objects like this:
|
|
//
|
|
// { handle: 11,
|
|
// type: 'script',
|
|
// name: 'node.js',
|
|
// id: 14,
|
|
// lineOffset: 0,
|
|
// columnOffset: 0,
|
|
// lineCount: 562,
|
|
// sourceStart: '(function(process) {\n\n ',
|
|
// sourceLength: 15939,
|
|
// scriptType: 2,
|
|
// compilationType: 0,
|
|
// context: { ref: 10 },
|
|
// text: 'node.js (lines: 562)' }
|
|
//
|
|
Client.prototype.reqScripts = function(cb) {
|
|
var self = this;
|
|
cb = cb || function() {};
|
|
|
|
this.req({ command: 'scripts' } , function(err, res) {
|
|
if (err) return cb(err);
|
|
|
|
for (var i = 0; i < res.length; i++) {
|
|
self._addHandle(res[i]);
|
|
}
|
|
cb(null);
|
|
});
|
|
};
|
|
|
|
|
|
Client.prototype.reqContinue = function(cb) {
|
|
this.currentFrame = NO_FRAME;
|
|
this.req({ command: 'continue' }, cb);
|
|
};
|
|
|
|
Client.prototype.listbreakpoints = function(cb) {
|
|
this.req({ command: 'listbreakpoints' }, cb);
|
|
};
|
|
|
|
Client.prototype.setBreakpoint = function(req, cb) {
|
|
req = {
|
|
command: 'setbreakpoint',
|
|
arguments: req
|
|
};
|
|
|
|
this.req(req, cb);
|
|
};
|
|
|
|
Client.prototype.clearBreakpoint = function(req, cb) {
|
|
var req = {
|
|
command: 'clearbreakpoint',
|
|
arguments: req
|
|
};
|
|
|
|
this.req(req, cb);
|
|
};
|
|
|
|
Client.prototype.reqSource = function(from, to, cb) {
|
|
var req = {
|
|
command: 'source',
|
|
fromLine: from,
|
|
toLine: to
|
|
};
|
|
|
|
this.req(req, cb);
|
|
};
|
|
|
|
|
|
// client.next(1, cb);
|
|
Client.prototype.step = function(action, count, cb) {
|
|
var req = {
|
|
command: 'continue',
|
|
arguments: { stepaction: action, stepcount: count }
|
|
};
|
|
|
|
this.currentFrame = NO_FRAME;
|
|
this.req(req, cb);
|
|
};
|
|
|
|
|
|
Client.prototype.mirrorObject = function(handle, depth, cb) {
|
|
var self = this;
|
|
|
|
var val;
|
|
|
|
if (handle.type === 'object') {
|
|
// The handle looks something like this:
|
|
// { handle: 8,
|
|
// type: 'object',
|
|
// className: 'Object',
|
|
// constructorFunction: { ref: 9 },
|
|
// protoObject: { ref: 4 },
|
|
// prototypeObject: { ref: 2 },
|
|
// properties: [ { name: 'hello', propertyType: 1, ref: 10 } ],
|
|
// text: '#<an Object>' }
|
|
|
|
// For now ignore the className and constructor and prototype.
|
|
// TJ's method of object inspection would probably be good for this:
|
|
// https://groups.google.com/forum/?pli=1#!topic/nodejs-dev/4gkWBOimiOg
|
|
|
|
var propertyRefs = handle.properties.map(function(p) {
|
|
return p.ref;
|
|
});
|
|
|
|
cb = cb || function() {};
|
|
this.reqLookup(propertyRefs, function(err, res) {
|
|
if (err) {
|
|
console.error('problem with reqLookup');
|
|
cb(null, handle);
|
|
return;
|
|
}
|
|
|
|
var mirror,
|
|
waiting = 1;
|
|
|
|
if (handle.className == 'Array') {
|
|
mirror = [];
|
|
} else if (handle.className == 'Date') {
|
|
mirror = new Date(handle.value);
|
|
} else {
|
|
mirror = {};
|
|
}
|
|
|
|
|
|
var keyValues = [];
|
|
handle.properties.forEach(function(prop, i) {
|
|
var value = res[prop.ref];
|
|
var mirrorValue;
|
|
if (value) {
|
|
mirrorValue = value.value ? value.value : value.text;
|
|
} else {
|
|
mirrorValue = '[?]';
|
|
}
|
|
|
|
if (Array.isArray(mirror) && typeof prop.name !== 'number') {
|
|
// Skip the 'length' property.
|
|
return;
|
|
}
|
|
|
|
keyValues[i] = {
|
|
name: prop.name,
|
|
value: mirrorValue
|
|
};
|
|
if (value && value.handle && depth > 0) {
|
|
waiting++;
|
|
self.mirrorObject(value, depth - 1, function(err, result) {
|
|
if (!err) keyValues[i].value = result;
|
|
waitForOthers();
|
|
});
|
|
}
|
|
});
|
|
|
|
waitForOthers();
|
|
function waitForOthers() {
|
|
if (--waiting === 0 && cb) {
|
|
keyValues.forEach(function(kv) {
|
|
mirror[kv.name] = kv.value;
|
|
});
|
|
cb(null, mirror);
|
|
}
|
|
};
|
|
});
|
|
return;
|
|
} else if (handle.type === 'function') {
|
|
val = function() {};
|
|
} else if (handle.type === 'null') {
|
|
val = null;
|
|
} else if (handle.value !== undefined) {
|
|
val = handle.value;
|
|
} else if (handle.type === 'undefined') {
|
|
val = undefined;
|
|
} else {
|
|
val = handle;
|
|
}
|
|
process.nextTick(function() {
|
|
cb(null, val);
|
|
});
|
|
};
|
|
|
|
|
|
Client.prototype.fullTrace = function(cb) {
|
|
var self = this;
|
|
|
|
cb = cb || function() {};
|
|
this.reqBacktrace(function(err, trace) {
|
|
if (err) return cb(err);
|
|
if (trace.totalFrames <= 0) return cb(Error('No frames'));
|
|
|
|
var refs = [];
|
|
|
|
for (var i = 0; i < trace.frames.length; i++) {
|
|
var frame = trace.frames[i];
|
|
// looks like this:
|
|
// { type: 'frame',
|
|
// index: 0,
|
|
// receiver: { ref: 1 },
|
|
// func: { ref: 0 },
|
|
// script: { ref: 7 },
|
|
// constructCall: false,
|
|
// atReturn: false,
|
|
// debuggerFrame: false,
|
|
// arguments: [],
|
|
// locals: [],
|
|
// position: 160,
|
|
// line: 7,
|
|
// column: 2,
|
|
// sourceLineText: ' debugger;',
|
|
// scopes: [ { type: 1, index: 0 }, { type: 0, index: 1 } ],
|
|
// text: '#00 blah() /home/ryan/projects/node/test-debug.js l...' }
|
|
refs.push(frame.script.ref);
|
|
refs.push(frame.func.ref);
|
|
refs.push(frame.receiver.ref);
|
|
}
|
|
|
|
self.reqLookup(refs, function(err, res) {
|
|
if (err) return cb(err);
|
|
|
|
for (var i = 0; i < trace.frames.length; i++) {
|
|
var frame = trace.frames[i];
|
|
frame.script = res[frame.script.ref];
|
|
frame.func = res[frame.func.ref];
|
|
frame.receiver = res[frame.receiver.ref];
|
|
}
|
|
|
|
cb(null, trace);
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const commands = [
|
|
[
|
|
'run (r)',
|
|
'cont (c)',
|
|
'next (n)',
|
|
'step (s)',
|
|
'out (o)',
|
|
'backtrace (bt)',
|
|
'setBreakpoint (sb)',
|
|
'clearBreakpoint (cb)'
|
|
],
|
|
[
|
|
'watch',
|
|
'unwatch',
|
|
'watchers',
|
|
'repl',
|
|
'restart',
|
|
'kill',
|
|
'list',
|
|
'scripts',
|
|
'breakOnException',
|
|
'breakpoints',
|
|
'version'
|
|
]
|
|
];
|
|
|
|
|
|
var helpMessage = 'Commands: ' + commands.map(function(group) {
|
|
return group.join(', ');
|
|
}).join(',\n');
|
|
|
|
|
|
function SourceUnderline(sourceText, position, repl) {
|
|
if (!sourceText) return '';
|
|
|
|
var head = sourceText.slice(0, position),
|
|
tail = sourceText.slice(position);
|
|
|
|
// Colourize char if stdout supports colours
|
|
if (repl.useColors) {
|
|
tail = tail.replace(/(.+?)([^\w]|$)/, '\u001b[32m$1\u001b[39m$2');
|
|
}
|
|
|
|
// Return source line with coloured char at `position`
|
|
return [
|
|
head,
|
|
tail
|
|
].join('');
|
|
}
|
|
|
|
|
|
function SourceInfo(body) {
|
|
var result = body.exception ? 'exception in ' : 'break in ';
|
|
|
|
if (body.script) {
|
|
if (body.script.name) {
|
|
var name = body.script.name,
|
|
dir = path.resolve() + '/';
|
|
|
|
// Change path to relative, if possible
|
|
if (name.indexOf(dir) === 0) {
|
|
name = name.slice(dir.length);
|
|
}
|
|
|
|
result += name;
|
|
} else {
|
|
result += '[unnamed]';
|
|
}
|
|
}
|
|
result += ':';
|
|
result += body.sourceLine + 1;
|
|
|
|
if (body.exception) result += '\n' + body.exception.text;
|
|
|
|
return result;
|
|
}
|
|
|
|
// This class is the repl-enabled debugger interface which is invoked on
|
|
// "node debug"
|
|
function Interface(stdin, stdout, args) {
|
|
var self = this;
|
|
|
|
this.stdin = stdin;
|
|
this.stdout = stdout;
|
|
this.args = args;
|
|
|
|
// Two eval modes are available: controlEval and debugEval
|
|
// But controlEval is used by default
|
|
var opts = {
|
|
prompt: 'debug> ',
|
|
input: this.stdin,
|
|
output: this.stdout,
|
|
eval: this.controlEval.bind(this),
|
|
useGlobal: false,
|
|
ignoreUndefined: true
|
|
};
|
|
if (parseInt(process.env['NODE_NO_READLINE'], 10)) {
|
|
opts.terminal = false;
|
|
} else if (parseInt(process.env['NODE_FORCE_READLINE'], 10)) {
|
|
opts.terminal = true;
|
|
|
|
// Emulate Ctrl+C if we're emulating terminal
|
|
if (!this.stdout.isTTY) {
|
|
process.on('SIGINT', function() {
|
|
self.repl.rli.emit('SIGINT');
|
|
});
|
|
}
|
|
}
|
|
if (parseInt(process.env['NODE_DISABLE_COLORS'], 10)) {
|
|
opts.useColors = false;
|
|
}
|
|
|
|
this.repl = repl.start(opts);
|
|
|
|
// Do not print useless warning
|
|
repl._builtinLibs.splice(repl._builtinLibs.indexOf('repl'), 1);
|
|
|
|
// Kill child process when main process dies
|
|
this.repl.on('exit', function() {
|
|
process.exit(0);
|
|
});
|
|
|
|
// Handle all possible exits
|
|
process.on('exit', this.killChild.bind(this));
|
|
process.once('SIGTERM', process.exit.bind(process, 0));
|
|
process.once('SIGHUP', process.exit.bind(process, 0));
|
|
|
|
var proto = Interface.prototype;
|
|
const ignored = ['pause', 'resume', 'exitRepl', 'handleBreak',
|
|
'requireConnection', 'killChild', 'trySpawn',
|
|
'controlEval', 'debugEval', 'print', 'childPrint',
|
|
'clearline'];
|
|
const shortcut = {
|
|
'run': 'r',
|
|
'cont': 'c',
|
|
'next': 'n',
|
|
'step': 's',
|
|
'out': 'o',
|
|
'backtrace': 'bt',
|
|
'setBreakpoint': 'sb',
|
|
'clearBreakpoint': 'cb',
|
|
'pause_': 'pause'
|
|
};
|
|
|
|
function defineProperty(key, protoKey) {
|
|
// Check arity
|
|
var fn = proto[protoKey].bind(self);
|
|
|
|
if (proto[protoKey].length === 0) {
|
|
Object.defineProperty(self.repl.context, key, {
|
|
get: fn,
|
|
enumerable: true,
|
|
configurable: false
|
|
});
|
|
} else {
|
|
self.repl.context[key] = fn;
|
|
}
|
|
};
|
|
|
|
// Copy all prototype methods in repl context
|
|
// Setup them as getters if possible
|
|
for (var i in proto) {
|
|
if (Object.prototype.hasOwnProperty.call(proto, i) &&
|
|
ignored.indexOf(i) === -1) {
|
|
defineProperty(i, i);
|
|
if (shortcut[i]) defineProperty(shortcut[i], i);
|
|
}
|
|
}
|
|
|
|
this.killed = false;
|
|
this.waiting = null;
|
|
this.paused = 0;
|
|
this.context = this.repl.context;
|
|
this.history = {
|
|
debug: [],
|
|
control: []
|
|
};
|
|
this.breakpoints = [];
|
|
this._watchers = [];
|
|
|
|
// Run script automatically
|
|
this.pause();
|
|
|
|
// XXX Need to figure out why we need this delay
|
|
setTimeout(function() {
|
|
|
|
self.run(function() {
|
|
self.resume();
|
|
});
|
|
}, 10);
|
|
}
|
|
|
|
|
|
// Stream control
|
|
|
|
|
|
Interface.prototype.pause = function() {
|
|
if (this.killed || this.paused++ > 0) return this;
|
|
this.repl.rli.pause();
|
|
this.stdin.pause();
|
|
return this;
|
|
};
|
|
|
|
Interface.prototype.resume = function(silent) {
|
|
if (this.killed || this.paused === 0 || --this.paused !== 0) return this;
|
|
this.repl.rli.resume();
|
|
if (silent !== true) {
|
|
this.repl.displayPrompt();
|
|
}
|
|
this.stdin.resume();
|
|
|
|
if (this.waiting) {
|
|
this.waiting();
|
|
this.waiting = null;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
|
|
// Clear current line
|
|
Interface.prototype.clearline = function() {
|
|
if (this.stdout.isTTY) {
|
|
this.stdout.cursorTo(0);
|
|
this.stdout.clearLine(1);
|
|
} else {
|
|
this.stdout.write('\b');
|
|
}
|
|
};
|
|
|
|
// Print text to output stream
|
|
Interface.prototype.print = function(text, oneline) {
|
|
if (this.killed) return;
|
|
this.clearline();
|
|
|
|
this.stdout.write(typeof text === 'string' ? text : util.inspect(text));
|
|
|
|
if (oneline !== true) {
|
|
this.stdout.write('\n');
|
|
}
|
|
};
|
|
|
|
// Format and print text from child process
|
|
Interface.prototype.childPrint = function(text) {
|
|
this.print(text.toString().split(/\r\n|\r|\n/g).filter(function(chunk) {
|
|
return chunk;
|
|
}).map(function(chunk) {
|
|
return '< ' + chunk;
|
|
}).join('\n'));
|
|
this.repl.displayPrompt(true);
|
|
};
|
|
|
|
// Errors formatting
|
|
Interface.prototype.error = function(text) {
|
|
this.print(text);
|
|
this.resume();
|
|
};
|
|
|
|
|
|
// Debugger's `break` event handler
|
|
Interface.prototype.handleBreak = function(r) {
|
|
var self = this;
|
|
|
|
this.pause();
|
|
|
|
// Save execution context's data
|
|
this.client.currentSourceLine = r.sourceLine;
|
|
this.client.currentSourceLineText = r.sourceLineText;
|
|
this.client.currentSourceColumn = r.sourceColumn;
|
|
this.client.currentFrame = 0;
|
|
this.client.currentScript = r.script && r.script.name;
|
|
|
|
// Print break data
|
|
this.print(SourceInfo(r));
|
|
|
|
// Show watchers' values
|
|
this.watchers(true, function(err) {
|
|
if (err) return self.error(err);
|
|
|
|
// And list source
|
|
self.list(2);
|
|
|
|
self.resume(true);
|
|
});
|
|
};
|
|
|
|
|
|
// Internal method for checking connection state
|
|
Interface.prototype.requireConnection = function() {
|
|
if (!this.client) {
|
|
this.error('App isn\'t running... Try `run` instead');
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
|
|
// Evals
|
|
|
|
// Used for debugger's commands evaluation and execution
|
|
Interface.prototype.controlEval = function(code, context, filename, callback) {
|
|
try {
|
|
// Repeat last command if empty line are going to be evaluated
|
|
if (this.repl.rli.history && this.repl.rli.history.length > 0) {
|
|
if (code === '\n') {
|
|
code = this.repl.rli.history[0] + '\n';
|
|
}
|
|
}
|
|
|
|
var result = vm.runInContext(code, context, filename);
|
|
|
|
// Repl should not ask for next command
|
|
// if current one was asynchronous.
|
|
if (this.paused === 0) return callback(null, result);
|
|
|
|
// Add a callback for asynchronous command
|
|
// (it will be automatically invoked by .resume() method
|
|
this.waiting = function() {
|
|
callback(null, result);
|
|
};
|
|
} catch (e) {
|
|
callback(e);
|
|
}
|
|
};
|
|
|
|
// Used for debugger's remote evaluation (`repl`) commands
|
|
Interface.prototype.debugEval = function(code, context, filename, callback) {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this,
|
|
client = this.client;
|
|
|
|
// Repl asked for scope variables
|
|
if (code === '.scope') {
|
|
client.reqScopes(callback);
|
|
return;
|
|
}
|
|
|
|
var frame = client.currentFrame === NO_FRAME ? frame : undefined;
|
|
|
|
self.pause();
|
|
|
|
// Request remote evaluation globally or in current frame
|
|
client.reqFrameEval(code, frame, function(err, res) {
|
|
if (err) {
|
|
callback(err);
|
|
self.resume(true);
|
|
return;
|
|
}
|
|
|
|
// Request object by handles (and it's sub-properties)
|
|
client.mirrorObject(res, 3, function(err, mirror) {
|
|
callback(null, mirror);
|
|
self.resume(true);
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
// Utils
|
|
|
|
// Adds spaces and prefix to number
|
|
// maxN is a maximum number we should have space for
|
|
function leftPad(n, prefix, maxN) {
|
|
var s = n.toString(),
|
|
nchars = Math.max(2, String(maxN).length) + 1,
|
|
nspaces = nchars - s.length - 1;
|
|
|
|
for (var i = 0; i < nspaces; i++) {
|
|
prefix += ' ';
|
|
}
|
|
|
|
return prefix + s;
|
|
}
|
|
|
|
|
|
// Commands
|
|
|
|
|
|
// Print help message
|
|
Interface.prototype.help = function() {
|
|
this.print(helpMessage);
|
|
};
|
|
|
|
|
|
// Run script
|
|
Interface.prototype.run = function() {
|
|
var callback = arguments[0];
|
|
|
|
if (this.child) {
|
|
this.error('App is already running... Try `restart` instead');
|
|
callback && callback(true);
|
|
} else {
|
|
this.trySpawn(callback);
|
|
}
|
|
};
|
|
|
|
|
|
// Restart script
|
|
Interface.prototype.restart = function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this;
|
|
|
|
self.pause();
|
|
self.killChild();
|
|
|
|
// XXX need to wait a little bit for the restart to work?
|
|
setTimeout(function() {
|
|
self.trySpawn();
|
|
self.resume();
|
|
}, 1000);
|
|
};
|
|
|
|
|
|
// Print version
|
|
Interface.prototype.version = function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this;
|
|
|
|
this.pause();
|
|
this.client.reqVersion(function(err, v) {
|
|
if (err) {
|
|
self.error(err);
|
|
} else {
|
|
self.print(v);
|
|
}
|
|
self.resume();
|
|
});
|
|
};
|
|
|
|
// List source code
|
|
Interface.prototype.list = function(delta) {
|
|
if (!this.requireConnection()) return;
|
|
|
|
delta || (delta = 5);
|
|
|
|
var self = this,
|
|
client = this.client,
|
|
from = client.currentSourceLine - delta + 1,
|
|
to = client.currentSourceLine + delta + 1;
|
|
|
|
self.pause();
|
|
client.reqSource(from, to, function(err, res) {
|
|
if (err || !res) {
|
|
self.error('You can\'t list source code right now');
|
|
self.resume();
|
|
return;
|
|
}
|
|
|
|
var lines = res.source.split('\n');
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var lineno = res.fromLine + i + 1;
|
|
if (lineno < from || lineno > to) continue;
|
|
|
|
var current = lineno == 1 + client.currentSourceLine,
|
|
breakpoint = client.breakpoints.some(function(bp) {
|
|
return (bp.scriptReq === client.currentScript ||
|
|
bp.script === client.currentScript) &&
|
|
bp.line == lineno;
|
|
});
|
|
|
|
if (lineno == 1) {
|
|
// The first line needs to have the module wrapper filtered out of
|
|
// it.
|
|
var wrapper = Module.wrapper[0];
|
|
lines[i] = lines[i].slice(wrapper.length);
|
|
|
|
client.currentSourceColumn -= wrapper.length;
|
|
}
|
|
|
|
// Highlight executing statement
|
|
var line;
|
|
if (current) {
|
|
line = SourceUnderline(lines[i],
|
|
client.currentSourceColumn,
|
|
self.repl);
|
|
} else {
|
|
line = lines[i];
|
|
}
|
|
|
|
var prefixChar = ' ';
|
|
if (current) {
|
|
prefixChar = '>';
|
|
} else if (breakpoint) {
|
|
prefixChar = '*';
|
|
}
|
|
|
|
self.print(leftPad(lineno, prefixChar, to) + ' ' + line);
|
|
}
|
|
self.resume();
|
|
});
|
|
};
|
|
|
|
// Print backtrace
|
|
Interface.prototype.backtrace = function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this,
|
|
client = this.client;
|
|
|
|
self.pause();
|
|
client.fullTrace(function(err, bt) {
|
|
if (err) {
|
|
self.error('Can\'t request backtrace now');
|
|
self.resume();
|
|
return;
|
|
}
|
|
|
|
if (bt.totalFrames == 0) {
|
|
self.print('(empty stack)');
|
|
} else {
|
|
var trace = [],
|
|
firstFrameNative = bt.frames[0].script.isNative;
|
|
|
|
for (var i = 0; i < bt.frames.length; i++) {
|
|
var frame = bt.frames[i];
|
|
if (!firstFrameNative && frame.script.isNative) break;
|
|
|
|
var text = '#' + i + ' ';
|
|
if (frame.func.inferredName && frame.func.inferredName.length > 0) {
|
|
text += frame.func.inferredName + ' ';
|
|
}
|
|
text += path.basename(frame.script.name) + ':';
|
|
text += (frame.line + 1) + ':' + (frame.column + 1);
|
|
|
|
trace.push(text);
|
|
}
|
|
|
|
self.print(trace.join('\n'));
|
|
}
|
|
|
|
self.resume();
|
|
});
|
|
};
|
|
|
|
|
|
// First argument tells if it should display internal node scripts or not
|
|
// (available only for internal debugger's functions)
|
|
Interface.prototype.scripts = function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var client = this.client,
|
|
displayNatives = arguments[0] || false,
|
|
scripts = [];
|
|
|
|
this.pause();
|
|
for (var id in client.scripts) {
|
|
var script = client.scripts[id];
|
|
if (script !== null && typeof script === 'object' && script.name) {
|
|
if (displayNatives ||
|
|
script.name == client.currentScript ||
|
|
!script.isNative) {
|
|
scripts.push(
|
|
(script.name == client.currentScript ? '* ' : ' ') +
|
|
id + ': ' +
|
|
path.basename(script.name)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
this.print(scripts.join('\n'));
|
|
this.resume();
|
|
};
|
|
|
|
|
|
// Continue execution of script
|
|
Interface.prototype.cont = function() {
|
|
if (!this.requireConnection()) return;
|
|
this.pause();
|
|
|
|
var self = this;
|
|
this.client.reqContinue(function(err) {
|
|
if (err) self.error(err);
|
|
self.resume();
|
|
});
|
|
};
|
|
|
|
|
|
// Step commands generator
|
|
Interface.stepGenerator = function(type, count) {
|
|
return function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this;
|
|
|
|
self.pause();
|
|
self.client.step(type, count, function(err, res) {
|
|
if (err) self.error(err);
|
|
self.resume();
|
|
});
|
|
};
|
|
};
|
|
|
|
|
|
// Jump to next command
|
|
Interface.prototype.next = Interface.stepGenerator('next', 1);
|
|
|
|
|
|
// Step in
|
|
Interface.prototype.step = Interface.stepGenerator('in', 1);
|
|
|
|
|
|
// Step out
|
|
Interface.prototype.out = Interface.stepGenerator('out', 1);
|
|
|
|
|
|
// Watch
|
|
Interface.prototype.watch = function(expr) {
|
|
this._watchers.push(expr);
|
|
};
|
|
|
|
// Unwatch
|
|
Interface.prototype.unwatch = function(expr) {
|
|
var index = this._watchers.indexOf(expr);
|
|
|
|
// Unwatch by expression
|
|
// or
|
|
// Unwatch by watcher number
|
|
this._watchers.splice(index !== -1 ? index : +expr, 1);
|
|
};
|
|
|
|
// List watchers
|
|
Interface.prototype.watchers = function() {
|
|
var self = this;
|
|
var verbose = arguments[0] || false;
|
|
var callback = arguments[1] || function() {};
|
|
var waiting = this._watchers.length;
|
|
var values = [];
|
|
|
|
this.pause();
|
|
|
|
if (!waiting) {
|
|
this.resume();
|
|
|
|
return callback();
|
|
}
|
|
|
|
this._watchers.forEach(function(watcher, i) {
|
|
self.debugEval(watcher, null, null, function(err, value) {
|
|
values[i] = err ? '<error>' : value;
|
|
wait();
|
|
});
|
|
});
|
|
|
|
function wait() {
|
|
if (--waiting === 0) {
|
|
if (verbose) self.print('Watchers:');
|
|
|
|
self._watchers.forEach(function(watcher, i) {
|
|
self.print(leftPad(i, ' ', self._watchers.length - 1) +
|
|
': ' + watcher + ' = ' +
|
|
JSON.stringify(values[i]));
|
|
});
|
|
|
|
if (verbose) self.print('');
|
|
|
|
self.resume();
|
|
|
|
callback(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Break on exception
|
|
Interface.prototype.breakOnException = function breakOnException() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this;
|
|
|
|
// Break on exceptions
|
|
this.pause();
|
|
this.client.reqSetExceptionBreak('all', function(err, res) {
|
|
self.resume();
|
|
});
|
|
};
|
|
|
|
// Add breakpoint
|
|
Interface.prototype.setBreakpoint = function(script, line,
|
|
condition, silent) {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this,
|
|
scriptId,
|
|
ambiguous;
|
|
|
|
// setBreakpoint() should insert breakpoint on current line
|
|
if (script === undefined) {
|
|
script = this.client.currentScript;
|
|
line = this.client.currentSourceLine + 1;
|
|
}
|
|
|
|
// setBreakpoint(line-number) should insert breakpoint in current script
|
|
if (line === undefined && typeof script === 'number') {
|
|
line = script;
|
|
script = this.client.currentScript;
|
|
}
|
|
|
|
if (script === undefined) {
|
|
this.print('Cannot determine the current script, ' +
|
|
'make sure the debugged process is paused.');
|
|
return;
|
|
}
|
|
|
|
if (/\(\)$/.test(script)) {
|
|
// setBreakpoint('functionname()');
|
|
var req = {
|
|
type: 'function',
|
|
target: script.replace(/\(\)$/, ''),
|
|
condition: condition
|
|
};
|
|
} else {
|
|
// setBreakpoint('scriptname')
|
|
if (script != +script && !this.client.scripts[script]) {
|
|
var scripts = this.client.scripts;
|
|
for (var id in scripts) {
|
|
if (scripts[id] &&
|
|
scripts[id].name &&
|
|
scripts[id].name.indexOf(script) !== -1) {
|
|
if (scriptId) {
|
|
ambiguous = true;
|
|
}
|
|
scriptId = id;
|
|
}
|
|
}
|
|
} else {
|
|
scriptId = script;
|
|
}
|
|
|
|
if (ambiguous) return this.error('Script name is ambiguous');
|
|
if (line <= 0) return this.error('Line should be a positive value');
|
|
|
|
var req;
|
|
if (scriptId) {
|
|
req = {
|
|
type: 'scriptId',
|
|
target: scriptId,
|
|
line: line - 1,
|
|
condition: condition
|
|
};
|
|
} else {
|
|
this.print('Warning: script \'' + script + '\' was not loaded yet.');
|
|
var escapedPath = script.replace(/([/\\.?*()^${}|[\]])/g, '\\$1');
|
|
var scriptPathRegex = '^(.*[\\/\\\\])?' + escapedPath + '$';
|
|
req = {
|
|
type: 'scriptRegExp',
|
|
target: scriptPathRegex,
|
|
line: line - 1,
|
|
condition: condition
|
|
};
|
|
}
|
|
}
|
|
|
|
self.pause();
|
|
self.client.setBreakpoint(req, function(err, res) {
|
|
if (err) {
|
|
if (!silent) {
|
|
self.error(err);
|
|
}
|
|
} else {
|
|
if (!silent) {
|
|
self.list(5);
|
|
}
|
|
|
|
// Try load scriptId and line from response
|
|
if (!scriptId) {
|
|
scriptId = res.script_id;
|
|
line = res.line + 1;
|
|
}
|
|
|
|
// Remember this breakpoint even if scriptId is not resolved yet
|
|
self.client.breakpoints.push({
|
|
id: res.breakpoint,
|
|
scriptId: scriptId,
|
|
script: (self.client.scripts[scriptId] || {}).name,
|
|
line: line,
|
|
condition: condition,
|
|
scriptReq: script
|
|
});
|
|
}
|
|
self.resume();
|
|
});
|
|
};
|
|
|
|
// Clear breakpoint
|
|
Interface.prototype.clearBreakpoint = function(script, line) {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var ambiguous,
|
|
breakpoint,
|
|
scriptId,
|
|
index;
|
|
|
|
this.client.breakpoints.some(function(bp, i) {
|
|
if (bp.scriptId === script ||
|
|
bp.scriptReq === script ||
|
|
(bp.script && bp.script.indexOf(script) !== -1)) {
|
|
if (index !== undefined) {
|
|
ambiguous = true;
|
|
}
|
|
scriptId = script;
|
|
if (bp.line === line) {
|
|
index = i;
|
|
breakpoint = bp.id;
|
|
return true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!scriptId && !this.client.scripts[script]) {
|
|
var scripts = this.client.scripts;
|
|
for (var id in scripts) {
|
|
if (scripts[id] &&
|
|
scripts[id].name &&
|
|
scripts[id].name.indexOf(script) !== -1) {
|
|
if (scriptId) {
|
|
ambiguous = true;
|
|
}
|
|
scriptId = id;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ambiguous) return this.error('Script name is ambiguous');
|
|
|
|
if (scriptId === undefined) {
|
|
return this.error('Script ' + script + ' not found');
|
|
}
|
|
|
|
if (breakpoint === undefined) {
|
|
return this.error('Breakpoint not found on line ' + line);
|
|
}
|
|
|
|
var self = this,
|
|
req = {
|
|
breakpoint: breakpoint
|
|
};
|
|
|
|
self.pause();
|
|
self.client.clearBreakpoint(req, function(err, res) {
|
|
if (err) {
|
|
self.error(err);
|
|
} else {
|
|
self.client.breakpoints.splice(index, 1);
|
|
self.list(5);
|
|
}
|
|
self.resume();
|
|
});
|
|
};
|
|
|
|
|
|
// Show breakpoints
|
|
Interface.prototype.breakpoints = function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
this.pause();
|
|
var self = this;
|
|
this.client.listbreakpoints(function(err, res) {
|
|
if (err) {
|
|
self.error(err);
|
|
} else {
|
|
self.print(res);
|
|
self.resume();
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
// Pause child process
|
|
Interface.prototype.pause_ = function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this,
|
|
cmd = 'process._debugPause();';
|
|
|
|
this.pause();
|
|
this.client.reqFrameEval(cmd, NO_FRAME, function(err, res) {
|
|
if (err) {
|
|
self.error(err);
|
|
} else {
|
|
self.resume();
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
// Kill child process
|
|
Interface.prototype.kill = function() {
|
|
if (!this.child) return;
|
|
this.killChild();
|
|
};
|
|
|
|
|
|
// Activate debug repl
|
|
Interface.prototype.repl = function() {
|
|
if (!this.requireConnection()) return;
|
|
|
|
var self = this;
|
|
|
|
self.print('Press Ctrl + C to leave debug repl');
|
|
|
|
// Don't display any default messages
|
|
var listeners = this.repl.rli.listeners('SIGINT').slice(0);
|
|
this.repl.rli.removeAllListeners('SIGINT');
|
|
|
|
// Exit debug repl on Ctrl + C
|
|
this.repl.rli.once('SIGINT', function() {
|
|
// Restore all listeners
|
|
process.nextTick(function() {
|
|
listeners.forEach(function(listener) {
|
|
self.repl.rli.on('SIGINT', listener);
|
|
});
|
|
});
|
|
|
|
// Exit debug repl
|
|
self.exitRepl();
|
|
});
|
|
|
|
// Set new
|
|
this.repl.eval = this.debugEval.bind(this);
|
|
this.repl.context = {};
|
|
|
|
// Swap history
|
|
this.history.control = this.repl.rli.history;
|
|
this.repl.rli.history = this.history.debug;
|
|
|
|
this.repl.rli.setPrompt('> ');
|
|
this.repl.displayPrompt();
|
|
};
|
|
|
|
|
|
// Exit debug repl
|
|
Interface.prototype.exitRepl = function() {
|
|
// Restore eval
|
|
this.repl.eval = this.controlEval.bind(this);
|
|
|
|
// Swap history
|
|
this.history.debug = this.repl.rli.history;
|
|
this.repl.rli.history = this.history.control;
|
|
|
|
this.repl.context = this.context;
|
|
this.repl.rli.setPrompt('debug> ');
|
|
this.repl.displayPrompt();
|
|
};
|
|
|
|
|
|
// Quit
|
|
Interface.prototype.quit = function() {
|
|
this.killChild();
|
|
process.exit(0);
|
|
};
|
|
|
|
|
|
// Kills child process
|
|
Interface.prototype.killChild = function() {
|
|
if (this.child) {
|
|
this.child.kill();
|
|
this.child = null;
|
|
}
|
|
|
|
if (this.client) {
|
|
// Save breakpoints
|
|
this.breakpoints = this.client.breakpoints;
|
|
|
|
this.client.destroy();
|
|
this.client = null;
|
|
}
|
|
};
|
|
|
|
|
|
// Spawns child process (and restores breakpoints)
|
|
Interface.prototype.trySpawn = function(cb) {
|
|
var self = this,
|
|
breakpoints = this.breakpoints || [],
|
|
port = exports.port,
|
|
host = 'localhost',
|
|
childArgs = this.args;
|
|
|
|
this.killChild();
|
|
assert(!this.child);
|
|
|
|
if (this.args.length === 2) {
|
|
var match = this.args[1].match(/^([^:]+):(\d+)$/);
|
|
|
|
if (match) {
|
|
// Connecting to remote debugger
|
|
// `node debug localhost:5858`
|
|
host = match[1];
|
|
port = parseInt(match[2], 10);
|
|
this.child = {
|
|
kill: function() {
|
|
// TODO Do we really need to handle it?
|
|
}
|
|
};
|
|
}
|
|
} else if (this.args.length === 3) {
|
|
// `node debug -p pid`
|
|
if (this.args[1] === '-p' && /^\d+$/.test(this.args[2])) {
|
|
this.child = {
|
|
kill: function() {
|
|
// TODO Do we really need to handle it?
|
|
}
|
|
};
|
|
process._debugProcess(parseInt(this.args[2], 10));
|
|
} else {
|
|
var match = this.args[1].match(/^--port=(\d+)$/);
|
|
if (match) {
|
|
// Start debugger on custom port
|
|
// `node debug --port=5858 app.js`
|
|
port = parseInt(match[1], 10);
|
|
childArgs = ['--debug-brk=' + port].concat(this.args.slice(2));
|
|
}
|
|
}
|
|
}
|
|
|
|
this.child = spawn(process.execPath, childArgs);
|
|
|
|
this.child.stdout.on('data', this.childPrint.bind(this));
|
|
this.child.stderr.on('data', this.childPrint.bind(this));
|
|
|
|
this.pause();
|
|
|
|
var client = self.client = new Client(),
|
|
connectionAttempts = 0;
|
|
|
|
client.once('ready', function() {
|
|
self.stdout.write(' ok\n');
|
|
|
|
// Restore breakpoints
|
|
breakpoints.forEach(function(bp) {
|
|
self.print('Restoring breakpoint ' + bp.scriptReq + ':' + bp.line);
|
|
self.setBreakpoint(bp.scriptReq, bp.line, bp.condition, true);
|
|
});
|
|
|
|
client.on('close', function() {
|
|
self.pause();
|
|
self.print('program terminated');
|
|
self.resume();
|
|
self.client = null;
|
|
self.killChild();
|
|
});
|
|
|
|
if (cb) cb();
|
|
self.resume();
|
|
});
|
|
|
|
client.on('unhandledResponse', function(res) {
|
|
self.pause();
|
|
self.print('\nunhandled res:' + JSON.stringify(res));
|
|
self.resume();
|
|
});
|
|
|
|
client.on('break', function(res) {
|
|
self.handleBreak(res.body);
|
|
});
|
|
|
|
client.on('exception', function(res) {
|
|
self.handleBreak(res.body);
|
|
});
|
|
|
|
client.on('error', connectError);
|
|
function connectError() {
|
|
// If it's failed to connect 4 times then don't catch the next error
|
|
if (connectionAttempts >= 10) {
|
|
client.removeListener('error', connectError);
|
|
}
|
|
setTimeout(attemptConnect, 500);
|
|
}
|
|
|
|
function attemptConnect() {
|
|
++connectionAttempts;
|
|
self.stdout.write('.');
|
|
client.connect(port, host);
|
|
}
|
|
|
|
this.child.stderr.once('data', function() {
|
|
setImmediate(function() {
|
|
self.print('connecting to port ' + port + '..', true);
|
|
attemptConnect();
|
|
});
|
|
});
|
|
};
|