UNPKG

trepanjs

Version:

A gdb-like debugger for nodejs. It is in the style of the trepanning debuggers.

552 lines (449 loc) 13.8 kB
"use strict"; // Copyright 2015 Rocky Bernstein // 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. // Routines which communicate with the backend nodejs to issue debugger // commands and get results back. interface.js handles provivdes the // and user higher level command but uses this to get the information. // protocol.js is is lower-level and handles network communication. It // doesn't try to grok the packets as they come back like this does. var net = require('net'), proto = require('./protocol'), utilCompat = require('./utilcompat'), util = require('util'), inherits = util.inherits; var NO_FRAME = -1; exports.NO_FRAME = NO_FRAME; function Client() { net.Stream.call(this); var protocol = this.protocol = new proto.Protocol(this); this._reqCallbacks = []; var socket = this; this.currentFrame = NO_FRAME; this.currentSourceLine = -1; this.handles = {}; this.scripts = {}; this.script2id = {}; this.breakpoints = []; // Note that 'Protocol' requires strings instead of Buffers. socket.setEncoding('utf8'); socket.on('data', function(d) { protocol.execute(d); }); socket.on('error', function(err) { console.log('...received an error', err.code); }); protocol.onResponse = this._onResponse.bind(this); } inherits(Client, net.Stream); exports.Client = Client; Client.prototype._addHandle = function(desc) { if (!utilCompat.isObject(desc) || !utilCompat.isNumber(desc.handle)) { 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) { this.script2id[desc.name] = desc.id; 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; } 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 (utilCompat.isObject(res[ref])) { 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); }; // reqFrame(cb) Client.prototype.reqFrame = function(cb, frame) { var self = this; (frame === undefined) && (frame = this.currentFrame); if (frame == NO_FRAME) { return cb(Error('No frame')); } this.req({ command: 'frame', arguments: { number: frame } } , 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, frame, cb) { var req = { command: 'source', frame: frame, 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 (utilCompat.isArray(mirror) && !utilCompat.isNumber(prop.name)) { // 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 (!utilCompat.isUndefined(handle.value)) { 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); }); }); };