// Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. var net = require('net'), repl = require('repl'), vm = require('vm'), inherits = require('util').inherits, spawn = require('child_process').spawn; exports.port = 5858; exports.start = function() { if (process.argv.length < 3) { console.error('Usage: node debug script.js'); process.exit(1); } var interface = new Interface(); 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); }); }; var args = process.argv.slice(2); args.unshift('--debug-brk'); // // 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 lines = res.raw.slice(0, endHeaderIndex).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.bodyStartIndex = endHeaderIndex + 4; this.state = 'body'; if (res.raw.length - this.bodyStartIndex < this.contentLength) break; // pass thru case 'body': if (res.raw.length - this.bodyStartIndex >= this.contentLength) { res.body = res.raw.slice(this.bodyStartIndex, this.bodyStartIndex + this.contentLength); // JSON parse body? res.body = res.body.length ? JSON.parse(res.body) : {}; // Done! this.onResponse(res); this._newRes(res.raw.slice(this.bodyStartIndex + this.contentLength)); } 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: ' + json.length + '\r\n\r\n' + json; }; var 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.currentSource = null; this.handles = {}; this.scripts = {}; // 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 (typeof desc != 'object' || typeof desc.handle != 'number') { return; } this.handles[desc.handle] = desc; if (desc.type == 'script') { this._addScript(desc); } }; var 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) { for (var i = 0; i < this._reqCallbacks.length; i++) { var cb = this._reqCallbacks[i]; if (this._reqCallbacks[i].request_seq == res.body.request_seq) break; } 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 == '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; } if (cb) { this._reqCallbacks.splice(i, 1); handled = true; cb(res.body); } 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) { this.req({ command: 'version' } , function(res) { if (cb) cb(res.body.V8Version, res.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 } }; this.req(req, function(res) { if (res.success) { for (var ref in res.body) { if (typeof res.body[ref] == 'object') { self._addHandle(res.body[ref]); } } } if (cb) cb(res); }); }; Client.prototype.reqScopes = function(cb) { var self = this, req = { command: 'scopes', arguments: {} }; this.req(req, function(res) { if (!res.success) return cb(Error(res.message) || true); var refs = res.body.scopes.map(function(scope) { return scope.object.ref; }); self.reqLookup(refs, function(res) { if (!res.success) return cb(Error(res.message) || true) var globals = Object.keys(res.body).map(function(key) { return res.body[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; } // Otherwise we need to get the current frame to see which scopes it has. this.reqBacktrace(function(bt) { if (!bt.frames) { // ?? cb({}); return; } 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 epxression 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(); this.reqFrameEval(expression, i, function(res) { if (res.success) { if (cb) cb(res); } else { 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; } this.req(req, function(res) { if (res.success) { self._addHandle(res.body); } if (cb) cb(res); }); }; // reqBacktrace(cb) // TODO: from, to, bottom Client.prototype.reqBacktrace = function(cb) { this.req({ command: 'backtrace' } , function(res) { if (cb) cb(res.body); }); }; // 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; this.req({ command: 'scripts' } , function(res) { for (var i = 0; i < res.body.length; i++) { self._addHandle(res.body[i]); } if (cb) cb(); }); }; Client.prototype.reqContinue = function(cb) { this.req({ command: 'continue' }, function(res) { if (cb) cb(res); }); }; Client.prototype.listbreakpoints = function(cb) { this.req({ command: 'listbreakpoints' }, function(res) { if (cb) cb(res); }); }; Client.prototype.reqSource = function(from, to, cb) { var req = { command: 'source', fromLine: from, toLine: to }; this.req(req, function(res) { if (cb) cb(res.body); }); }; // client.next(1, cb); Client.prototype.step = function(action, count, cb) { var req = { command: 'continue', arguments: { stepaction: action, stepcount: count } }; this.req(req, function(res) { if (cb) cb(res); }); }; Client.prototype.mirrorObject = function(handle, 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: '#' } // 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; }); this.reqLookup(propertyRefs, function(res) { if (!res.success) { console.error('problem with reqLookup'); if (cb) cb(handle); return; } var mirror; if (handle.className == 'Array') { mirror = []; } else { mirror = {}; } for (var i = 0; i < handle.properties.length; i++) { var value = res.body[handle.properties[i].ref]; var mirrorValue; if (value) { mirrorValue = value.value ? value.value : value.text; } else { mirrorValue = '[?]'; } if (Array.isArray(mirror) && typeof handle.properties[i].name != 'number') { // Skip the 'length' property. continue; } mirror[handle.properties[i].name] = mirrorValue; } if (cb) cb(mirror); }); return; } else if (handle.type === 'function') { val = function() {}; } else if (handle.value) { val = handle.value; } else if (handle.type === 'undefined') { val = undefined; } else { val = handle; } process.nextTick(function() { cb(val); }); }; Client.prototype.fullTrace = function(cb) { var self = this; this.reqBacktrace(function(trace) { 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(res) { for (var i = 0; i < trace.frames.length; i++) { var frame = trace.frames[i]; frame.script = res.body[frame.script.ref]; frame.func = res.body[frame.func.ref]; frame.receiver = res.body[frame.receiver.ref]; } if (cb) cb(trace); }); }); }; var commands = [ 'help', 'run', 'restart', 'cont', 'next', 'step', 'out', 'repl', 'backtrace', 'breakpoints', 'kill', 'list', 'scripts', 'version' ]; var helpMessage = 'Commands: ' + commands.join(', '); function SourceUnderline(sourceText, position) { if (!sourceText) return; var wrapper = require('module').wrapper[0]; if (sourceText.indexOf(wrapper) === 0) { sourceText = sourceText.slice(wrapper.length); position -= wrapper.length; } // Create an underline with a caret pointing to the source position. If the // source contains a tab character the underline will have a tab character in // the same place otherwise the underline will have a space character. var underline = ''; for (var i = 0; i < position; i++) { if (sourceText[i] == '\t') { underline += '\t'; } else { underline += ' '; } } underline += '^'; // Return the source line text with the underline beneath. return sourceText + '\n' + underline; } function SourceInvocation(body) { return body.invocationText.replace(/\n/g, '') .replace(/^([^(]*)\(.*\)([^)]*)$/, '$1(...)$2'); } function SourceInfo(body) { var result = 'break in ' + SourceInvocation(body); if (body.script) { if (body.script.name) { result += ', ' + body.script.name; } else { result += ', [unnamed]'; } } result += ':'; result += body.sourceLine + 1; return result; } // This class is the repl-enabled debugger interface which is invoked on // "node debug" function Interface() { var self = this, child; process.on('exit', function() { self.killChild(); }); this.repl = new repl.REPLServer('debug> ', null, this.controlEval.bind(this)); // Lift all instance methods to repl context var proto = Interface.prototype, ignored = ['pause', 'resume', 'exitRepl', 'handleBreak', 'requireConnection', 'killChild', 'trySpawn', 'controlEval', 'debugEval'], synonym = { 'run': 'r', 'cont': 'c', 'next': 'n', 'step': 's', 'out': 'o' }; function defineProperty(key, protoKey) { Object.defineProperty(self.repl.context, key, { get: proto[protoKey].bind(self), enumerable: true }); }; for (var i in proto) { if (proto.hasOwnProperty(i) && ignored.indexOf(i) === -1) { defineProperty(i, i); if (synonym[i]) defineProperty(synonym[i], i); } } this.waiting = null; this.paused = 0; this.context = this.repl.context; }; // Stream control Interface.prototype.pause = function() { if (this.paused++ > 0) return false; this.repl.rli.pause(); process.stdin.pause(); }; Interface.prototype.resume = function(silent) { if (this.paused === 0 || --this.paused !== 0) return false; this.repl.rli.resume(); if (silent !== true) { this.repl.displayPrompt(); } process.stdin.resume(); if (this.waiting) { this.waiting(); this.waiting = null; } }; Interface.prototype.handleBreak = function(r) { var expected = this.paused !== 0; this.pause(); this.client.currentSourceLine = r.sourceLine; this.client.currentFrame = 0; this.client.currentScript = r.script.name; if (!expected) { console.log(''); } console.log(SourceInfo(r)); console.log(SourceUnderline(r.sourceLineText, r.sourceColumn)); this.resume(); }; Interface.prototype.requireConnection = function() { if (!this.client) throw Error('App isn\'t running... Try `run` instead'); }; Interface.prototype.controlEval = function(code, context, filename, callback) { try { var result = vm.runInContext(code, context, filename); if (this.paused === 0) return callback(null, result); this.waiting = function() { callback(null, result); }; } catch (e) { callback(e); } }; Interface.prototype.debugEval = function(code, context, filename, callback) { var self = this, client = this.client; if (code === '.scope') { client.reqScopes(callback); return; } self.pause(); client.reqEval(code, function(res) { if (!res.success) { if (res.message) { callback(res.message); } else { callback(null); } self.resume(true); return; } client.mirrorObject(res.body, function(mirror) { callback(null, mirror); self.resume(true); }); }); }; // Commands function intChars(n) { // TODO dumb: if (n < 50) { return 2; } else if (n < 950) { return 3; } else if (n < 9950) { return 4; } else { return 5; } } function leftPad(n) { var s = n.toString(); var nchars = intChars(n); var nspaces = nchars - s.length; for (var i = 0; i < nspaces; i++) { s = ' ' + s; } return s; } // Print help message Interface.prototype.help = function() { console.log(helpMessage); }; // Run script Interface.prototype.run = function() { if (this.child) { throw Error('App is already running... Try `restart` instead'); } else { this.trySpawn(); } }; // Restart script Interface.prototype.restart = function() { if (!this.child) throw Error('App isn\'t running... Try `run` instead'); var self = this; this.killChild(); // XXX need to wait a little bit for the restart to work? setTimeout(function() { self.trySpawn(); }, 1000); }; // Print version Interface.prototype.version = function() { this.requireConnection(); var self = this; this.pause(); this.client.reqVersion(function(v) { process.stdout.write(v); self.resume(); }); }; // List source code Interface.prototype.list = function() { this.requireConnection(); var self = this, client = this.client, from = client.currentSourceLine - 5, to = client.currentSourceLine + 5; self.pause(); client.reqSource(from, to, function(res) { 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; if (lineno == 1) { // The first line needs to have the module wrapper filtered out of // it. var wrapper = require('module').wrapper[0]; lines[i] = lines[i].slice(wrapper.length); } if (lineno == 1 + client.currentSourceLine) { var nchars = intChars(lineno); var pointer = ''; for (var j = 0; j < nchars - 1; j++) { pointer += '='; } pointer += '>'; console.log(pointer + ' ' + lines[i]); } else { console.log(leftPad(lineno) + ' ' + lines[i]); } } self.resume(); }); }; // Print backtrace Interface.prototype.backtrace = function() { this.requireConnection(); var self = this, client = this.client; self.pause(); client.fullTrace(function(bt) { if (bt.totalFrames == 0) { console.log('(empty stack)'); } else { var text = ''; var 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; text += '#' + i + ' '; if (frame.func.inferredName && frame.func.inferredName.length > 0) { text += frame.func.inferredName + ' '; } text += require('path').basename(frame.script.name) + ':'; text += (frame.line + 1) + ':' + (frame.column + 1); text += '\n'; } console.log(text); } self.resume(); }); }; // argument full tells if it should display internal node scripts or not Interface.prototype.scripts = function(displayNatives) { this.requireConnection(); var client = this.client; var text = ''; this.pause(); for (var id in client.scripts) { var script = client.scripts[id]; if (typeof script == 'object' && script.name) { if (displayNatives || script.name == client.currentScript || !script.isNative) { text += script.name == client.currentScript ? '* ' : ' '; text += require('path').basename(script.name) + '\n'; } } } console.log(text); this.resume(); }; // Continue execution of script Interface.prototype.cont = function() { this.requireConnection(); this.pause(); var self = this; this.client.reqContinue(function() { process.nextTick(function() { self.resume(); }); }); }; // Jump to next command Interface.prototype.next = function() { this.requireConnection(); this.pause(); var self = this; this.client.step('next', 1, function(res) { process.nextTick(function() { self.resume(); }); }); }; // Step in Interface.prototype.step = function() { this.requireConnection(); this.pause(); var self = this; this.client.step('in', 1, function(res) { process.nextTick(function() { self.resume(); }); }); }; // Step out Interface.prototype.out = function() { this.requireConnection(); this.pause(); var self = this; this.client.step('out', 1, function(res) { process.nextTick(function() { self.resume(); }); }); }; // Show breakpoints Interface.prototype.breakpoints = function() { this.requireConnection(); this.pause(); var self = this; this.client.listbreakpoints(function(res) { if (res.success) { console.log(res.body); } else { throw Error(res.message || 'Some error happened'); } self.resume(); }); }; // Kill child process Interface.prototype.kill = function() { if (!this.child) return; this.killChild(); }; // Activate debug repl Interface.prototype.repl = function() { this.requireConnection(); var self = this; console.log('Press Ctrl + C to leave debug repl'); // Don't display any default messages var listeners = this.repl.rli.listeners('SIGINT'); 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 = {}; this.repl.prompt = '> '; this.repl.rli.setPrompt('> '); this.repl.displayPrompt(); }; // Exit debug repl Interface.prototype.exitRepl = function() { this.repl.eval = this.controlEval.bind(this); this.repl.context = this.context; this.repl.prompt = 'debug> '; this.repl.rli.setPrompt('debug> '); this.repl.displayPrompt(); }; Interface.prototype.killChild = function() { if (this.child) { this.child.kill(); this.child = null; } if (this.client) { this.client.destroy(); this.client = null; } this.resume(); }; Interface.prototype.trySpawn = function(cb) { var self = this; this.killChild(); this.child = spawn(process.execPath, args, { customFds: [0, 1, 2] }); this.pause(); var client = self.client = new Client(); var connectionAttempts = 0; client.once('ready', function() { process.stdout.write(' ok\n'); // since we did debug-brk, we're hitting a break point immediately // continue before anything else. client.reqContinue(function() { self.resume(); if (cb) cb(); }); client.on('close', function() { self.pause() console.log('program terminated'); self.resume(); self.client = null; self.killChild(); }); }); client.on('unhandledResponse', function(res) { self.pause(); console.log('\r\nunhandled res:'); console.log(res); self.resume(); }); client.on('break', 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 >= 4) { client.removeListener('error', connectError); } setTimeout(attemptConnect, 50); } function attemptConnect() { ++connectionAttempts; process.stdout.write('.'); client.connect(exports.port); } setTimeout(function() { process.stdout.write('connecting..'); attemptConnect(); }, 50); };