UNPKG

komodo-debug

Version:

This package contains the bits required for debugging node.js application with Komodo IDE remotely.

1,438 lines (1,317 loc) 56 kB
// Copyright (c) 2011 - 2012 ActiveState Software Inc. // See the LICENSE file for licensing information. var v8dbg = require('./v8dbg'), cmdln = require('./cmdln'), util = require('util'), events = require('events'), net = require('net'), path = require('path'), url = require('url'), os = require('os'), assert = require('assert'), Log = require('log'); var log = new Log('error'); //var log = new Log('debug'); var DBGP_ERROR_CODES = { 'INVALID_OPTIONS': 3, 'BREAKPOINT_COULD_NOT_BE_SET': 200, 'BREAKPOINT_TYPE_NOT_SUPPORTED': 201, 'BREAKPOINT_INVALID': 202, 'NO_CODE_ON_BREAKPOINT_LINE': 203, 'INVALID_BREAKPOINT_STATE': 204, 'NO_SUCH_BREAKPOINT': 205, 'ERROR_EVALUATING_CODE': 206, 'INVALID_EXPRESSION': 207, 'CANNOT_GET_PROPERTY': 300, 'STACK_DEPTH_INVALID': 301, 'CONTEXT_INVALID': 302, 'INTERNAL_ERROR': 998, 'UNKNOWN_ERROR': 999 }; var V8_SCOPE_TYPES = { 0: 'Global', 1: 'Local', 2: 'With', 3: 'Closure', 4: 'Catch' }; var DBGP_CONTEXT_IDS = { // Map DBGP context IDs to V8 scope types, so we can use 0 for locals. 0: 1, // Local 1: 2, // With 2: 3, // Closure 3: 4, // Catch 4: 0 // Global }; var DBGP_TYPEMAP = { 'undefined': 'undefined', 'null': 'object', 'boolean': 'bool', 'number': 'float', 'string': 'string', 'object': 'object', 'function': 'function' } var INTERNAL_GLOBAL_NAMES = [ "Array", "Boolean", "Buffer", "clearInterval", "clearTimeout", "console", "Date", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "Error", "escape", "eval", "EvalError", "execScript", "Function", "GLOBAL", "global", "Infinity", "isFinite", "isNaN", "JSON", "Math", "NaN", "Number", "Object", "parseFloat", "parseInt", "process", "RangeError", "ReferenceError", "RegExp", "root", "setInterval", "setTimeout", "String", "SyntaxError", "TypeError", "undefined", "unescape", "URIError", "v8debug" ]; var INTERNAL_LOCAL_NAMES = [ "__dirname", "__filename", "exports", "module", "require" ]; //function ts(o) { // var s = JSON.stringify(o); // var lim = 100; // if (s.length > lim) { // s = s.substr(0, lim) + "..."; // } // return s; //} function Client(engine) { net.Socket.call(this); this.engine = engine; this.setEncoding('ascii'); this._messageChunk = ''; this.on('data', function(data) { log.info('Got a chunk: ' + data); this._messageChunk += data; var eom, messages = []; while ((eom = this._messageChunk.indexOf('\x00')) != -1) { messages.push(this._messageChunk.slice(0, eom)); this._messageChunk = this._messageChunk.slice(eom+1); } messages.forEach(function(msg) { this.engine.handleMessage(msg); }.bind(this)); }); this.on('closed', function() { this.engine.emit('dbgpClosed'); }); } util.inherits(Client, net.Socket); Client.prototype.sendPacket = function(xml) { var packet = ''; packet += encodeURIComponent(xml).replace(/%[A-F\d]{2}/g, 'U').length + '\x00' + xml + '\x00'; this.write(packet); log.info("Message packet sent: " + packet); }; exports.Client = Client; function Engine(debuggee, fileUri, port, host) { var version = process.version.match(/(\d+)\.(\d+)\.(\d+)/) if (Number(version[1]) == 0 && (Number(version[2]) < 11 || (Number(version[2]) == 11 && Number(version[3] <= 12)))) { // NodeJS v0.11.12 and lower needs this. events.EventEmitter.apply(this); } else { // NodeJS v0.11.13 and higher needs this. events.call(this); } this.init = function(child, fileUri, port, host) { this.fileUri = fileUri; this.port = port; this.host = host; // may be undefined; that's okay. this.status = 'starting'; this.reason = 'ok'; this.stdout = 0; // 0-disable, 1-copy data, 2-redirect this.stderr = 0; // 0-disable, 1-copy data, 2-redirect this._firstStderrMessage = true; this._initV8Client(); this._initDBGPClient(); this._initChild(child); assert.ok(this.child); }; this._initV8Client = function() { this.v8Client = new v8dbg.Client(); this.v8Client.once('ready', function(resp) { this.v8Client.req({command: 'setexceptionbreak', arguments: {type: 'uncaught', enabled: true}}, function(err, resp) { if (err) { log.error("Failed to initialize v8 debugger, aborting."); this.terminate(); } else { // Some times the initial breakpoint hasn't been reached yet; wait a // bit to make sure that happens. Mostly happens on Windows, but this // has been observed on OSX too. setTimeout(function() { log.info('dbgp ready'); this.emit('ready'); }.bind(this), 100); } }.bind(this)) }.bind(this)); this.v8Client.on('break', function(resp) { resp = resp.body; log.info('V8 Debugger Client ran into breakpoint:\n\t' + resp.sourceLine + ':' + resp.sourceColumn + ' ' + resp.sourceLineText); for (var p in resp) { log.info(p + ": " + String(resp[p]).split(/\r\n?|\n/)[0]); } log.info("resp.script: " ); for (var p in resp.script) { log.info(" " + p + ": " + resp.script[p]); } this.status = 'break'; this.reason = 'ok'; this.emit('v8ExecutionStopped'); }.bind(this)); this.v8Client.on('exception', function(resp) { // Note that this only works for uncaught exceptions. // TODO This doesn't fire on my machine. Looks like a v8 bug to me. resp = resp.body; log.error('Debuggee process ran into uncaught exception:\n\t' + resp.script.name + ':' + resp.sourceLine + ':' + resp.sourceColumn + ' ' + resp.sourceLineText); this.status = 'break'; this.reason = 'exception'; this.emit('v8ExecutionStopped'); }.bind(this)); this.v8Client.on('error', function(e) { log.debug("V8 Debugger Client error: " + e.message); this.status = 'stopped'; this.reason = 'error'; this.emit('v8ExecutionStopped'); }.bind(this)); this.v8Client.on('close', function() { log.info('V8 Debugger Client closed'); this.status = 'stopped'; this.reason = 'ok'; this.emit('v8ExecutionStopped'); }.bind(this)); }; this._initDBGPClient = function() { this.dbgpClient = new Client(this); this.dbgpClient.on('connect', function() { log.info('DBGP client connected'); this.dbgpClient.sendPacket(this.makeInit()); }.bind(this)); this.dbgpClient.on('close', function() { log.info('DBGP client disconnected'); this.emit('exit'); }.bind(this)); }; this._initChild = function(childProcess) { this.child = childProcess; // the debuggee child process this.child.on('exit', function() { // Note: Node >= v0.11.13 will never emit this event. // Thus the Komodo user needs to press 'Stop' manually. // https://github.com/nodejs/node/issues/1788 this.status = 'stopped'; log.info('Child process exited'); this.emit('v8ExecutionStopped'); }.bind(this)); this.child.stdout.on('data', function(data) { if (this.stdout == 0) process.stdout.write(data); else if (this.stdout == 1) this.stdoutCopy(data); else if (this.stdout == 2) this.stdoutRedirect(data); }.bind(this)); this.child.stderr.on('data', function(data) { if (this._firstStderrMessage) { this._firstStderrMessage = false; var ignore_str = "ebugger listening on port "; // v0.10 uses 'd' and v0.12 uses 'D' if (data.toString().substr(1, ignore_str.length) == ignore_str) { // Ignore this stderr message that comes from the node debugger. return; } } if (this.stderr == 0) process.stderr.write(data); else if (this.stderr == 1) this.stderrCopy(data); else if (this.stderr == 2) this.stderrRedirect(data); }.bind(this)); }; /** * Features that are supported in feature_get */ this._readOnlyFeatures = { 'breakpoint_types': 'line', // TODO implement more breakpoint_types 'data_encoding': 'base64', 'encoding': 'utf-8', 'language_supports_threads': '0', 'language_name': 'Node.js', 'language_version': '' + process.version, 'multiple_sessions': '0', 'protocol_version': '1', 'supports_async': '1', 'supports_postmortem': '1', // TODO make writable. // must either be limited or we should stop printing function definitions 'max_data': 30000 }; /** * Features that are supported in feature_get and feature_set */ this._writableFeatures = { 'max_children': 25, 'max_depth': 0, 'show_hidden': 0 }; this.getFeature = function(name) { if (this._readOnlyFeatures.hasOwnProperty(name)) { return this._readOnlyFeatures[name]; } else if (this._writableFeatures.hasOwnProperty(name)) { return this._writableFeatures[name]; } else { return undefined; } }; this.setFeature = function(name, value) { if (this._writableFeatures.hasOwnProperty(name)) { this._writableFeatures[name] = value; return true; } else { return false; } }; /** * ----- DBGP procotol message handlers */ this.on('feature_get', function(args) { if (!("n" in args)) { this.sendUnsuccessfulResponse('feature_get', args['i'], null, this.makeError(DBGP_ERROR_CODES.INVALID_OPTIONS, "No feature name given"), true); return; } var featureName = '' + args['n']; var response = {feature_name: featureName}; var featureValue, message = ""; if (!featureName) { message = this.makeError(DBGP_ERROR_CODES['INVALID_OPTIONS'], 'missing parameter'); } else { featureValue = this.getFeature(featureName); if (typeof featureValue !== 'undefined') { message = featureValue; response['supported'] = '1'; } else if (featureName in this._events) { response['supported'] = '1'; } else { response['supported'] = '0'; } } this.sendResponse('feature_get', args['i'], response, '' + message); }); this.on('feature_set', function(args) { if (!("n" in args)) { this.sendUnsuccessfulResponse('feature_get', args['i'], null, this.makeError(DBGP_ERROR_CODES.INVALID_OPTIONS, "No feature name given"), true); return; } if (!("v" in args)) { this.sendUnsuccessfulResponse('feature_get', args['i'], null, this.makeError(DBGP_ERROR_CODES.INVALID_OPTIONS, "No feature value given"), true); return; } var featureName = '' + args['n']; var newValue = args['v']; var message = null; var response = { featureName: featureName, success: 0 }; var errorCode, status = 0, success = 0; if (!featureName) { errorCode = 'INVALID_OPTIONS'; message = 'missing parameter'; } else if (this._readOnlyFeatures.hasOwnProperty(featureName)) { message = 'Command ' + featureName + ' not modifiable'; // This is not an error condition -- just let the client know the feature wasn't changed. } else if (isNaN(newValue = parseInt(newValue, 10))) { // only numerical features at this point errorCode = 'INVALID_OPTIONS'; message = 'invalid value'; } else if (!this.setFeature(featureName, newValue)) { errorCode = 'INVALID_OPTIONS'; message = 'Unknown feature name'; } else { response['success'] = '1'; } if (errorCode) { log.info("feature_set: got message:" + message + ", errorCode:" + errorCode); message = this.makeError(DBGP_ERROR_CODES[errorCode], message); this.sendUnsuccessfulResponse('feature_set', args['i'], {'feature_name': args['n']}, message, true); } else { log.info("feature_set: " + featureName + " succeeded"); this.sendResponse('feature_set', args['i'], response); } }); this.on('status', function(args) { this.sendStatusResponse('status', args['i']); }); this.on('run', function(args) { log.info('Starting debuggee...'); this.v8Client.reqContinue(function(err, resp) { if (!err && resp.running) { log.info('debuggee running!'); this.status = 'running'; this.reason = 'ok'; this.once('v8ExecutionStopped', function() { this.sendStatusResponse('run', args['i']); }.bind(this)); } else { this.status = 'stopped'; this.reason = 'error'; this.sendStatusResponse('run', args['i'], null, this.makeError(DBGP_ERROR_CODES.UNKNOWN_ERROR, err || JSON.stringify(resp)), true); } }.bind(this)); }); this.on('step_into', function(args) { this._firstBreak = true; this.v8Client.step('in', 1, function(err, resp) { if (!err && resp.success&& resp.running) { log.info('debuggee running!'); this.status = 'running'; this.reason = 'ok'; this.once('v8ExecutionStopped', function() { this.sendStatusResponse('step_into', args['i']); }.bind(this)); } else { this.status = 'stopped'; this.reason = 'error'; this.sendStatusResponse('step_into', args['i']); } }.bind(this)); }); this.on('step_over', function(args) { this.v8Client.step('next', 1, function(err, resp) { if (!err && resp.success && resp.running) { this.status = 'running'; this.reason = 'ok'; this.once('v8ExecutionStopped', function() { this.sendStatusResponse('step_over', args['i']); }.bind(this)); } else { this.status = 'stopped'; this.reason = 'error'; this.sendStatusResponse('step_over', args['i']); } }.bind(this)); }); this.on('step_out', function(args) { this.v8Client.step('out', 1, function(err, resp) { if (!err && resp.success&& resp.running) { this.status = 'running'; this.reason = 'ok'; this.once('v8ExecutionStopped', function() { this.sendStatusResponse('step_out', args['i']); }.bind(this)); } else { this.status = 'stopped'; this.reason = 'error'; this.sendStatusResponse('step_out', args['i']); } }.bind(this)); }); this.on('stop', function(args) { if (this.status == "stopped") { log.info('Stopping debuggee... already stopped'); return; } log.info('Stopping debuggee...'); if (this.child) { // Kill the debuggee process, tell the IDE that the session stopped. this.once('v8ExecutionStopped', function() { this.status = 'stopped'; this.reason = 'ok'; this.sendStatusResponse('stop', args['i']); this.dbgpClient.end(); }.bind(this)); this.child.kill(); this.child = null; } else { this.sendStatusResponse('stop', args['i']); } }); this.on('stack_get', function stack_get(args) { var stackDepth = args['d']; // XXX marky: Avoid using this.v8Client.fullTrace, it tries to look up // things like the receiver, which we don't care about. this.v8Client.reqBacktrace(function(err, trace) { if (err) { this.sendUnsuccessfulResponse('stack_get', args['i'], null, this.makeError(DBGP_ERROR_CODES.INTERNAL_ERROR, err), true); return; } if (!trace.frames || !trace.frames.length) { this.sendUnsuccessfulResponse('stack_get', args['i'], null, this.makeError(DBGP_ERROR_CODES.CANNOT_GET_PROPERTY), true) } var frames = []; var scripts = {}; this.v8Client._ko_reqScripts( this.v8Client._ko_SCRIPT_TYPES.ALL, trace.frames.map(function(frame) {return frame.func.scriptId;}), /* source? */ false, /* filter */ null, /* key */ "id", function (err, scripts) { if (err || !scripts) { this.sendUnsuccessfulResponse('stack_get', args['i'], null, this.makeError(DBGP_ERROR_CODES.INTERNAL_ERROR, err), true); return; } // Collect requested frames if (stackDepth >= 0) { // Requested a single frame for (var i = 0; i < trace.frames.length; i++) { var frame = trace.frames[i]; frame.script = scripts[frame.func.scriptId]; if (!frame.script || frame.script.isNative) { // Note that we currently don't expose node-internal stuff. // This might change. continue; } else if (frame.index == stackDepth) { frames.push(frame); break; } } if (frames.length != 1) { // Requested stack depth is invalid. this.sendUnsuccessfulResponse('stack_get', args['i'], null, this.makeError(DBGP_ERROR_CODES.STACK_DEPTH_INVALID), true); return; } } else { // Requested a full stack trace for (var i = 0; i < trace.frames.length; i++) { var frame = trace.frames[i]; frame.script = scripts[frame.func.scriptId]; if (!frame.script || frame.script.isNative) continue; else frames.push(frame); } } // Make a DBGP stack trace and send as reponse var framesStr = ''; for (var i = 0; i < frames.length; i++) { var frame = frames[i]; frame.line += 1; // V8 starts indexing at 0 framesStr += this.makeFrame(frame.index, 'file', frame.script.name, frame.line, frame.func.inferredName); } this.sendResponse('stack_get', args['i'], { 'success': '1' }, framesStr, true); }.bind(this)); }.bind(this)); }); this.on('breakpoint_set', function(args) { // Only partially implemented. var type = args['t'], state = (args['s'] != 'disabled'), uri = args['f'], lineno = parseInt(args['n'], 10); var target = this._filepathFromUri(uri); // V8 starts indexing lines at 0: lineno -= 1; var func = args['m']; switch (type) { case 'line': this.v8Client.setBreakpoint({ 'type': 'script', 'target': target, 'line': lineno, 'column': 0, 'enabled': state, }, function(err, res) { if (!err && res) { this.sendResponse('breakpoint_set', args['i'], { 'id': res.breakpoint, 'state': state ? 'enabled' : 'disabled' }); } else { var error = this.makeError(DBGP_ERROR_CODES.BREAKPOINT_COULD_NOT_BE_SET, err); this.sendResponse('breakpoint_set', args['i'], { 'status': 'disabled' }, error, true); } }.bind(this)); break; default: var error = this.makeError(DBGP_ERROR_CODES['BREAKPOINT_TYPE_NOT_SUPPORTED']); this.sendResponse('breakpoint_set', args['i'], { 'status': 'disabled' }, error, true); } }); /* this.on('breakpoint_get', function(args) { }); */ this.on('breakpoint_update', function(args) { if (!('d' in args)) { sendError.call(this, DBGP_ERROR_CODES.INVALID_OPTIONS, "No breakpoint to update"); return; } var breakpointId = parseInt(args['d'], 10), state = args['s'], hitValue = parseInt(args['h'], 10); if (('n' in args) || ('o' in args && args.o != '>=')) { // Unsupported arguments sendError.call(this, DBGP_ERROR_CODES['BREAKPOINT_TYPE_NOT_SUPPORTED'], "V8 debugger does not support the specified operation"); return; } var bpArgs = { command: 'changebreakpoint', arguments: { breakpoint: breakpointId, enabled: state != 'disabled', ignoreCount: (hitValue >= 0) ? hitValue : 0, }, }; this.v8Client.req(bpArgs, function cb(err, resp) { if (!err && resp && resp.success) { this.sendResponse('breakpoint_update', args['i'], {success: 1}, null, true); } else { sendError.call(this, DBGP_ERROR_CODES.BREAKPOINT_COULD_NOT_BE_SET); } }.bind(this)); function sendError(errorCode, message) { var error = this.makeError(errorCode, message); this.sendUnsuccessfulResponse('breakpoint_update', args['i'], null, error, true); } }); this.on('breakpoint_remove', function(args) { var id = args['d']; this.v8Client.clearBreakpoint({ 'breakpoint': id }, function(res) { if (res && res.success && res.body) { this.sendResponse('breakpoint_remove', args['i']); } else { var error = this.makeError(DBGP_ERROR_CODES['BREAKPOINT_COULD_NOT_BE_SET']); this.sendResponse('breakpoint_set', args['i'], { 'status': 'disabled' }, error, true); } }.bind(this)); }); this.on('breakpoint_list', function(args) { var id = args['d']; this.v8Client.listbreakpoints(function(err, res) { if (err || !res || !res.breakpoints) { var error = this.makeError(DBGP_ERROR_CODES.UNKNOWN_ERROR, err); this.sendResponse('breakpoint_list', args['i'], null, error, true); return; } var resultStrings = []; res.breakpoints.forEach(function(bp) { resultStrings.push(this.makeBreakpoint(bp.number, { type: 'line', state: bp.active ? "enabled" : "disabled", filename: bp.script_name, lineno: bp.line + 1, // V8 is 0-based, dbgp is 1-based hit_count: bp.hit_count, })); }.bind(this)); this.sendResponse('breakpoint_list', args['i'], {}, resultStrings.join("\n"), true); }.bind(this)); }); this.on('stdout', function(args) { var target = parseInt(args['c'], 10); if ([0,1,2].indexOf(target) != -1) { this.stdout = target; this.sendResponse('stdout', args['i'], { 'success': 1 }); } else { this.sendUnsuccessfulResponse('stdout', args['i']); } }); this.on('stderr', function(args) { var target = parseInt(args['c'], 10); if ([0,1,2].indexOf(target) != -1) { this.stderr = target; this.sendResponse('stderr', args['i'], { 'success': 1 }); } else { this.sendUnsuccessfulResponse('stderr', args['i']); } }); this.on('property_get', function(args) { // TODO respect other parameters such as max_children/pages // TODO use frameReceiver when asked for `this` var propertyName = args['n']; var contextId = parseInt(args['c'], 10) || 0; var scopeId = this._scopeForContext(contextId); var stackDepth = parseInt(args['d'], 10) || 0; this.pageIndex = parseInt(args['p']) || 0; this.pageSize = this.getFeature('max_children') || 25; if (!propertyName || !(scopeId >= 0) || !(stackDepth >= 0)) { sendError.call(this, DBGP_ERROR_CODES.INVALID_OPTIONS, 'invalid options'); return; } var isGlobal = scopeId == 0; var sendPropertyGetResponse = function(propertiesStr) { this.sendResponse('property_get', args['i'], null, propertiesStr, true); }.bind(this); var sendPropertyGetError = function(code, message) { var error = this.makeError(code, message); this.sendUnsuccessfulResponse('property_get', args['i'], null, error, true); }.bind(this); var callbacks = { onSuccess: sendPropertyGetResponse, onFailure: sendPropertyGetError }; this.v8Client.reqFrameEval(propertyName, isGlobal ? -1 : stackDepth, function(err, res) { if (err || !res) { var reason; if (err) { reason = err; } else if (!res) { reason = 'invalid stack depth'; } else { reason = ("can't evaluate " + propertyName + " at stack " + stackDepth); } sendPropertyGetError.call(this, DBGP_ERROR_CODES.INVALID_EXPRESSION, reason); return; } if (!res.name) res.name = propertyName; this.makeProperty(res, propertyName, this.getFeature('max_depth') || 1, callbacks); }.bind(this)); function sendError(errorCode) { var error = this.makeError(errorCode); this.sendResponse('property_get', args['i'], null, errorCode, true); } }); this.on('property_value', function(args) { var propertyName = args['n']; var contextId = parseInt(args['c'], 10) || 0; var scopeId = this._scopeForContext(contextId); var stackDepth = parseInt(args['d'], 10); if (!propertyName || !(scopeId >= 0) || !(stackDepth >= 0)) { sendError.call(this, DBGP_ERROR_CODES.INVALID_OPTIONS, 'invalid options'); return; } var isGlobal = scopeId == 0; var jsonExpression = "JSON.stringify(" + propertyName + ")"; this.v8Client.evaluateExpression(jsonExpression, stackDepth, isGlobal, function(resp) { var resValue; if (!resp) { sendError.call(this, DBGP_ERROR_CODES.ERROR_EVALUATING_CODE, "Can't evaluate " + propertyName); return; } else if ((!resp.body || !resp.success) && resp.message) { resValue = propertyName + ": " + resp.message; } else { resValue = resp.body.text.toString(); } resValue = this.makeSafeCdataMarkedSection(resValue); this.sendResponse('property_value', args['i'], null, resValue, false); }.bind(this)); function sendError(errorCode, message) { var error = this.makeError(errorCode, message); this.sendUnsuccessfulResponse('property_value', args['i'], null, error, true); } }); this.builtinTypes = ["undefined", "null", "boolean", "number", "string", "function"]; this.on('context_names', function(args) { // Hard-wired to always send all of V8's scope names var contexts = ''; for (var id in DBGP_CONTEXT_IDS) { var v8Id = DBGP_CONTEXT_IDS[id]; var name = V8_SCOPE_TYPES[v8Id]; if (typeof name !== 'string') { log.error("DBGP context with id " + id + " is not a V8 scope.") } contexts += this.makeContext(id, name); } this.sendResponse('context_names', args['i'], null, contexts, true); }); this.on('context_get', function(args) { var contextId = parseInt(args['c'], 10) || 0; var scopeId = this._scopeForContext(contextId); var stackDepth = parseInt(args['d'], 10); this.v8Client.req({command: 'frame', arguments: {number: stackDepth}}, function(err, frame) { var sendContextGetResponse = function(propertiesStr) { this.sendResponse('context_get', args['i'], null, propertiesStr, true); }.bind(this); var sendContextGetError = function(code, message) { var error = this.makeError(code, message); this.sendUnsuccessfulResponse('context_get', args['i'], null, error, true); }.bind(this); if (err || !frame) { sendContextGetError(DBGP_ERROR_CODES.INVALID_OPTIONS, err || 'stack depth invalid'); return; } var scopes = frame.scopes || []; var scopeNumber; scopes.forEach(function(scope) { if (scope.type == scopeId) { scopeNumber = scope.index; log.debug("Found scope, number " + scopeNumber); } }.bind(this)); if (typeof scopeNumber === "undefined") { // Probably temporarily not applicable, like "with" or "catch" scopes this.sendResponse('context_get', args['i'], null, '', false); return; } this.v8Client.req({command: 'scope', arguments: {number: scopeNumber, framenumber: stackDepth}}, function(err, resp) { if (err || !resp) { log.debug("Failed to get scope"); sendContextGetError(DBGP_ERROR_CODES.INVALID_OPTIONS, err || 'context id invalid'); return; } this.v8Client.reqLookup([resp.object.ref], function(err, scopes){ if (err || !scopes) { log.debug("Failed to lookup scope"); sendContextGetError(DBGP_ERROR_CODES.INVALID_OPTIONS, err || 'failed to lookup scope'); } var scope = scopes[resp.object.ref]; log.debug("got scope #" + scopeId + ": " + util.inspect(scope, {colors: true})); // If the local scope was requested, include a reference to the frame's // receiver object as `this`. if (scopeId == 1) { // local var thisRef = { "name": "this", "ref": frame.receiver.ref, }; scope.properties.push(thisRef); } if (scopeId == 0) { // global this._filterGlobalScope(scope); } else if (scopeId == 1) { // local this._filterLocalScope(scope); } var maxDepth = this.getFeature('max_depth') || 1; var ContextGetResponse = function() { var callbacks = { onFailure: sendContextGetError }; var contextProperties = scope.properties; if (/[v ]0\.6\./.test(process.version)) { // For Node 0.6 on Darwin, sometimes we get a crazy empty-name property... contextProperties = contextProperties.filter(function(n) { return n.name} ); } log.debug("contextProperties: " + util.inspect(contextProperties, {colors: true})); var contextPropertyStrings = []; // tuple of (property, string) var wrapPropertyStrings = function wrapPropertyStrings() { sendContextGetResponse(contextPropertyStrings .sort(function(a, b) { return a[0].name.localeCompare(b[0].name); }) .map(function(a) { return a[1]; }) .join("\n")); }.bind(this); var cpsCallbacks = { onFailure: sendContextGetError }; var collectPropertyStrings = function(ix) { var cb = function(propertyStr) { if (propertyStr) { contextPropertyStrings.push([contextProperty, propertyStr]); } else { log.debug("Failed to get property string for " + contextProperty.name); } ix += 1; if (ix < contextProperties.length) { collectPropertyStrings(ix); } else { wrapPropertyStrings(); } }.bind(this); cpsCallbacks.onSuccess = cb; var contextProperty = contextProperties[ix]; if (contextProperty.name == "this" && contextProperty.value && contextProperty.value.type =="object" && contextProperty.value.className =="global") { // skip the global 'this' cb(null, null); return; } this.pageIndex = 0; this.pageSize = this.getFeature('max_children') || 25; if (!contextProperty.value) { cb = function(inlinedProperty) { this.makeProperty(inlinedProperty.value, contextProperty.name, 0, cpsCallbacks); }.bind(this); this.v8Client.inlineObjectRef(contextProperty, cb); } else { this.makeProperty(contextProperty.value, contextProperty.name, 0, cpsCallbacks); } }.bind(this); if (contextProperties.length) { collectPropertyStrings(0); } else { wrapPropertyStrings(); } }.bind(this); this.v8Client.reqLookup(scope.properties.map(function(p) {return p.ref}), function(err, props) { scope.properties.forEach(function(property) { if ((property.ref in props) && !("value" in property)) { property.value = props[property.ref] } }); ContextGetResponse(); }); }.bind(this)); }.bind(this)); }.bind(this)); }); this.on('source', function(args) { var uri = args['f']; try { var filename = this._filepathFromUri(uri); } catch(e) { // Might fail if, for example, this is from eval() this.sendUnsuccessfulResponse('source', args['i']); return; } this.v8Client._ko_reqScripts(0, null, true, filename, null, function(err, scripts) { if (err) { this.sendUnsuccessfulResponse('source', args['i'], null, this.makeError(DBGP_ERROR_CODES.UNKNOWN_ERROR, err), true); return; } scripts = scripts.filter(function(script) {return script.name == filename;}); if (scripts.length) { this.sendResponse('source', args['i'], {success: 1}, scripts[0].source, false); } else { this.sendUnsuccessfulResponse('source', args['i']); } }.bind(this)); }); this.on('typemap_get', function(args) { var response = ''; for (var typeName in DBGP_TYPEMAP) { var typeValue = DBGP_TYPEMAP[typeName]; response += this.makeType(typeName, typeValue); } this.sendResponse('typemap_get', args['i'], null, response, true); }); /** * ----- Helper functions */ /** * Make a <response> message that contains the given command and transaction * id, as well as the other given parameters, and message (which can be an * xml string or data for a CDATA entity). Then send it. */ this.sendResponse = function(cmd, transactionId, parameters, message, xml) { var response = this.makeResponse(cmd, transactionId, parameters, message, xml); this.dbgpClient.sendPacket(response); }; /** * `sendResponse()` with success="0". */ this.sendUnsuccessfulResponse = function(cmd, transactionId, parameters, message, xml) { if (!parameters) parameters = {}; parameters['success'] = 0; this.sendResponse(cmd, transactionId, parameters, message, xml); }; /** * `sendResponse()` with status=`this.status` and reason=`this.reason`. */ this.sendStatusResponse = function(cmd, transactionId, parameters, message, xml) { if (!parameters) parameters = {}; parameters.status = this.status; parameters.reason = this.reason; this.sendResponse(cmd, transactionId, parameters, message, xml); }; /** * Make a <response> message with the given parameters and message. The * message can be an xml string or data, which will be base64-encoded. */ this.makeResponse = function(cmd, transactionId, parameters, message, xml) { if (!parameters) parameters = {}; parameters['command'] = cmd; parameters['transaction_id'] = transactionId; if (this.getFeature('data_encoding') == 'base64') parameters['encoding'] = 'base64'; message = message || ''; var response = '<?xml version="1.0" encoding="UTF-8"?>'; response += '<response xmlns="urn:debugger_protocol_v1" '; for (var name in parameters) { // FIXME needs escaping var value = parameters[name]; response += name + '="' + value + '" '; } response = response.trimRight(); if (message) { response += ">"; if (xml) { response += message; } else { response += this._encodeData(message); } response += '</response>'; } else { response += " />"; } return response; }; /** * Encodes data according to the current 'data_encoding' feature value. */ this._encodeData = function(data) { var dataEncoding = this.getFeature('data_encoding'); if (dataEncoding == 'none') return data; else if (dataEncoding == 'base64') return (new Buffer('' + data)).toString('base64'); else throw 'Feature data_encoding has an unsupported value!'; }; /** * Generate an <init> message. */ this.makeInit = function() { var ideKey = process.env['IDE_KEY'] || ''; var dbgpCookie = process.env['DBGP_COOKIE'] || ''; var appId = process.env['APPID'] || ''; var init = '<?xml version="1.0" encoding="UTF-8"?>'; init += '<init xmlns="urn:debugger_protocol_v1" appid="dbgp.js" ' + 'idekey="'+ ideKey + '" ' + 'session="'+ dbgpCookie + '" ' + 'thread="1" ' + // ? 'parent="' + appId + '" ' + // ? 'language="node.js" ' + 'protocol_version="1.0" ' + 'fileuri="' + this.fileUri + '"/>'; // TODO return init; }; /** * Wrap the supplied text in <![CDATA[...]]>, but handle any * internal occurrences of "]]>" that would prematurely terminate the * cdata section and cause a parsing error. For each "]]>" we find in * the text, we end the previous cdata section, write in "]]&gt;" as * plain text, and then start a new cdata section. * * On the IDE side the server always allows for more than one child * element where it's processing text. * * Ref bug 90063 */ this.makeSafeCdataMarkedSection = function(value) { if (/[\<\>\&]/.test(value)) { return "<![CDATA[" + value.replace(/\]\]>/g, "]]>]]&gt;<![CDATA[") + "]]>"; } else { return value; } }; /** * Generate an <error> message with the given error code message text. */ this.makeError = function(code, message) { if (typeof(message) == "undefined") { message = ""; } var error = ('<error code="' + code + '">' + '<message>' + this.makeSafeCdataMarkedSection(message) + '</message>' + '</error>'); return error; }; /** * Generate a (misnamed) <stack> element for stacktraces. */ this.makeFrame = function(level, type, filename, lineno, where, cmdbegin, cmdend, extra) { var params = { 'level': level, 'type': type, 'filename': filename, 'lineno': lineno}; if (where) params['where'] = where; if (cmdbegin) params['cmdbegin'] = cmdbegin; if (where) params['where'] = where; if (extra) { Object.keys(extra).forEach(function(prop) {params[prop] = extra[prop];}); } // Why is <stack> not called <frame>? var frame = this._makeXmlTag('stack', params, true); return frame; }; this._isBlank = function(s) { return !s && s !== 0; }; /** makeProperty - transform info about a variable into a dbgp property */ this.makeProperty = function(property, fullName, maxDepth, finalCallbacks) { var propArgs = { 'name': property.name, fullname: fullName, 'type': property.type }; if (this._isBlank(propArgs.name)) { propArgs.name = fullName; } if (this.builtinTypes.indexOf(propArgs.type) >= 0) { this.makeScalarProperty(propArgs, fullName, property, finalCallbacks); } else { this.makeCompoundProperty(propArgs, fullName, property, maxDepth, finalCallbacks); } }; /** makeInnerProperty - similar to makeProperty, but if the property is * compound we need to call inlineObjectRef to get the properties field * on the property object. */ this.makeInnerProperty = function(property, fullName, maxDepth, callbacks) { if (!property.value) { var cb = function(inlinedProperty) { if (!('value' in inlinedProperty)) { callbacks.onFailure(DBGP_ERROR_CODES['CANNOT_GET_PROPERTY'], ("Failed to find a ref for " + fullName)); } else { property.value = inlinedProperty.value; this.makeInnerProperty(property, fullName, maxDepth, callbacks); } }.bind(this); this.v8Client.inlineObjectRef(property, cb); return; } var propArgs = { 'name': property.name, fullname: fullName, 'type': property.value.type }; if (this._isBlank(propArgs.name)) { propArgs.name = fullName; } if (this.builtinTypes.indexOf(propArgs.type) >= 0) { this.makeScalarProperty(propArgs, fullName, property.value, callbacks); } else { this.makeCompoundProperty(propArgs, fullName, property.value, maxDepth, callbacks); } }; this.makeScalarProperty = function(propArgs, propName, property, finalCallbacks) { propArgs.children = '0'; var cb; if (['undefined', 'null', 'function'].indexOf(propArgs.type) >= 0) { finalCallbacks.onSuccess(this.wrapInPropertyTag(propArgs, '')); return; } else if (!('value' in property) && !('text' in property)) { // We can use both property.text and property.value, so don't refresh // if one of them is present. cb = function(inlinedProperty) { if (!('value' in inlinedProperty)) { finalCallbacks.onFailure(DBGP_ERROR_CODES['CANNOT_GET_PROPERTY'], ("Failed to find an object for " + propName)); } else { this.makeScalarProperty(propArgs, propName, inlinedProperty.value, finalCallbacks); } }.bind(this); this.v8Client.inlineObjectRef(property, cb); } else { var value; switch (property.type) { case 'boolean': case 'number': value = property.text || property.value; break; case 'string': value = property.text || property.value; var maxData = this.getFeature('max_data') || -1; // Actually, we want to track the total size of a packet. if (value.length > maxData) { value = value.slice(0, maxData) + "..."; } break; default: log.error("unexpected type: " + propType); value = ''; } if (typeof(value) == 'undefined') { value = ''; } finalCallbacks.onSuccess( this.wrapInPropertyTag(propArgs, this.makeSafeCdataMarkedSection(value))); } }; var compareStrings = function(aname, bname) { if (aname < bname) return -1; else if (aname > bname) return 1; else return 0; } var sortByName = function(a, b) { var aname = a.name; var bname = b.name; if (typeof(aname) == "number" && typeof(bname) == "number") { return aname - bname; } else if (typeof(aname) == "string" && typeof(bname) == "string") { // If two strings match ignoring case, do a case-sensitive comparison return (compareStrings(aname.toLowerCase(), bname.toLowerCase()) || compareStrings(aname, bname)); } else if (typeof(aname) == "number") { // Favor putting numbers first return -1; } else if (typeof(bname) == "number") { return 1; } else { return compareStrings(String(aname), String(bname)); } }; this.makeCompoundProperty = function(propArgs, propertyName, resp, maxDepth, finalCallbacks) { if (!('properties' in resp)) { var cb = function(inlinedProperty) { if (!('value' in inlinedProperty) || !('properties' in inlinedProperty.value)) { finalCallbacks.onFailure(DBGP_ERROR_CODES['CANNOT_GET_PROPERTY'], ("Failed to find value for " + propertyName)); } else { this.makeCompoundProperty(propArgs, propertyName, inlinedProperty.value, maxDepth, finalCallbacks); } }.bind(this); this.v8Client.inlineObjectRef(resp, cb); return; } var respProperties = resp.properties; if (!respProperties) { finalCallbacks.onFailure(DBGP_ERROR_CODES['CANNOT_GET_PROPERTY'], ("Failed to find properties for " + propertyName)); } propArgs.type = resp.className; var isArray; if ((propArgs.type == "object" || propArgs.type == "Array") && resp.className == "Array") { isArray = true; var respPropertiesByNumber = []; var respPropertiesByName = []; respProperties.forEach(function(prop) { var name = prop.name; if (/^\d+$/.test(name)) { respPropertiesByNumber.push(prop); } else if (name != "length" && !this._shouldIgnoreProperty(prop)) { respPropertiesByName.push(prop); } }, this); respPropertiesByName.sort(sortByName); respProperties = respPropertiesByNumber.concat(respPropertiesByName); } else { isArray = false; respProperties = respProperties.filter(function(property) { return !this._shouldIgnoreProperty(property); }, this); respProperties.sort(sortByName); } if (respProperties.length) { propArgs.children = 1; propArgs.numchildren = respProperties.length; // Keep only the properties we were asked to show var startIdx = this.pageIndex * this.pageSize; var endIdx = startIdx + this.pageSize; respProperties = respProperties.slice(startIdx, endIdx); } else { propArgs.children = 0; } var propertyStrings = []; var wrapPropertyStrings = function() { var childString; if (propertyStrings.length) { childString = propertyStrings.join(""); } else { childString = ''; } finalCallbacks.onSuccess( this.wrapInPropertyTag(propArgs, childString)); }.bind(this); if (maxDepth <= 0) { wrapPropertyStrings(); return; } var buildCompoundPropertyStrings = function(i) { if (i >= respProperties.length) { wrapPropertyStrings(); return; } var innerProperty = respProperties[i]; if (!innerProperty) { wrapPropertyStrings(); return; } var key; var name = innerProperty.name; if (/^\d+$/.test(name)) { key = '[' + name + ']'; } else if (/^\w+$/.test(name)) { key = '.' + name; } else { key = '[' + JSON.stringify(String(name)) + ']'; } var cb = function(propertyStr) { propertyStrings.push(propertyStr); buildCompoundPropertyStrings(i + 1); }.bind(this); var innerCallbacks = { onSuccess: cb, onFailure: finalCallbacks.onFailure }; this.makeInnerProperty(innerProperty, propertyName + key, maxDepth - 1, innerCallbacks); }.bind(this); buildCompoundPropertyStrings(0); }; this.wrapInPropertyTag = function(propArgs, s) { var propXml = (this._makeXmlTag('property', propArgs, false) + s + '</property>'); return propXml; }; /** * Generate a <breakpoint> element. */ this.makeBreakpoint = function(id, params, expression) { params['id'] = id; var bp = this._makeXmlTag('breakpoint', params, false); if (expression) { bp += this._makeXmlTag('expression'); bp += this.makeSafeCdataMarkedSection(expression); bp += '</expression>'; } bp += '</breakpoint>'; return bp; }; this.makeContext = function(id, name) { return this._makeXmlTag('context', { 'id': id, 'name': name }, true); }; this.makeType = function(name, type) { return this._makeXmlTag('map', { 'name': name, 'type': type }, true); }; /** * Send data as a dbgp stdout message and to our stdout file. */ this.stdoutCopy = function(data) {