michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: let B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}"; michael@0: michael@0: let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array", michael@0: "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array", michael@0: "Float64Array"]; michael@0: michael@0: // Number of items to preview in objects, arrays, maps, sets, lists, michael@0: // collections, etc. michael@0: let OBJECT_PREVIEW_MAX_ITEMS = 10; michael@0: michael@0: let addonManager = null; michael@0: michael@0: /** michael@0: * This is a wrapper around amIAddonManager.mapURIToAddonID which always returns michael@0: * false on B2G to avoid loading the add-on manager there and reports any michael@0: * exceptions rather than throwing so that the caller doesn't have to worry michael@0: * about them. michael@0: */ michael@0: function mapURIToAddonID(uri, id) { michael@0: if (Services.appinfo.ID == B2G_ID) { michael@0: return false; michael@0: } michael@0: michael@0: if (!addonManager) { michael@0: addonManager = Cc["@mozilla.org/addons/integration;1"]. michael@0: getService(Ci.amIAddonManager); michael@0: } michael@0: michael@0: try { michael@0: return addonManager.mapURIToAddonID(uri, id); michael@0: } michael@0: catch (e) { michael@0: DevtoolsUtils.reportException("mapURIToAddonID", e); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * BreakpointStore objects keep track of all breakpoints that get set so that we michael@0: * can reset them when the same script is introduced to the thread again (such michael@0: * as after a refresh). michael@0: */ michael@0: function BreakpointStore() { michael@0: this._size = 0; michael@0: michael@0: // If we have a whole-line breakpoint set at LINE in URL, then michael@0: // michael@0: // this._wholeLineBreakpoints[URL][LINE] michael@0: // michael@0: // is an object michael@0: // michael@0: // { url, line[, actor] } michael@0: // michael@0: // where the `actor` property is optional. michael@0: this._wholeLineBreakpoints = Object.create(null); michael@0: michael@0: // If we have a breakpoint set at LINE, COLUMN in URL, then michael@0: // michael@0: // this._breakpoints[URL][LINE][COLUMN] michael@0: // michael@0: // is an object michael@0: // michael@0: // { url, line, column[, actor] } michael@0: // michael@0: // where the `actor` property is optional. michael@0: this._breakpoints = Object.create(null); michael@0: } michael@0: michael@0: BreakpointStore.prototype = { michael@0: _size: null, michael@0: get size() { return this._size; }, michael@0: michael@0: /** michael@0: * Add a breakpoint to the breakpoint store. michael@0: * michael@0: * @param Object aBreakpoint michael@0: * The breakpoint to be added (not copied). It is an object with the michael@0: * following properties: michael@0: * - url michael@0: * - line michael@0: * - column (optional; omission implies that the breakpoint is for michael@0: * the whole line) michael@0: * - condition (optional) michael@0: * - actor (optional) michael@0: */ michael@0: addBreakpoint: function (aBreakpoint) { michael@0: let { url, line, column } = aBreakpoint; michael@0: let updating = false; michael@0: michael@0: if (column != null) { michael@0: if (!this._breakpoints[url]) { michael@0: this._breakpoints[url] = []; michael@0: } michael@0: if (!this._breakpoints[url][line]) { michael@0: this._breakpoints[url][line] = []; michael@0: } michael@0: this._breakpoints[url][line][column] = aBreakpoint; michael@0: } else { michael@0: // Add a breakpoint that breaks on the whole line. michael@0: if (!this._wholeLineBreakpoints[url]) { michael@0: this._wholeLineBreakpoints[url] = []; michael@0: } michael@0: this._wholeLineBreakpoints[url][line] = aBreakpoint; michael@0: } michael@0: michael@0: this._size++; michael@0: }, michael@0: michael@0: /** michael@0: * Remove a breakpoint from the breakpoint store. michael@0: * michael@0: * @param Object aBreakpoint michael@0: * The breakpoint to be removed. It is an object with the following michael@0: * properties: michael@0: * - url michael@0: * - line michael@0: * - column (optional) michael@0: */ michael@0: removeBreakpoint: function ({ url, line, column }) { michael@0: if (column != null) { michael@0: if (this._breakpoints[url]) { michael@0: if (this._breakpoints[url][line]) { michael@0: if (this._breakpoints[url][line][column]) { michael@0: delete this._breakpoints[url][line][column]; michael@0: this._size--; michael@0: michael@0: // If this was the last breakpoint on this line, delete the line from michael@0: // `this._breakpoints[url]` as well. Otherwise `_iterLines` will yield michael@0: // this line even though we no longer have breakpoints on michael@0: // it. Furthermore, we use Object.keys() instead of just checking michael@0: // `this._breakpoints[url].length` directly, because deleting michael@0: // properties from sparse arrays doesn't update the `length` property michael@0: // like adding them does. michael@0: if (Object.keys(this._breakpoints[url][line]).length === 0) { michael@0: delete this._breakpoints[url][line]; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } else { michael@0: if (this._wholeLineBreakpoints[url]) { michael@0: if (this._wholeLineBreakpoints[url][line]) { michael@0: delete this._wholeLineBreakpoints[url][line]; michael@0: this._size--; michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get a breakpoint from the breakpoint store. Will throw an error if the michael@0: * breakpoint is not found. michael@0: * michael@0: * @param Object aLocation michael@0: * The location of the breakpoint you are retrieving. It is an object michael@0: * with the following properties: michael@0: * - url michael@0: * - line michael@0: * - column (optional) michael@0: */ michael@0: getBreakpoint: function (aLocation) { michael@0: let { url, line, column } = aLocation; michael@0: dbg_assert(url != null); michael@0: dbg_assert(line != null); michael@0: michael@0: var foundBreakpoint = this.hasBreakpoint(aLocation); michael@0: if (foundBreakpoint == null) { michael@0: throw new Error("No breakpoint at url = " + url michael@0: + ", line = " + line michael@0: + ", column = " + column); michael@0: } michael@0: michael@0: return foundBreakpoint; michael@0: }, michael@0: michael@0: /** michael@0: * Checks if the breakpoint store has a requested breakpoint. michael@0: * michael@0: * @param Object aLocation michael@0: * The location of the breakpoint you are retrieving. It is an object michael@0: * with the following properties: michael@0: * - url michael@0: * - line michael@0: * - column (optional) michael@0: * @returns The stored breakpoint if it exists, null otherwise. michael@0: */ michael@0: hasBreakpoint: function (aLocation) { michael@0: let { url, line, column } = aLocation; michael@0: dbg_assert(url != null); michael@0: dbg_assert(line != null); michael@0: for (let bp of this.findBreakpoints(aLocation)) { michael@0: // We will get whole line breakpoints before individual columns, so just michael@0: // return the first one and if they didn't specify a column then they will michael@0: // get the whole line breakpoint, and otherwise we will find the correct michael@0: // one. michael@0: return bp; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Iterate over the breakpoints in this breakpoint store. You can optionally michael@0: * provide search parameters to filter the set of breakpoints down to those michael@0: * that match your parameters. michael@0: * michael@0: * @param Object aSearchParams michael@0: * Optional. An object with the following properties: michael@0: * - url michael@0: * - line (optional; requires the url property) michael@0: * - column (optional; requires the line property) michael@0: */ michael@0: findBreakpoints: function* (aSearchParams={}) { michael@0: if (aSearchParams.column != null) { michael@0: dbg_assert(aSearchParams.line != null); michael@0: } michael@0: if (aSearchParams.line != null) { michael@0: dbg_assert(aSearchParams.url != null); michael@0: } michael@0: michael@0: for (let url of this._iterUrls(aSearchParams.url)) { michael@0: for (let line of this._iterLines(url, aSearchParams.line)) { michael@0: // Always yield whole line breakpoints first. See comment in michael@0: // |BreakpointStore.prototype.hasBreakpoint|. michael@0: if (aSearchParams.column == null michael@0: && this._wholeLineBreakpoints[url] michael@0: && this._wholeLineBreakpoints[url][line]) { michael@0: yield this._wholeLineBreakpoints[url][line]; michael@0: } michael@0: for (let column of this._iterColumns(url, line, aSearchParams.column)) { michael@0: yield this._breakpoints[url][line][column]; michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _iterUrls: function* (aUrl) { michael@0: if (aUrl) { michael@0: if (this._breakpoints[aUrl] || this._wholeLineBreakpoints[aUrl]) { michael@0: yield aUrl; michael@0: } michael@0: } else { michael@0: for (let url of Object.keys(this._wholeLineBreakpoints)) { michael@0: yield url; michael@0: } michael@0: for (let url of Object.keys(this._breakpoints)) { michael@0: if (url in this._wholeLineBreakpoints) { michael@0: continue; michael@0: } michael@0: yield url; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _iterLines: function* (aUrl, aLine) { michael@0: if (aLine != null) { michael@0: if ((this._wholeLineBreakpoints[aUrl] michael@0: && this._wholeLineBreakpoints[aUrl][aLine]) michael@0: || (this._breakpoints[aUrl] && this._breakpoints[aUrl][aLine])) { michael@0: yield aLine; michael@0: } michael@0: } else { michael@0: const wholeLines = this._wholeLineBreakpoints[aUrl] michael@0: ? Object.keys(this._wholeLineBreakpoints[aUrl]) michael@0: : []; michael@0: const columnLines = this._breakpoints[aUrl] michael@0: ? Object.keys(this._breakpoints[aUrl]) michael@0: : []; michael@0: michael@0: const lines = wholeLines.concat(columnLines).sort(); michael@0: michael@0: let lastLine; michael@0: for (let line of lines) { michael@0: if (line === lastLine) { michael@0: continue; michael@0: } michael@0: yield line; michael@0: lastLine = line; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _iterColumns: function* (aUrl, aLine, aColumn) { michael@0: if (!this._breakpoints[aUrl] || !this._breakpoints[aUrl][aLine]) { michael@0: return; michael@0: } michael@0: michael@0: if (aColumn != null) { michael@0: if (this._breakpoints[aUrl][aLine][aColumn]) { michael@0: yield aColumn; michael@0: } michael@0: } else { michael@0: for (let column in this._breakpoints[aUrl][aLine]) { michael@0: yield column; michael@0: } michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Manages pushing event loops and automatically pops and exits them in the michael@0: * correct order as they are resolved. michael@0: * michael@0: * @param nsIJSInspector inspector michael@0: * The underlying JS inspector we use to enter and exit nested event michael@0: * loops. michael@0: * @param ThreadActor thread michael@0: * The thread actor instance that owns this EventLoopStack. michael@0: * @param DebuggerServerConnection connection michael@0: * The remote protocol connection associated with this event loop stack. michael@0: * @param Object hooks michael@0: * An object with the following properties: michael@0: * - url: The URL string of the debuggee we are spinning an event loop michael@0: * for. michael@0: * - preNest: function called before entering a nested event loop michael@0: * - postNest: function called after exiting a nested event loop michael@0: */ michael@0: function EventLoopStack({ inspector, thread, connection, hooks }) { michael@0: this._inspector = inspector; michael@0: this._hooks = hooks; michael@0: this._thread = thread; michael@0: this._connection = connection; michael@0: } michael@0: michael@0: EventLoopStack.prototype = { michael@0: /** michael@0: * The number of nested event loops on the stack. michael@0: */ michael@0: get size() { michael@0: return this._inspector.eventLoopNestLevel; michael@0: }, michael@0: michael@0: /** michael@0: * The URL of the debuggee who pushed the event loop on top of the stack. michael@0: */ michael@0: get lastPausedUrl() { michael@0: let url = null; michael@0: if (this.size > 0) { michael@0: try { michael@0: url = this._inspector.lastNestRequestor.url michael@0: } catch (e) { michael@0: // The tab's URL getter may throw if the tab is destroyed by the time michael@0: // this code runs, but we don't really care at this point. michael@0: dumpn(e); michael@0: } michael@0: } michael@0: return url; michael@0: }, michael@0: michael@0: /** michael@0: * The DebuggerServerConnection of the debugger who pushed the event loop on michael@0: * top of the stack michael@0: */ michael@0: get lastConnection() { michael@0: return this._inspector.lastNestRequestor._connection; michael@0: }, michael@0: michael@0: /** michael@0: * Push a new nested event loop onto the stack. michael@0: * michael@0: * @returns EventLoop michael@0: */ michael@0: push: function () { michael@0: return new EventLoop({ michael@0: inspector: this._inspector, michael@0: thread: this._thread, michael@0: connection: this._connection, michael@0: hooks: this._hooks michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * An object that represents a nested event loop. It is used as the nest michael@0: * requestor with nsIJSInspector instances. michael@0: * michael@0: * @param nsIJSInspector inspector michael@0: * The JS Inspector that runs nested event loops. michael@0: * @param ThreadActor thread michael@0: * The thread actor that is creating this nested event loop. michael@0: * @param DebuggerServerConnection connection michael@0: * The remote protocol connection associated with this event loop. michael@0: * @param Object hooks michael@0: * The same hooks object passed into EventLoopStack during its michael@0: * initialization. michael@0: */ michael@0: function EventLoop({ inspector, thread, connection, hooks }) { michael@0: this._inspector = inspector; michael@0: this._thread = thread; michael@0: this._hooks = hooks; michael@0: this._connection = connection; michael@0: michael@0: this.enter = this.enter.bind(this); michael@0: this.resolve = this.resolve.bind(this); michael@0: } michael@0: michael@0: EventLoop.prototype = { michael@0: entered: false, michael@0: resolved: false, michael@0: get url() { return this._hooks.url; }, michael@0: michael@0: /** michael@0: * Enter this nested event loop. michael@0: */ michael@0: enter: function () { michael@0: let nestData = this._hooks.preNest michael@0: ? this._hooks.preNest() michael@0: : null; michael@0: michael@0: this.entered = true; michael@0: this._inspector.enterNestedEventLoop(this); michael@0: michael@0: // Keep exiting nested event loops while the last requestor is resolved. michael@0: if (this._inspector.eventLoopNestLevel > 0) { michael@0: const { resolved } = this._inspector.lastNestRequestor; michael@0: if (resolved) { michael@0: this._inspector.exitNestedEventLoop(); michael@0: } michael@0: } michael@0: michael@0: dbg_assert(this._thread.state === "running", michael@0: "Should be in the running state"); michael@0: michael@0: if (this._hooks.postNest) { michael@0: this._hooks.postNest(nestData); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Resolve this nested event loop. michael@0: * michael@0: * @returns boolean michael@0: * True if we exited this nested event loop because it was on top of michael@0: * the stack, false if there is another nested event loop above this michael@0: * one that hasn't resolved yet. michael@0: */ michael@0: resolve: function () { michael@0: if (!this.entered) { michael@0: throw new Error("Can't resolve an event loop before it has been entered!"); michael@0: } michael@0: if (this.resolved) { michael@0: throw new Error("Already resolved this nested event loop!"); michael@0: } michael@0: this.resolved = true; michael@0: if (this === this._inspector.lastNestRequestor) { michael@0: this._inspector.exitNestedEventLoop(); michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * JSD2 actors. michael@0: */ michael@0: /** michael@0: * Creates a ThreadActor. michael@0: * michael@0: * ThreadActors manage a JSInspector object and manage execution/inspection michael@0: * of debuggees. michael@0: * michael@0: * @param aHooks object michael@0: * An object with preNest and postNest methods for calling when entering michael@0: * and exiting a nested event loop. michael@0: * @param aGlobal object [optional] michael@0: * An optional (for content debugging only) reference to the content michael@0: * window. michael@0: */ michael@0: function ThreadActor(aHooks, aGlobal) michael@0: { michael@0: this._state = "detached"; michael@0: this._frameActors = []; michael@0: this._hooks = aHooks; michael@0: this.global = aGlobal; michael@0: // A map of actorID -> actor for breakpoints created and managed by the server. michael@0: this._hiddenBreakpoints = new Map(); michael@0: michael@0: this.findGlobals = this.globalManager.findGlobals.bind(this); michael@0: this.onNewGlobal = this.globalManager.onNewGlobal.bind(this); michael@0: this.onNewSource = this.onNewSource.bind(this); michael@0: this._allEventsListener = this._allEventsListener.bind(this); michael@0: michael@0: this._options = { michael@0: useSourceMaps: false michael@0: }; michael@0: michael@0: this._gripDepth = 0; michael@0: } michael@0: michael@0: /** michael@0: * The breakpoint store must be shared across instances of ThreadActor so that michael@0: * page reloads don't blow away all of our breakpoints. michael@0: */ michael@0: ThreadActor.breakpointStore = new BreakpointStore(); michael@0: michael@0: ThreadActor.prototype = { michael@0: // Used by the ObjectActor to keep track of the depth of grip() calls. michael@0: _gripDepth: null, michael@0: michael@0: actorPrefix: "context", michael@0: michael@0: get state() { return this._state; }, michael@0: get attached() this.state == "attached" || michael@0: this.state == "running" || michael@0: this.state == "paused", michael@0: michael@0: get breakpointStore() { return ThreadActor.breakpointStore; }, michael@0: michael@0: get threadLifetimePool() { michael@0: if (!this._threadLifetimePool) { michael@0: this._threadLifetimePool = new ActorPool(this.conn); michael@0: this.conn.addActorPool(this._threadLifetimePool); michael@0: this._threadLifetimePool.objectActors = new WeakMap(); michael@0: } michael@0: return this._threadLifetimePool; michael@0: }, michael@0: michael@0: get sources() { michael@0: if (!this._sources) { michael@0: this._sources = new ThreadSources(this, this._options.useSourceMaps, michael@0: this._allowSource, this.onNewSource); michael@0: } michael@0: return this._sources; michael@0: }, michael@0: michael@0: get youngestFrame() { michael@0: if (this.state != "paused") { michael@0: return null; michael@0: } michael@0: return this.dbg.getNewestFrame(); michael@0: }, michael@0: michael@0: _prettyPrintWorker: null, michael@0: get prettyPrintWorker() { michael@0: if (!this._prettyPrintWorker) { michael@0: this._prettyPrintWorker = new ChromeWorker( michael@0: "resource://gre/modules/devtools/server/actors/pretty-print-worker.js"); michael@0: michael@0: this._prettyPrintWorker.addEventListener( michael@0: "error", this._onPrettyPrintError, false); michael@0: michael@0: if (dumpn.wantLogging) { michael@0: this._prettyPrintWorker.addEventListener("message", this._onPrettyPrintMsg, false); michael@0: michael@0: const postMsg = this._prettyPrintWorker.postMessage; michael@0: this._prettyPrintWorker.postMessage = data => { michael@0: dumpn("Sending message to prettyPrintWorker: " michael@0: + JSON.stringify(data, null, 2) + "\n"); michael@0: return postMsg.call(this._prettyPrintWorker, data); michael@0: }; michael@0: } michael@0: } michael@0: return this._prettyPrintWorker; michael@0: }, michael@0: michael@0: _onPrettyPrintError: function ({ message, filename, lineno }) { michael@0: reportError(new Error(message + " @ " + filename + ":" + lineno)); michael@0: }, michael@0: michael@0: _onPrettyPrintMsg: function ({ data }) { michael@0: dumpn("Received message from prettyPrintWorker: " michael@0: + JSON.stringify(data, null, 2) + "\n"); michael@0: }, michael@0: michael@0: /** michael@0: * Keep track of all of the nested event loops we use to pause the debuggee michael@0: * when we hit a breakpoint/debugger statement/etc in one place so we can michael@0: * resolve them when we get resume packets. We have more than one (and keep michael@0: * them in a stack) because we can pause within client evals. michael@0: */ michael@0: _threadPauseEventLoops: null, michael@0: _pushThreadPause: function () { michael@0: if (!this._threadPauseEventLoops) { michael@0: this._threadPauseEventLoops = []; michael@0: } michael@0: const eventLoop = this._nestedEventLoops.push(); michael@0: this._threadPauseEventLoops.push(eventLoop); michael@0: eventLoop.enter(); michael@0: }, michael@0: _popThreadPause: function () { michael@0: const eventLoop = this._threadPauseEventLoops.pop(); michael@0: dbg_assert(eventLoop, "Should have an event loop."); michael@0: eventLoop.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Remove all debuggees and clear out the thread's sources. michael@0: */ michael@0: clearDebuggees: function () { michael@0: if (this.dbg) { michael@0: this.dbg.removeAllDebuggees(); michael@0: } michael@0: this._sources = null; michael@0: }, michael@0: michael@0: /** michael@0: * Add a debuggee global to the Debugger object. michael@0: * michael@0: * @returns the Debugger.Object that corresponds to the global. michael@0: */ michael@0: addDebuggee: function (aGlobal) { michael@0: let globalDebugObject; michael@0: try { michael@0: globalDebugObject = this.dbg.addDebuggee(aGlobal); michael@0: } catch (e) { michael@0: // Ignore attempts to add the debugger's compartment as a debuggee. michael@0: dumpn("Ignoring request to add the debugger's compartment as a debuggee"); michael@0: } michael@0: return globalDebugObject; michael@0: }, michael@0: michael@0: /** michael@0: * Initialize the Debugger. michael@0: */ michael@0: _initDebugger: function () { michael@0: this.dbg = new Debugger(); michael@0: this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this); michael@0: this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this); michael@0: this.dbg.onNewScript = this.onNewScript.bind(this); michael@0: this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this); michael@0: // Keep the debugger disabled until a client attaches. michael@0: this.dbg.enabled = this._state != "detached"; michael@0: }, michael@0: michael@0: /** michael@0: * Remove a debuggee global from the JSInspector. michael@0: */ michael@0: removeDebugee: function (aGlobal) { michael@0: try { michael@0: this.dbg.removeDebuggee(aGlobal); michael@0: } catch(ex) { michael@0: // XXX: This debuggee has code currently executing on the stack, michael@0: // we need to save this for later. michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add the provided window and all windows in its frame tree as debuggees. michael@0: * michael@0: * @returns the Debugger.Object that corresponds to the window. michael@0: */ michael@0: _addDebuggees: function (aWindow) { michael@0: let globalDebugObject = this.addDebuggee(aWindow); michael@0: let frames = aWindow.frames; michael@0: if (frames) { michael@0: for (let i = 0; i < frames.length; i++) { michael@0: this._addDebuggees(frames[i]); michael@0: } michael@0: } michael@0: return globalDebugObject; michael@0: }, michael@0: michael@0: /** michael@0: * An object that will be used by ThreadActors to tailor their behavior michael@0: * depending on the debugging context being required (chrome or content). michael@0: */ michael@0: globalManager: { michael@0: findGlobals: function () { michael@0: const { gDevToolsExtensions: { michael@0: getContentGlobals michael@0: } } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {}); michael@0: michael@0: this.globalDebugObject = this._addDebuggees(this.global); michael@0: michael@0: // global may not be a window michael@0: try { michael@0: getContentGlobals({ michael@0: 'inner-window-id': getInnerId(this.global) michael@0: }).forEach(this.addDebuggee.bind(this)); michael@0: } michael@0: catch(e) {} michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when a new global object michael@0: * (for example a sandbox) has been created. michael@0: * michael@0: * @param aGlobal Debugger.Object michael@0: * The new global object that was created. michael@0: */ michael@0: onNewGlobal: function (aGlobal) { michael@0: let useGlobal = (aGlobal.hostAnnotations && michael@0: aGlobal.hostAnnotations.type == "document" && michael@0: aGlobal.hostAnnotations.element === this.global); michael@0: michael@0: // check if the global is a sdk page-mod sandbox michael@0: if (!useGlobal) { michael@0: let metadata = {}; michael@0: let id = ""; michael@0: try { michael@0: id = getInnerId(this.global); michael@0: metadata = Cu.getSandboxMetadata(aGlobal.unsafeDereference()); michael@0: } michael@0: catch (e) {} michael@0: michael@0: useGlobal = (metadata['inner-window-id'] && metadata['inner-window-id'] == id); michael@0: } michael@0: michael@0: // Content debugging only cares about new globals in the contant window, michael@0: // like iframe children. michael@0: if (useGlobal) { michael@0: this.addDebuggee(aGlobal); michael@0: // Notify the client. michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: type: "newGlobal", michael@0: // TODO: after bug 801084 lands see if we need to JSONify this. michael@0: hostAnnotations: aGlobal.hostAnnotations michael@0: }); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: disconnect: function () { michael@0: dumpn("in ThreadActor.prototype.disconnect"); michael@0: if (this._state == "paused") { michael@0: this.onResume(); michael@0: } michael@0: michael@0: this.clearDebuggees(); michael@0: this.conn.removeActorPool(this._threadLifetimePool); michael@0: this._threadLifetimePool = null; michael@0: michael@0: if (this._prettyPrintWorker) { michael@0: this._prettyPrintWorker.removeEventListener( michael@0: "error", this._onPrettyPrintError, false); michael@0: this._prettyPrintWorker.removeEventListener( michael@0: "message", this._onPrettyPrintMsg, false); michael@0: this._prettyPrintWorker.terminate(); michael@0: this._prettyPrintWorker = null; michael@0: } michael@0: michael@0: if (!this.dbg) { michael@0: return; michael@0: } michael@0: this.dbg.enabled = false; michael@0: this.dbg = null; michael@0: }, michael@0: michael@0: /** michael@0: * Disconnect the debugger and put the actor in the exited state. michael@0: */ michael@0: exit: function () { michael@0: this.disconnect(); michael@0: this._state = "exited"; michael@0: }, michael@0: michael@0: // Request handlers michael@0: onAttach: function (aRequest) { michael@0: if (this.state === "exited") { michael@0: return { type: "exited" }; michael@0: } michael@0: michael@0: if (this.state !== "detached") { michael@0: return { error: "wrongState", michael@0: message: "Current state is " + this.state }; michael@0: } michael@0: michael@0: this._state = "attached"; michael@0: michael@0: update(this._options, aRequest.options || {}); michael@0: michael@0: // Initialize an event loop stack. This can't be done in the constructor, michael@0: // because this.conn is not yet initialized by the actor pool at that time. michael@0: this._nestedEventLoops = new EventLoopStack({ michael@0: inspector: DebuggerServer.xpcInspector, michael@0: hooks: this._hooks, michael@0: connection: this.conn, michael@0: thread: this michael@0: }); michael@0: michael@0: if (!this.dbg) { michael@0: this._initDebugger(); michael@0: } michael@0: this.findGlobals(); michael@0: this.dbg.enabled = true; michael@0: try { michael@0: // Put ourselves in the paused state. michael@0: let packet = this._paused(); michael@0: if (!packet) { michael@0: return { error: "notAttached" }; michael@0: } michael@0: packet.why = { type: "attached" }; michael@0: michael@0: this._restoreBreakpoints(); michael@0: michael@0: // Send the response to the attach request now (rather than michael@0: // returning it), because we're going to start a nested event loop michael@0: // here. michael@0: this.conn.send(packet); michael@0: michael@0: // Start a nested event loop. michael@0: this._pushThreadPause(); michael@0: michael@0: // We already sent a response to this request, don't send one michael@0: // now. michael@0: return null; michael@0: } catch (e) { michael@0: reportError(e); michael@0: return { error: "notAttached", message: e.toString() }; michael@0: } michael@0: }, michael@0: michael@0: onDetach: function (aRequest) { michael@0: this.disconnect(); michael@0: this._state = "detached"; michael@0: michael@0: dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet"); michael@0: return { michael@0: type: "detached" michael@0: }; michael@0: }, michael@0: michael@0: onReconfigure: function (aRequest) { michael@0: if (this.state == "exited") { michael@0: return { error: "wrongState" }; michael@0: } michael@0: michael@0: update(this._options, aRequest.options || {}); michael@0: // Clear existing sources, so they can be recreated on next access. michael@0: this._sources = null; michael@0: michael@0: return {}; michael@0: }, michael@0: michael@0: /** michael@0: * Pause the debuggee, by entering a nested event loop, and return a 'paused' michael@0: * packet to the client. michael@0: * michael@0: * @param Debugger.Frame aFrame michael@0: * The newest debuggee frame in the stack. michael@0: * @param object aReason michael@0: * An object with a 'type' property containing the reason for the pause. michael@0: * @param function onPacket michael@0: * Hook to modify the packet before it is sent. Feel free to return a michael@0: * promise. michael@0: */ michael@0: _pauseAndRespond: function (aFrame, aReason, onPacket=function (k) { return k; }) { michael@0: try { michael@0: let packet = this._paused(aFrame); michael@0: if (!packet) { michael@0: return undefined; michael@0: } michael@0: packet.why = aReason; michael@0: michael@0: this.sources.getOriginalLocation(packet.frame.where).then(aOrigPosition => { michael@0: packet.frame.where = aOrigPosition; michael@0: resolve(onPacket(packet)) michael@0: .then(null, error => { michael@0: reportError(error); michael@0: return { michael@0: error: "unknownError", michael@0: message: error.message + "\n" + error.stack michael@0: }; michael@0: }) michael@0: .then(packet => { michael@0: this.conn.send(packet); michael@0: }); michael@0: }); michael@0: michael@0: this._pushThreadPause(); michael@0: } catch(e) { michael@0: reportError(e, "Got an exception during TA__pauseAndRespond: "); michael@0: } michael@0: michael@0: return undefined; michael@0: }, michael@0: michael@0: /** michael@0: * Handle resume requests that include a forceCompletion request. michael@0: * michael@0: * @param Object aRequest michael@0: * The request packet received over the RDP. michael@0: * @returns A response packet. michael@0: */ michael@0: _forceCompletion: function (aRequest) { michael@0: // TODO: remove this when Debugger.Frame.prototype.pop is implemented in michael@0: // bug 736733. michael@0: return { michael@0: error: "notImplemented", michael@0: message: "forced completion is not yet implemented." michael@0: }; michael@0: }, michael@0: michael@0: _makeOnEnterFrame: function ({ pauseAndRespond }) { michael@0: return aFrame => { michael@0: const generatedLocation = getFrameLocation(aFrame); michael@0: let { url } = this.synchronize(this.sources.getOriginalLocation( michael@0: generatedLocation)); michael@0: michael@0: return this.sources.isBlackBoxed(url) michael@0: ? undefined michael@0: : pauseAndRespond(aFrame); michael@0: }; michael@0: }, michael@0: michael@0: _makeOnPop: function ({ thread, pauseAndRespond, createValueGrip }) { michael@0: return function (aCompletion) { michael@0: // onPop is called with 'this' set to the current frame. michael@0: michael@0: const generatedLocation = getFrameLocation(this); michael@0: const { url } = thread.synchronize(thread.sources.getOriginalLocation( michael@0: generatedLocation)); michael@0: michael@0: if (thread.sources.isBlackBoxed(url)) { michael@0: return undefined; michael@0: } michael@0: michael@0: // Note that we're popping this frame; we need to watch for michael@0: // subsequent step events on its caller. michael@0: this.reportedPop = true; michael@0: michael@0: return pauseAndRespond(this, aPacket => { michael@0: aPacket.why.frameFinished = {}; michael@0: if (!aCompletion) { michael@0: aPacket.why.frameFinished.terminated = true; michael@0: } else if (aCompletion.hasOwnProperty("return")) { michael@0: aPacket.why.frameFinished.return = createValueGrip(aCompletion.return); michael@0: } else if (aCompletion.hasOwnProperty("yield")) { michael@0: aPacket.why.frameFinished.return = createValueGrip(aCompletion.yield); michael@0: } else { michael@0: aPacket.why.frameFinished.throw = createValueGrip(aCompletion.throw); michael@0: } michael@0: return aPacket; michael@0: }); michael@0: }; michael@0: }, michael@0: michael@0: _makeOnStep: function ({ thread, pauseAndRespond, startFrame, michael@0: startLocation }) { michael@0: return function () { michael@0: // onStep is called with 'this' set to the current frame. michael@0: michael@0: const generatedLocation = getFrameLocation(this); michael@0: const newLocation = thread.synchronize(thread.sources.getOriginalLocation( michael@0: generatedLocation)); michael@0: michael@0: // Cases when we should pause because we have executed enough to consider michael@0: // a "step" to have occured: michael@0: // michael@0: // 1.1. We change frames. michael@0: // 1.2. We change URLs (can happen without changing frames thanks to michael@0: // source mapping). michael@0: // 1.3. We change lines. michael@0: // michael@0: // Cases when we should always continue execution, even if one of the michael@0: // above cases is true: michael@0: // michael@0: // 2.1. We are in a source mapped region, but inside a null mapping michael@0: // (doesn't correlate to any region of original source) michael@0: // 2.2. The source we are in is black boxed. michael@0: michael@0: // Cases 2.1 and 2.2 michael@0: if (newLocation.url == null michael@0: || thread.sources.isBlackBoxed(newLocation.url)) { michael@0: return undefined; michael@0: } michael@0: michael@0: // Cases 1.1, 1.2 and 1.3 michael@0: if (this !== startFrame michael@0: || startLocation.url !== newLocation.url michael@0: || startLocation.line !== newLocation.line) { michael@0: return pauseAndRespond(this); michael@0: } michael@0: michael@0: // Otherwise, let execution continue (we haven't executed enough code to michael@0: // consider this a "step" yet). michael@0: return undefined; michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Define the JS hook functions for stepping. michael@0: */ michael@0: _makeSteppingHooks: function (aStartLocation) { michael@0: // Bind these methods and state because some of the hooks are called michael@0: // with 'this' set to the current frame. Rather than repeating the michael@0: // binding in each _makeOnX method, just do it once here and pass it michael@0: // in to each function. michael@0: const steppingHookState = { michael@0: pauseAndRespond: (aFrame, onPacket=(k)=>k) => { michael@0: this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket); michael@0: }, michael@0: createValueGrip: this.createValueGrip.bind(this), michael@0: thread: this, michael@0: startFrame: this.youngestFrame, michael@0: startLocation: aStartLocation michael@0: }; michael@0: michael@0: return { michael@0: onEnterFrame: this._makeOnEnterFrame(steppingHookState), michael@0: onPop: this._makeOnPop(steppingHookState), michael@0: onStep: this._makeOnStep(steppingHookState) michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle attaching the various stepping hooks we need to attach when we michael@0: * receive a resume request with a resumeLimit property. michael@0: * michael@0: * @param Object aRequest michael@0: * The request packet received over the RDP. michael@0: * @returns A promise that resolves to true once the hooks are attached, or is michael@0: * rejected with an error packet. michael@0: */ michael@0: _handleResumeLimit: function (aRequest) { michael@0: let steppingType = aRequest.resumeLimit.type; michael@0: if (["step", "next", "finish"].indexOf(steppingType) == -1) { michael@0: return reject({ error: "badParameterType", michael@0: message: "Unknown resumeLimit type" }); michael@0: } michael@0: michael@0: const generatedLocation = getFrameLocation(this.youngestFrame); michael@0: return this.sources.getOriginalLocation(generatedLocation) michael@0: .then(originalLocation => { michael@0: const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation); michael@0: michael@0: // Make sure there is still a frame on the stack if we are to continue michael@0: // stepping. michael@0: let stepFrame = this._getNextStepFrame(this.youngestFrame); michael@0: if (stepFrame) { michael@0: switch (steppingType) { michael@0: case "step": michael@0: this.dbg.onEnterFrame = onEnterFrame; michael@0: // Fall through. michael@0: case "next": michael@0: if (stepFrame.script) { michael@0: stepFrame.onStep = onStep; michael@0: } michael@0: stepFrame.onPop = onPop; michael@0: break; michael@0: case "finish": michael@0: stepFrame.onPop = onPop; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Clear the onStep and onPop hooks from the given frame and all of the frames michael@0: * below it. michael@0: * michael@0: * @param Debugger.Frame aFrame michael@0: * The frame we want to clear the stepping hooks from. michael@0: */ michael@0: _clearSteppingHooks: function (aFrame) { michael@0: while (aFrame) { michael@0: aFrame.onStep = undefined; michael@0: aFrame.onPop = undefined; michael@0: aFrame = aFrame.older; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Listen to the debuggee's DOM events if we received a request to do so. michael@0: * michael@0: * @param Object aRequest michael@0: * The resume request packet received over the RDP. michael@0: */ michael@0: _maybeListenToEvents: function (aRequest) { michael@0: // Break-on-DOMEvents is only supported in content debugging. michael@0: let events = aRequest.pauseOnDOMEvents; michael@0: if (this.global && events && michael@0: (events == "*" || michael@0: (Array.isArray(events) && events.length))) { michael@0: this._pauseOnDOMEvents = events; michael@0: let els = Cc["@mozilla.org/eventlistenerservice;1"] michael@0: .getService(Ci.nsIEventListenerService); michael@0: els.addListenerForAllEvents(this.global, this._allEventsListener, true); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to resume execution of the debuggee. michael@0: */ michael@0: onResume: function (aRequest) { michael@0: if (this._state !== "paused") { michael@0: return { michael@0: error: "wrongState", michael@0: message: "Can't resume when debuggee isn't paused. Current state is '" michael@0: + this._state + "'" michael@0: }; michael@0: } michael@0: michael@0: // In case of multiple nested event loops (due to multiple debuggers open in michael@0: // different tabs or multiple debugger clients connected to the same tab) michael@0: // only allow resumption in a LIFO order. michael@0: if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl michael@0: && (this._nestedEventLoops.lastPausedUrl !== this._hooks.url michael@0: || this._nestedEventLoops.lastConnection !== this.conn)) { michael@0: return { michael@0: error: "wrongOrder", michael@0: message: "trying to resume in the wrong order.", michael@0: lastPausedUrl: this._nestedEventLoops.lastPausedUrl michael@0: }; michael@0: } michael@0: michael@0: if (aRequest && aRequest.forceCompletion) { michael@0: return this._forceCompletion(aRequest); michael@0: } michael@0: michael@0: let resumeLimitHandled; michael@0: if (aRequest && aRequest.resumeLimit) { michael@0: resumeLimitHandled = this._handleResumeLimit(aRequest) michael@0: } else { michael@0: this._clearSteppingHooks(this.youngestFrame); michael@0: resumeLimitHandled = resolve(true); michael@0: } michael@0: michael@0: return resumeLimitHandled.then(() => { michael@0: if (aRequest) { michael@0: this._options.pauseOnExceptions = aRequest.pauseOnExceptions; michael@0: this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions; michael@0: this.maybePauseOnExceptions(); michael@0: this._maybeListenToEvents(aRequest); michael@0: } michael@0: michael@0: let packet = this._resumed(); michael@0: this._popThreadPause(); michael@0: return packet; michael@0: }, error => { michael@0: return error instanceof Error michael@0: ? { error: "unknownError", michael@0: message: DevToolsUtils.safeErrorString(error) } michael@0: // It is a known error, and the promise was rejected with an error michael@0: // packet. michael@0: : error; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Spin up a nested event loop so we can synchronously resolve a promise. michael@0: * michael@0: * @param aPromise michael@0: * The promise we want to resolve. michael@0: * @returns The promise's resolution. michael@0: */ michael@0: synchronize: function(aPromise) { michael@0: let needNest = true; michael@0: let eventLoop; michael@0: let returnVal; michael@0: michael@0: aPromise michael@0: .then((aResolvedVal) => { michael@0: needNest = false; michael@0: returnVal = aResolvedVal; michael@0: }) michael@0: .then(null, (aError) => { michael@0: reportError(aError, "Error inside synchronize:"); michael@0: }) michael@0: .then(() => { michael@0: if (eventLoop) { michael@0: eventLoop.resolve(); michael@0: } michael@0: }); michael@0: michael@0: if (needNest) { michael@0: eventLoop = this._nestedEventLoops.push(); michael@0: eventLoop.enter(); michael@0: } michael@0: michael@0: return returnVal; michael@0: }, michael@0: michael@0: /** michael@0: * Set the debugging hook to pause on exceptions if configured to do so. michael@0: */ michael@0: maybePauseOnExceptions: function() { michael@0: if (this._options.pauseOnExceptions) { michael@0: this.dbg.onExceptionUnwind = this.onExceptionUnwind.bind(this); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * A listener that gets called for every event fired on the page, when a list michael@0: * of interesting events was provided with the pauseOnDOMEvents property. It michael@0: * is used to set server-managed breakpoints on any existing event listeners michael@0: * for those events. michael@0: * michael@0: * @param Event event michael@0: * The event that was fired. michael@0: */ michael@0: _allEventsListener: function(event) { michael@0: if (this._pauseOnDOMEvents == "*" || michael@0: this._pauseOnDOMEvents.indexOf(event.type) != -1) { michael@0: for (let listener of this._getAllEventListeners(event.target)) { michael@0: if (event.type == listener.type || this._pauseOnDOMEvents == "*") { michael@0: this._breakOnEnter(listener.script); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Return an array containing all the event listeners attached to the michael@0: * specified event target and its ancestors in the event target chain. michael@0: * michael@0: * @param EventTarget eventTarget michael@0: * The target the event was dispatched on. michael@0: * @returns Array michael@0: */ michael@0: _getAllEventListeners: function(eventTarget) { michael@0: let els = Cc["@mozilla.org/eventlistenerservice;1"] michael@0: .getService(Ci.nsIEventListenerService); michael@0: michael@0: let targets = els.getEventTargetChainFor(eventTarget); michael@0: let listeners = []; michael@0: michael@0: for (let target of targets) { michael@0: let handlers = els.getListenerInfoFor(target); michael@0: for (let handler of handlers) { michael@0: // Null is returned for all-events handlers, and native event listeners michael@0: // don't provide any listenerObject, which makes them not that useful to michael@0: // a JS debugger. michael@0: if (!handler || !handler.listenerObject || !handler.type) michael@0: continue; michael@0: // Create a listener-like object suitable for our purposes. michael@0: let l = Object.create(null); michael@0: l.type = handler.type; michael@0: let listener = handler.listenerObject; michael@0: l.script = this.globalDebugObject.makeDebuggeeValue(listener).script; michael@0: // Chrome listeners won't be converted to debuggee values, since their michael@0: // compartment is not added as a debuggee. michael@0: if (!l.script) michael@0: continue; michael@0: listeners.push(l); michael@0: } michael@0: } michael@0: return listeners; michael@0: }, michael@0: michael@0: /** michael@0: * Set a breakpoint on the first bytecode offset in the provided script. michael@0: */ michael@0: _breakOnEnter: function(script) { michael@0: let offsets = script.getAllOffsets(); michael@0: for (let line = 0, n = offsets.length; line < n; line++) { michael@0: if (offsets[line]) { michael@0: let location = { url: script.url, line: line }; michael@0: let resp = this._createAndStoreBreakpoint(location); michael@0: dbg_assert(!resp.actualLocation, "No actualLocation should be returned"); michael@0: if (resp.error) { michael@0: reportError(new Error("Unable to set breakpoint on event listener")); michael@0: return; michael@0: } michael@0: let bp = this.breakpointStore.getBreakpoint(location); michael@0: let bpActor = bp.actor; michael@0: dbg_assert(bp, "Breakpoint must exist"); michael@0: dbg_assert(bpActor, "Breakpoint actor must be created"); michael@0: this._hiddenBreakpoints.set(bpActor.actorID, bpActor); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Helper method that returns the next frame when stepping. michael@0: */ michael@0: _getNextStepFrame: function (aFrame) { michael@0: let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame; michael@0: if (!stepFrame || !stepFrame.script) { michael@0: stepFrame = null; michael@0: } michael@0: return stepFrame; michael@0: }, michael@0: michael@0: onClientEvaluate: function (aRequest) { michael@0: if (this.state !== "paused") { michael@0: return { error: "wrongState", michael@0: message: "Debuggee must be paused to evaluate code." }; michael@0: } michael@0: michael@0: let frame = this._requestFrame(aRequest.frame); michael@0: if (!frame) { michael@0: return { error: "unknownFrame", michael@0: message: "Evaluation frame not found" }; michael@0: } michael@0: michael@0: if (!frame.environment) { michael@0: return { error: "notDebuggee", michael@0: message: "cannot access the environment of this frame." }; michael@0: } michael@0: michael@0: let youngest = this.youngestFrame; michael@0: michael@0: // Put ourselves back in the running state and inform the client. michael@0: let resumedPacket = this._resumed(); michael@0: this.conn.send(resumedPacket); michael@0: michael@0: // Run the expression. michael@0: // XXX: test syntax errors michael@0: let completion = frame.eval(aRequest.expression); michael@0: michael@0: // Put ourselves back in the pause state. michael@0: let packet = this._paused(youngest); michael@0: packet.why = { type: "clientEvaluated", michael@0: frameFinished: this.createProtocolCompletionValue(completion) }; michael@0: michael@0: // Return back to our previous pause's event loop. michael@0: return packet; michael@0: }, michael@0: michael@0: onFrames: function (aRequest) { michael@0: if (this.state !== "paused") { michael@0: return { error: "wrongState", michael@0: message: "Stack frames are only available while the debuggee is paused."}; michael@0: } michael@0: michael@0: let start = aRequest.start ? aRequest.start : 0; michael@0: let count = aRequest.count; michael@0: michael@0: // Find the starting frame... michael@0: let frame = this.youngestFrame; michael@0: let i = 0; michael@0: while (frame && (i < start)) { michael@0: frame = frame.older; michael@0: i++; michael@0: } michael@0: michael@0: // Return request.count frames, or all remaining michael@0: // frames if count is not defined. michael@0: let frames = []; michael@0: let promises = []; michael@0: for (; frame && (!count || i < (start + count)); i++, frame=frame.older) { michael@0: let form = this._createFrameActor(frame).form(); michael@0: form.depth = i; michael@0: frames.push(form); michael@0: michael@0: let promise = this.sources.getOriginalLocation(form.where) michael@0: .then((aOrigLocation) => { michael@0: form.where = aOrigLocation; michael@0: let source = this.sources.source({ url: form.where.url }); michael@0: if (source) { michael@0: form.source = source.form(); michael@0: } michael@0: }); michael@0: promises.push(promise); michael@0: } michael@0: michael@0: return all(promises).then(function () { michael@0: return { frames: frames }; michael@0: }); michael@0: }, michael@0: michael@0: onReleaseMany: function (aRequest) { michael@0: if (!aRequest.actors) { michael@0: return { error: "missingParameter", michael@0: message: "no actors were specified" }; michael@0: } michael@0: michael@0: let res; michael@0: for each (let actorID in aRequest.actors) { michael@0: let actor = this.threadLifetimePool.get(actorID); michael@0: if (!actor) { michael@0: if (!res) { michael@0: res = { error: "notReleasable", michael@0: message: "Only thread-lifetime actors can be released." }; michael@0: } michael@0: continue; michael@0: } michael@0: actor.onRelease(); michael@0: } michael@0: return res ? res : {}; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to set a breakpoint. michael@0: */ michael@0: onSetBreakpoint: function (aRequest) { michael@0: if (this.state !== "paused") { michael@0: return { error: "wrongState", michael@0: message: "Breakpoints can only be set while the debuggee is paused."}; michael@0: } michael@0: michael@0: let { url: originalSource, michael@0: line: originalLine, michael@0: column: originalColumn } = aRequest.location; michael@0: michael@0: let locationPromise = this.sources.getGeneratedLocation(aRequest.location); michael@0: return locationPromise.then(({url, line, column}) => { michael@0: if (line == null || michael@0: line < 0 || michael@0: this.dbg.findScripts({ url: url }).length == 0) { michael@0: return { michael@0: error: "noScript", michael@0: message: "Requested setting a breakpoint on " michael@0: + url + ":" + line michael@0: + (column != null ? ":" + column : "") michael@0: + " but there is no Debugger.Script at that location" michael@0: }; michael@0: } michael@0: michael@0: let response = this._createAndStoreBreakpoint({ michael@0: url: url, michael@0: line: line, michael@0: column: column, michael@0: condition: aRequest.condition michael@0: }); michael@0: // If the original location of our generated location is different from michael@0: // the original location we attempted to set the breakpoint on, we will michael@0: // need to know so that we can set actualLocation on the response. michael@0: let originalLocation = this.sources.getOriginalLocation({ michael@0: url: url, michael@0: line: line, michael@0: column: column michael@0: }); michael@0: michael@0: return all([response, originalLocation]) michael@0: .then(([aResponse, {url, line}]) => { michael@0: if (aResponse.actualLocation) { michael@0: let actualOrigLocation = this.sources.getOriginalLocation(aResponse.actualLocation); michael@0: return actualOrigLocation.then(({ url, line, column }) => { michael@0: if (url !== originalSource michael@0: || line !== originalLine michael@0: || column !== originalColumn) { michael@0: aResponse.actualLocation = { michael@0: url: url, michael@0: line: line, michael@0: column: column michael@0: }; michael@0: } michael@0: return aResponse; michael@0: }); michael@0: } michael@0: michael@0: if (url !== originalSource || line !== originalLine) { michael@0: aResponse.actualLocation = { url: url, line: line }; michael@0: } michael@0: michael@0: return aResponse; michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Create a breakpoint at the specified location and store it in the michael@0: * cache. Takes ownership of `aLocation`. michael@0: * michael@0: * @param Object aLocation michael@0: * An object of the form { url, line[, column] } michael@0: */ michael@0: _createAndStoreBreakpoint: function (aLocation) { michael@0: // Add the breakpoint to the store for later reuse, in case it belongs to a michael@0: // script that hasn't appeared yet. michael@0: this.breakpointStore.addBreakpoint(aLocation); michael@0: return this._setBreakpoint(aLocation); michael@0: }, michael@0: michael@0: /** michael@0: * Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint michael@0: * is being set contains no code, then the breakpoint will slide down to the michael@0: * next line that has runnable code. In this case the server breakpoint cache michael@0: * will be updated, so callers that iterate over the breakpoint cache should michael@0: * take that into account. michael@0: * michael@0: * @param object aLocation michael@0: * The location of the breakpoint (in the generated source, if source michael@0: * mapping). michael@0: */ michael@0: _setBreakpoint: function (aLocation) { michael@0: let actor; michael@0: let storedBp = this.breakpointStore.getBreakpoint(aLocation); michael@0: if (storedBp.actor) { michael@0: actor = storedBp.actor; michael@0: actor.condition = aLocation.condition; michael@0: } else { michael@0: storedBp.actor = actor = new BreakpointActor(this, { michael@0: url: aLocation.url, michael@0: line: aLocation.line, michael@0: column: aLocation.column, michael@0: condition: aLocation.condition michael@0: }); michael@0: this.threadLifetimePool.addActor(actor); michael@0: } michael@0: michael@0: // Find all scripts matching the given location michael@0: let scripts = this.dbg.findScripts(aLocation); michael@0: if (scripts.length == 0) { michael@0: return { michael@0: error: "noScript", michael@0: message: "Requested setting a breakpoint on " michael@0: + aLocation.url + ":" + aLocation.line michael@0: + (aLocation.column != null ? ":" + aLocation.column : "") michael@0: + " but there is no Debugger.Script at that location", michael@0: actor: actor.actorID michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * For each script, if the given line has at least one entry point, set a michael@0: * breakpoint on the bytecode offets for each of them. michael@0: */ michael@0: michael@0: // Debugger.Script -> array of offset mappings michael@0: let scriptsAndOffsetMappings = new Map(); michael@0: michael@0: for (let script of scripts) { michael@0: this._findClosestOffsetMappings(aLocation, michael@0: script, michael@0: scriptsAndOffsetMappings); michael@0: } michael@0: michael@0: if (scriptsAndOffsetMappings.size > 0) { michael@0: for (let [script, mappings] of scriptsAndOffsetMappings) { michael@0: for (let offsetMapping of mappings) { michael@0: script.setBreakpoint(offsetMapping.offset, actor); michael@0: } michael@0: actor.addScript(script, this); michael@0: } michael@0: michael@0: return { michael@0: actor: actor.actorID michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * If we get here, no breakpoint was set. This is because the given line michael@0: * has no entry points, for example because it is empty. As a fallback michael@0: * strategy, we try to set the breakpoint on the smallest line greater michael@0: * than or equal to the given line that as at least one entry point. michael@0: */ michael@0: michael@0: // Find all innermost scripts matching the given location michael@0: let scripts = this.dbg.findScripts({ michael@0: url: aLocation.url, michael@0: line: aLocation.line, michael@0: innermost: true michael@0: }); michael@0: michael@0: /** michael@0: * For each innermost script, look for the smallest line greater than or michael@0: * equal to the given line that has one or more entry points. If found, set michael@0: * a breakpoint on the bytecode offset for each of its entry points. michael@0: */ michael@0: let actualLocation; michael@0: let found = false; michael@0: for (let script of scripts) { michael@0: let offsets = script.getAllOffsets(); michael@0: for (let line = aLocation.line; line < offsets.length; ++line) { michael@0: if (offsets[line]) { michael@0: for (let offset of offsets[line]) { michael@0: script.setBreakpoint(offset, actor); michael@0: } michael@0: actor.addScript(script, this); michael@0: if (!actualLocation) { michael@0: actualLocation = { michael@0: url: aLocation.url, michael@0: line: line michael@0: }; michael@0: } michael@0: found = true; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: if (found) { michael@0: let existingBp = this.breakpointStore.hasBreakpoint(actualLocation); michael@0: michael@0: if (existingBp && existingBp.actor) { michael@0: /** michael@0: * We already have a breakpoint actor for the actual location, so michael@0: * actor we created earlier is now redundant. Delete it, update the michael@0: * breakpoint store, and return the actor for the actual location. michael@0: */ michael@0: actor.onDelete(); michael@0: this.breakpointStore.removeBreakpoint(aLocation); michael@0: return { michael@0: actor: existingBp.actor.actorID, michael@0: actualLocation: actualLocation michael@0: }; michael@0: } else { michael@0: /** michael@0: * We don't have a breakpoint actor for the actual location yet. michael@0: * Instead or creating a new actor, reuse the actor we created earlier, michael@0: * and update the breakpoint store. michael@0: */ michael@0: actor.location = actualLocation; michael@0: this.breakpointStore.addBreakpoint({ michael@0: actor: actor, michael@0: url: actualLocation.url, michael@0: line: actualLocation.line, michael@0: column: actualLocation.column michael@0: }); michael@0: this.breakpointStore.removeBreakpoint(aLocation); michael@0: return { michael@0: actor: actor.actorID, michael@0: actualLocation: actualLocation michael@0: }; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * If we get here, no line matching the given line was found, so just michael@0: * fail epically. michael@0: */ michael@0: return { michael@0: error: "noCodeAtLineColumn", michael@0: actor: actor.actorID michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Find all of the offset mappings associated with `aScript` that are closest michael@0: * to `aTargetLocation`. If new offset mappings are found that are closer to michael@0: * `aTargetOffset` than the existing offset mappings inside michael@0: * `aScriptsAndOffsetMappings`, we empty that map and only consider the michael@0: * closest offset mappings. If there is no column in `aTargetLocation`, we add michael@0: * all offset mappings that are on the given line. michael@0: * michael@0: * @param Object aTargetLocation michael@0: * An object of the form { url, line[, column] }. michael@0: * @param Debugger.Script aScript michael@0: * The script in which we are searching for offsets. michael@0: * @param Map aScriptsAndOffsetMappings michael@0: * A Map object which maps Debugger.Script instances to arrays of michael@0: * offset mappings. This is an out param. michael@0: */ michael@0: _findClosestOffsetMappings: function (aTargetLocation, michael@0: aScript, michael@0: aScriptsAndOffsetMappings) { michael@0: // If we are given a column, we will try and break only at that location, michael@0: // otherwise we will break anytime we get on that line. michael@0: michael@0: if (aTargetLocation.column == null) { michael@0: let offsetMappings = aScript.getLineOffsets(aTargetLocation.line) michael@0: .map(o => ({ michael@0: line: aTargetLocation.line, michael@0: offset: o michael@0: })); michael@0: if (offsetMappings.length) { michael@0: aScriptsAndOffsetMappings.set(aScript, offsetMappings); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let offsetMappings = aScript.getAllColumnOffsets() michael@0: .filter(({ lineNumber }) => lineNumber === aTargetLocation.line); michael@0: michael@0: // Attempt to find the current closest offset distance from the target michael@0: // location by grabbing any offset mapping in the map by doing one iteration michael@0: // and then breaking (they all have the same distance from the target michael@0: // location). michael@0: let closestDistance = Infinity; michael@0: if (aScriptsAndOffsetMappings.size) { michael@0: for (let mappings of aScriptsAndOffsetMappings.values()) { michael@0: closestDistance = Math.abs(aTargetLocation.column - mappings[0].columnNumber); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: for (let mapping of offsetMappings) { michael@0: let currentDistance = Math.abs(aTargetLocation.column - mapping.columnNumber); michael@0: michael@0: if (currentDistance > closestDistance) { michael@0: continue; michael@0: } else if (currentDistance < closestDistance) { michael@0: closestDistance = currentDistance; michael@0: aScriptsAndOffsetMappings.clear(); michael@0: aScriptsAndOffsetMappings.set(aScript, [mapping]); michael@0: } else { michael@0: if (!aScriptsAndOffsetMappings.has(aScript)) { michael@0: aScriptsAndOffsetMappings.set(aScript, []); michael@0: } michael@0: aScriptsAndOffsetMappings.get(aScript).push(mapping); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get the script and source lists from the debugger. michael@0: */ michael@0: _discoverSources: function () { michael@0: // Only get one script per url. michael@0: const sourcesToScripts = new Map(); michael@0: for (let s of this.dbg.findScripts()) { michael@0: if (s.source) { michael@0: sourcesToScripts.set(s.source, s); michael@0: } michael@0: } michael@0: michael@0: return all([this.sources.sourcesForScript(script) michael@0: for (script of sourcesToScripts.values())]); michael@0: }, michael@0: michael@0: onSources: function (aRequest) { michael@0: return this._discoverSources().then(() => { michael@0: return { michael@0: sources: [s.form() for (s of this.sources.iter())] michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Disassociate all breakpoint actors from their scripts and clear the michael@0: * breakpoint handlers. This method can be used when the thread actor intends michael@0: * to keep the breakpoint store, but needs to clear any actual breakpoints, michael@0: * e.g. due to a page navigation. This way the breakpoint actors' script michael@0: * caches won't hold on to the Debugger.Script objects leaking memory. michael@0: */ michael@0: disableAllBreakpoints: function () { michael@0: for (let bp of this.breakpointStore.findBreakpoints()) { michael@0: if (bp.actor) { michael@0: bp.actor.removeScripts(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to pause the debuggee. michael@0: */ michael@0: onInterrupt: function (aRequest) { michael@0: if (this.state == "exited") { michael@0: return { type: "exited" }; michael@0: } else if (this.state == "paused") { michael@0: // TODO: return the actual reason for the existing pause. michael@0: return { type: "paused", why: { type: "alreadyPaused" } }; michael@0: } else if (this.state != "running") { michael@0: return { error: "wrongState", michael@0: message: "Received interrupt request in " + this.state + michael@0: " state." }; michael@0: } michael@0: michael@0: try { michael@0: // Put ourselves in the paused state. michael@0: let packet = this._paused(); michael@0: if (!packet) { michael@0: return { error: "notInterrupted" }; michael@0: } michael@0: packet.why = { type: "interrupted" }; michael@0: michael@0: // Send the response to the interrupt request now (rather than michael@0: // returning it), because we're going to start a nested event loop michael@0: // here. michael@0: this.conn.send(packet); michael@0: michael@0: // Start a nested event loop. michael@0: this._pushThreadPause(); michael@0: michael@0: // We already sent a response to this request, don't send one michael@0: // now. michael@0: return null; michael@0: } catch (e) { michael@0: reportError(e); michael@0: return { error: "notInterrupted", message: e.toString() }; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to retrieve all the event listeners on the page. michael@0: */ michael@0: onEventListeners: function (aRequest) { michael@0: // This request is only supported in content debugging. michael@0: if (!this.global) { michael@0: return { michael@0: error: "notImplemented", michael@0: message: "eventListeners request is only supported in content debugging" michael@0: }; michael@0: } michael@0: michael@0: let els = Cc["@mozilla.org/eventlistenerservice;1"] michael@0: .getService(Ci.nsIEventListenerService); michael@0: michael@0: let nodes = this.global.document.getElementsByTagName("*"); michael@0: nodes = [this.global].concat([].slice.call(nodes)); michael@0: let listeners = []; michael@0: michael@0: for (let node of nodes) { michael@0: let handlers = els.getListenerInfoFor(node); michael@0: michael@0: for (let handler of handlers) { michael@0: // Create a form object for serializing the listener via the protocol. michael@0: let listenerForm = Object.create(null); michael@0: let listener = handler.listenerObject; michael@0: // Native event listeners don't provide any listenerObject or type and michael@0: // are not that useful to a JS debugger. michael@0: if (!listener || !handler.type) { michael@0: continue; michael@0: } michael@0: michael@0: // There will be no tagName if the event listener is set on the window. michael@0: let selector = node.tagName ? findCssSelector(node) : "window"; michael@0: let nodeDO = this.globalDebugObject.makeDebuggeeValue(node); michael@0: listenerForm.node = { michael@0: selector: selector, michael@0: object: this.createValueGrip(nodeDO) michael@0: }; michael@0: listenerForm.type = handler.type; michael@0: listenerForm.capturing = handler.capturing; michael@0: listenerForm.allowsUntrusted = handler.allowsUntrusted; michael@0: listenerForm.inSystemEventGroup = handler.inSystemEventGroup; michael@0: listenerForm.isEventHandler = !!node["on" + listenerForm.type]; michael@0: // Get the Debugger.Object for the listener object. michael@0: let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener); michael@0: listenerForm.function = this.createValueGrip(listenerDO); michael@0: listeners.push(listenerForm); michael@0: } michael@0: } michael@0: return { listeners: listeners }; michael@0: }, michael@0: michael@0: /** michael@0: * Return the Debug.Frame for a frame mentioned by the protocol. michael@0: */ michael@0: _requestFrame: function (aFrameID) { michael@0: if (!aFrameID) { michael@0: return this.youngestFrame; michael@0: } michael@0: michael@0: if (this._framePool.has(aFrameID)) { michael@0: return this._framePool.get(aFrameID).frame; michael@0: } michael@0: michael@0: return undefined; michael@0: }, michael@0: michael@0: _paused: function (aFrame) { michael@0: // We don't handle nested pauses correctly. Don't try - if we're michael@0: // paused, just continue running whatever code triggered the pause. michael@0: // We don't want to actually have nested pauses (although we michael@0: // have nested event loops). If code runs in the debuggee during michael@0: // a pause, it should cause the actor to resume (dropping michael@0: // pause-lifetime actors etc) and then repause when complete. michael@0: michael@0: if (this.state === "paused") { michael@0: return undefined; michael@0: } michael@0: michael@0: // Clear stepping hooks. michael@0: this.dbg.onEnterFrame = undefined; michael@0: this.dbg.onExceptionUnwind = undefined; michael@0: if (aFrame) { michael@0: aFrame.onStep = undefined; michael@0: aFrame.onPop = undefined; michael@0: } michael@0: // Clear DOM event breakpoints. michael@0: // XPCShell tests don't use actual DOM windows for globals and cause michael@0: // removeListenerForAllEvents to throw. michael@0: if (this.global && !this.global.toString().contains("Sandbox")) { michael@0: let els = Cc["@mozilla.org/eventlistenerservice;1"] michael@0: .getService(Ci.nsIEventListenerService); michael@0: els.removeListenerForAllEvents(this.global, this._allEventsListener, true); michael@0: for (let [,bp] of this._hiddenBreakpoints) { michael@0: bp.onDelete(); michael@0: } michael@0: this._hiddenBreakpoints.clear(); michael@0: } michael@0: michael@0: this._state = "paused"; michael@0: michael@0: // Create the actor pool that will hold the pause actor and its michael@0: // children. michael@0: dbg_assert(!this._pausePool, "No pause pool should exist yet"); michael@0: this._pausePool = new ActorPool(this.conn); michael@0: this.conn.addActorPool(this._pausePool); michael@0: michael@0: // Give children of the pause pool a quick link back to the michael@0: // thread... michael@0: this._pausePool.threadActor = this; michael@0: michael@0: // Create the pause actor itself... michael@0: dbg_assert(!this._pauseActor, "No pause actor should exist yet"); michael@0: this._pauseActor = new PauseActor(this._pausePool); michael@0: this._pausePool.addActor(this._pauseActor); michael@0: michael@0: // Update the list of frames. michael@0: let poppedFrames = this._updateFrames(); michael@0: michael@0: // Send off the paused packet and spin an event loop. michael@0: let packet = { from: this.actorID, michael@0: type: "paused", michael@0: actor: this._pauseActor.actorID }; michael@0: if (aFrame) { michael@0: packet.frame = this._createFrameActor(aFrame).form(); michael@0: } michael@0: michael@0: if (poppedFrames) { michael@0: packet.poppedFrames = poppedFrames; michael@0: } michael@0: michael@0: return packet; michael@0: }, michael@0: michael@0: _resumed: function () { michael@0: this._state = "running"; michael@0: michael@0: // Drop the actors in the pause actor pool. michael@0: this.conn.removeActorPool(this._pausePool); michael@0: michael@0: this._pausePool = null; michael@0: this._pauseActor = null; michael@0: michael@0: return { from: this.actorID, type: "resumed" }; michael@0: }, michael@0: michael@0: /** michael@0: * Expire frame actors for frames that have been popped. michael@0: * michael@0: * @returns A list of actor IDs whose frames have been popped. michael@0: */ michael@0: _updateFrames: function () { michael@0: let popped = []; michael@0: michael@0: // Create the actor pool that will hold the still-living frames. michael@0: let framePool = new ActorPool(this.conn); michael@0: let frameList = []; michael@0: michael@0: for each (let frameActor in this._frameActors) { michael@0: if (frameActor.frame.live) { michael@0: framePool.addActor(frameActor); michael@0: frameList.push(frameActor); michael@0: } else { michael@0: popped.push(frameActor.actorID); michael@0: } michael@0: } michael@0: michael@0: // Remove the old frame actor pool, this will expire michael@0: // any actors that weren't added to the new pool. michael@0: if (this._framePool) { michael@0: this.conn.removeActorPool(this._framePool); michael@0: } michael@0: michael@0: this._frameActors = frameList; michael@0: this._framePool = framePool; michael@0: this.conn.addActorPool(framePool); michael@0: michael@0: return popped; michael@0: }, michael@0: michael@0: _createFrameActor: function (aFrame) { michael@0: if (aFrame.actor) { michael@0: return aFrame.actor; michael@0: } michael@0: michael@0: let actor = new FrameActor(aFrame, this); michael@0: this._frameActors.push(actor); michael@0: this._framePool.addActor(actor); michael@0: aFrame.actor = actor; michael@0: michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * Create and return an environment actor that corresponds to the provided michael@0: * Debugger.Environment. michael@0: * @param Debugger.Environment aEnvironment michael@0: * The lexical environment we want to extract. michael@0: * @param object aPool michael@0: * The pool where the newly-created actor will be placed. michael@0: * @return The EnvironmentActor for aEnvironment or undefined for host michael@0: * functions or functions scoped to a non-debuggee global. michael@0: */ michael@0: createEnvironmentActor: function (aEnvironment, aPool) { michael@0: if (!aEnvironment) { michael@0: return undefined; michael@0: } michael@0: michael@0: if (aEnvironment.actor) { michael@0: return aEnvironment.actor; michael@0: } michael@0: michael@0: let actor = new EnvironmentActor(aEnvironment, this); michael@0: aPool.addActor(actor); michael@0: aEnvironment.actor = actor; michael@0: michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * Create a grip for the given debuggee value. If the value is an michael@0: * object, will create an actor with the given lifetime. michael@0: */ michael@0: createValueGrip: function (aValue, aPool=false) { michael@0: if (!aPool) { michael@0: aPool = this._pausePool; michael@0: } michael@0: michael@0: switch (typeof aValue) { michael@0: case "boolean": michael@0: return aValue; michael@0: case "string": michael@0: if (this._stringIsLong(aValue)) { michael@0: return this.longStringGrip(aValue, aPool); michael@0: } michael@0: return aValue; michael@0: case "number": michael@0: if (aValue === Infinity) { michael@0: return { type: "Infinity" }; michael@0: } else if (aValue === -Infinity) { michael@0: return { type: "-Infinity" }; michael@0: } else if (Number.isNaN(aValue)) { michael@0: return { type: "NaN" }; michael@0: } else if (!aValue && 1 / aValue === -Infinity) { michael@0: return { type: "-0" }; michael@0: } michael@0: return aValue; michael@0: case "undefined": michael@0: return { type: "undefined" }; michael@0: case "object": michael@0: if (aValue === null) { michael@0: return { type: "null" }; michael@0: } michael@0: return this.objectGrip(aValue, aPool); michael@0: default: michael@0: dbg_assert(false, "Failed to provide a grip for: " + aValue); michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Return a protocol completion value representing the given michael@0: * Debugger-provided completion value. michael@0: */ michael@0: createProtocolCompletionValue: function (aCompletion) { michael@0: let protoValue = {}; michael@0: if ("return" in aCompletion) { michael@0: protoValue.return = this.createValueGrip(aCompletion.return); michael@0: } else if ("yield" in aCompletion) { michael@0: protoValue.return = this.createValueGrip(aCompletion.yield); michael@0: } else if ("throw" in aCompletion) { michael@0: protoValue.throw = this.createValueGrip(aCompletion.throw); michael@0: } else { michael@0: protoValue.terminated = true; michael@0: } michael@0: return protoValue; michael@0: }, michael@0: michael@0: /** michael@0: * Create a grip for the given debuggee object. michael@0: * michael@0: * @param aValue Debugger.Object michael@0: * The debuggee object value. michael@0: * @param aPool ActorPool michael@0: * The actor pool where the new object actor will be added. michael@0: */ michael@0: objectGrip: function (aValue, aPool) { michael@0: if (!aPool.objectActors) { michael@0: aPool.objectActors = new WeakMap(); michael@0: } michael@0: michael@0: if (aPool.objectActors.has(aValue)) { michael@0: return aPool.objectActors.get(aValue).grip(); michael@0: } else if (this.threadLifetimePool.objectActors.has(aValue)) { michael@0: return this.threadLifetimePool.objectActors.get(aValue).grip(); michael@0: } michael@0: michael@0: let actor = new PauseScopedObjectActor(aValue, this); michael@0: aPool.addActor(actor); michael@0: aPool.objectActors.set(aValue, actor); michael@0: return actor.grip(); michael@0: }, michael@0: michael@0: /** michael@0: * Create a grip for the given debuggee object with a pause lifetime. michael@0: * michael@0: * @param aValue Debugger.Object michael@0: * The debuggee object value. michael@0: */ michael@0: pauseObjectGrip: function (aValue) { michael@0: if (!this._pausePool) { michael@0: throw "Object grip requested while not paused."; michael@0: } michael@0: michael@0: return this.objectGrip(aValue, this._pausePool); michael@0: }, michael@0: michael@0: /** michael@0: * Extend the lifetime of the provided object actor to thread lifetime. michael@0: * michael@0: * @param aActor object michael@0: * The object actor. michael@0: */ michael@0: threadObjectGrip: function (aActor) { michael@0: // We want to reuse the existing actor ID, so we just remove it from the michael@0: // current pool's weak map and then let pool.addActor do the rest. michael@0: aActor.registeredPool.objectActors.delete(aActor.obj); michael@0: this.threadLifetimePool.addActor(aActor); michael@0: this.threadLifetimePool.objectActors.set(aActor.obj, aActor); michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to promote multiple pause-lifetime grips to michael@0: * thread-lifetime grips. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onThreadGrips: function (aRequest) { michael@0: if (this.state != "paused") { michael@0: return { error: "wrongState" }; michael@0: } michael@0: michael@0: if (!aRequest.actors) { michael@0: return { error: "missingParameter", michael@0: message: "no actors were specified" }; michael@0: } michael@0: michael@0: for (let actorID of aRequest.actors) { michael@0: let actor = this._pausePool.get(actorID); michael@0: if (actor) { michael@0: this.threadObjectGrip(actor); michael@0: } michael@0: } michael@0: return {}; michael@0: }, michael@0: michael@0: /** michael@0: * Create a grip for the given string. michael@0: * michael@0: * @param aString String michael@0: * The string we are creating a grip for. michael@0: * @param aPool ActorPool michael@0: * The actor pool where the new actor will be added. michael@0: */ michael@0: longStringGrip: function (aString, aPool) { michael@0: if (!aPool.longStringActors) { michael@0: aPool.longStringActors = {}; michael@0: } michael@0: michael@0: if (aPool.longStringActors.hasOwnProperty(aString)) { michael@0: return aPool.longStringActors[aString].grip(); michael@0: } michael@0: michael@0: let actor = new LongStringActor(aString, this); michael@0: aPool.addActor(actor); michael@0: aPool.longStringActors[aString] = actor; michael@0: return actor.grip(); michael@0: }, michael@0: michael@0: /** michael@0: * Create a long string grip that is scoped to a pause. michael@0: * michael@0: * @param aString String michael@0: * The string we are creating a grip for. michael@0: */ michael@0: pauseLongStringGrip: function (aString) { michael@0: return this.longStringGrip(aString, this._pausePool); michael@0: }, michael@0: michael@0: /** michael@0: * Create a long string grip that is scoped to a thread. michael@0: * michael@0: * @param aString String michael@0: * The string we are creating a grip for. michael@0: */ michael@0: threadLongStringGrip: function (aString) { michael@0: return this.longStringGrip(aString, this._threadLifetimePool); michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if the string is long enough to use a LongStringActor instead michael@0: * of passing the value directly over the protocol. michael@0: * michael@0: * @param aString String michael@0: * The string we are checking the length of. michael@0: */ michael@0: _stringIsLong: function (aString) { michael@0: return aString.length >= DebuggerServer.LONG_STRING_LENGTH; michael@0: }, michael@0: michael@0: // JS Debugger API hooks. michael@0: michael@0: /** michael@0: * A function that the engine calls when a call to a debug event hook, michael@0: * breakpoint handler, watchpoint handler, or similar function throws some michael@0: * exception. michael@0: * michael@0: * @param aException exception michael@0: * The exception that was thrown in the debugger code. michael@0: */ michael@0: uncaughtExceptionHook: function (aException) { michael@0: dumpn("Got an exception: " + aException.message + "\n" + aException.stack); michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when a debugger statement has been michael@0: * executed in the specified frame. michael@0: * michael@0: * @param aFrame Debugger.Frame michael@0: * The stack frame that contained the debugger statement. michael@0: */ michael@0: onDebuggerStatement: function (aFrame) { michael@0: // Don't pause if we are currently stepping (in or over) or the frame is michael@0: // black-boxed. michael@0: const generatedLocation = getFrameLocation(aFrame); michael@0: const { url } = this.synchronize(this.sources.getOriginalLocation( michael@0: generatedLocation)); michael@0: michael@0: return this.sources.isBlackBoxed(url) || aFrame.onStep michael@0: ? undefined michael@0: : this._pauseAndRespond(aFrame, { type: "debuggerStatement" }); michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when an exception has been thrown and has michael@0: * propagated to the specified frame. michael@0: * michael@0: * @param aFrame Debugger.Frame michael@0: * The youngest remaining stack frame. michael@0: * @param aValue object michael@0: * The exception that was thrown. michael@0: */ michael@0: onExceptionUnwind: function (aFrame, aValue) { michael@0: let willBeCaught = false; michael@0: for (let frame = aFrame; frame != null; frame = frame.older) { michael@0: if (frame.script.isInCatchScope(frame.offset)) { michael@0: willBeCaught = true; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (willBeCaught && this._options.ignoreCaughtExceptions) { michael@0: return undefined; michael@0: } michael@0: michael@0: const generatedLocation = getFrameLocation(aFrame); michael@0: const { url } = this.synchronize(this.sources.getOriginalLocation( michael@0: generatedLocation)); michael@0: michael@0: if (this.sources.isBlackBoxed(url)) { michael@0: return undefined; michael@0: } michael@0: michael@0: try { michael@0: let packet = this._paused(aFrame); michael@0: if (!packet) { michael@0: return undefined; michael@0: } michael@0: michael@0: packet.why = { type: "exception", michael@0: exception: this.createValueGrip(aValue) }; michael@0: this.conn.send(packet); michael@0: michael@0: this._pushThreadPause(); michael@0: } catch(e) { michael@0: reportError(e, "Got an exception during TA_onExceptionUnwind: "); michael@0: } michael@0: michael@0: return undefined; michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when a new script has been loaded into the michael@0: * scope of the specified debuggee global. michael@0: * michael@0: * @param aScript Debugger.Script michael@0: * The source script that has been loaded into a debuggee compartment. michael@0: * @param aGlobal Debugger.Object michael@0: * A Debugger.Object instance whose referent is the global object. michael@0: */ michael@0: onNewScript: function (aScript, aGlobal) { michael@0: this._addScript(aScript); michael@0: michael@0: // |onNewScript| is only fired for top level scripts (AKA staticLevel == 0), michael@0: // so we have to make sure to call |_addScript| on every child script as michael@0: // well to restore breakpoints in those scripts. michael@0: for (let s of aScript.getChildScripts()) { michael@0: this._addScript(s); michael@0: } michael@0: michael@0: this.sources.sourcesForScript(aScript); michael@0: }, michael@0: michael@0: onNewSource: function (aSource) { michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: type: "newSource", michael@0: source: aSource.form() michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Check if scripts from the provided source URL are allowed to be stored in michael@0: * the cache. michael@0: * michael@0: * @param aSourceUrl String michael@0: * The url of the script's source that will be stored. michael@0: * @returns true, if the script can be added, false otherwise. michael@0: */ michael@0: _allowSource: function (aSourceUrl) { michael@0: // Ignore anything we don't have a URL for (eval scripts, for example). michael@0: if (!aSourceUrl) michael@0: return false; michael@0: // Ignore XBL bindings for content debugging. michael@0: if (aSourceUrl.indexOf("chrome://") == 0) { michael@0: return false; michael@0: } michael@0: // Ignore about:* pages for content debugging. michael@0: if (aSourceUrl.indexOf("about:") == 0) { michael@0: return false; michael@0: } michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Restore any pre-existing breakpoints to the scripts that we have access to. michael@0: */ michael@0: _restoreBreakpoints: function () { michael@0: if (this.breakpointStore.size === 0) { michael@0: return; michael@0: } michael@0: michael@0: for (let s of this.dbg.findScripts()) { michael@0: this._addScript(s); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add the provided script to the server cache. michael@0: * michael@0: * @param aScript Debugger.Script michael@0: * The source script that will be stored. michael@0: * @returns true, if the script was added; false otherwise. michael@0: */ michael@0: _addScript: function (aScript) { michael@0: if (!this._allowSource(aScript.url)) { michael@0: return false; michael@0: } michael@0: michael@0: // Set any stored breakpoints. michael@0: michael@0: let endLine = aScript.startLine + aScript.lineCount - 1; michael@0: for (let bp of this.breakpointStore.findBreakpoints({ url: aScript.url })) { michael@0: // Only consider breakpoints that are not already associated with michael@0: // scripts, and limit search to the line numbers contained in the new michael@0: // script. michael@0: if (!bp.actor.scripts.length michael@0: && bp.line >= aScript.startLine michael@0: && bp.line <= endLine) { michael@0: this._setBreakpoint(bp); michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Get prototypes and properties of multiple objects. michael@0: */ michael@0: onPrototypesAndProperties: function (aRequest) { michael@0: let result = {}; michael@0: for (let actorID of aRequest.actors) { michael@0: // This code assumes that there are no lazily loaded actors returned michael@0: // by this call. michael@0: let actor = this.conn.getActor(actorID); michael@0: if (!actor) { michael@0: return { from: this.actorID, michael@0: error: "noSuchActor" }; michael@0: } michael@0: let handler = actor.onPrototypeAndProperties; michael@0: if (!handler) { michael@0: return { from: this.actorID, michael@0: error: "unrecognizedPacketType", michael@0: message: ('Actor "' + actorID + michael@0: '" does not recognize the packet type ' + michael@0: '"prototypeAndProperties"') }; michael@0: } michael@0: result[actorID] = handler.call(actor, {}); michael@0: } michael@0: return { from: this.actorID, michael@0: actors: result }; michael@0: } michael@0: michael@0: }; michael@0: michael@0: ThreadActor.prototype.requestTypes = { michael@0: "attach": ThreadActor.prototype.onAttach, michael@0: "detach": ThreadActor.prototype.onDetach, michael@0: "reconfigure": ThreadActor.prototype.onReconfigure, michael@0: "resume": ThreadActor.prototype.onResume, michael@0: "clientEvaluate": ThreadActor.prototype.onClientEvaluate, michael@0: "frames": ThreadActor.prototype.onFrames, michael@0: "interrupt": ThreadActor.prototype.onInterrupt, michael@0: "eventListeners": ThreadActor.prototype.onEventListeners, michael@0: "releaseMany": ThreadActor.prototype.onReleaseMany, michael@0: "setBreakpoint": ThreadActor.prototype.onSetBreakpoint, michael@0: "sources": ThreadActor.prototype.onSources, michael@0: "threadGrips": ThreadActor.prototype.onThreadGrips, michael@0: "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Creates a PauseActor. michael@0: * michael@0: * PauseActors exist for the lifetime of a given debuggee pause. Used to michael@0: * scope pause-lifetime grips. michael@0: * michael@0: * @param ActorPool aPool michael@0: * The actor pool created for this pause. michael@0: */ michael@0: function PauseActor(aPool) michael@0: { michael@0: this.pool = aPool; michael@0: } michael@0: michael@0: PauseActor.prototype = { michael@0: actorPrefix: "pause" michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * A base actor for any actors that should only respond receive messages in the michael@0: * paused state. Subclasses may expose a `threadActor` which is used to help michael@0: * determine when we are in a paused state. Subclasses should set their own michael@0: * "constructor" property if they want better error messages. You should never michael@0: * instantiate a PauseScopedActor directly, only through subclasses. michael@0: */ michael@0: function PauseScopedActor() michael@0: { michael@0: } michael@0: michael@0: /** michael@0: * A function decorator for creating methods to handle protocol messages that michael@0: * should only be received while in the paused state. michael@0: * michael@0: * @param aMethod Function michael@0: * The function we are decorating. michael@0: */ michael@0: PauseScopedActor.withPaused = function (aMethod) { michael@0: return function () { michael@0: if (this.isPaused()) { michael@0: return aMethod.apply(this, arguments); michael@0: } else { michael@0: return this._wrongState(); michael@0: } michael@0: }; michael@0: }; michael@0: michael@0: PauseScopedActor.prototype = { michael@0: michael@0: /** michael@0: * Returns true if we are in the paused state. michael@0: */ michael@0: isPaused: function () { michael@0: // When there is not a ThreadActor available (like in the webconsole) we michael@0: // have to be optimistic and assume that we are paused so that we can michael@0: // respond to requests. michael@0: return this.threadActor ? this.threadActor.state === "paused" : true; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the wrongState response packet for this actor. michael@0: */ michael@0: _wrongState: function () { michael@0: return { michael@0: error: "wrongState", michael@0: message: this.constructor.name + michael@0: " actors can only be accessed while the thread is paused." michael@0: }; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Resolve a URI back to physical file. michael@0: * michael@0: * Of course, this works only for URIs pointing to local resources. michael@0: * michael@0: * @param aURI michael@0: * URI to resolve michael@0: * @return michael@0: * resolved nsIURI michael@0: */ michael@0: function resolveURIToLocalPath(aURI) { michael@0: switch (aURI.scheme) { michael@0: case "jar": michael@0: case "file": michael@0: return aURI; michael@0: michael@0: case "chrome": michael@0: let resolved = Cc["@mozilla.org/chrome/chrome-registry;1"]. michael@0: getService(Ci.nsIChromeRegistry).convertChromeURL(aURI); michael@0: return resolveURIToLocalPath(resolved); michael@0: michael@0: case "resource": michael@0: resolved = Cc["@mozilla.org/network/protocol;1?name=resource"]. michael@0: getService(Ci.nsIResProtocolHandler).resolveURI(aURI); michael@0: aURI = Services.io.newURI(resolved, null, null); michael@0: return resolveURIToLocalPath(aURI); michael@0: michael@0: default: michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A SourceActor provides information about the source of a script. michael@0: * michael@0: * @param String url michael@0: * The url of the source we are representing. michael@0: * @param ThreadActor thread michael@0: * The current thread actor. michael@0: * @param SourceMapConsumer sourceMap michael@0: * Optional. The source map that introduced this source, if available. michael@0: * @param String generatedSource michael@0: * Optional, passed in when aSourceMap is also passed in. The generated michael@0: * source url that introduced this source. michael@0: * @param String text michael@0: * Optional. The content text of this source, if immediately available. michael@0: * @param String contentType michael@0: * Optional. The content type of this source, if immediately available. michael@0: */ michael@0: function SourceActor({ url, thread, sourceMap, generatedSource, text, michael@0: contentType }) { michael@0: this._threadActor = thread; michael@0: this._url = url; michael@0: this._sourceMap = sourceMap; michael@0: this._generatedSource = generatedSource; michael@0: this._text = text; michael@0: this._contentType = contentType; michael@0: michael@0: this.onSource = this.onSource.bind(this); michael@0: this._invertSourceMap = this._invertSourceMap.bind(this); michael@0: this._saveMap = this._saveMap.bind(this); michael@0: this._getSourceText = this._getSourceText.bind(this); michael@0: michael@0: this._mapSourceToAddon(); michael@0: michael@0: if (this.threadActor.sources.isPrettyPrinted(this.url)) { michael@0: this._init = this.onPrettyPrint({ michael@0: indent: this.threadActor.sources.prettyPrintIndent(this.url) michael@0: }).then(null, error => { michael@0: DevToolsUtils.reportException("SourceActor", error); michael@0: }); michael@0: } else { michael@0: this._init = null; michael@0: } michael@0: } michael@0: michael@0: SourceActor.prototype = { michael@0: constructor: SourceActor, michael@0: actorPrefix: "source", michael@0: michael@0: _oldSourceMap: null, michael@0: _init: null, michael@0: _addonID: null, michael@0: _addonPath: null, michael@0: michael@0: get threadActor() this._threadActor, michael@0: get url() this._url, michael@0: get addonID() this._addonID, michael@0: get addonPath() this._addonPath, michael@0: michael@0: get prettyPrintWorker() { michael@0: return this.threadActor.prettyPrintWorker; michael@0: }, michael@0: michael@0: form: function () { michael@0: return { michael@0: actor: this.actorID, michael@0: url: this._url, michael@0: addonID: this._addonID, michael@0: addonPath: this._addonPath, michael@0: isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url), michael@0: isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url) michael@0: // TODO bug 637572: introductionScript michael@0: }; michael@0: }, michael@0: michael@0: disconnect: function () { michael@0: if (this.registeredPool && this.registeredPool.sourceActors) { michael@0: delete this.registeredPool.sourceActors[this.actorID]; michael@0: } michael@0: }, michael@0: michael@0: _mapSourceToAddon: function() { michael@0: try { michael@0: var nsuri = Services.io.newURI(this._url.split(" -> ").pop(), null, null); michael@0: } michael@0: catch (e) { michael@0: // We can't do anything with an invalid URI michael@0: return; michael@0: } michael@0: michael@0: let localURI = resolveURIToLocalPath(nsuri); michael@0: michael@0: let id = {}; michael@0: if (localURI && mapURIToAddonID(localURI, id)) { michael@0: this._addonID = id.value; michael@0: michael@0: if (localURI instanceof Ci.nsIJARURI) { michael@0: // The path in the add-on is easy for jar: uris michael@0: this._addonPath = localURI.JAREntry; michael@0: } michael@0: else if (localURI instanceof Ci.nsIFileURL) { michael@0: // For file: uris walk up to find the last directory that is part of the michael@0: // add-on michael@0: let target = localURI.file; michael@0: let path = target.leafName; michael@0: michael@0: // We can assume that the directory containing the source file is part michael@0: // of the add-on michael@0: let root = target.parent; michael@0: let file = root.parent; michael@0: while (file && mapURIToAddonID(Services.io.newFileURI(file), {})) { michael@0: path = root.leafName + "/" + path; michael@0: root = file; michael@0: file = file.parent; michael@0: } michael@0: michael@0: if (!file) { michael@0: const error = new Error("Could not find the root of the add-on for " + this._url); michael@0: DevToolsUtils.reportException("SourceActor.prototype._mapSourceToAddon", error) michael@0: return; michael@0: } michael@0: michael@0: this._addonPath = path; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _getSourceText: function () { michael@0: const toResolvedContent = t => resolve({ michael@0: content: t, michael@0: contentType: this._contentType michael@0: }); michael@0: michael@0: let sc; michael@0: if (this._sourceMap && (sc = this._sourceMap.sourceContentFor(this._url))) { michael@0: return toResolvedContent(sc); michael@0: } michael@0: michael@0: if (this._text) { michael@0: return toResolvedContent(this._text); michael@0: } michael@0: michael@0: // XXX bug 865252: Don't load from the cache if this is a source mapped michael@0: // source because we can't guarantee that the cache has the most up to date michael@0: // content for this source like we can if it isn't source mapped. michael@0: let sourceFetched = fetch(this._url, { loadFromCache: !this._sourceMap }); michael@0: michael@0: // Record the contentType we just learned during fetching michael@0: sourceFetched.then(({ contentType }) => { michael@0: this._contentType = contentType; michael@0: }); michael@0: michael@0: return sourceFetched; michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the "source" packet. michael@0: */ michael@0: onSource: function () { michael@0: return resolve(this._init) michael@0: .then(this._getSourceText) michael@0: .then(({ content, contentType }) => { michael@0: return { michael@0: from: this.actorID, michael@0: source: this.threadActor.createValueGrip( michael@0: content, this.threadActor.threadLifetimePool), michael@0: contentType: contentType michael@0: }; michael@0: }) michael@0: .then(null, aError => { michael@0: reportError(aError, "Got an exception during SA_onSource: "); michael@0: return { michael@0: "from": this.actorID, michael@0: "error": "loadSourceError", michael@0: "message": "Could not load the source for " + this._url + ".\n" michael@0: + DevToolsUtils.safeErrorString(aError) michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the "prettyPrint" packet. michael@0: */ michael@0: onPrettyPrint: function ({ indent }) { michael@0: this.threadActor.sources.prettyPrint(this._url, indent); michael@0: return this._getSourceText() michael@0: .then(this._sendToPrettyPrintWorker(indent)) michael@0: .then(this._invertSourceMap) michael@0: .then(this._saveMap) michael@0: .then(() => { michael@0: // We need to reset `_init` now because we have already done the work of michael@0: // pretty printing, and don't want onSource to wait forever for michael@0: // initialization to complete. michael@0: this._init = null; michael@0: }) michael@0: .then(this.onSource) michael@0: .then(null, error => { michael@0: this.onDisablePrettyPrint(); michael@0: return { michael@0: from: this.actorID, michael@0: error: "prettyPrintError", michael@0: message: DevToolsUtils.safeErrorString(error) michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Return a function that sends a request to the pretty print worker, waits on michael@0: * the worker's response, and then returns the pretty printed code. michael@0: * michael@0: * @param Number aIndent michael@0: * The number of spaces to indent by the code by, when we send the michael@0: * request to the pretty print worker. michael@0: * @returns Function michael@0: * Returns a function which takes an AST, and returns a promise that michael@0: * is resolved with `{ code, mappings }` where `code` is the pretty michael@0: * printed code, and `mappings` is an array of source mappings. michael@0: */ michael@0: _sendToPrettyPrintWorker: function (aIndent) { michael@0: return ({ content }) => { michael@0: const deferred = promise.defer(); michael@0: const id = Math.random(); michael@0: michael@0: const onReply = ({ data }) => { michael@0: if (data.id !== id) { michael@0: return; michael@0: } michael@0: this.prettyPrintWorker.removeEventListener("message", onReply, false); michael@0: michael@0: if (data.error) { michael@0: deferred.reject(new Error(data.error)); michael@0: } else { michael@0: deferred.resolve(data); michael@0: } michael@0: }; michael@0: michael@0: this.prettyPrintWorker.addEventListener("message", onReply, false); michael@0: this.prettyPrintWorker.postMessage({ michael@0: id: id, michael@0: url: this._url, michael@0: indent: aIndent, michael@0: source: content michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Invert a source map. So if a source map maps from a to b, return a new michael@0: * source map from b to a. We need to do this because the source map we get michael@0: * from _generatePrettyCodeAndMap goes the opposite way we want it to for michael@0: * debugging. michael@0: * michael@0: * Note that the source map is modified in place. michael@0: */ michael@0: _invertSourceMap: function ({ code, mappings }) { michael@0: const generator = new SourceMapGenerator({ file: this._url }); michael@0: return DevToolsUtils.yieldingEach(mappings, m => { michael@0: let mapping = { michael@0: generated: { michael@0: line: m.generatedLine, michael@0: column: m.generatedColumn michael@0: } michael@0: }; michael@0: if (m.source) { michael@0: mapping.source = m.source; michael@0: mapping.original = { michael@0: line: m.originalLine, michael@0: column: m.originalColumn michael@0: }; michael@0: mapping.name = m.name; michael@0: } michael@0: generator.addMapping(mapping); michael@0: }).then(() => { michael@0: generator.setSourceContent(this._url, code); michael@0: const consumer = SourceMapConsumer.fromSourceMap(generator); michael@0: michael@0: // XXX bug 918802: Monkey punch the source map consumer, because iterating michael@0: // over all mappings and inverting each of them, and then creating a new michael@0: // SourceMapConsumer is slow. michael@0: michael@0: const getOrigPos = consumer.originalPositionFor.bind(consumer); michael@0: const getGenPos = consumer.generatedPositionFor.bind(consumer); michael@0: michael@0: consumer.originalPositionFor = ({ line, column }) => { michael@0: const location = getGenPos({ michael@0: line: line, michael@0: column: column, michael@0: source: this._url michael@0: }); michael@0: location.source = this._url; michael@0: return location; michael@0: }; michael@0: michael@0: consumer.generatedPositionFor = ({ line, column }) => getOrigPos({ michael@0: line: line, michael@0: column: column michael@0: }); michael@0: michael@0: return { michael@0: code: code, michael@0: map: consumer michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Save the source map back to our thread's ThreadSources object so that michael@0: * stepping, breakpoints, debugger statements, etc can use it. If we are michael@0: * pretty printing a source mapped source, we need to compose the existing michael@0: * source map with our new one. michael@0: */ michael@0: _saveMap: function ({ map }) { michael@0: if (this._sourceMap) { michael@0: // Compose the source maps michael@0: this._oldSourceMap = this._sourceMap; michael@0: this._sourceMap = SourceMapGenerator.fromSourceMap(this._sourceMap); michael@0: this._sourceMap.applySourceMap(map, this._url); michael@0: this._sourceMap = SourceMapConsumer.fromSourceMap(this._sourceMap); michael@0: this._threadActor.sources.saveSourceMap(this._sourceMap, michael@0: this._generatedSource); michael@0: } else { michael@0: this._sourceMap = map; michael@0: this._threadActor.sources.saveSourceMap(this._sourceMap, this._url); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the "disablePrettyPrint" packet. michael@0: */ michael@0: onDisablePrettyPrint: function () { michael@0: this._sourceMap = this._oldSourceMap; michael@0: this.threadActor.sources.saveSourceMap(this._sourceMap, michael@0: this._generatedSource || this._url); michael@0: this.threadActor.sources.disablePrettyPrint(this._url); michael@0: return this.onSource(); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the "blackbox" packet. michael@0: */ michael@0: onBlackBox: function (aRequest) { michael@0: this.threadActor.sources.blackBox(this.url); michael@0: let packet = { michael@0: from: this.actorID michael@0: }; michael@0: if (this.threadActor.state == "paused" michael@0: && this.threadActor.youngestFrame michael@0: && this.threadActor.youngestFrame.script.url == this.url) { michael@0: packet.pausedInSource = true; michael@0: } michael@0: return packet; michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the "unblackbox" packet. michael@0: */ michael@0: onUnblackBox: function (aRequest) { michael@0: this.threadActor.sources.unblackBox(this.url); michael@0: return { michael@0: from: this.actorID michael@0: }; michael@0: } michael@0: }; michael@0: michael@0: SourceActor.prototype.requestTypes = { michael@0: "source": SourceActor.prototype.onSource, michael@0: "blackbox": SourceActor.prototype.onBlackBox, michael@0: "unblackbox": SourceActor.prototype.onUnblackBox, michael@0: "prettyPrint": SourceActor.prototype.onPrettyPrint, michael@0: "disablePrettyPrint": SourceActor.prototype.onDisablePrettyPrint michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Determine if a given value is non-primitive. michael@0: * michael@0: * @param Any aValue michael@0: * The value to test. michael@0: * @return Boolean michael@0: * Whether the value is non-primitive. michael@0: */ michael@0: function isObject(aValue) { michael@0: const type = typeof aValue; michael@0: return type == "object" ? aValue !== null : type == "function"; michael@0: } michael@0: michael@0: /** michael@0: * Create a function that can safely stringify Debugger.Objects of a given michael@0: * builtin type. michael@0: * michael@0: * @param Function aCtor michael@0: * The builtin class constructor. michael@0: * @return Function michael@0: * The stringifier for the class. michael@0: */ michael@0: function createBuiltinStringifier(aCtor) { michael@0: return aObj => aCtor.prototype.toString.call(aObj.unsafeDereference()); michael@0: } michael@0: michael@0: /** michael@0: * Stringify a Debugger.Object-wrapped Error instance. michael@0: * michael@0: * @param Debugger.Object aObj michael@0: * The object to stringify. michael@0: * @return String michael@0: * The stringification of the object. michael@0: */ michael@0: function errorStringify(aObj) { michael@0: let name = DevToolsUtils.getProperty(aObj, "name"); michael@0: if (name === "" || name === undefined) { michael@0: name = aObj.class; michael@0: } else if (isObject(name)) { michael@0: name = stringify(name); michael@0: } michael@0: michael@0: let message = DevToolsUtils.getProperty(aObj, "message"); michael@0: if (isObject(message)) { michael@0: message = stringify(message); michael@0: } michael@0: michael@0: if (message === "" || message === undefined) { michael@0: return name; michael@0: } michael@0: return name + ": " + message; michael@0: } michael@0: michael@0: /** michael@0: * Stringify a Debugger.Object based on its class. michael@0: * michael@0: * @param Debugger.Object aObj michael@0: * The object to stringify. michael@0: * @return String michael@0: * The stringification for the object. michael@0: */ michael@0: function stringify(aObj) { michael@0: if (aObj.class == "DeadObject") { michael@0: const error = new Error("Dead object encountered."); michael@0: DevToolsUtils.reportException("stringify", error); michael@0: return ""; michael@0: } michael@0: const stringifier = stringifiers[aObj.class] || stringifiers.Object; michael@0: return stringifier(aObj); michael@0: } michael@0: michael@0: // Used to prevent infinite recursion when an array is found inside itself. michael@0: let seen = null; michael@0: michael@0: let stringifiers = { michael@0: Error: errorStringify, michael@0: EvalError: errorStringify, michael@0: RangeError: errorStringify, michael@0: ReferenceError: errorStringify, michael@0: SyntaxError: errorStringify, michael@0: TypeError: errorStringify, michael@0: URIError: errorStringify, michael@0: Boolean: createBuiltinStringifier(Boolean), michael@0: Function: createBuiltinStringifier(Function), michael@0: Number: createBuiltinStringifier(Number), michael@0: RegExp: createBuiltinStringifier(RegExp), michael@0: String: createBuiltinStringifier(String), michael@0: Object: obj => "[object " + obj.class + "]", michael@0: Array: obj => { michael@0: // If we're at the top level then we need to create the Set for tracking michael@0: // previously stringified arrays. michael@0: const topLevel = !seen; michael@0: if (topLevel) { michael@0: seen = new Set(); michael@0: } else if (seen.has(obj)) { michael@0: return ""; michael@0: } michael@0: michael@0: seen.add(obj); michael@0: michael@0: const len = DevToolsUtils.getProperty(obj, "length"); michael@0: let string = ""; michael@0: michael@0: // The following check is only required because the debuggee could possibly michael@0: // be a Proxy and return any value. For normal objects, array.length is michael@0: // always a non-negative integer. michael@0: if (typeof len == "number" && len > 0) { michael@0: for (let i = 0; i < len; i++) { michael@0: const desc = obj.getOwnPropertyDescriptor(i); michael@0: if (desc) { michael@0: const { value } = desc; michael@0: if (value != null) { michael@0: string += isObject(value) ? stringify(value) : value; michael@0: } michael@0: } michael@0: michael@0: if (i < len - 1) { michael@0: string += ","; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (topLevel) { michael@0: seen = null; michael@0: } michael@0: michael@0: return string; michael@0: }, michael@0: DOMException: obj => { michael@0: const message = DevToolsUtils.getProperty(obj, "message") || ""; michael@0: const result = (+DevToolsUtils.getProperty(obj, "result")).toString(16); michael@0: const code = DevToolsUtils.getProperty(obj, "code"); michael@0: const name = DevToolsUtils.getProperty(obj, "name") || ""; michael@0: michael@0: return '[Exception... "' + message + '" ' + michael@0: 'code: "' + code +'" ' + michael@0: 'nsresult: "0x' + result + ' (' + name + ')"]'; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates an actor for the specified object. michael@0: * michael@0: * @param aObj Debugger.Object michael@0: * The debuggee object. michael@0: * @param aThreadActor ThreadActor michael@0: * The parent thread actor for this object. michael@0: */ michael@0: function ObjectActor(aObj, aThreadActor) michael@0: { michael@0: dbg_assert(!aObj.optimizedOut, "Should not create object actors for optimized out values!"); michael@0: this.obj = aObj; michael@0: this.threadActor = aThreadActor; michael@0: } michael@0: michael@0: ObjectActor.prototype = { michael@0: actorPrefix: "obj", michael@0: michael@0: /** michael@0: * Returns a grip for this actor for returning in a protocol message. michael@0: */ michael@0: grip: function () { michael@0: this.threadActor._gripDepth++; michael@0: michael@0: let g = { michael@0: "type": "object", michael@0: "class": this.obj.class, michael@0: "actor": this.actorID, michael@0: "extensible": this.obj.isExtensible(), michael@0: "frozen": this.obj.isFrozen(), michael@0: "sealed": this.obj.isSealed() michael@0: }; michael@0: michael@0: if (this.obj.class != "DeadObject") { michael@0: let raw = Cu.unwaiveXrays(this.obj.unsafeDereference()); michael@0: if (!DevToolsUtils.isSafeJSObject(raw)) { michael@0: raw = null; michael@0: } michael@0: michael@0: let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] || michael@0: DebuggerServer.ObjectActorPreviewers.Object; michael@0: for (let fn of previewers) { michael@0: try { michael@0: if (fn(this, g, raw)) { michael@0: break; michael@0: } michael@0: } catch (e) { michael@0: DevToolsUtils.reportException("ObjectActor.prototype.grip previewer function", e); michael@0: } michael@0: } michael@0: } michael@0: michael@0: this.threadActor._gripDepth--; michael@0: return g; michael@0: }, michael@0: michael@0: /** michael@0: * Releases this actor from the pool. michael@0: */ michael@0: release: function () { michael@0: if (this.registeredPool.objectActors) { michael@0: this.registeredPool.objectActors.delete(this.obj); michael@0: } michael@0: this.registeredPool.removeActor(this); michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the definition site of this function michael@0: * object. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onDefinitionSite: function OA_onDefinitionSite(aRequest) { michael@0: if (this.obj.class != "Function") { michael@0: return { michael@0: from: this.actorID, michael@0: error: "objectNotFunction", michael@0: message: this.actorID + " is not a function." michael@0: }; michael@0: } michael@0: michael@0: if (!this.obj.script) { michael@0: return { michael@0: from: this.actorID, michael@0: error: "noScript", michael@0: message: this.actorID + " has no Debugger.Script" michael@0: }; michael@0: } michael@0: michael@0: const generatedLocation = { michael@0: url: this.obj.script.url, michael@0: line: this.obj.script.startLine, michael@0: // TODO bug 901138: use Debugger.Script.prototype.startColumn. michael@0: column: 0 michael@0: }; michael@0: michael@0: return this.threadActor.sources.getOriginalLocation(generatedLocation) michael@0: .then(({ url, line, column }) => { michael@0: return { michael@0: from: this.actorID, michael@0: url: url, michael@0: line: line, michael@0: column: column michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the names of the properties defined on michael@0: * the object and not its prototype. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onOwnPropertyNames: function (aRequest) { michael@0: return { from: this.actorID, michael@0: ownPropertyNames: this.obj.getOwnPropertyNames() }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the prototype and own properties of michael@0: * the object. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onPrototypeAndProperties: function (aRequest) { michael@0: let ownProperties = Object.create(null); michael@0: let names; michael@0: try { michael@0: names = this.obj.getOwnPropertyNames(); michael@0: } catch (ex) { michael@0: // The above can throw if this.obj points to a dead object. michael@0: // TODO: we should use Cu.isDeadWrapper() - see bug 885800. michael@0: return { from: this.actorID, michael@0: prototype: this.threadActor.createValueGrip(null), michael@0: ownProperties: ownProperties, michael@0: safeGetterValues: Object.create(null) }; michael@0: } michael@0: for (let name of names) { michael@0: ownProperties[name] = this._propertyDescriptor(name); michael@0: } michael@0: return { from: this.actorID, michael@0: prototype: this.threadActor.createValueGrip(this.obj.proto), michael@0: ownProperties: ownProperties, michael@0: safeGetterValues: this._findSafeGetterValues(ownProperties) }; michael@0: }, michael@0: michael@0: /** michael@0: * Find the safe getter values for the current Debugger.Object, |this.obj|. michael@0: * michael@0: * @private michael@0: * @param object aOwnProperties michael@0: * The object that holds the list of known ownProperties for michael@0: * |this.obj|. michael@0: * @param number [aLimit=0] michael@0: * Optional limit of getter values to find. michael@0: * @return object michael@0: * An object that maps property names to safe getter descriptors as michael@0: * defined by the remote debugging protocol. michael@0: */ michael@0: _findSafeGetterValues: function (aOwnProperties, aLimit = 0) michael@0: { michael@0: let safeGetterValues = Object.create(null); michael@0: let obj = this.obj; michael@0: let level = 0, i = 0; michael@0: michael@0: while (obj) { michael@0: let getters = this._findSafeGetters(obj); michael@0: for (let name of getters) { michael@0: // Avoid overwriting properties from prototypes closer to this.obj. Also michael@0: // avoid providing safeGetterValues from prototypes if property |name| michael@0: // is already defined as an own property. michael@0: if (name in safeGetterValues || michael@0: (obj != this.obj && name in aOwnProperties)) { michael@0: continue; michael@0: } michael@0: michael@0: let desc = null, getter = null; michael@0: try { michael@0: desc = obj.getOwnPropertyDescriptor(name); michael@0: getter = desc.get; michael@0: } catch (ex) { michael@0: // The above can throw if the cache becomes stale. michael@0: } michael@0: if (!getter) { michael@0: obj._safeGetters = null; michael@0: continue; michael@0: } michael@0: michael@0: let result = getter.call(this.obj); michael@0: if (result && !("throw" in result)) { michael@0: let getterValue = undefined; michael@0: if ("return" in result) { michael@0: getterValue = result.return; michael@0: } else if ("yield" in result) { michael@0: getterValue = result.yield; michael@0: } michael@0: // WebIDL attributes specified with the LenientThis extended attribute michael@0: // return undefined and should be ignored. michael@0: if (getterValue !== undefined) { michael@0: safeGetterValues[name] = { michael@0: getterValue: this.threadActor.createValueGrip(getterValue), michael@0: getterPrototypeLevel: level, michael@0: enumerable: desc.enumerable, michael@0: writable: level == 0 ? desc.writable : true, michael@0: }; michael@0: if (aLimit && ++i == aLimit) { michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: if (aLimit && i == aLimit) { michael@0: break; michael@0: } michael@0: michael@0: obj = obj.proto; michael@0: level++; michael@0: } michael@0: michael@0: return safeGetterValues; michael@0: }, michael@0: michael@0: /** michael@0: * Find the safe getters for a given Debugger.Object. Safe getters are native michael@0: * getters which are safe to execute. michael@0: * michael@0: * @private michael@0: * @param Debugger.Object aObject michael@0: * The Debugger.Object where you want to find safe getters. michael@0: * @return Set michael@0: * A Set of names of safe getters. This result is cached for each michael@0: * Debugger.Object. michael@0: */ michael@0: _findSafeGetters: function (aObject) michael@0: { michael@0: if (aObject._safeGetters) { michael@0: return aObject._safeGetters; michael@0: } michael@0: michael@0: let getters = new Set(); michael@0: let names = []; michael@0: try { michael@0: names = aObject.getOwnPropertyNames() michael@0: } catch (ex) { michael@0: // Calling getOwnPropertyNames() on some wrapped native prototypes is not michael@0: // allowed: "cannot modify properties of a WrappedNative". See bug 952093. michael@0: } michael@0: michael@0: for (let name of names) { michael@0: let desc = null; michael@0: try { michael@0: desc = aObject.getOwnPropertyDescriptor(name); michael@0: } catch (e) { michael@0: // Calling getOwnPropertyDescriptor on wrapped native prototypes is not michael@0: // allowed (bug 560072). michael@0: } michael@0: if (!desc || desc.value !== undefined || !("get" in desc)) { michael@0: continue; michael@0: } michael@0: michael@0: if (DevToolsUtils.hasSafeGetter(desc)) { michael@0: getters.add(name); michael@0: } michael@0: } michael@0: michael@0: aObject._safeGetters = getters; michael@0: return getters; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the prototype of the object. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onPrototype: function (aRequest) { michael@0: return { from: this.actorID, michael@0: prototype: this.threadActor.createValueGrip(this.obj.proto) }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the property descriptor of the michael@0: * object's specified property. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onProperty: function (aRequest) { michael@0: if (!aRequest.name) { michael@0: return { error: "missingParameter", michael@0: message: "no property name was specified" }; michael@0: } michael@0: michael@0: return { from: this.actorID, michael@0: descriptor: this._propertyDescriptor(aRequest.name) }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the display string for the object. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onDisplayString: function (aRequest) { michael@0: const string = stringify(this.obj); michael@0: return { from: this.actorID, michael@0: displayString: this.threadActor.createValueGrip(string) }; michael@0: }, michael@0: michael@0: /** michael@0: * A helper method that creates a property descriptor for the provided object, michael@0: * properly formatted for sending in a protocol response. michael@0: * michael@0: * @private michael@0: * @param string aName michael@0: * The property that the descriptor is generated for. michael@0: * @param boolean [aOnlyEnumerable] michael@0: * Optional: true if you want a descriptor only for an enumerable michael@0: * property, false otherwise. michael@0: * @return object|undefined michael@0: * The property descriptor, or undefined if this is not an enumerable michael@0: * property and aOnlyEnumerable=true. michael@0: */ michael@0: _propertyDescriptor: function (aName, aOnlyEnumerable) { michael@0: let desc; michael@0: try { michael@0: desc = this.obj.getOwnPropertyDescriptor(aName); michael@0: } catch (e) { michael@0: // Calling getOwnPropertyDescriptor on wrapped native prototypes is not michael@0: // allowed (bug 560072). Inform the user with a bogus, but hopefully michael@0: // explanatory, descriptor. michael@0: return { michael@0: configurable: false, michael@0: writable: false, michael@0: enumerable: false, michael@0: value: e.name michael@0: }; michael@0: } michael@0: michael@0: if (!desc || aOnlyEnumerable && !desc.enumerable) { michael@0: return undefined; michael@0: } michael@0: michael@0: let retval = { michael@0: configurable: desc.configurable, michael@0: enumerable: desc.enumerable michael@0: }; michael@0: michael@0: if ("value" in desc) { michael@0: retval.writable = desc.writable; michael@0: retval.value = this.threadActor.createValueGrip(desc.value); michael@0: } else { michael@0: if ("get" in desc) { michael@0: retval.get = this.threadActor.createValueGrip(desc.get); michael@0: } michael@0: if ("set" in desc) { michael@0: retval.set = this.threadActor.createValueGrip(desc.set); michael@0: } michael@0: } michael@0: return retval; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the source code of a function. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onDecompile: function (aRequest) { michael@0: if (this.obj.class !== "Function") { michael@0: return { error: "objectNotFunction", michael@0: message: "decompile request is only valid for object grips " + michael@0: "with a 'Function' class." }; michael@0: } michael@0: michael@0: return { from: this.actorID, michael@0: decompiledCode: this.obj.decompile(!!aRequest.pretty) }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the parameters of a function. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onParameterNames: function (aRequest) { michael@0: if (this.obj.class !== "Function") { michael@0: return { error: "objectNotFunction", michael@0: message: "'parameterNames' request is only valid for object " + michael@0: "grips with a 'Function' class." }; michael@0: } michael@0: michael@0: return { parameterNames: this.obj.parameterNames }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to release a thread-lifetime grip. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onRelease: function (aRequest) { michael@0: this.release(); michael@0: return {}; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to provide the lexical scope of a function. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onScope: function (aRequest) { michael@0: if (this.obj.class !== "Function") { michael@0: return { error: "objectNotFunction", michael@0: message: "scope request is only valid for object grips with a" + michael@0: " 'Function' class." }; michael@0: } michael@0: michael@0: let envActor = this.threadActor.createEnvironmentActor(this.obj.environment, michael@0: this.registeredPool); michael@0: if (!envActor) { michael@0: return { error: "notDebuggee", michael@0: message: "cannot access the environment of this function." }; michael@0: } michael@0: michael@0: return { from: this.actorID, scope: envActor.form() }; michael@0: } michael@0: }; michael@0: michael@0: ObjectActor.prototype.requestTypes = { michael@0: "definitionSite": ObjectActor.prototype.onDefinitionSite, michael@0: "parameterNames": ObjectActor.prototype.onParameterNames, michael@0: "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, michael@0: "prototype": ObjectActor.prototype.onPrototype, michael@0: "property": ObjectActor.prototype.onProperty, michael@0: "displayString": ObjectActor.prototype.onDisplayString, michael@0: "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, michael@0: "decompile": ObjectActor.prototype.onDecompile, michael@0: "release": ObjectActor.prototype.onRelease, michael@0: "scope": ObjectActor.prototype.onScope, michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Functions for adding information to ObjectActor grips for the purpose of michael@0: * having customized output. This object holds arrays mapped by michael@0: * Debugger.Object.prototype.class. michael@0: * michael@0: * In each array you can add functions that take two michael@0: * arguments: michael@0: * - the ObjectActor instance to make a preview for, michael@0: * - the grip object being prepared for the client, michael@0: * - the raw JS object after calling Debugger.Object.unsafeDereference(). This michael@0: * argument is only provided if the object is safe for reading properties and michael@0: * executing methods. See DevToolsUtils.isSafeJSObject(). michael@0: * michael@0: * Functions must return false if they cannot provide preview michael@0: * information for the debugger object, or true otherwise. michael@0: */ michael@0: DebuggerServer.ObjectActorPreviewers = { michael@0: String: [function({obj, threadActor}, aGrip) { michael@0: let result = genericObjectPreviewer("String", String, obj, threadActor); michael@0: if (result) { michael@0: let length = DevToolsUtils.getProperty(obj, "length"); michael@0: if (typeof length != "number") { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.displayString = result.value; michael@0: return true; michael@0: } michael@0: michael@0: return true; michael@0: }], michael@0: michael@0: Boolean: [function({obj, threadActor}, aGrip) { michael@0: let result = genericObjectPreviewer("Boolean", Boolean, obj, threadActor); michael@0: if (result) { michael@0: aGrip.preview = result; michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }], michael@0: michael@0: Number: [function({obj, threadActor}, aGrip) { michael@0: let result = genericObjectPreviewer("Number", Number, obj, threadActor); michael@0: if (result) { michael@0: aGrip.preview = result; michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }], michael@0: michael@0: Function: [function({obj, threadActor}, aGrip) { michael@0: if (obj.name) { michael@0: aGrip.name = obj.name; michael@0: } michael@0: michael@0: if (obj.displayName) { michael@0: aGrip.displayName = obj.displayName.substr(0, 500); michael@0: } michael@0: michael@0: if (obj.parameterNames) { michael@0: aGrip.parameterNames = obj.parameterNames; michael@0: } michael@0: michael@0: // Check if the developer has added a de-facto standard displayName michael@0: // property for us to use. michael@0: let userDisplayName; michael@0: try { michael@0: userDisplayName = obj.getOwnPropertyDescriptor("displayName"); michael@0: } catch (e) { michael@0: // Calling getOwnPropertyDescriptor with displayName might throw michael@0: // with "permission denied" errors for some functions. michael@0: dumpn(e); michael@0: } michael@0: michael@0: if (userDisplayName && typeof userDisplayName.value == "string" && michael@0: userDisplayName.value) { michael@0: aGrip.userDisplayName = threadActor.createValueGrip(userDisplayName.value); michael@0: } michael@0: michael@0: return true; michael@0: }], michael@0: michael@0: RegExp: [function({obj, threadActor}, aGrip) { michael@0: // Avoid having any special preview for the RegExp.prototype itself. michael@0: if (!obj.proto || obj.proto.class != "RegExp") { michael@0: return false; michael@0: } michael@0: michael@0: let str = RegExp.prototype.toString.call(obj.unsafeDereference()); michael@0: aGrip.displayString = threadActor.createValueGrip(str); michael@0: return true; michael@0: }], michael@0: michael@0: Date: [function({obj, threadActor}, aGrip) { michael@0: if (!obj.proto || obj.proto.class != "Date") { michael@0: return false; michael@0: } michael@0: michael@0: let time = Date.prototype.getTime.call(obj.unsafeDereference()); michael@0: michael@0: aGrip.preview = { michael@0: timestamp: threadActor.createValueGrip(time), michael@0: }; michael@0: return true; michael@0: }], michael@0: michael@0: Array: [function({obj, threadActor}, aGrip) { michael@0: let length = DevToolsUtils.getProperty(obj, "length"); michael@0: if (typeof length != "number") { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "ArrayLike", michael@0: length: length, michael@0: }; michael@0: michael@0: if (threadActor._gripDepth > 1) { michael@0: return true; michael@0: } michael@0: michael@0: let raw = obj.unsafeDereference(); michael@0: let items = aGrip.preview.items = []; michael@0: michael@0: for (let [i, value] of Array.prototype.entries.call(raw)) { michael@0: if (Object.hasOwnProperty.call(raw, i)) { michael@0: value = makeDebuggeeValueIfNeeded(obj, value); michael@0: items.push(threadActor.createValueGrip(value)); michael@0: } else { michael@0: items.push(null); michael@0: } michael@0: michael@0: if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }], // Array michael@0: michael@0: Set: [function({obj, threadActor}, aGrip) { michael@0: let size = DevToolsUtils.getProperty(obj, "size"); michael@0: if (typeof size != "number") { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "ArrayLike", michael@0: length: size, michael@0: }; michael@0: michael@0: // Avoid recursive object grips. michael@0: if (threadActor._gripDepth > 1) { michael@0: return true; michael@0: } michael@0: michael@0: let raw = obj.unsafeDereference(); michael@0: let items = aGrip.preview.items = []; michael@0: for (let item of Set.prototype.values.call(raw)) { michael@0: item = makeDebuggeeValueIfNeeded(obj, item); michael@0: items.push(threadActor.createValueGrip(item)); michael@0: if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }], // Set michael@0: michael@0: Map: [function({obj, threadActor}, aGrip) { michael@0: let size = DevToolsUtils.getProperty(obj, "size"); michael@0: if (typeof size != "number") { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "MapLike", michael@0: size: size, michael@0: }; michael@0: michael@0: if (threadActor._gripDepth > 1) { michael@0: return true; michael@0: } michael@0: michael@0: let raw = obj.unsafeDereference(); michael@0: let entries = aGrip.preview.entries = []; michael@0: for (let [key, value] of Map.prototype.entries.call(raw)) { michael@0: key = makeDebuggeeValueIfNeeded(obj, key); michael@0: value = makeDebuggeeValueIfNeeded(obj, value); michael@0: entries.push([threadActor.createValueGrip(key), michael@0: threadActor.createValueGrip(value)]); michael@0: if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }], // Map michael@0: michael@0: DOMStringMap: [function({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj) { michael@0: return false; michael@0: } michael@0: michael@0: let keys = obj.getOwnPropertyNames(); michael@0: aGrip.preview = { michael@0: kind: "MapLike", michael@0: size: keys.length, michael@0: }; michael@0: michael@0: if (threadActor._gripDepth > 1) { michael@0: return true; michael@0: } michael@0: michael@0: let entries = aGrip.preview.entries = []; michael@0: for (let key of keys) { michael@0: let value = makeDebuggeeValueIfNeeded(obj, aRawObj[key]); michael@0: entries.push([key, threadActor.createValueGrip(value)]); michael@0: if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }], // DOMStringMap michael@0: }; // DebuggerServer.ObjectActorPreviewers michael@0: michael@0: /** michael@0: * Generic previewer for "simple" classes like String, Number and Boolean. michael@0: * michael@0: * @param string aClassName michael@0: * Class name to expect. michael@0: * @param object aClass michael@0: * The class to expect, eg. String. The valueOf() method of the class is michael@0: * invoked on the given object. michael@0: * @param Debugger.Object aObj michael@0: * The debugger object we need to preview. michael@0: * @param object aThreadActor michael@0: * The thread actor to use to create a value grip. michael@0: * @return object|null michael@0: * An object with one property, "value", which holds the value grip that michael@0: * represents the given object. Null is returned if we cant preview the michael@0: * object. michael@0: */ michael@0: function genericObjectPreviewer(aClassName, aClass, aObj, aThreadActor) { michael@0: if (!aObj.proto || aObj.proto.class != aClassName) { michael@0: return null; michael@0: } michael@0: michael@0: let raw = aObj.unsafeDereference(); michael@0: let v = null; michael@0: try { michael@0: v = aClass.prototype.valueOf.call(raw); michael@0: } catch (ex) { michael@0: // valueOf() can throw if the raw JS object is "misbehaved". michael@0: return null; michael@0: } michael@0: michael@0: if (v !== null) { michael@0: v = aThreadActor.createValueGrip(makeDebuggeeValueIfNeeded(aObj, v)); michael@0: return { value: v }; michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: // Preview functions that do not rely on the object class. michael@0: DebuggerServer.ObjectActorPreviewers.Object = [ michael@0: function TypedArray({obj, threadActor}, aGrip) { michael@0: if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) { michael@0: return false; michael@0: } michael@0: michael@0: let length = DevToolsUtils.getProperty(obj, "length"); michael@0: if (typeof length != "number") { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "ArrayLike", michael@0: length: length, michael@0: }; michael@0: michael@0: if (threadActor._gripDepth > 1) { michael@0: return true; michael@0: } michael@0: michael@0: let raw = obj.unsafeDereference(); michael@0: let global = Cu.getGlobalForObject(DebuggerServer); michael@0: let classProto = global[obj.class].prototype; michael@0: let safeView = classProto.subarray.call(raw, 0, OBJECT_PREVIEW_MAX_ITEMS); michael@0: let items = aGrip.preview.items = []; michael@0: for (let i = 0; i < safeView.length; i++) { michael@0: items.push(safeView[i]); michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: function Error({obj, threadActor}, aGrip) { michael@0: switch (obj.class) { michael@0: case "Error": michael@0: case "EvalError": michael@0: case "RangeError": michael@0: case "ReferenceError": michael@0: case "SyntaxError": michael@0: case "TypeError": michael@0: case "URIError": michael@0: let name = DevToolsUtils.getProperty(obj, "name"); michael@0: let msg = DevToolsUtils.getProperty(obj, "message"); michael@0: let stack = DevToolsUtils.getProperty(obj, "stack"); michael@0: let fileName = DevToolsUtils.getProperty(obj, "fileName"); michael@0: let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); michael@0: let columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); michael@0: aGrip.preview = { michael@0: kind: "Error", michael@0: name: threadActor.createValueGrip(name), michael@0: message: threadActor.createValueGrip(msg), michael@0: stack: threadActor.createValueGrip(stack), michael@0: fileName: threadActor.createValueGrip(fileName), michael@0: lineNumber: threadActor.createValueGrip(lineNumber), michael@0: columnNumber: threadActor.createValueGrip(columnNumber), michael@0: }; michael@0: return true; michael@0: default: michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: function CSSMediaRule({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSMediaRule)) { michael@0: return false; michael@0: } michael@0: aGrip.preview = { michael@0: kind: "ObjectWithText", michael@0: text: threadActor.createValueGrip(aRawObj.conditionText), michael@0: }; michael@0: return true; michael@0: }, michael@0: michael@0: function CSSStyleRule({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleRule)) { michael@0: return false; michael@0: } michael@0: aGrip.preview = { michael@0: kind: "ObjectWithText", michael@0: text: threadActor.createValueGrip(aRawObj.selectorText), michael@0: }; michael@0: return true; michael@0: }, michael@0: michael@0: function ObjectWithURL({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj || michael@0: !(aRawObj instanceof Ci.nsIDOMCSSImportRule || michael@0: aRawObj instanceof Ci.nsIDOMCSSStyleSheet || michael@0: aRawObj instanceof Ci.nsIDOMLocation || michael@0: aRawObj instanceof Ci.nsIDOMWindow)) { michael@0: return false; michael@0: } michael@0: michael@0: let url; michael@0: if (aRawObj instanceof Ci.nsIDOMWindow && aRawObj.location) { michael@0: url = aRawObj.location.href; michael@0: } else if (aRawObj.href) { michael@0: url = aRawObj.href; michael@0: } else { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "ObjectWithURL", michael@0: url: threadActor.createValueGrip(url), michael@0: }; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: function ArrayLike({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj || michael@0: obj.class != "DOMStringList" && michael@0: obj.class != "DOMTokenList" && michael@0: !(aRawObj instanceof Ci.nsIDOMMozNamedAttrMap || michael@0: aRawObj instanceof Ci.nsIDOMCSSRuleList || michael@0: aRawObj instanceof Ci.nsIDOMCSSValueList || michael@0: aRawObj instanceof Ci.nsIDOMFileList || michael@0: aRawObj instanceof Ci.nsIDOMFontFaceList || michael@0: aRawObj instanceof Ci.nsIDOMMediaList || michael@0: aRawObj instanceof Ci.nsIDOMNodeList || michael@0: aRawObj instanceof Ci.nsIDOMStyleSheetList)) { michael@0: return false; michael@0: } michael@0: michael@0: if (typeof aRawObj.length != "number") { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "ArrayLike", michael@0: length: aRawObj.length, michael@0: }; michael@0: michael@0: if (threadActor._gripDepth > 1) { michael@0: return true; michael@0: } michael@0: michael@0: let items = aGrip.preview.items = []; michael@0: michael@0: for (let i = 0; i < aRawObj.length && michael@0: items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) { michael@0: let value = makeDebuggeeValueIfNeeded(obj, aRawObj[i]); michael@0: items.push(threadActor.createValueGrip(value)); michael@0: } michael@0: michael@0: return true; michael@0: }, // ArrayLike michael@0: michael@0: function CSSStyleDeclaration({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "MapLike", michael@0: size: aRawObj.length, michael@0: }; michael@0: michael@0: let entries = aGrip.preview.entries = []; michael@0: michael@0: for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && michael@0: i < aRawObj.length; i++) { michael@0: let prop = aRawObj[i]; michael@0: let value = aRawObj.getPropertyValue(prop); michael@0: entries.push([prop, threadActor.createValueGrip(value)]); michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: function DOMNode({obj, threadActor}, aGrip, aRawObj) { michael@0: if (obj.class == "Object" || !aRawObj || !(aRawObj instanceof Ci.nsIDOMNode)) { michael@0: return false; michael@0: } michael@0: michael@0: let preview = aGrip.preview = { michael@0: kind: "DOMNode", michael@0: nodeType: aRawObj.nodeType, michael@0: nodeName: aRawObj.nodeName, michael@0: }; michael@0: michael@0: if (aRawObj instanceof Ci.nsIDOMDocument && aRawObj.location) { michael@0: preview.location = threadActor.createValueGrip(aRawObj.location.href); michael@0: } else if (aRawObj instanceof Ci.nsIDOMDocumentFragment) { michael@0: preview.childNodesLength = aRawObj.childNodes.length; michael@0: michael@0: if (threadActor._gripDepth < 2) { michael@0: preview.childNodes = []; michael@0: for (let node of aRawObj.childNodes) { michael@0: let actor = threadActor.createValueGrip(obj.makeDebuggeeValue(node)); michael@0: preview.childNodes.push(actor); michael@0: if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } else if (aRawObj instanceof Ci.nsIDOMElement) { michael@0: // Add preview for DOM element attributes. michael@0: if (aRawObj instanceof Ci.nsIDOMHTMLElement) { michael@0: preview.nodeName = preview.nodeName.toLowerCase(); michael@0: } michael@0: michael@0: let i = 0; michael@0: preview.attributes = {}; michael@0: preview.attributesLength = aRawObj.attributes.length; michael@0: for (let attr of aRawObj.attributes) { michael@0: preview.attributes[attr.nodeName] = threadActor.createValueGrip(attr.value); michael@0: if (++i == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: } else if (aRawObj instanceof Ci.nsIDOMAttr) { michael@0: preview.value = threadActor.createValueGrip(aRawObj.value); michael@0: } else if (aRawObj instanceof Ci.nsIDOMText || michael@0: aRawObj instanceof Ci.nsIDOMComment) { michael@0: preview.textContent = threadActor.createValueGrip(aRawObj.textContent); michael@0: } michael@0: michael@0: return true; michael@0: }, // DOMNode michael@0: michael@0: function DOMEvent({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMEvent)) { michael@0: return false; michael@0: } michael@0: michael@0: let preview = aGrip.preview = { michael@0: kind: "DOMEvent", michael@0: type: aRawObj.type, michael@0: properties: Object.create(null), michael@0: }; michael@0: michael@0: if (threadActor._gripDepth < 2) { michael@0: let target = obj.makeDebuggeeValue(aRawObj.target); michael@0: preview.target = threadActor.createValueGrip(target); michael@0: } michael@0: michael@0: let props = []; michael@0: if (aRawObj instanceof Ci.nsIDOMMouseEvent) { michael@0: props.push("buttons", "clientX", "clientY", "layerX", "layerY"); michael@0: } else if (aRawObj instanceof Ci.nsIDOMKeyEvent) { michael@0: let modifiers = []; michael@0: if (aRawObj.altKey) { michael@0: modifiers.push("Alt"); michael@0: } michael@0: if (aRawObj.ctrlKey) { michael@0: modifiers.push("Control"); michael@0: } michael@0: if (aRawObj.metaKey) { michael@0: modifiers.push("Meta"); michael@0: } michael@0: if (aRawObj.shiftKey) { michael@0: modifiers.push("Shift"); michael@0: } michael@0: preview.eventKind = "key"; michael@0: preview.modifiers = modifiers; michael@0: michael@0: props.push("key", "charCode", "keyCode"); michael@0: } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent || michael@0: aRawObj instanceof Ci.nsIDOMAnimationEvent) { michael@0: props.push("animationName", "pseudoElement"); michael@0: } else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) { michael@0: props.push("clipboardData"); michael@0: } michael@0: michael@0: // Add event-specific properties. michael@0: for (let prop of props) { michael@0: let value = aRawObj[prop]; michael@0: if (value && (typeof value == "object" || typeof value == "function")) { michael@0: // Skip properties pointing to objects. michael@0: if (threadActor._gripDepth > 1) { michael@0: continue; michael@0: } michael@0: value = obj.makeDebuggeeValue(value); michael@0: } michael@0: preview.properties[prop] = threadActor.createValueGrip(value); michael@0: } michael@0: michael@0: // Add any properties we find on the event object. michael@0: if (!props.length) { michael@0: let i = 0; michael@0: for (let prop in aRawObj) { michael@0: let value = aRawObj[prop]; michael@0: if (prop == "target" || prop == "type" || value === null || michael@0: typeof value == "function") { michael@0: continue; michael@0: } michael@0: if (value && typeof value == "object") { michael@0: if (threadActor._gripDepth > 1) { michael@0: continue; michael@0: } michael@0: value = obj.makeDebuggeeValue(value); michael@0: } michael@0: preview.properties[prop] = threadActor.createValueGrip(value); michael@0: if (++i == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: }, // DOMEvent michael@0: michael@0: function DOMException({obj, threadActor}, aGrip, aRawObj) { michael@0: if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMDOMException)) { michael@0: return false; michael@0: } michael@0: michael@0: aGrip.preview = { michael@0: kind: "DOMException", michael@0: name: threadActor.createValueGrip(aRawObj.name), michael@0: message: threadActor.createValueGrip(aRawObj.message), michael@0: code: threadActor.createValueGrip(aRawObj.code), michael@0: result: threadActor.createValueGrip(aRawObj.result), michael@0: filename: threadActor.createValueGrip(aRawObj.filename), michael@0: lineNumber: threadActor.createValueGrip(aRawObj.lineNumber), michael@0: columnNumber: threadActor.createValueGrip(aRawObj.columnNumber), michael@0: }; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: function GenericObject(aObjectActor, aGrip) { michael@0: let {obj, threadActor} = aObjectActor; michael@0: if (aGrip.preview || aGrip.displayString || threadActor._gripDepth > 1) { michael@0: return false; michael@0: } michael@0: michael@0: let i = 0, names = []; michael@0: let preview = aGrip.preview = { michael@0: kind: "Object", michael@0: ownProperties: Object.create(null), michael@0: }; michael@0: michael@0: try { michael@0: names = obj.getOwnPropertyNames(); michael@0: } catch (ex) { michael@0: // Calling getOwnPropertyNames() on some wrapped native prototypes is not michael@0: // allowed: "cannot modify properties of a WrappedNative". See bug 952093. michael@0: } michael@0: michael@0: preview.ownPropertiesLength = names.length; michael@0: michael@0: for (let name of names) { michael@0: let desc = aObjectActor._propertyDescriptor(name, true); michael@0: if (!desc) { michael@0: continue; michael@0: } michael@0: michael@0: preview.ownProperties[name] = desc; michael@0: if (++i == OBJECT_PREVIEW_MAX_ITEMS) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (i < OBJECT_PREVIEW_MAX_ITEMS) { michael@0: preview.safeGetterValues = aObjectActor. michael@0: _findSafeGetterValues(preview.ownProperties, michael@0: OBJECT_PREVIEW_MAX_ITEMS - i); michael@0: } michael@0: michael@0: return true; michael@0: }, // GenericObject michael@0: ]; // DebuggerServer.ObjectActorPreviewers.Object michael@0: michael@0: /** michael@0: * Creates a pause-scoped actor for the specified object. michael@0: * @see ObjectActor michael@0: */ michael@0: function PauseScopedObjectActor() michael@0: { michael@0: ObjectActor.apply(this, arguments); michael@0: } michael@0: michael@0: PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype); michael@0: michael@0: update(PauseScopedObjectActor.prototype, ObjectActor.prototype); michael@0: michael@0: update(PauseScopedObjectActor.prototype, { michael@0: constructor: PauseScopedObjectActor, michael@0: actorPrefix: "pausedobj", michael@0: michael@0: onOwnPropertyNames: michael@0: PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames), michael@0: michael@0: onPrototypeAndProperties: michael@0: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties), michael@0: michael@0: onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype), michael@0: onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty), michael@0: onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile), michael@0: michael@0: onDisplayString: michael@0: PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString), michael@0: michael@0: onParameterNames: michael@0: PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames), michael@0: michael@0: /** michael@0: * Handle a protocol request to promote a pause-lifetime grip to a michael@0: * thread-lifetime grip. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onThreadGrip: PauseScopedActor.withPaused(function (aRequest) { michael@0: this.threadActor.threadObjectGrip(this); michael@0: return {}; michael@0: }), michael@0: michael@0: /** michael@0: * Handle a protocol request to release a thread-lifetime grip. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onRelease: PauseScopedActor.withPaused(function (aRequest) { michael@0: if (this.registeredPool !== this.threadActor.threadLifetimePool) { michael@0: return { error: "notReleasable", michael@0: message: "Only thread-lifetime actors can be released." }; michael@0: } michael@0: michael@0: this.release(); michael@0: return {}; michael@0: }), michael@0: }); michael@0: michael@0: update(PauseScopedObjectActor.prototype.requestTypes, { michael@0: "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip, michael@0: }); michael@0: michael@0: michael@0: /** michael@0: * Creates an actor for the specied "very long" string. "Very long" is specified michael@0: * at the server's discretion. michael@0: * michael@0: * @param aString String michael@0: * The string. michael@0: */ michael@0: function LongStringActor(aString) michael@0: { michael@0: this.string = aString; michael@0: this.stringLength = aString.length; michael@0: } michael@0: michael@0: LongStringActor.prototype = { michael@0: michael@0: actorPrefix: "longString", michael@0: michael@0: disconnect: function () { michael@0: // Because longStringActors is not a weak map, we won't automatically leave michael@0: // it so we need to manually leave on disconnect so that we don't leak michael@0: // memory. michael@0: if (this.registeredPool && this.registeredPool.longStringActors) { michael@0: delete this.registeredPool.longStringActors[this.actorID]; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a grip for this actor for returning in a protocol message. michael@0: */ michael@0: grip: function () { michael@0: return { michael@0: "type": "longString", michael@0: "initial": this.string.substring( michael@0: 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), michael@0: "length": this.stringLength, michael@0: "actor": this.actorID michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a request to extract part of this actor's string. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onSubstring: function (aRequest) { michael@0: return { michael@0: "from": this.actorID, michael@0: "substring": this.string.substring(aRequest.start, aRequest.end) michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a request to release this LongStringActor instance. michael@0: */ michael@0: onRelease: function () { michael@0: // TODO: also check if registeredPool === threadActor.threadLifetimePool michael@0: // when the web console moves aray from manually releasing pause-scoped michael@0: // actors. michael@0: if (this.registeredPool.longStringActors) { michael@0: delete this.registeredPool.longStringActors[this.actorID]; michael@0: } michael@0: this.registeredPool.removeActor(this); michael@0: return {}; michael@0: }, michael@0: }; michael@0: michael@0: LongStringActor.prototype.requestTypes = { michael@0: "substring": LongStringActor.prototype.onSubstring, michael@0: "release": LongStringActor.prototype.onRelease michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Creates an actor for the specified stack frame. michael@0: * michael@0: * @param aFrame Debugger.Frame michael@0: * The debuggee frame. michael@0: * @param aThreadActor ThreadActor michael@0: * The parent thread actor for this frame. michael@0: */ michael@0: function FrameActor(aFrame, aThreadActor) michael@0: { michael@0: this.frame = aFrame; michael@0: this.threadActor = aThreadActor; michael@0: } michael@0: michael@0: FrameActor.prototype = { michael@0: actorPrefix: "frame", michael@0: michael@0: /** michael@0: * A pool that contains frame-lifetime objects, like the environment. michael@0: */ michael@0: _frameLifetimePool: null, michael@0: get frameLifetimePool() { michael@0: if (!this._frameLifetimePool) { michael@0: this._frameLifetimePool = new ActorPool(this.conn); michael@0: this.conn.addActorPool(this._frameLifetimePool); michael@0: } michael@0: return this._frameLifetimePool; michael@0: }, michael@0: michael@0: /** michael@0: * Finalization handler that is called when the actor is being evicted from michael@0: * the pool. michael@0: */ michael@0: disconnect: function () { michael@0: this.conn.removeActorPool(this._frameLifetimePool); michael@0: this._frameLifetimePool = null; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a frame form for use in a protocol message. michael@0: */ michael@0: form: function () { michael@0: let form = { actor: this.actorID, michael@0: type: this.frame.type }; michael@0: if (this.frame.type === "call") { michael@0: form.callee = this.threadActor.createValueGrip(this.frame.callee); michael@0: } michael@0: michael@0: if (this.frame.environment) { michael@0: let envActor = this.threadActor michael@0: .createEnvironmentActor(this.frame.environment, michael@0: this.frameLifetimePool); michael@0: form.environment = envActor.form(); michael@0: } michael@0: form.this = this.threadActor.createValueGrip(this.frame.this); michael@0: form.arguments = this._args(); michael@0: if (this.frame.script) { michael@0: form.where = getFrameLocation(this.frame); michael@0: } michael@0: michael@0: if (!this.frame.older) { michael@0: form.oldest = true; michael@0: } michael@0: michael@0: return form; michael@0: }, michael@0: michael@0: _args: function () { michael@0: if (!this.frame.arguments) { michael@0: return []; michael@0: } michael@0: michael@0: return [this.threadActor.createValueGrip(arg) michael@0: for each (arg in this.frame.arguments)]; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to pop this frame from the stack. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onPop: function (aRequest) { michael@0: // TODO: remove this when Debugger.Frame.prototype.pop is implemented michael@0: if (typeof this.frame.pop != "function") { michael@0: return { error: "notImplemented", michael@0: message: "Popping frames is not yet implemented." }; michael@0: } michael@0: michael@0: while (this.frame != this.threadActor.dbg.getNewestFrame()) { michael@0: this.threadActor.dbg.getNewestFrame().pop(); michael@0: } michael@0: this.frame.pop(aRequest.completionValue); michael@0: michael@0: // TODO: return the watches property when frame pop watch actors are michael@0: // implemented. michael@0: return { from: this.actorID }; michael@0: } michael@0: }; michael@0: michael@0: FrameActor.prototype.requestTypes = { michael@0: "pop": FrameActor.prototype.onPop, michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Creates a BreakpointActor. BreakpointActors exist for the lifetime of their michael@0: * containing thread and are responsible for deleting breakpoints, handling michael@0: * breakpoint hits and associating breakpoints with scripts. michael@0: * michael@0: * @param ThreadActor aThreadActor michael@0: * The parent thread actor that contains this breakpoint. michael@0: * @param object aLocation michael@0: * The location of the breakpoint as specified in the protocol. michael@0: */ michael@0: function BreakpointActor(aThreadActor, { url, line, column, condition }) michael@0: { michael@0: this.scripts = []; michael@0: this.threadActor = aThreadActor; michael@0: this.location = { url: url, line: line, column: column }; michael@0: this.condition = condition; michael@0: } michael@0: michael@0: BreakpointActor.prototype = { michael@0: actorPrefix: "breakpoint", michael@0: condition: null, michael@0: michael@0: /** michael@0: * Called when this same breakpoint is added to another Debugger.Script michael@0: * instance, in the case of a page reload. michael@0: * michael@0: * @param aScript Debugger.Script michael@0: * The new source script on which the breakpoint has been set. michael@0: * @param ThreadActor aThreadActor michael@0: * The parent thread actor that contains this breakpoint. michael@0: */ michael@0: addScript: function (aScript, aThreadActor) { michael@0: this.threadActor = aThreadActor; michael@0: this.scripts.push(aScript); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the breakpoints from associated scripts and clear the script cache. michael@0: */ michael@0: removeScripts: function () { michael@0: for (let script of this.scripts) { michael@0: script.clearBreakpoint(this); michael@0: } michael@0: this.scripts = []; michael@0: }, michael@0: michael@0: /** michael@0: * Check if this breakpoint has a condition that doesn't error and michael@0: * evaluates to true in aFrame michael@0: * michael@0: * @param aFrame Debugger.Frame michael@0: * The frame to evaluate the condition in michael@0: */ michael@0: isValidCondition: function(aFrame) { michael@0: if(!this.condition) { michael@0: return true; michael@0: } michael@0: var res = aFrame.eval(this.condition); michael@0: return res.return; michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when a breakpoint has been hit. michael@0: * michael@0: * @param aFrame Debugger.Frame michael@0: * The stack frame that contained the breakpoint. michael@0: */ michael@0: hit: function (aFrame) { michael@0: // Don't pause if we are currently stepping (in or over) or the frame is michael@0: // black-boxed. michael@0: let { url } = this.threadActor.synchronize( michael@0: this.threadActor.sources.getOriginalLocation({ michael@0: url: this.location.url, michael@0: line: this.location.line, michael@0: column: this.location.column michael@0: })); michael@0: michael@0: if (this.threadActor.sources.isBlackBoxed(url) michael@0: || aFrame.onStep michael@0: || !this.isValidCondition(aFrame)) { michael@0: return undefined; michael@0: } michael@0: michael@0: let reason = {}; michael@0: if (this.threadActor._hiddenBreakpoints.has(this.actorID)) { michael@0: reason.type = "pauseOnDOMEvents"; michael@0: } else { michael@0: reason.type = "breakpoint"; michael@0: // TODO: add the rest of the breakpoints on that line (bug 676602). michael@0: reason.actors = [ this.actorID ]; michael@0: } michael@0: return this.threadActor._pauseAndRespond(aFrame, reason); michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to remove this breakpoint. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onDelete: function (aRequest) { michael@0: // Remove from the breakpoint store. michael@0: this.threadActor.breakpointStore.removeBreakpoint(this.location); michael@0: this.threadActor.threadLifetimePool.removeActor(this); michael@0: // Remove the actual breakpoint from the associated scripts. michael@0: this.removeScripts(); michael@0: return { from: this.actorID }; michael@0: } michael@0: }; michael@0: michael@0: BreakpointActor.prototype.requestTypes = { michael@0: "delete": BreakpointActor.prototype.onDelete michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Creates an EnvironmentActor. EnvironmentActors are responsible for listing michael@0: * the bindings introduced by a lexical environment and assigning new values to michael@0: * those identifier bindings. michael@0: * michael@0: * @param Debugger.Environment aEnvironment michael@0: * The lexical environment that will be used to create the actor. michael@0: * @param ThreadActor aThreadActor michael@0: * The parent thread actor that contains this environment. michael@0: */ michael@0: function EnvironmentActor(aEnvironment, aThreadActor) michael@0: { michael@0: this.obj = aEnvironment; michael@0: this.threadActor = aThreadActor; michael@0: } michael@0: michael@0: EnvironmentActor.prototype = { michael@0: actorPrefix: "environment", michael@0: michael@0: /** michael@0: * Return an environment form for use in a protocol message. michael@0: */ michael@0: form: function () { michael@0: let form = { actor: this.actorID }; michael@0: michael@0: // What is this environment's type? michael@0: if (this.obj.type == "declarative") { michael@0: form.type = this.obj.callee ? "function" : "block"; michael@0: } else { michael@0: form.type = this.obj.type; michael@0: } michael@0: michael@0: // Does this environment have a parent? michael@0: if (this.obj.parent) { michael@0: form.parent = (this.threadActor michael@0: .createEnvironmentActor(this.obj.parent, michael@0: this.registeredPool) michael@0: .form()); michael@0: } michael@0: michael@0: // Does this environment reflect the properties of an object as variables? michael@0: if (this.obj.type == "object" || this.obj.type == "with") { michael@0: form.object = this.threadActor.createValueGrip(this.obj.object); michael@0: } michael@0: michael@0: // Is this the environment created for a function call? michael@0: if (this.obj.callee) { michael@0: form.function = this.threadActor.createValueGrip(this.obj.callee); michael@0: } michael@0: michael@0: // Shall we list this environment's bindings? michael@0: if (this.obj.type == "declarative") { michael@0: form.bindings = this._bindings(); michael@0: } michael@0: michael@0: return form; michael@0: }, michael@0: michael@0: /** michael@0: * Return the identifier bindings object as required by the remote protocol michael@0: * specification. michael@0: */ michael@0: _bindings: function () { michael@0: let bindings = { arguments: [], variables: {} }; michael@0: michael@0: // TODO: this part should be removed in favor of the commented-out part michael@0: // below when getVariableDescriptor lands (bug 725815). michael@0: if (typeof this.obj.getVariable != "function") { michael@0: //if (typeof this.obj.getVariableDescriptor != "function") { michael@0: return bindings; michael@0: } michael@0: michael@0: let parameterNames; michael@0: if (this.obj.callee) { michael@0: parameterNames = this.obj.callee.parameterNames; michael@0: } michael@0: for each (let name in parameterNames) { michael@0: let arg = {}; michael@0: michael@0: let value = this.obj.getVariable(name); michael@0: // The slot is optimized out. michael@0: // FIXME: Need actual UI, bug 941287. michael@0: if (value && value.optimizedOut) { michael@0: continue; michael@0: } michael@0: michael@0: // TODO: this part should be removed in favor of the commented-out part michael@0: // below when getVariableDescriptor lands (bug 725815). michael@0: let desc = { michael@0: value: value, michael@0: configurable: false, michael@0: writable: true, michael@0: enumerable: true michael@0: }; michael@0: michael@0: // let desc = this.obj.getVariableDescriptor(name); michael@0: let descForm = { michael@0: enumerable: true, michael@0: configurable: desc.configurable michael@0: }; michael@0: if ("value" in desc) { michael@0: descForm.value = this.threadActor.createValueGrip(desc.value); michael@0: descForm.writable = desc.writable; michael@0: } else { michael@0: descForm.get = this.threadActor.createValueGrip(desc.get); michael@0: descForm.set = this.threadActor.createValueGrip(desc.set); michael@0: } michael@0: arg[name] = descForm; michael@0: bindings.arguments.push(arg); michael@0: } michael@0: michael@0: for each (let name in this.obj.names()) { michael@0: if (bindings.arguments.some(function exists(element) { michael@0: return !!element[name]; michael@0: })) { michael@0: continue; michael@0: } michael@0: michael@0: let value = this.obj.getVariable(name); michael@0: // The slot is optimized out or arguments on a dead scope. michael@0: // FIXME: Need actual UI, bug 941287. michael@0: if (value && (value.optimizedOut || value.missingArguments)) { michael@0: continue; michael@0: } michael@0: michael@0: // TODO: this part should be removed in favor of the commented-out part michael@0: // below when getVariableDescriptor lands. michael@0: let desc = { michael@0: value: value, michael@0: configurable: false, michael@0: writable: true, michael@0: enumerable: true michael@0: }; michael@0: michael@0: //let desc = this.obj.getVariableDescriptor(name); michael@0: let descForm = { michael@0: enumerable: true, michael@0: configurable: desc.configurable michael@0: }; michael@0: if ("value" in desc) { michael@0: descForm.value = this.threadActor.createValueGrip(desc.value); michael@0: descForm.writable = desc.writable; michael@0: } else { michael@0: descForm.get = this.threadActor.createValueGrip(desc.get); michael@0: descForm.set = this.threadActor.createValueGrip(desc.set); michael@0: } michael@0: bindings.variables[name] = descForm; michael@0: } michael@0: michael@0: return bindings; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to change the value of a variable bound in this michael@0: * lexical environment. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onAssign: function (aRequest) { michael@0: // TODO: enable the commented-out part when getVariableDescriptor lands michael@0: // (bug 725815). michael@0: /*let desc = this.obj.getVariableDescriptor(aRequest.name); michael@0: michael@0: if (!desc.writable) { michael@0: return { error: "immutableBinding", michael@0: message: "Changing the value of an immutable binding is not " + michael@0: "allowed" }; michael@0: }*/ michael@0: michael@0: try { michael@0: this.obj.setVariable(aRequest.name, aRequest.value); michael@0: } catch (e if e instanceof Debugger.DebuggeeWouldRun) { michael@0: return { error: "threadWouldRun", michael@0: cause: e.cause ? e.cause : "setter", michael@0: message: "Assigning a value would cause the debuggee to run" }; michael@0: } michael@0: return { from: this.actorID }; michael@0: }, michael@0: michael@0: /** michael@0: * Handle a protocol request to fully enumerate the bindings introduced by the michael@0: * lexical environment. michael@0: * michael@0: * @param aRequest object michael@0: * The protocol request object. michael@0: */ michael@0: onBindings: function (aRequest) { michael@0: return { from: this.actorID, michael@0: bindings: this._bindings() }; michael@0: } michael@0: }; michael@0: michael@0: EnvironmentActor.prototype.requestTypes = { michael@0: "assign": EnvironmentActor.prototype.onAssign, michael@0: "bindings": EnvironmentActor.prototype.onBindings michael@0: }; michael@0: michael@0: /** michael@0: * Override the toString method in order to get more meaningful script output michael@0: * for debugging the debugger. michael@0: */ michael@0: Debugger.Script.prototype.toString = function() { michael@0: let output = ""; michael@0: if (this.url) { michael@0: output += this.url; michael@0: } michael@0: if (typeof this.startLine != "undefined") { michael@0: output += ":" + this.startLine; michael@0: if (this.lineCount && this.lineCount > 1) { michael@0: output += "-" + (this.startLine + this.lineCount - 1); michael@0: } michael@0: } michael@0: if (this.strictMode) { michael@0: output += ":strict"; michael@0: } michael@0: return output; michael@0: }; michael@0: michael@0: /** michael@0: * Helper property for quickly getting to the line number a stack frame is michael@0: * currently paused at. michael@0: */ michael@0: Object.defineProperty(Debugger.Frame.prototype, "line", { michael@0: configurable: true, michael@0: get: function() { michael@0: if (this.script) { michael@0: return this.script.getOffsetLine(this.offset); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: }); michael@0: michael@0: michael@0: /** michael@0: * Creates an actor for handling chrome debugging. ChromeDebuggerActor is a michael@0: * thin wrapper over ThreadActor, slightly changing some of its behavior. michael@0: * michael@0: * @param aConnection object michael@0: * The DebuggerServerConnection with which this ChromeDebuggerActor michael@0: * is associated. (Currently unused, but required to make this michael@0: * constructor usable with addGlobalActor.) michael@0: * michael@0: * @param aHooks object michael@0: * An object with preNest and postNest methods for calling when entering michael@0: * and exiting a nested event loop. michael@0: */ michael@0: function ChromeDebuggerActor(aConnection, aHooks) michael@0: { michael@0: ThreadActor.call(this, aHooks); michael@0: } michael@0: michael@0: ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype); michael@0: michael@0: update(ChromeDebuggerActor.prototype, { michael@0: constructor: ChromeDebuggerActor, michael@0: michael@0: // A constant prefix that will be used to form the actor ID by the server. michael@0: actorPrefix: "chromeDebugger", michael@0: michael@0: /** michael@0: * Override the eligibility check for scripts and sources to make sure every michael@0: * script and source with a URL is stored when debugging chrome. michael@0: */ michael@0: _allowSource: function(aSourceURL) !!aSourceURL, michael@0: michael@0: /** michael@0: * An object that will be used by ThreadActors to tailor their behavior michael@0: * depending on the debugging context being required (chrome or content). michael@0: * The methods that this object provides must be bound to the ThreadActor michael@0: * before use. michael@0: */ michael@0: globalManager: { michael@0: findGlobals: function () { michael@0: // Add every global known to the debugger as debuggee. michael@0: this.dbg.addAllGlobalsAsDebuggees(); michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when a new global object has been michael@0: * created. michael@0: * michael@0: * @param aGlobal Debugger.Object michael@0: * The new global object that was created. michael@0: */ michael@0: onNewGlobal: function (aGlobal) { michael@0: this.addDebuggee(aGlobal); michael@0: // Notify the client. michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: type: "newGlobal", michael@0: // TODO: after bug 801084 lands see if we need to JSONify this. michael@0: hostAnnotations: aGlobal.hostAnnotations michael@0: }); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Creates an actor for handling add-on debugging. AddonThreadActor is michael@0: * a thin wrapper over ThreadActor. michael@0: * michael@0: * @param aConnection object michael@0: * The DebuggerServerConnection with which this AddonThreadActor michael@0: * is associated. (Currently unused, but required to make this michael@0: * constructor usable with addGlobalActor.) michael@0: * michael@0: * @param aHooks object michael@0: * An object with preNest and postNest methods for calling michael@0: * when entering and exiting a nested event loops. michael@0: * michael@0: * @param aAddonID string michael@0: * ID of the add-on this actor will debug. It will be used to michael@0: * filter out globals marked for debugging. michael@0: */ michael@0: michael@0: function AddonThreadActor(aConnect, aHooks, aAddonID) { michael@0: this.addonID = aAddonID; michael@0: ThreadActor.call(this, aHooks); michael@0: } michael@0: michael@0: AddonThreadActor.prototype = Object.create(ThreadActor.prototype); michael@0: michael@0: update(AddonThreadActor.prototype, { michael@0: constructor: AddonThreadActor, michael@0: michael@0: // A constant prefix that will be used to form the actor ID by the server. michael@0: actorPrefix: "addonThread", michael@0: michael@0: onAttach: function(aRequest) { michael@0: if (!this.attached) { michael@0: Services.obs.addObserver(this, "chrome-document-global-created", false); michael@0: Services.obs.addObserver(this, "content-document-global-created", false); michael@0: } michael@0: return ThreadActor.prototype.onAttach.call(this, aRequest); michael@0: }, michael@0: michael@0: disconnect: function() { michael@0: if (this.attached) { michael@0: Services.obs.removeObserver(this, "content-document-global-created"); michael@0: Services.obs.removeObserver(this, "chrome-document-global-created"); michael@0: } michael@0: return ThreadActor.prototype.disconnect.call(this); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a new DOM document global is created. Check if the DOM was michael@0: * loaded from an add-on and if so make the window a debuggee. michael@0: */ michael@0: observe: function(aSubject, aTopic, aData) { michael@0: let id = {}; michael@0: if (mapURIToAddonID(aSubject.location, id) && id.value === this.addonID) { michael@0: this.dbg.addDebuggee(aSubject.defaultView); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Override the eligibility check for scripts and sources to make michael@0: * sure every script and source with a URL is stored when debugging michael@0: * add-ons. michael@0: */ michael@0: _allowSource: function(aSourceURL) { michael@0: // Hide eval scripts michael@0: if (!aSourceURL) { michael@0: return false; michael@0: } michael@0: michael@0: // XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it michael@0: if (aSourceURL == "resource://gre/modules/addons/XPIProvider.jsm") { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * An object that will be used by ThreadActors to tailor their michael@0: * behaviour depending on the debugging context being required (chrome, michael@0: * addon or content). The methods that this object provides must michael@0: * be bound to the ThreadActor before use. michael@0: */ michael@0: globalManager: { michael@0: findGlobals: function ADA_findGlobals() { michael@0: for (let global of this.dbg.findAllGlobals()) { michael@0: if (this._checkGlobal(global)) { michael@0: this.dbg.addDebuggee(global); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * A function that the engine calls when a new global object michael@0: * has been created. michael@0: * michael@0: * @param aGlobal Debugger.Object michael@0: * The new global object that was created. michael@0: */ michael@0: onNewGlobal: function ADA_onNewGlobal(aGlobal) { michael@0: if (this._checkGlobal(aGlobal)) { michael@0: this.addDebuggee(aGlobal); michael@0: // Notify the client. michael@0: this.conn.send({ michael@0: from: this.actorID, michael@0: type: "newGlobal", michael@0: // TODO: after bug 801084 lands see if we need to JSONify this. michael@0: hostAnnotations: aGlobal.hostAnnotations michael@0: }); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Checks if the provided global belongs to the debugged add-on. michael@0: * michael@0: * @param aGlobal Debugger.Object michael@0: */ michael@0: _checkGlobal: function ADA_checkGlobal(aGlobal) { michael@0: let obj = null; michael@0: try { michael@0: obj = aGlobal.unsafeDereference(); michael@0: } michael@0: catch (e) { michael@0: // Because of bug 991399 we sometimes get bad objects here. If we can't michael@0: // dereference them then they won't be useful to us michael@0: return false; michael@0: } michael@0: michael@0: try { michael@0: // This will fail for non-Sandbox objects, hence the try-catch block. michael@0: let metadata = Cu.getSandboxMetadata(obj); michael@0: if (metadata) { michael@0: return metadata.addonID === this.addonID; michael@0: } michael@0: } catch (e) { michael@0: } michael@0: michael@0: if (obj instanceof Ci.nsIDOMWindow) { michael@0: let id = {}; michael@0: if (mapURIToAddonID(obj.document.documentURIObject, id)) { michael@0: return id.value === this.addonID; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: // Check the global for a __URI__ property and then try to map that to an michael@0: // add-on michael@0: let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__"); michael@0: if (uridescriptor && "value" in uridescriptor && uridescriptor.value) { michael@0: let uri; michael@0: try { michael@0: uri = Services.io.newURI(uridescriptor.value, null, null); michael@0: } michael@0: catch (e) { michael@0: DevToolsUtils.reportException("AddonThreadActor.prototype._checkGlobal", michael@0: new Error("Invalid URI: " + uridescriptor.value)); michael@0: return false; michael@0: } michael@0: michael@0: let id = {}; michael@0: if (mapURIToAddonID(uri, id)) { michael@0: return id.value === this.addonID; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: }); michael@0: michael@0: AddonThreadActor.prototype.requestTypes = Object.create(ThreadActor.prototype.requestTypes); michael@0: update(AddonThreadActor.prototype.requestTypes, { michael@0: "attach": AddonThreadActor.prototype.onAttach michael@0: }); michael@0: michael@0: /** michael@0: * Manages the sources for a thread. Handles source maps, locations in the michael@0: * sources, etc for ThreadActors. michael@0: */ michael@0: function ThreadSources(aThreadActor, aUseSourceMaps, aAllowPredicate, michael@0: aOnNewSource) { michael@0: this._thread = aThreadActor; michael@0: this._useSourceMaps = aUseSourceMaps; michael@0: this._allow = aAllowPredicate; michael@0: this._onNewSource = aOnNewSource; michael@0: michael@0: // generated source url --> promise of SourceMapConsumer michael@0: this._sourceMapsByGeneratedSource = Object.create(null); michael@0: // original source url --> promise of SourceMapConsumer michael@0: this._sourceMapsByOriginalSource = Object.create(null); michael@0: // source url --> SourceActor michael@0: this._sourceActors = Object.create(null); michael@0: // original url --> generated url michael@0: this._generatedUrlsByOriginalUrl = Object.create(null); michael@0: } michael@0: michael@0: /** michael@0: * Must be a class property because it needs to persist across reloads, same as michael@0: * the breakpoint store. michael@0: */ michael@0: ThreadSources._blackBoxedSources = new Set(["self-hosted"]); michael@0: ThreadSources._prettyPrintedSources = new Map(); michael@0: michael@0: ThreadSources.prototype = { michael@0: /** michael@0: * Return the source actor representing |url|, creating one if none michael@0: * exists already. Returns null if |url| is not allowed by the 'allow' michael@0: * predicate. michael@0: * michael@0: * Right now this takes a URL, but in the future it should michael@0: * take a Debugger.Source. See bug 637572. michael@0: * michael@0: * @param String url michael@0: * The source URL. michael@0: * @param optional SourceMapConsumer sourceMap michael@0: * The source map that introduced this source, if any. michael@0: * @param optional String generatedSource michael@0: * The generated source url that introduced this source via source map, michael@0: * if any. michael@0: * @param optional String text michael@0: * The text content of the source, if immediately available. michael@0: * @param optional String contentType michael@0: * The content type of the source, if immediately available. michael@0: * @returns a SourceActor representing the source at aURL or null. michael@0: */ michael@0: source: function ({ url, sourceMap, generatedSource, text, contentType }) { michael@0: if (!this._allow(url)) { michael@0: return null; michael@0: } michael@0: michael@0: if (url in this._sourceActors) { michael@0: return this._sourceActors[url]; michael@0: } michael@0: michael@0: let actor = new SourceActor({ michael@0: url: url, michael@0: thread: this._thread, michael@0: sourceMap: sourceMap, michael@0: generatedSource: generatedSource, michael@0: text: text, michael@0: contentType: contentType michael@0: }); michael@0: this._thread.threadLifetimePool.addActor(actor); michael@0: this._sourceActors[url] = actor; michael@0: try { michael@0: this._onNewSource(actor); michael@0: } catch (e) { michael@0: reportError(e); michael@0: } michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * Only to be used when we aren't source mapping. michael@0: */ michael@0: _sourceForScript: function (aScript) { michael@0: const spec = { michael@0: url: aScript.url michael@0: }; michael@0: michael@0: // XXX bug 915433: We can't rely on Debugger.Source.prototype.text if the michael@0: // source is an HTML-embedded