UNPKG

strong-debugger

Version:

DevTools Remote Debugging Protocol provider for Node.js and io.js

335 lines (289 loc) 9.55 kB
'use strict'; /* Methods provided by the C++ bindings bindings.sendFrontEndMessage(string) bindings.closeFrontEndConnection() bindings.enableDebugger() bindings.disableDebugger() bindings.sendDebuggerCommand(string) bindings.log(string) Methods exported by the script for consumption by C++ backend bindings.onConnection() bindings.onFrontEndCommand(string) bindings.onDebuggerEnabled() bindings.onDebuggerDisabled() bindings.onDebuggerMessage(string) */ /* global bindings:false */ var context = { agents: {}, eventHandlers: {}, _reqId: 0, _reqCallbacks: {}, _enabled: false, _enableCallbacks: [], _afterEnableCallbacks: [], }; /** * Log a message to process.stderr, but only when debuglogs are enabled for * strong-debugger module (e.g. DEBUG=strong-debugger). * The method accepts a list of values to log, the values are converted * to string or json and concatenated with a space. */ var debuglog = context.debuglog = function() { var msg = Array.prototype.map.call(arguments, inspect).join(' '); bindings.log(msg); function inspect(val) { return typeof val === 'object' ? JSON.stringify(val) : val === undefined ? 'undefined' : val.toString(); } }; bindings.onConnection = function() { // no-op }; /** * Switch the V8 runtime to a debugger mode. * @param {function()} cb The callback to call after the debugger was enabled. */ context.enableDebugger = function(cb) { if (!context._enableCallbacks.length) { // call binding only if the operation is not already in progress bindings.enableDebugger(); } context._enableCallbacks.push(cb); }; /** * Defer execution of the provided function until the debugger is enabled. * @param {function()} cb The function to execute. */ context.waitForDebuggerEnabled = function(cb) { if (context._enabled) return cb(); debuglog('Debugger is was enabled yet, scheduling the callback for later.'); context._afterEnableCallbacks.push(cb); }; bindings.onDebuggerEnabled = function() { context._enabled = true; if (!context._enableCallbacks.length) return; context._enableCallbacks.forEach(function(cb) { cb(); }); context._enableCallbacks = []; context._afterEnableCallbacks.forEach(function(cb) { cb(); }); context._afterEnableCallbacks = []; }; /** * Turn off the V8 debugger. * @param {function()} cb The callback to call after the debugger was disabled. */ context.disableDebugger = function(cb) { if (context._disableCallbacks) { // disable operation is already in progress context._disableCallbacks.push(cb); return; } context._disableCallbacks = [cb]; bindings.disableDebugger(); }; bindings.onDebuggerDisabled = function() { context._disabled = false; if (!context._disableCallbacks) return; context._disableCallbacks.forEach(function(cb) { cb(); }); delete context._disableCallbacks; }; /** * Send a request to the V8 debugger and get back a reply. * @param {String} cmd The command name. * @param {Object=} args Request (command) arguments. * @param {function(Error, Object, Object)} cb Callback to receive the result * of the request. The second callback argument contains the response body, * the third argument contains a lookup map to resolve references. */ context.sendDebuggerRequest = function(cmd, args, cb) { if (!context._enabled) return cb('Debugger is not enabled, call "Debugger.enable" first.'); var id = ++context._reqId; // Note: we must not add args object if it was not sent. // E.g. resume (V8 request 'continue') does no work // correctly when args are empty instead of undefined if (args && !('maxStringLength' in args)) args.maxStringLength = 10000; var req = { seq: id, type: 'request', command: cmd, arguments: args }; context._reqCallbacks[id] = cb; debuglog('Send V8 debugger request', req); bindings.sendDebuggerCommand(JSON.stringify(req)); }; bindings.onDebuggerMessage = function(data) { debuglog('V8 debugger message received', data.slice(0, Math.min(255, data.length)), data.length > 255 ? '(...)' : ''); var msg; try { msg = JSON.parse(data); } catch (err) { debuglog('Cannot parse V8 message: ' + err.message); return; } if (msg.type === 'event') { if (!msg.event) { debuglog('V8 debugger event does not provide event name (?!)', msg); return; } if (context.eventHandlers.hasOwnProperty(msg.event)) { var handler = context.eventHandlers[msg.event]; handler(msg); } else { debuglog('Ignoring V8 debugger event', data); } return; } if (msg.type !== 'response') { debuglog('Ignoring V8 debugger message of unknown type.', msg); return; } // Ignore the response to "disconnect" request sent by disableDebugger() if (msg.command === 'disconnect') return; // jscs:disable requireCamelCaseOrUpperCaseIdentifiers var id = msg.request_seq; // jscs:enable requireCamelCaseOrUpperCaseIdentifiers if (!id) { debuglog('Ignoring V8 debugger response without request_seq', data); return; } if (!context._reqCallbacks.hasOwnProperty(id)) { debuglog('No callback registered for request id', id); return; } var cb = context._reqCallbacks[id]; if (!msg.success) { var err = new Error(msg.message); err.name = 'V8 Error'; return cb(err); } var refsLookup = Object.create(null); (msg.refs || []).forEach(function(r) { refsLookup[r.handle] = r; }); cb(null, msg.body, refsLookup); }; context._STUBBED_RESPONSES = { 'CSS.enable': {}, 'Console.enable': {}, 'DOM.enable': {}, 'DOMStorage.enable': {}, 'Database.enable': {}, 'IndexedDB.enable': {}, 'IndexedDB.requestDatabaseNames': { databaseNames: [] }, 'Inspector.enable': {}, 'Network.enable': {}, 'Page.canEmulate': { result: false }, 'Page.canScreencast': { result: false }, 'Page.enable': {}, 'Page.setShowViewportSizeOnResize': {}, 'Profiler.enable': {}, // TODO detect the status of the debugged process // return result:true when --debug-brk 'Runtime.isRunRequired': {}, 'Profiler.setSamplingInterval': {}, 'Runtime.enable': {}, 'Timeline.enable': {}, 'Worker.canInspectWorkers': { result: false }, 'Worker.setAutoconnectToWorkers': {}, }; bindings.onFrontEndCommand = function(line) { debuglog('Front-end command received', line); var command; try { command = JSON.parse(line); } catch (err) { return context.reportError( 'Invalid message: ' + JSON.stringify(err.message)); } // Custom Extension: send `{ "close": true }` to close the client connection if (!command.method && command.close) { return bindings.closeFrontEndConnection(); } var requestId = command.id; context._handleFrontEndRequest(command, function(err, result) { var response = { id: requestId }; if (err) { response.error = { message: err.message || err }; } else { response.result = result || {}; } context._sendFrontEndMessage(response); }); }; context._handleFrontEndRequest = function(request, cb) { if (typeof request.method !== 'string' || !request.method.length) { return cb('Invalid request method of type ' + typeof request.method); } // hasOwnProperty is needed to handle msg.method == '__proto__' if (context._STUBBED_RESPONSES.hasOwnProperty(request.method)) { return cb(null, context._STUBBED_RESPONSES[request.method]); } var m = request.method.match(/^([^.]+)\.([^.]+)$/); if (!m) return cb('Unknown method ' + JSON.stringify(request.method)); var agentName = m[1]; var methodName = m[2]; if (!context.agents.hasOwnProperty(agentName)) return cb('Unknown agent ' + JSON.stringify(request.method)); var agent = context.agents[agentName]; if (!agent.hasOwnProperty(methodName) || methodName[0] === '_') return cb('Unknown method ' + JSON.stringify(request.method)); var method = agent[methodName]; if (typeof method !== 'function') return cb('Unknown method ' + JSON.stringify(request.method)); try { method.call(agent, request.params, cb); } catch (err) { debuglog('Unhandled error in ' + request.method + '.\n', err.stack || err); cb(err); } }; context._sendFrontEndMessage = function(msg) { debuglog('Send front-end message', msg); bindings.sendFrontEndMessage(JSON.stringify(msg)); }; /** * Send an event to DevTools frontend. * @param {String} method Method name, e.g. `Debugger.paused`. * @param {Object} params Method parameters - a named map argument:value. */ context.sendFrontEndEvent = function(method, params) { context._sendFrontEndMessage({ method: method, params: params || {} }); }; /** * Report an error that is not associated with any particular request id. * @param {String|Error} err The error to report. */ context.reportError = function(err) { debuglog('Unhandled error', err.stack || err); context._sendFrontEndMessage({ error: { message: err.message || err } }); }; /** * Evaluate an expression in the global context of the debugged process * by calling V8 debugger's command "evaluate". */ context.evaluateGlobal = function(expression, cb) { context.sendDebuggerRequest( 'evaluate', { expression: expression, global: true, maxStringLength: -1 }, function(err, result, refs) { if (err) return cb(err); if (result.type === 'string') { return cb(null, result.value); } cb('evaluateGlobal does not yet support response type ' + result.type); }); };