From b06c454002b915769d01a0ea66f5213e2932fa89 Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Wed, 26 Sep 2012 18:02:04 +0100 Subject: [PATCH] Bug 768096 - Web Console remote debugging protocol support - Part 2: window.console API and JS evaluation; r=past,robcee --- browser/devtools/webconsole/HUDService.jsm | 16 +- browser/devtools/webconsole/PropertyPanel.jsm | 220 ++--- ...console_bug_632347_iterators_generators.js | 2 +- ...owser_webconsole_bug_659907_console_dir.js | 2 +- .../test/browser_webconsole_jsterm.js | 10 +- browser/devtools/webconsole/webconsole.js | 620 ++++++++------ toolkit/devtools/debugger/dbg-client.jsm | 15 + .../devtools/webconsole/WebConsoleClient.jsm | 69 ++ .../devtools/webconsole/WebConsoleUtils.jsm | 779 +++++++++++++++++- .../webconsole/dbg-webconsole-actors.js | 491 ++++++++++- 10 files changed, 1828 insertions(+), 396 deletions(-) diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index 0762942d6a9..945e21c1b5b 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -536,12 +536,8 @@ WebConsole.prototype = { * @private * @type array */ - _messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI", - "WebConsole:CachedMessages", "WebConsole:Initialized", "JSTerm:EvalResult", - "JSTerm:AutocompleteProperties", "JSTerm:ClearOutput", - "JSTerm:InspectObject", "WebConsole:NetworkActivity", - "WebConsole:FileActivity", "WebConsole:LocationChange", - "JSTerm:NonNativeConsoleAPI"], + _messageListeners: ["WebConsole:Initialized", "WebConsole:NetworkActivity", + "WebConsole:FileActivity", "WebConsole:LocationChange"], /** * The xul:panel that holds the Web Console when it is positioned as a window. @@ -901,10 +897,10 @@ WebConsole.prototype = { /** * The clear output button handler. + * @private */ - onClearButton: function WC_onClearButton() + _onClearButton: function WC__onClearButton() { - this.ui.jsterm.clearOutput(true); this.chromeWindow.DeveloperToolbar.resetErrorsCount(this.tab); }, @@ -924,10 +920,8 @@ WebConsole.prototype = { }, this); let message = { - features: ["ConsoleAPI", "JSTerm", "NetworkMonitor", "LocationChange"], - cachedMessages: ["ConsoleAPI", "PageError"], + features: ["NetworkMonitor", "LocationChange"], NetworkMonitor: { monitorFileActivity: true }, - JSTerm: { notifyNonNativeConsoleAPI: true }, preferences: { "NetworkMonitor.saveRequestAndResponseBodies": this.ui.saveRequestAndResponseBodies, diff --git a/browser/devtools/webconsole/PropertyPanel.jsm b/browser/devtools/webconsole/PropertyPanel.jsm index 6fcbdd124d3..38c36364616 100644 --- a/browser/devtools/webconsole/PropertyPanel.jsm +++ b/browser/devtools/webconsole/PropertyPanel.jsm @@ -27,7 +27,7 @@ var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"]; */ var PropertyTreeView = function() { this._rows = []; - this._objectCache = {}; + this._objectActors = []; }; PropertyTreeView.prototype = { @@ -44,10 +44,24 @@ PropertyTreeView.prototype = { _treeBox: null, /** - * Stores cached information about local objects being inspected. + * Track known object actor IDs. We clean these when the panel is + * destroyed/cleaned up. + * * @private + * @type array */ - _objectCache: null, + _objectActors: null, + + /** + * Map fake object actors to their IDs. This is used when we inspect local + * objects. + * @private + * @type Object + */ + _localObjectActors: null, + + _releaseObject: null, + _objectPropertiesProvider: null, /** * Use this setter to update the content of the tree. @@ -58,54 +72,47 @@ PropertyTreeView.prototype = { * - object: * This is the raw object you want to display. You can only provide * this object if you want the property panel to work in sync mode. - * - remoteObject: + * - objectProperties: * An array that holds information on the remote object being * inspected. Each element in this array describes each property in the - * remote object. See WebConsoleUtils.namesAndValuesOf() for details. - * - rootCacheId: - * The cache ID where the objects referenced in remoteObject are found. - * - panelCacheId: - * The cache ID where any object retrieved by this property panel - * instance should be stored into. - * - remoteObjectProvider: + * remote object. See WebConsoleUtils.inspectObject() for details. + * - objectPropertiesProvider: * A function that is invoked when a new object is needed. This is * called when the user tries to expand an inspectable property. The * callback must take four arguments: - * - fromCacheId: - * Tells from where to retrieve the object the user picked (from - * which cache ID). - * - objectId: - * The object ID the user wants. - * - panelCacheId: - * Tells in which cache ID to store the objects referenced by - * objectId so they can be retrieved later. + * - actorID: + * The object actor ID from which we request the properties. * - callback: * The callback function to be invoked when the remote object is - * received. This function takes one argument: the raw message - * received from the Web Console content script. + * received. This function takes one argument: the array of + * descriptors for each property in the object represented by the + * actor. + * - releaseObject: + * Function to invoke when an object actor should be released. The + * function must take one argument: the object actor ID. */ set data(aData) { let oldLen = this._rows.length; - this._cleanup(); + this.cleanup(); if (!aData) { return; } - if (aData.remoteObject) { - this._rootCacheId = aData.rootCacheId; - this._panelCacheId = aData.panelCacheId; - this._remoteObjectProvider = aData.remoteObjectProvider; - this._rows = [].concat(aData.remoteObject); - this._updateRemoteObject(this._rows, 0); + if (aData.objectPropertiesProvider) { + this._objectPropertiesProvider = aData.objectPropertiesProvider; + this._releaseObject = aData.releaseObject; + this._propertiesToRows(aData.objectProperties, 0); + this._rows = aData.objectProperties; } else if (aData.object) { + this._localObjectActors = Object.create(null); this._rows = this._inspectObject(aData.object); } else { - throw new Error("First argument must have a .remoteObject or " + - "an .object property!"); + throw new Error("First argument must have an objectActor or an " + + "object property!"); } if (this._treeBox) { @@ -128,13 +135,22 @@ PropertyTreeView.prototype = { * @param number aLevel * The level you want to give to each property in the remote object. */ - _updateRemoteObject: function PTV__updateRemoteObject(aObject, aLevel) + _propertiesToRows: function PTV__propertiesToRows(aObject, aLevel) { - aObject.forEach(function(aElement) { - aElement.level = aLevel; - aElement.isOpened = false; - aElement.children = null; - }); + aObject.forEach(function(aItem) { + aItem._level = aLevel; + aItem._open = false; + aItem._children = null; + + if (this._releaseObject) { + ["value", "get", "set"].forEach(function(aProp) { + let val = aItem[aProp]; + if (val && val.actor) { + this._objectActors.push(val.actor); + } + }, this); + } + }, this); }, /** @@ -143,42 +159,53 @@ PropertyTreeView.prototype = { * @private * @param object aObject * The object you want to inspect. + * @return array + * The array of properties, each being described in a way that is + * usable by the tree view. */ _inspectObject: function PTV__inspectObject(aObject) { - this._objectCache = {}; - this._remoteObjectProvider = this._localObjectProvider.bind(this); - let children = WebConsoleUtils.namesAndValuesOf(aObject, this._objectCache); - this._updateRemoteObject(children, 0); + this._objectPropertiesProvider = this._localPropertiesProvider.bind(this); + let children = + WebConsoleUtils.inspectObject(aObject, this._localObjectGrip.bind(this)); + this._propertiesToRows(children, 0); return children; }, /** - * An object provider for when the user inspects local objects (not remote + * Make a local fake object actor for the given object. + * + * @private + * @param object aObject + * The object to make an actor for. + * @return object + * The fake actor grip that represents the given object. + */ + _localObjectGrip: function PTV__localObjectGrip(aObject) + { + let grip = WebConsoleUtils.getObjectGrip(aObject); + grip.actor = "obj" + gSequenceId(); + this._localObjectActors[grip.actor] = aObject; + return grip; + }, + + /** + * A properties provider for when the user inspects local objects (not remote * ones). * * @private - * @param string aFromCacheId - * The cache ID from where to retrieve the desired object. - * @param string aObjectId - * The ID of the object you want. - * @param string aDestCacheId - * The ID of the cache where to store any objects referenced by the - * desired object. + * @param string aActor + * The ID of the object actor you want. * @param function aCallback - * The function you want to receive the object. + * The function you want to receive the list of properties. */ - _localObjectProvider: - function PTV__localObjectProvider(aFromCacheId, aObjectId, aDestCacheId, - aCallback) + _localPropertiesProvider: + function PTV__localPropertiesProvider(aActor, aCallback) { - let object = WebConsoleUtils.namesAndValuesOf(this._objectCache[aObjectId], - this._objectCache); - aCallback({cacheId: aFromCacheId, - objectId: aObjectId, - object: object, - childrenCacheId: aDestCacheId || aFromCacheId, - }); + let object = this._localObjectActors[aActor]; + let properties = + WebConsoleUtils.inspectObject(object, this._localObjectGrip.bind(this)); + aCallback(properties); }, /** nsITreeView interface implementation **/ @@ -187,18 +214,20 @@ PropertyTreeView.prototype = { get rowCount() { return this._rows.length; }, setTree: function(treeBox) { this._treeBox = treeBox; }, - getCellText: function(idx, column) { + getCellText: function PTV_getCellText(idx, column) + { let row = this._rows[idx]; - return row.name + ": " + row.value; + return row.name + ": " + WebConsoleUtils.getPropertyPanelValue(row); }, getLevel: function(idx) { - return this._rows[idx].level; + return this._rows[idx]._level; }, isContainer: function(idx) { - return !!this._rows[idx].inspectable; + return typeof this._rows[idx].value == "object" && this._rows[idx].value && + this._rows[idx].value.inspectable; }, isContainerOpen: function(idx) { - return this._rows[idx].isOpened; + return this._rows[idx]._open; }, isContainerEmpty: function(idx) { return false; }, isSeparator: function(idx) { return false; }, @@ -221,22 +250,22 @@ PropertyTreeView.prototype = { hasNextSibling: function(idx, after) { - var thisLevel = this.getLevel(idx); - return this._rows.slice(after + 1).some(function (r) r.level == thisLevel); + let thisLevel = this.getLevel(idx); + return this._rows.slice(after + 1).some(function (r) r._level == thisLevel); }, toggleOpenState: function(idx) { let item = this._rows[idx]; - if (!item.inspectable) { + if (!this.isContainer(idx)) { return; } - if (item.isOpened) { + if (item._open) { this._treeBox.beginUpdateBatch(); - item.isOpened = false; + item._open = false; - var thisLevel = item.level; + var thisLevel = item._level; var t = idx + 1, deleteCount = 0; while (t < this._rows.length && this.getLevel(t++) > thisLevel) { deleteCount++; @@ -251,31 +280,27 @@ PropertyTreeView.prototype = { } else { let levelUpdate = true; - let callback = function _onRemoteResponse(aResponse) { + let callback = function _onRemoteResponse(aProperties) { this._treeBox.beginUpdateBatch(); - item.isOpened = true; - if (levelUpdate) { - this._updateRemoteObject(aResponse.object, item.level + 1); - item.children = aResponse.object; + this._propertiesToRows(aProperties, item._level + 1); + item._children = aProperties; } - this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item.children)); + this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item._children)); - this._treeBox.rowCountChanged(idx + 1, item.children.length); + this._treeBox.rowCountChanged(idx + 1, item._children.length); this._treeBox.invalidateRow(idx); this._treeBox.endUpdateBatch(); + item._open = true; }.bind(this); - if (!item.children) { - let fromCacheId = item.level > 0 ? this._panelCacheId : - this._rootCacheId; - this._remoteObjectProvider(fromCacheId, item.objectId, - this._panelCacheId, callback); + if (!item._children) { + this._objectPropertiesProvider(item.value.actor, callback); } else { levelUpdate = false; - callback({object: item.children}); + callback(item._children); } } }, @@ -298,18 +323,23 @@ PropertyTreeView.prototype = { drop: function(index, orientation, dataTransfer) { }, canDrop: function(index, orientation, dataTransfer) { return false; }, - _cleanup: function PTV__cleanup() + /** + * Cleanup the property tree view. + */ + cleanup: function PTV_cleanup() { - if (this._rows.length) { - // Reset the existing _rows children to the initial state. - this._updateRemoteObject(this._rows, 0); - this._rows = []; + if (this._releaseObject) { + this._objectActors.forEach(this._releaseObject); + delete this._objectPropertiesProvider; + delete this._releaseObject; + } + if (this._localObjectActors) { + delete this._localObjectActors; + delete this._objectPropertiesProvider; } - delete this._objectCache; - delete this._rootCacheId; - delete this._panelCacheId; - delete this._remoteObjectProvider; + this._rows = []; + this._objectActors = []; }, }; @@ -459,3 +489,9 @@ PropertyPanel.prototype.destroy = function PP_destroy() this.tree = null; } + +function gSequenceId() +{ + return gSequenceId.n++; +} +gSequenceId.n = 0; diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js index 589f7aff57e..2859de03206 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js @@ -117,7 +117,7 @@ function testPropertyPanel(aPanel) { ok(find("iter1: Iterator", false), "iter1 is correctly displayed in the Property Panel"); - ok(find("iter2: Iterator", false), + ok(find("iter2: Object", false), "iter2 is correctly displayed in the Property Panel"); executeSoon(finishTest); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js index 365797b552e..46ea40497f6 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js @@ -41,7 +41,7 @@ function testConsoleDir(outputNode) { if (text == "querySelectorAll: function querySelectorAll()") { foundQSA = true; } - else if (text == "location: Object") { + else if (text == "location: Location") { foundLocation = true; } else if (text == "write: function write()") { diff --git a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js index 0c1fe6e2d0b..323f9402147 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js +++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js @@ -111,10 +111,10 @@ function testJSTerm(hud) let foundTab = null; waitForSuccess({ - name: "help tab opened", + name: "help tabs opened", validatorFn: function() { - let newTabOpen = gBrowser.tabs.length == tabs + 1; + let newTabOpen = gBrowser.tabs.length == tabs + 3; if (!newTabOpen) { return false; } @@ -124,7 +124,9 @@ function testJSTerm(hud) }, successFn: function() { - gBrowser.removeTab(foundTab); + gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); nextTest(); }, failureFn: nextTest, @@ -176,7 +178,7 @@ function testJSTerm(hud) jsterm.clearOutput(); jsterm.execute("pprint(print)"); checkResult(function(nodes) { - return nodes[0].textContent.indexOf("aJSTerm.") > -1; + return nodes[0].textContent.indexOf("aOwner.helperResult") > -1; }, "pprint(function) shows source", 1); yield; diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index b034661fb82..ab9742080f9 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -49,6 +49,8 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/en/Security/MixedContent"; +const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; + // The amount of time in milliseconds that must pass between messages to // trigger the display of a new group. const NEW_GROUP_DELAY = 5000; @@ -307,6 +309,12 @@ WebConsoleFrame.prototype = { */ filterBox: null, + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null, + _saveRequestAndResponseBodies: false, /** @@ -413,8 +421,10 @@ WebConsoleFrame.prototype = { this.owner.onCloseButton.bind(this.owner)); let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0]; - clearButton.addEventListener("command", - this.owner.onClearButton.bind(this.owner)); + clearButton.addEventListener("command", function WCF__onClearButton() { + this.owner._onClearButton(); + this.jsterm.clearOutput(true); + }.bind(this)); }, /** @@ -657,27 +667,9 @@ WebConsoleFrame.prototype = { } switch (aMessage.name) { - case "JSTerm:EvalResult": - case "JSTerm:EvalObject": - case "JSTerm:AutocompleteProperties": - this.owner._receiveMessageWithCallback(aMessage.json); - break; - case "JSTerm:ClearOutput": - this.jsterm.clearOutput(); - break; - case "JSTerm:InspectObject": - this.jsterm.handleInspectObject(aMessage.json); - break; - case "WebConsole:ConsoleAPI": - this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, - [aMessage.json]); - break; case "WebConsole:Initialized": this._onMessageManagerInitComplete(); break; - case "WebConsole:CachedMessages": - this._displayCachedConsoleMessages(aMessage.json.messages); - break; case "WebConsole:NetworkActivity": this.handleNetworkActivity(aMessage.json); break; @@ -688,9 +680,6 @@ WebConsoleFrame.prototype = { case "WebConsole:LocationChange": this.owner.onLocationChange(aMessage.json); break; - case "JSTerm:NonNativeConsoleAPI": - this.outputMessage(CATEGORY_JS, this.logWarningAboutReplacedAPI); - break; } }, @@ -1033,13 +1022,11 @@ WebConsoleFrame.prototype = { * Display cached messages that may have been collected before the UI is * displayed. * - * @private * @param array aRemoteMessages * Array of cached messages coming from the remote Web Console * content instance. */ - _displayCachedConsoleMessages: - function WCF__displayCachedConsoleMessages(aRemoteMessages) + displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages) { if (!aRemoteMessages.length) { return; @@ -1062,19 +1049,11 @@ WebConsoleFrame.prototype = { }, /** - * Logs a message to the Web Console that originates from the remote Web - * Console instance. + * Logs a message to the Web Console that originates from the Web Console + * server. * * @param object aMessage - * The message received from the remote Web Console instance. - * console service. This object needs to hold: - * - hudId - the Web Console ID. - * - apiMessage - a representation of the object sent by the console - * storage service. This object holds the console message level, the - * arguments that were passed to the console method and other - * information. - * - argumentsToString - the array of arguments passed to the console - * method, each converted to a string. + * The message received from the server. * @return nsIDOMElement|undefined * The message element to display in the Web Console output. */ @@ -1084,9 +1063,9 @@ WebConsoleFrame.prototype = { let clipboardText = null; let sourceURL = null; let sourceLine = 0; - let level = aMessage.apiMessage.level; - let args = aMessage.apiMessage.arguments; - let argsToString = aMessage.argumentsToString; + let level = aMessage.level; + let args = aMessage.arguments; + let objectActors = []; switch (level) { case "log": @@ -1094,17 +1073,36 @@ WebConsoleFrame.prototype = { case "warn": case "error": case "debug": - body = { - cacheId: aMessage.objectsCacheId, - remoteObjects: args, - argsToString: argsToString, - }; - clipboardText = argsToString.join(" "); - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; - break; + case "dir": + case "groupEnd": { + body = { arguments: args }; + let clipboardArray = []; + args.forEach(function(aValue) { + clipboardArray.push(WebConsoleUtils.objectActorGripToString(aValue)); + if (aValue && typeof aValue == "object" && aValue.actor) { + objectActors.push(aValue.actor); + } + }, this); + clipboardText = clipboardArray.join(" "); + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; - case "trace": + if (level == "dir") { + body.objectProperties = aMessage.objectProperties; + } + else if (level == "groupEnd") { + objectActors.forEach(this._releaseObject, this); + + if (this.groupDepth > 0) { + this.groupDepth--; + } + return; // no need to continue + } + + break; + } + + case "trace": { let filename = WebConsoleUtils.abbreviateSourceURL(args[0].filename); let functionName = args[0].functionName || l10n.getStr("stacktrace.anonymousFunction"); @@ -1126,34 +1124,16 @@ WebConsoleFrame.prototype = { clipboardText = clipboardText.trimRight(); break; - - case "dir": - body = { - cacheId: aMessage.objectsCacheId, - resultString: argsToString[0], - remoteObject: args[0], - remoteObjectProvider: - this.jsterm.remoteObjectProvider.bind(this.jsterm), - }; - clipboardText = body.resultString; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; - break; + } case "group": case "groupCollapsed": clipboardText = body = args; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; this.groupDepth++; break; - case "groupEnd": - if (this.groupDepth > 0) { - this.groupDepth--; - } - return; - case "time": if (!args) { return; @@ -1164,8 +1144,8 @@ WebConsoleFrame.prototype = { } body = l10n.getFormatStr("timerStarted", [args.name]); clipboardText = body; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; break; case "timeEnd": @@ -1174,8 +1154,8 @@ WebConsoleFrame.prototype = { } body = l10n.getFormatStr("timeEnd", [args.name, args.duration]); clipboardText = body; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; break; default: @@ -1187,6 +1167,10 @@ WebConsoleFrame.prototype = { sourceURL, sourceLine, clipboardText, level, aMessage.timeStamp); + if (objectActors.length) { + node._objectActors = objectActors; + } + // Make the node bring up the property panel, to allow the user to inspect // the stack trace. if (level == "trace") { @@ -1208,10 +1192,6 @@ WebConsoleFrame.prototype = { } if (level == "dir") { - // Make sure the cached evaluated object will be purged when the node is - // removed. - node._evalCacheId = aMessage.objectsCacheId; - // Initialize the inspector message node, by setting the PropertyTreeView // object on the tree view. This has to be done *after* the node is // shown, because the tree binding must be attached first. @@ -1223,6 +1203,18 @@ WebConsoleFrame.prototype = { return node; }, + /** + * Handle ConsoleAPICall objects received from the server. This method outputs + * the window.console API call. + * + * @param object aMessage + * The console API message received from the server. + */ + handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage) + { + this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]); + }, + /** * The click event handler for objects shown inline coming from the * window.console API. @@ -1233,11 +1225,11 @@ WebConsoleFrame.prototype = { * @param nsIDOMNode aAnchor * The object inspector anchor element. This is the clickable element * in the console.log message we display. - * @param array aRemoteObject - * The remote object representation. + * @param object aObjectActor + * The object actor grip. */ _consoleLogClick: - function WCF__consoleLogClick(aMessage, aAnchor, aRemoteObject) + function WCF__consoleLogClick(aMessage, aAnchor, aObjectActor) { if (aAnchor._panelOpen) { return; @@ -1249,29 +1241,28 @@ WebConsoleFrame.prototype = { // Data to inspect. data: { - // This is where the resultObject children are cached. - rootCacheId: aMessage._evalCacheId, - remoteObject: aRemoteObject, - // This is where all objects retrieved by the panel will be cached. - panelCacheId: "HUDPanel-" + gSequenceId(), - remoteObjectProvider: this.jsterm.remoteObjectProvider.bind(this.jsterm), + objectPropertiesProvider: this.objectPropertiesProvider.bind(this), + releaseObject: this._releaseObject.bind(this), }, }; - let propPanel = this.jsterm.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); - - let onPopupHide = function JST__evalInspectPopupHide() { + let propPanel; + let onPopupHide = function _onPopupHide() { propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - this.jsterm.clearObjectCache(options.data.panelCacheId); - - if (!aMessage.parentNode && aMessage._evalCacheId) { - this.jsterm.clearObjectCache(aMessage._evalCacheId); + if (!aMessage.parentNode && aMessage._objectActors) { + aMessage._objectActors.forEach(this._releaseObject, this); + aMessage._objectActors = null; } }.bind(this); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + this.objectPropertiesProvider(aObjectActor.actor, + function _onObjectProperties(aProperties) { + options.data.objectProperties = aProperties; + propPanel = this.jsterm.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }.bind(this)); }, /** @@ -1453,14 +1444,12 @@ WebConsoleFrame.prototype = { /** * Inform user that the Web Console API has been replaced by a script * in a content page. - * - * @return nsIDOMElement|undefined - * The message element to display in the Web Console output. */ logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI() { - return this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, - l10n.getStr("ConsoleAPIDisabled")); + let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, + l10n.getStr("ConsoleAPIDisabled")); + this.outputMessage(CATEGORY_JS, node); }, /** @@ -1852,8 +1841,8 @@ WebConsoleFrame.prototype = { { let [category, methodOrNode, args] = aItem; if (typeof methodOrNode != "function" && - methodOrNode._evalCacheId && !methodOrNode._panelOpen) { - this.jsterm.clearObjectCache(methodOrNode._evalCacheId); + methodOrNode._objectActors && !methodOrNode._panelOpen) { + methodOrNode._objectActors.forEach(this._releaseObject, this); } if (category == CATEGORY_NETWORK) { @@ -1870,9 +1859,29 @@ WebConsoleFrame.prototype = { } else if (category == CATEGORY_WEBDEV && methodOrNode == this.logConsoleAPIMessage) { - let level = args[0].apiMessage.level; - if (level == "dir") { - this.jsterm.clearObjectCache(args[0].objectsCacheId); + let level = args[0].level; + let releaseObject = function _releaseObject(aValue) { + if (aValue && typeof aValue == "object" && aValue.actor) { + this._releaseObject(aValue.actor); + } + }.bind(this); + switch (level) { + case "log": + case "info": + case "warn": + case "error": + case "debug": + case "dir": + case "groupEnd": { + args[0].arguments.forEach(releaseObject); + if (level == "dir") { + args[0].objectProperties.forEach(function(aObject) { + ["value", "get", "set"].forEach(function(aProp) { + releaseObject(aObject[aProp]); + }); + }); + } + } } } }, @@ -1916,10 +1925,8 @@ WebConsoleFrame.prototype = { let tree = aMessageNode.querySelector("tree"); tree.parentNode.removeChild(tree); + aMessageNode.propertyTreeView.data = null; aMessageNode.propertyTreeView = null; - if (tree.view) { - tree.view.data = null; - } tree.view = null; }, @@ -1931,8 +1938,8 @@ WebConsoleFrame.prototype = { */ removeOutputMessage: function WCF_removeOutputMessage(aNode) { - if (aNode._evalCacheId && !aNode._panelOpen) { - this.jsterm.clearObjectCache(aNode._evalCacheId); + if (aNode._objectActors && !aNode._panelOpen) { + aNode._objectActors.forEach(this._releaseObject, this); } if (aNode.classList.contains("webconsole-msg-cssparser")) { @@ -2057,7 +2064,7 @@ WebConsoleFrame.prototype = { else { let str = undefined; if (aLevel == "dir") { - str = aBody.resultString; + str = WebConsoleUtils.objectActorGripToString(aBody.arguments[0]); } else if (["log", "info", "warn", "error", "debug"].indexOf(aLevel) > -1 && typeof aBody == "object") { @@ -2132,10 +2139,9 @@ WebConsoleFrame.prototype = { let treeView = node.propertyTreeView = new PropertyTreeView(); treeView.data = { - rootCacheId: body.cacheId, - panelCacheId: body.cacheId, - remoteObject: Array.isArray(body.remoteObject) ? body.remoteObject : [], - remoteObjectProvider: body.remoteObjectProvider, + objectPropertiesProvider: this.objectPropertiesProvider.bind(this), + releaseObject: this._releaseObject.bind(this), + objectProperties: body.objectProperties, }; tree.setAttribute("rows", treeView.rowCount); @@ -2164,13 +2170,12 @@ WebConsoleFrame.prototype = { * output. * @param object aBody * The object given by this.logConsoleAPIMessage(). This object holds - * the call information that we need to display. + * the call information that we need to display - mainly the arguments + * array of the given API call. */ _makeConsoleLogMessageBody: function WCF__makeConsoleLogMessageBody(aMessage, aContainer, aBody) { - aMessage._evalCacheId = aBody.cacheId; - Object.defineProperty(aMessage, "_panelOpen", { get: function() { let nodes = aContainer.querySelectorAll(".hud-clickable"); @@ -2182,17 +2187,19 @@ WebConsoleFrame.prototype = { configurable: false }); - aBody.remoteObjects.forEach(function(aItem, aIndex) { + aBody.arguments.forEach(function(aItem) { if (aContainer.firstChild) { aContainer.appendChild(this.document.createTextNode(" ")); } - let text = aBody.argsToString[aIndex]; - if (!Array.isArray(aItem)) { + let text = WebConsoleUtils.objectActorGripToString(aItem); + + if (aItem && typeof aItem != "object" || !aItem.inspectable) { aContainer.appendChild(this.document.createTextNode(text)); return; } + // For inspectable objects. let elem = this.document.createElement("description"); elem.classList.add("hud-clickable"); elem.setAttribute("aria-haspopup", "true"); @@ -2393,6 +2400,41 @@ WebConsoleFrame.prototype = { clipboardHelper.copyString(strings.join("\n"), this.document); }, + /** + * Object properties provider. This function gives you the properties of the + * remote object you want. + * + * @param string aActor + * The object actor ID from which you want the properties. + * @param function aCallback + * Function you want invoked once the properties are received. + */ + objectPropertiesProvider: + function WCF_objectPropertiesProvider(aActor, aCallback) + { + this.webConsoleClient.inspectObjectProperties(aActor, + function(aResponse) { + if (aResponse.error) { + Cu.reportError("Failed to retrieve the object properties from the " + + "server. Error: " + aResponse.error); + return; + } + aCallback(aResponse.properties); + }); + }, + + /** + * Release an object actor. + * + * @private + * @param string aActor + * The object actor ID you want to release. + */ + _releaseObject: function WCF__releaseObject(aActor) + { + this.proxy.releaseActor(aActor); + }, + /** * Open the selected item's URL in a new tab. */ @@ -2478,6 +2520,12 @@ JSTerm.prototype = { */ get outputNode() this.hud.outputNode, + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() this.hud.webConsoleClient, + COMPLETE_FORWARD: 0, COMPLETE_BACKWARD: 1, COMPLETE_HINT_ONLY: 2, @@ -2499,59 +2547,59 @@ JSTerm.prototype = { }, /** - * Asynchronously evaluate a string in the content process sandbox. - * - * @param string aString - * String to evaluate in the content process JavaScript sandbox. - * @param function [aCallback] - * Optional function to be invoked when the evaluation result is - * received. - */ - evalInContentSandbox: function JST_evalInContentSandbox(aString, aCallback) - { - let message = { - str: aString, - resultCacheId: "HUDEval-" + gSequenceId(), - }; - - this.hud.owner.sendMessageToContent("JSTerm:EvalRequest", message, aCallback); - - return message; - }, - - /** - * The "JSTerm:EvalResult" message handler. This is the JSTerm execution - * result callback which is invoked whenever JavaScript code evaluation - * results come from the content process. + * The JavaScript evaluation response handler. * * @private + * @param nsIDOMElement [aAfterNode] + * Optional DOM element after which the evaluation result will be + * inserted. * @param function [aCallback] * Optional function to invoke when the evaluation result is added to * the output. * @param object aResponse - * The JSTerm:EvalResult message received from the content process. See - * JSTerm.handleEvalRequest() in HUDService-content.js for further - * details. - * @param object aRequest - * The JSTerm:EvalRequest message we sent to the content process. - * @see JSTerm.handleEvalRequest() in HUDService-content.js + * The message received from the server. */ _executeResultCallback: - function JST__executeResultCallback(aCallback, aResponse, aRequest) + function JST__executeResultCallback(aAfterNode, aCallback, aResponse) { let errorMessage = aResponse.errorMessage; - let resultString = aResponse.resultString; + let result = aResponse.result; + let inspectable = result && typeof result == "object" && result.inspectable; + let helperResult = aResponse.helperResult; + let helperHasRawOutput = !!(helperResult || {}).rawOutput; + let resultString = + WebConsoleUtils.objectActorGripToString(result, + !helperHasRawOutput); - // Hide undefined results coming from JSTerm helper functions. - if (!errorMessage && - resultString == "undefined" && - aResponse.helperResult && - !aResponse.inspectable && - !aResponse.helperRawOutput) { - return; + if (helperResult && helperResult.type) { + switch (helperResult.type) { + case "clearOutput": + this.clearOutput(); + break; + case "inspectObject": + this.handleInspectObject(helperResult.input, helperResult.object); + break; + case "error": + try { + errorMessage = l10n.getStr(helperResult.message); + } + catch (ex) { + errorMessage = helperResult.message; + } + break; + case "help": + this.hud.owner.openLink(HELP_URL); + break; + } } - let afterNode = aRequest.outputNode; + // Hide undefined results coming from JSTerm helper functions. + if (!errorMessage && result && typeof result == "object" && + result.type == "undefined" && + helperResult && !helperHasRawOutput) { + aCallback && aCallback(); + return; + } if (aCallback) { let oldFlushCallback = this.hud._flushCallback; @@ -2562,19 +2610,24 @@ JSTerm.prototype = { }.bind(this); } - if (aResponse.errorMessage) { - this.writeOutput(aResponse.errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR, - afterNode, aResponse.timestamp); + let node; + + if (errorMessage) { + node = this.writeOutput(errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR, + aAfterNode, aResponse.timestamp); } - else if (aResponse.inspectable) { - let node = this.writeOutputJS(aResponse.resultString, - this._evalOutputClick.bind(this, aResponse), - afterNode, aResponse.timestamp); - node._evalCacheId = aResponse.childrenCacheId; + else if (inspectable) { + node = this.writeOutputJS(resultString, + this._evalOutputClick.bind(this, aResponse), + aAfterNode, aResponse.timestamp); } else { - this.writeOutput(aResponse.resultString, CATEGORY_OUTPUT, SEVERITY_LOG, - afterNode, aResponse.timestamp); + node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG, + aAfterNode, aResponse.timestamp); + } + + if (result && typeof result == "object" && result.actor) { + node._objectActors = [result.actor]; } }, @@ -2597,10 +2650,9 @@ JSTerm.prototype = { } let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG); + let onResult = this._executeResultCallback.bind(this, node, aCallback); - let onResult = this._executeResultCallback.bind(this, aCallback); - let messageToContent = this.evalInContentSandbox(aExecuteString, onResult); - messageToContent.outputNode = node; + this.webConsoleClient.evaluateJS(aExecuteString, onResult); this.history.push(aExecuteString); this.historyIndex++; @@ -2751,7 +2803,7 @@ JSTerm.prototype = { hud._cssNodes = {}; if (aClearStorage) { - hud.owner.sendMessageToContent("ConsoleAPI:ClearCache", {}); + this.webConsoleClient.clearMessagesCache(); } }, @@ -3078,25 +3130,30 @@ JSTerm.prototype = { return; } - let message = { - id: "HUDComplete-" + gSequenceId(), - input: this.inputNode.value, - }; + let requestId = gSequenceId(); + let input = this.inputNode.value; + let cursor = this.inputNode.selectionStart; + // TODO: Bug 787986 - throttle/disable updates, deal with slow/high latency + // network connections. this.lastCompletion = { - requestId: message.id, + requestId: requestId, completionType: aType, value: null, }; - let callback = this._receiveAutocompleteProperties.bind(this, aCallback); - this.hud.owner.sendMessageToContent("JSTerm:Autocomplete", message, callback); + + let callback = this._receiveAutocompleteProperties.bind(this, requestId, + aCallback); + this.webConsoleClient.autocomplete(input, cursor, callback); }, /** - * Handler for the "JSTerm:AutocompleteProperties" message. This method takes - * the completion result received from the content process and updates the UI + * Handler for the autocompletion results. This method takes + * the completion result received from the server and updates the UI * accordingly. * + * @param number aRequestId + * Request ID. * @param function [aCallback=null] * Optional, function to invoke when the completion result is received. * @param object aMessage @@ -3104,13 +3161,12 @@ JSTerm.prototype = { * the content process. */ _receiveAutocompleteProperties: - function JST__receiveAutocompleteProperties(aCallback, aMessage) + function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage) { let inputNode = this.inputNode; let inputValue = inputNode.value; - if (aMessage.input != inputValue || - this.lastCompletion.value == inputValue || - aMessage.id != this.lastCompletion.requestId) { + if (this.lastCompletion.value == inputValue || + aRequestId != this.lastCompletion.requestId) { return; } @@ -3266,7 +3322,7 @@ JSTerm.prototype = { }, /** - * The "JSTerm:InspectObject" remote message handler. This allows the content + * The JSTerm InspectObject remote message handler. This allows the remote * process to open the Property Panel for a given object. * * @param object aRequest @@ -3274,29 +3330,31 @@ JSTerm.prototype = { * the user input string that was evaluated to inspect an object and * the result object which is to be inspected. */ - handleInspectObject: function JST_handleInspectObject(aRequest) + handleInspectObject: function JST_handleInspectObject(aInput, aActor) { let options = { - title: aRequest.input, + title: aInput, data: { - rootCacheId: aRequest.objectCacheId, - panelCacheId: aRequest.objectCacheId, - remoteObject: aRequest.resultObject, - remoteObjectProvider: this.remoteObjectProvider.bind(this), + objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud), + releaseObject: this.hud._releaseObject.bind(this.hud), }, }; - let propPanel = this.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); + let propPanel; let onPopupHide = function JST__onPopupHide() { propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - - this.clearObjectCache(options.data.panelCacheId); + this.hud._releaseObject(aActor.actor); }.bind(this); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + this.hud.objectPropertiesProvider(aActor.actor, + function _onObjectProperties(aProperties) { + options.data.objectProperties = aProperties; + propPanel = this.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }.bind(this)); }, /** @@ -3304,7 +3362,7 @@ JSTerm.prototype = { * * @private * @param object aResponse - * The JSTerm:EvalResult message received from the content process. + * The JavaScript evaluation response received from the server. * @param nsIDOMNode aLink * The message node for which we are handling events. */ @@ -3320,35 +3378,36 @@ JSTerm.prototype = { // Data to inspect. data: { - // This is where the resultObject children are cached. - rootCacheId: aResponse.childrenCacheId, - remoteObject: aResponse.resultObject, - // This is where all objects retrieved by the panel will be cached. - panelCacheId: "HUDPanel-" + gSequenceId(), - remoteObjectProvider: this.remoteObjectProvider.bind(this), + objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud), + releaseObject: this.hud._releaseObject.bind(this.hud), }, }; - options.updateButtonCallback = function JST__evalUpdateButton() { - this.evalInContentSandbox(aResponse.input, - this._evalOutputUpdatePanelCallback.bind(this, options, propPanel, - aResponse)); - }.bind(this); + let propPanel; - let propPanel = this.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); + options.updateButtonCallback = function JST__evalUpdateButton() { + let onResult = + this._evalOutputUpdatePanelCallback.bind(this, options, propPanel, + aResponse); + this.webConsoleClient.evaluateJS(aResponse.input, onResult); + }.bind(this); let onPopupHide = function JST__evalInspectPopupHide() { propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - this.clearObjectCache(options.data.panelCacheId); - - if (!aLinkNode.parentNode && aLinkNode._evalCacheId) { - this.clearObjectCache(aLinkNode._evalCacheId); + if (!aLinkNode.parentNode && aLinkNode._objectActors) { + aLinkNode._objectActors.forEach(this.hud._releaseObject, this.hud); + aLinkNode._objectActors = null; } }.bind(this); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + this.hud.objectPropertiesProvider(aResponse.result.actor, + function _onObjectProperties(aProperties) { + options.data.objectProperties = aProperties; + propPanel = this.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }.bind(this)); }, /** @@ -3377,32 +3436,40 @@ JSTerm.prototype = { return; } - if (!aNewResponse.inspectable) { + let result = aNewResponse.result; + let inspectable = result && typeof result == "object" && result.inspectable; + let newActor = result && typeof result == "object" ? result.actor : null; + + let anchor = aOptions.anchor; + if (anchor && newActor) { + if (!anchor._objectActors) { + anchor._objectActors = []; + } + if (anchor._objectActors.indexOf(newActor) == -1) { + anchor._objectActors.push(newActor); + } + } + + if (!inspectable) { this.writeOutput(l10n.getStr("JSTerm.updateNotInspectable"), CATEGORY_OUTPUT, SEVERITY_ERROR); return; } - this.clearObjectCache(aOptions.data.panelCacheId); - this.clearObjectCache(aOptions.data.rootCacheId); - - if (aOptions.anchor && aOptions.anchor._evalCacheId) { - aOptions.anchor._evalCacheId = aNewResponse.childrenCacheId; - } - // Update the old response object such that when the panel is reopen, the // user sees the new response. - aOldResponse.id = aNewResponse.id; - aOldResponse.childrenCacheId = aNewResponse.childrenCacheId; - aOldResponse.resultObject = aNewResponse.resultObject; - aOldResponse.resultString = aNewResponse.resultString; + aOldResponse.result = aNewResponse.result; + aOldResponse.error = aNewResponse.error; + aOldResponse.errorMessage = aNewResponse.errorMessage; + aOldResponse.timestamp = aNewResponse.timestamp; - aOptions.data.rootCacheId = aNewResponse.childrenCacheId; - aOptions.data.remoteObject = aNewResponse.resultObject; - - // TODO: This updates the value of the tree. - // However, the states of open nodes is not saved. - // See bug 586246. - aPropPanel.treeView.data = aOptions.data; + this.hud.objectPropertiesProvider(newActor, + function _onObjectProperties(aProperties) { + aOptions.data.objectProperties = aProperties; + // TODO: This updates the value of the tree. + // However, the states of open nodes is not saved. + // See bug 586246. + aPropPanel.treeView.data = aOptions.data; + }.bind(this)); }, /** @@ -3633,6 +3700,7 @@ function WebConsoleConnectionProxy(aWebConsole) this.owner = aWebConsole; this._onPageError = this._onPageError.bind(this); + this._onConsoleAPICall = this._onConsoleAPICall.bind(this); } WebConsoleConnectionProxy.prototype = { @@ -3666,6 +3734,14 @@ WebConsoleConnectionProxy.prototype = { */ _consoleActor: null, + /** + * Tells if the window.console object of the remote web page is the native + * object or not. + * @private + * @type boolean + */ + _hasNativeConsoleAPI: false, + /** * Initialize the debugger server. */ @@ -3689,8 +3765,9 @@ WebConsoleConnectionProxy.prototype = { let client = this.client = new DebuggerClient(transport); client.addListener("pageError", this._onPageError); + client.addListener("consoleAPICall", this._onConsoleAPICall); - let listeners = ["PageError"]; + let listeners = ["PageError", "ConsoleAPI"]; client.connect(function(aType, aTraits) { client.listTabs(function(aResponse) { @@ -3725,6 +3802,36 @@ WebConsoleConnectionProxy.prototype = { this.webConsoleClient = aWebConsoleClient; + this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI; + + let msgs = ["PageError", "ConsoleAPI"]; + this.webConsoleClient.getCachedMessages(msgs, + this._onCachedMessages.bind(this, aCallback)); + }, + + /** + * The "cachedMessages" response handler. + * + * @private + * @param function [aCallback] + * Optional function to invoke once the connection is established. + * @param object aResponse + * The JSON response object received from the server. + */ + _onCachedMessages: function WCCP__onCachedMessages(aCallback, aResponse) + { + if (aResponse.error) { + Cu.reportError("Web Console getCachedMessages error: " + aResponse.error + + " " + aResponse.message); + return; + } + + this.owner.displayCachedMessages(aResponse.messages); + + if (!this._hasNativeConsoleAPI) { + this.owner.logWarningAboutReplacedAPI(); + } + this.connected = true; aCallback && aCallback(); }, @@ -3746,6 +3853,36 @@ WebConsoleConnectionProxy.prototype = { } }, + /** + * The "consoleAPICall" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string aType + * Message type. + * @param object aPacket + * The message received from the server. + */ + _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket) + { + if (this.owner && aPacket.from == this._consoleActor) { + this.owner.handleConsoleAPICall(aPacket.message); + } + }, + + /** + * Release an object actor. + * + * @param string aActor + * The actor ID to send the request to. + */ + releaseActor: function WCCP_releaseActor(aActor) + { + if (this.client) { + this.client.release(aActor); + } + }, + /** * Disconnect the Web Console from the remote server. * @@ -3760,6 +3897,7 @@ WebConsoleConnectionProxy.prototype = { } this.client.removeListener("pageError", this._onPageError); + this.client.removeListener("consoleAPICall", this._onConsoleAPICall); this.client.close(aOnDisconnect); this.client = null; diff --git a/toolkit/devtools/debugger/dbg-client.jsm b/toolkit/devtools/debugger/dbg-client.jsm index 6598bcafa2d..0c1eb8f321a 100644 --- a/toolkit/devtools/debugger/dbg-client.jsm +++ b/toolkit/devtools/debugger/dbg-client.jsm @@ -167,6 +167,7 @@ const ThreadStateTypes = { * by the client. */ const UnsolicitedNotifications = { + "consoleAPICall": "consoleAPICall", "newScript": "newScript", "tabDetached": "tabDetached", "tabNavigated": "tabNavigated", @@ -371,6 +372,20 @@ DebuggerClient.prototype = { }); }, + /** + * Release an object actor. + * + * @param string aActor + * The actor ID to send the request to. + */ + release: function DC_release(aActor) { + let packet = { + to: aActor, + type: "release", + }; + this.request(packet); + }, + /** * Send a request to the debugging server. * diff --git a/toolkit/devtools/webconsole/WebConsoleClient.jsm b/toolkit/devtools/webconsole/WebConsoleClient.jsm index cf4fd743411..45e7301118f 100644 --- a/toolkit/devtools/webconsole/WebConsoleClient.jsm +++ b/toolkit/devtools/webconsole/WebConsoleClient.jsm @@ -48,6 +48,75 @@ WebConsoleClient.prototype = { this._client.request(packet, aOnResponse); }, + /** + * Inspect the properties of an object. + * + * @param string aActor + * The WebConsoleObjectActor ID to send the request to. + * @param function aOnResponse + * The function invoked when the response is received. + */ + inspectObjectProperties: + function WCC_inspectObjectProperties(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "inspectProperties", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Evaluate a JavaScript expression. + * + * @param string aString + * The code you want to evaluate. + * @param function aOnResponse + * The function invoked when the response is received. + */ + evaluateJS: function WCC_evaluateJS(aString, aOnResponse) + { + let packet = { + to: this._actor, + type: "evaluateJS", + text: aString, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Autocomplete a JavaScript expression. + * + * @param string aString + * The code you want to autocomplete. + * @param number aCursor + * Cursor location inside the string. Index starts from 0. + * @param function aOnResponse + * The function invoked when the response is received. + */ + autocomplete: function WCC_autocomplete(aString, aCursor, aOnResponse) + { + let packet = { + to: this._actor, + type: "autocomplete", + text: aString, + cursor: aCursor, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Clear the cache of messages (page errors and console API calls). + */ + clearMessagesCache: function WCC_clearMessagesCache() + { + let packet = { + to: this._actor, + type: "clearMessagesCache", + }; + this._client.request(packet); + }, + /** * Start the given Web Console listeners. * diff --git a/toolkit/devtools/webconsole/WebConsoleUtils.jsm b/toolkit/devtools/webconsole/WebConsoleUtils.jsm index c600eabf18e..992f0aa9e41 100644 --- a/toolkit/devtools/webconsole/WebConsoleUtils.jsm +++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm @@ -15,8 +15,22 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); -var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", - "PageErrorListener"]; +XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage", + "resource://gre/modules/ConsoleAPIStorage.jsm"); + +var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "JSTermHelpers", + "PageErrorListener", "ConsoleAPIListener"]; + +// Match the function name from the result of toString() or toSource(). +// +// Examples: +// (function foobar(a, b) { ... +// function foobar2(a) { ... +// function() { ... +const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/; + +// Match the function arguments from the result of toString() or toSource(). +const REGEX_MATCH_FUNCTION_ARGS = /^\(?function\s*[^\s(]*\s*\((.+?)\)/; const TYPES = { OBJECT: 0, FUNCTION: 1, @@ -213,7 +227,12 @@ var WebConsoleUtils = { case "error": case "number": case "regexp": - output = aResult.toString(); + try { + output = aResult + ""; + } + catch (ex) { + output = ex; + } break; case "null": case "undefined": @@ -309,8 +328,15 @@ var WebConsoleUtils = { getResultType: function WCU_getResultType(aResult) { let type = aResult === null ? "null" : typeof aResult; - if (type == "object" && aResult.constructor && aResult.constructor.name) { - type = aResult.constructor.name; + try { + if (type == "object" && aResult.constructor && aResult.constructor.name) { + type = aResult.constructor.name + ""; + } + } + catch (ex) { + // Prevent potential exceptions in page-provided objects from taking down + // the Web Console. If the constructor.name is a getter that throws, or + // something else bad happens. } return type.toLowerCase(); @@ -442,27 +468,43 @@ var WebConsoleUtils = { if (typeof aObject != "object") { return false; } - let desc; + let desc = this.getPropertyDescriptor(aObject, aProp); + return desc && desc.get && !this.isNativeFunction(desc.get); + }, + + /** + * Get the property descriptor for the given object. + * + * @param object aObject + * The object that contains the property. + * @param string aProp + * The property you want to get the descriptor for. + * @return object + * Property descriptor. + */ + getPropertyDescriptor: function WCU_getPropertyDescriptor(aObject, aProp) + { + let desc = null; while (aObject) { try { if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) { break; } } - catch (ex) { + catch (ex if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" || + ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" || + ex.name == "TypeError")) { // Native getters throw here. See bug 520882. - if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" || - ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") { - return false; - } - throw ex; + // null throws TypeError. + } + try { + aObject = Object.getPrototypeOf(aObject); + } + catch (ex if (ex.name == "TypeError")) { + return desc; } - aObject = Object.getPrototypeOf(aObject); } - if (desc && desc.get && !this.isNativeFunction(desc.get)) { - return true; - } - return false; + return desc; }, /** @@ -549,37 +591,209 @@ var WebConsoleUtils = { pairs.push(pair); } - pairs.sort(function(a, b) - { - // Convert the pair.name to a number for later sorting. - let aNumber = parseFloat(a.name); - let bNumber = parseFloat(b.name); - - // Sort numbers. - if (!isNaN(aNumber) && isNaN(bNumber)) { - return -1; - } - else if (isNaN(aNumber) && !isNaN(bNumber)) { - return 1; - } - else if (!isNaN(aNumber) && !isNaN(bNumber)) { - return aNumber - bNumber; - } - // Sort string. - else if (a.name < b.name) { - return -1; - } - else if (a.name > b.name) { - return 1; - } - else { - return 0; - } - }); + pairs.sort(this.propertiesSort); return pairs; }, + /** + * Sort function for object properties. + * + * @param object a + * Property descriptor. + * @param object b + * Property descriptor. + * @return integer + * -1 if a.name < b.name, + * 1 if a.name > b.name, + * 0 otherwise. + */ + propertiesSort: function WCU_propertiesSort(a, b) + { + // Convert the pair.name to a number for later sorting. + let aNumber = parseFloat(a.name); + let bNumber = parseFloat(b.name); + + // Sort numbers. + if (!isNaN(aNumber) && isNaN(bNumber)) { + return -1; + } + else if (isNaN(aNumber) && !isNaN(bNumber)) { + return 1; + } + else if (!isNaN(aNumber) && !isNaN(bNumber)) { + return aNumber - bNumber; + } + // Sort string. + else if (a.name < b.name) { + return -1; + } + else if (a.name > b.name) { + return 1; + } + else { + return 0; + } + }, + + /** + * Inspect the properties of the given object. For each property a descriptor + * object is created. The descriptor gives you information about the property + * name, value, type, getter and setter. When the property value references + * another object you get a wrapper that holds information about that object. + * + * @see this.inspectObjectProperty + * @param object aObject + * The object you want to inspect. + * @param function aObjectWrapper + * The function that creates wrappers for property values which + * reference other objects. This function must take one argument, the + * object to wrap, and it must return an object grip that gives + * information about the referenced object. + * @return array + * An array of property descriptors. + */ + inspectObject: function WCU_inspectObject(aObject, aObjectWrapper) + { + let properties = []; + let isDOMDocument = aObject instanceof Ci.nsIDOMDocument; + let deprecated = ["width", "height", "inputEncoding"]; + + for (let name in aObject) { + // See bug 632275: skip deprecated properties. + if (isDOMDocument && deprecated.indexOf(name) > -1) { + continue; + } + + properties.push(this.inspectObjectProperty(aObject, name, aObjectWrapper)); + } + + return properties.sort(this.propertiesSort); + }, + + /** + * A helper method that creates a property descriptor for the provided object, + * properly formatted for sending in a protocol response. + * + * The property value can reference other objects. Since actual objects cannot + * be sent to the client, we need to send simple object grips - descriptors + * for those objects. This is why you need to give an object wrapper function + * that creates object grips. + * + * @param string aProperty + * Property name for which we have the descriptor. + * @param object aObject + * The object that the descriptor is generated for. + * @param function aObjectWrapper + * This function is given the property value. Whatever the function + * returns is used as the representation of the property value. + * @return object + * The property descriptor formatted for sending to the client. + */ + inspectObjectProperty: + function WCU_inspectObjectProperty(aObject, aProperty, aObjectWrapper) + { + let descriptor = this.getPropertyDescriptor(aObject, aProperty) || {}; + + let result = { name: aProperty }; + result.configurable = descriptor.configurable; + result.enumerable = descriptor.enumerable; + result.writable = descriptor.writable; + if (descriptor.value !== undefined) { + result.value = this.createValueGrip(descriptor.value, aObjectWrapper); + } + else if (descriptor.get) { + if (this.isNativeFunction(descriptor.get)) { + result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper); + } + else { + result.get = this.createValueGrip(descriptor.get, aObjectWrapper); + result.set = this.createValueGrip(descriptor.set, aObjectWrapper); + } + } + + // There are cases with properties that have no value and no getter. For + // example window.screen.width. + if (result.value === undefined && result.get === undefined) { + result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper); + } + + return result; + }, + + /** + * Make an object grip for the given object. An object grip of the simplest + * form with minimal information about the given object is returned. This + * method is usually combined with other functions that add further state + * information and object ID such that, later, the client is able to retrieve + * more information about the object being represented by this grip. + * + * @param object aObject + * The object you want to create a grip for. + * @return object + * The object grip. + */ + getObjectGrip: function WCU_getObjectGrip(aObject) + { + let className = null; + let type = typeof aObject; + + let result = { + "type": type, + "className": this.getObjectClassName(aObject), + "displayString": this.formatResult(aObject), + "inspectable": this.isObjectInspectable(aObject), + }; + + if (type == "function") { + result.functionName = this.getFunctionName(aObject); + result.functionArguments = this.getFunctionArguments(aObject); + } + + return result; + }, + + /** + * Create a grip for the given value. If the value is an object, + * an object wrapper will be created. + * + * @param mixed aValue + * The value you want to create a grip for, before sending it to the + * client. + * @param function aObjectWrapper + * If the value is an object then the aObjectWrapper function is + * invoked to give us an object grip. See this.getObjectGrip(). + * @return mixed + * The value grip. + */ + createValueGrip: function WCU_createValueGrip(aValue, aObjectWrapper) + { + let type = typeof(aValue); + switch (type) { + case "boolean": + case "string": + case "number": + return aValue; + case "object": + case "function": + if (aValue) { + return aObjectWrapper(aValue); + } + default: + if (aValue === null) { + return { type: "null" }; + } + + if (aValue === undefined) { + return { type: "undefined" }; + } + + Cu.reportError("Failed to provide a grip for value of " + type + ": " + + aValue); + return null; + } + }, + /** * Check if the given object is an iterator or a generator. * @@ -638,6 +852,159 @@ var WebConsoleUtils = { return false; } }, + + /** + * Make a string representation for an object actor grip. + * + * @param object aGrip + * The object grip received from the server. + * @param boolean [aFormatString=false] + * Optional boolean that tells if you want strings to be unevaled or + * not. + * @return string + * The object grip converted to a string. + */ + objectActorGripToString: function WCU_objectActorGripToString(aGrip, aFormatString) + { + // Primitives like strings and numbers are not sent as objects. + // But null and undefined are sent as objects with the type property + // telling which type of value we have. + let type = typeof(aGrip); + if (aGrip && type == "object") { + return aGrip.displayString || aGrip.className || aGrip.type || type; + } + return type == "string" && aFormatString ? + this.formatResultString(aGrip) : aGrip + ""; + }, + + /** + * Helper function to deduce the name of the provided function. + * + * @param funtion aFunction + * The function whose name will be returned. + * @return string + * Function name. + */ + getFunctionName: function WCF_getFunctionName(aFunction) + { + let name = null; + if (aFunction.name) { + name = aFunction.name; + } + else { + let desc; + try { + desc = aFunction.getOwnPropertyDescriptor("displayName"); + } + catch (ex) { } + if (desc && typeof desc.value == "string") { + name = desc.value; + } + } + if (!name) { + try { + let str = (aFunction.toString() || aFunction.toSource()) + ""; + name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1]; + } + catch (ex) { } + } + return name; + }, + + /** + * Helper function to deduce the arguments of the provided function. + * + * @param funtion aFunction + * The function whose name will be returned. + * @return array + * Function arguments. + */ + getFunctionArguments: function WCF_getFunctionArguments(aFunction) + { + let args = []; + try { + let str = (aFunction.toString() || aFunction.toSource()) + ""; + let argsString = (str.match(REGEX_MATCH_FUNCTION_ARGS) || [])[1]; + if (argsString) { + args = argsString.split(/\s*,\s*/); + } + } + catch (ex) { } + return args; + }, + + /** + * Get the object class name. For example, the |window| object has the Window + * class name (based on [object Window]). + * + * @param object aObject + * The object you want to get the class name for. + * @return string + * The object class name. + */ + getObjectClassName: function WCF_getObjectClassName(aObject) + { + if (aObject === null) { + return "null"; + } + if (aObject === undefined) { + return "undefined"; + } + + let type = typeof aObject; + if (type != "object") { + return type; + } + + let className; + + try { + className = ((aObject + "").match(/^\[object (\S+)\]$/) || [])[1]; + if (!className) { + className = ((aObject.constructor + "").match(/^\[object (\S+)\]$/) || [])[1]; + } + if (!className && typeof aObject.constructor == "function") { + className = this.getFunctionName(aObject.constructor); + } + } + catch (ex) { } + + return className; + }, + + /** + * Determine the string to display as a property value in the property panel. + * + * @param object aActor + * Object actor grip. + * @return string + * Property value as suited for the property panel. + */ + getPropertyPanelValue: function WCU_getPropertyPanelValue(aActor) + { + if (aActor.get) { + return "Getter"; + } + + let val = aActor.value; + if (typeof val == "string") { + return this.formatResultString(val); + } + + if (typeof val != "object" || !val) { + return val; + } + + if (val.type == "function" && val.functionName) { + return "function " + val.functionName + "(" + + val.functionArguments.join(", ") + ")"; + } + if (val.type == "object" && val.className) { + return val.className; + } + + return val.displayString || val.type; + }, }; ////////////////////////////////////////////////////////////////////////// @@ -1163,3 +1530,327 @@ PageErrorListener.prototype = this.listener = this.window = null; }, }; + + +/////////////////////////////////////////////////////////////////////////////// +// The window.console API observer +/////////////////////////////////////////////////////////////////////////////// + +/** + * The window.console API observer. This allows the window.console API messages + * to be sent to the remote Web Console instance. + * + * @constructor + * @param nsIDOMWindow aWindow + * The window object for which we are created. + * @param object aOwner + * The owner object must have the following methods: + * - onConsoleAPICall(). This method is invoked with one argument, the + * Console API message that comes from the observer service, whenever + * a relevant console API call is received. + */ +function ConsoleAPIListener(aWindow, aOwner) +{ + this.window = aWindow; + this.owner = aOwner; +} + +ConsoleAPIListener.prototype = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + /** + * The content window for which we listen to window.console API calls. + * @type nsIDOMWindow + */ + window: null, + + /** + * The owner object which is notified of window.console API calls. It must + * have a onConsoleAPICall method which is invoked with one argument: the + * console API call object that comes from the observer service. + * + * @type object + * @see WebConsoleActor + */ + owner: null, + + /** + * Initialize the window.console API observer. + */ + init: function CAL_init() + { + // Note that the observer is process-wide. We will filter the messages as + // needed, see CAL_observe(). + Services.obs.addObserver(this, "console-api-log-event", false); + }, + + /** + * The console API message observer. When messages are received from the + * observer service we forward them to the remote Web Console instance. + * + * @param object aMessage + * The message object receives from the observer service. + * @param string aTopic + * The message topic received from the observer service. + */ + observe: function CAL_observe(aMessage, aTopic) + { + if (!this.owner || !this.window) { + return; + } + + let apiMessage = aMessage.wrappedJSObject; + let msgWindow = WebConsoleUtils.getWindowByOuterId(apiMessage.ID, + this.window); + if (!msgWindow || msgWindow.top != this.window) { + // Not the same window! + return; + } + + this.owner.onConsoleAPICall(apiMessage); + }, + + /** + * Get the cached messages for the current inner window. + * + * @return array + * The array of cached messages. Each element is a Console API + * prepared to be sent to the remote Web Console instance. + */ + getCachedMessages: function CAL_getCachedMessages() + { + let innerWindowId = WebConsoleUtils.getInnerWindowId(this.window); + let messages = ConsoleAPIStorage.getEvents(innerWindowId); + return messages; + }, + + /** + * Destroy the console API listener. + */ + destroy: function CAL_destroy() + { + Services.obs.removeObserver(this, "console-api-log-event"); + this.window = this.owner = null; + }, +}; + + + +/** + * JSTerm helper functions. + * + * Defines a set of functions ("helper functions") that are available from the + * Web Console but not from the web page. + * + * A list of helper functions used by Firebug can be found here: + * http://getfirebug.com/wiki/index.php/Command_Line_API + * + * @param object aOwner + * The owning object. + */ +function JSTermHelpers(aOwner) +{ + /** + * Find a node by ID. + * + * @param string aId + * The ID of the element you want. + * @return nsIDOMNode or null + * The result of calling document.querySelector(aSelector). + */ + aOwner.sandbox.$ = function JSTH_$(aSelector) + { + return aOwner.window.document.querySelector(aSelector); + }; + + /** + * Find the nodes matching a CSS selector. + * + * @param string aSelector + * A string that is passed to window.document.querySelectorAll. + * @return nsIDOMNodeList + * Returns the result of document.querySelectorAll(aSelector). + */ + aOwner.sandbox.$$ = function JSTH_$$(aSelector) + { + return aOwner.window.document.querySelectorAll(aSelector); + }; + + /** + * Runs an xPath query and returns all matched nodes. + * + * @param string aXPath + * xPath search query to execute. + * @param [optional] nsIDOMNode aContext + * Context to run the xPath query on. Uses window.document if not set. + * @return array of nsIDOMNode + */ + aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext) + { + let nodes = []; + let doc = aOwner.window.document; + let aContext = aContext || doc; + + try { + let results = doc.evaluate(aXPath, aContext, null, + Ci.nsIDOMXPathResult.ANY_TYPE, null); + let node; + while (node = results.iterateNext()) { + nodes.push(node); + } + } + catch (ex) { + aOwner.window.console.error(ex.message); + } + + return nodes; + }; + + /** + * Returns the currently selected object in the highlighter. + * + * TODO: this implementation crosses the client/server boundaries! This is not + * usable within a remote browser. To implement this feature correctly we need + * support for remote inspection capabilities within the Inspector as well. + * See bug 787975. + * + * @return nsIDOMElement|null + * The DOM element currently selected in the highlighter. + */ + Object.defineProperty(aOwner.sandbox, "$0", { + get: function() { + try { + return aOwner.chromeWindow().InspectorUI.selection; + } + catch (ex) { + aOwner.window.console.error(ex.message); + } + }, + enumerable: true, + configurable: false + }); + + /** + * Clears the output of the JSTerm. + */ + aOwner.sandbox.clear = function JSTH_clear() + { + aOwner.helperResult = { + type: "clearOutput", + }; + }; + + /** + * Returns the result of Object.keys(aObject). + * + * @param object aObject + * Object to return the property names from. + * @return array of strings + */ + aOwner.sandbox.keys = function JSTH_keys(aObject) + { + return Object.keys(WebConsoleUtils.unwrap(aObject)); + }; + + /** + * Returns the values of all properties on aObject. + * + * @param object aObject + * Object to display the values from. + * @return array of string + */ + aOwner.sandbox.values = function JSTH_values(aObject) + { + let arrValues = []; + let obj = WebConsoleUtils.unwrap(aObject); + + try { + for (let prop in obj) { + arrValues.push(obj[prop]); + } + } + catch (ex) { + aOwner.window.console.error(ex.message); + } + + return arrValues; + }; + + /** + * Opens a help window in MDN. + */ + aOwner.sandbox.help = function JSTH_help() + { + aOwner.helperResult = { type: "help" }; + }; + + /** + * Inspects the passed aObject. This is done by opening the PropertyPanel. + * + * @param object aObject + * Object to inspect. + */ + aOwner.sandbox.inspect = function JSTH_inspect(aObject) + { + let obj = WebConsoleUtils.unwrap(aObject); + if (!WebConsoleUtils.isObjectInspectable(obj)) { + return aObject; + } + + aOwner.helperResult = { + type: "inspectObject", + input: aOwner.evalInput, + object: aOwner.createValueGrip(obj), + }; + }; + + /** + * Prints aObject to the output. + * + * @param object aObject + * Object to print to the output. + * @return string + */ + aOwner.sandbox.pprint = function JSTH_pprint(aObject) + { + if (aObject === null || aObject === undefined || aObject === true || + aObject === false) { + aOwner.helperResult = { + type: "error", + message: "helperFuncUnsupportedTypeError", + }; + return; + } + + aOwner.helperResult = { rawOutput: true }; + + if (typeof aObject == "function") { + return aObject + "\n"; + } + + let output = []; + let getObjectGrip = WebConsoleUtils.getObjectGrip.bind(WebConsoleUtils); + let obj = WebConsoleUtils.unwrap(aObject); + let props = WebConsoleUtils.inspectObject(obj, getObjectGrip); + props.forEach(function(aProp) { + output.push(aProp.name + ": " + + WebConsoleUtils.getPropertyPanelValue(aProp)); + }); + + return " " + output.join("\n "); + }; + + /** + * Print a string to the output, as-is. + * + * @param string aString + * A string you want to output. + * @return void + */ + aOwner.sandbox.print = function JSTH_print(aString) + { + aOwner.helperResult = { rawOutput: true }; + return String(aString); + }; +} diff --git a/toolkit/devtools/webconsole/dbg-webconsole-actors.js b/toolkit/devtools/webconsole/dbg-webconsole-actors.js index a85fd525f74..dec4f6754d4 100644 --- a/toolkit/devtools/webconsole/dbg-webconsole-actors.js +++ b/toolkit/devtools/webconsole/dbg-webconsole-actors.js @@ -15,9 +15,25 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIListener", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "JSTermHelpers", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "JSPropertyProvider", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage", + "resource://gre/modules/ConsoleAPIStorage.jsm"); + + /** * The WebConsoleActor implements capabilities needed for the Web Console * feature. @@ -32,6 +48,9 @@ function WebConsoleActor(aConnection, aTabActor) { this.conn = aConnection; this._browser = aTabActor.browser; + + this._objectActorsPool = new ActorPool(this.conn); + this.conn.addActorPool(this._objectActorsPool); } WebConsoleActor.prototype = @@ -43,6 +62,29 @@ WebConsoleActor.prototype = */ _browser: null, + /** + * Actor pool for all of the object actors for objects we send to the client. + * @private + * @type object + * @see ActorPool + * @see this.objectGrip() + */ + _objectActorsPool: null, + + /** + * Tells the current page location associated to the sandbox. When the page + * location is changed, we recreate the sandbox. + * @private + * @type object + */ + _sandboxLocation: null, + + /** + * The JavaScript Sandbox where code is evaluated. + * @type object + */ + sandbox: null, + /** * The debugger server connection instance. * @type object @@ -61,6 +103,11 @@ WebConsoleActor.prototype = */ pageErrorListener: null, + /** + * The ConsoleAPIListener instance. + */ + consoleAPIListener: null, + actorPrefix: "console", grip: function WCA_grip() @@ -68,6 +115,24 @@ WebConsoleActor.prototype = return { actor: this.actorID }; }, + /** + * Tells if the window.console object is native or overwritten by script in + * the page. + * + * @return boolean + * True if the window.console object is native, or false otherwise. + */ + hasNativeConsoleAPI: function WCA_hasNativeConsoleAPI() + { + let isNative = false; + try { + let consoleObject = WebConsoleUtils.unwrap(this.window).console; + isNative = "__mozillaConsole__" in consoleObject; + } + catch (ex) { } + return isNative; + }, + /** * Destroy the current WebConsoleActor instance. */ @@ -77,9 +142,69 @@ WebConsoleActor.prototype = this.pageErrorListener.destroy(); this.pageErrorListener = null; } + if (this.consoleAPIListener) { + this.consoleAPIListener.destroy(); + this.consoleAPIListener = null; + } + this.conn.removeActorPool(this._objectActorsPool); + this._objectActorsPool = null; + this._sandboxLocation = this.sandbox = null; this.conn = this._browser = null; }, + /** + * Create a grip for the given value. If the value is an object, + * a WebConsoleObjectActor will be created. + * + * @param mixed aValue + * @return object + */ + createValueGrip: function WCA_createValueGrip(aValue) + { + return WebConsoleUtils.createValueGrip(aValue, + this.createObjectActor.bind(this)); + }, + + /** + * Create a grip for the given object. + * + * @param object aObject + * The object you want. + * @param object + * The object grip. + */ + createObjectActor: function WCA_createObjectActor(aObject) + { + // We need to unwrap the object, otherwise we cannot access the properties + // and methods added by the content scripts. + let obj = WebConsoleUtils.unwrap(aObject); + let actor = new WebConsoleObjectActor(obj, this); + this._objectActorsPool.addActor(actor); + return actor.grip(); + }, + + /** + * Get an object actor by its ID. + * + * @param string aActorID + * @return object + */ + getObjectActorByID: function WCA_getObjectActorByID(aActorID) + { + return this._objectActorsPool.get(aActorID); + }, + + /** + * Release an object grip for the given object actor. + * + * @param object aActor + * The WebConsoleObjectActor instance you want to release. + */ + releaseObject: function WCA_releaseObject(aActor) + { + this._objectActorsPool.removeActor(aActor.actorID); + }, + /** * Handler for the "startListeners" request. * @@ -103,9 +228,20 @@ WebConsoleActor.prototype = } startedListeners.push(listener); break; + case "ConsoleAPI": + if (!this.consoleAPIListener) { + this.consoleAPIListener = + new ConsoleAPIListener(this.window, this); + this.consoleAPIListener.init(); + } + startedListeners.push(listener); + break; } } - return { startedListeners: startedListeners }; + return { + startedListeners: startedListeners, + nativeConsoleAPI: this.hasNativeConsoleAPI(), + }; }, /** @@ -123,7 +259,7 @@ WebConsoleActor.prototype = // If no specific listeners are requested to be detached, we stop all // listeners. - let toDetach = aRequest.listeners || ["PageError"]; + let toDetach = aRequest.listeners || ["PageError", "ConsoleAPI"]; while (toDetach.length > 0) { let listener = toDetach.shift(); @@ -135,12 +271,209 @@ WebConsoleActor.prototype = } stoppedListeners.push(listener); break; + case "ConsoleAPI": + if (this.consoleAPIListener) { + this.consoleAPIListener.destroy(); + this.consoleAPIListener = null; + } + stoppedListeners.push(listener); + break; } } return { stoppedListeners: stoppedListeners }; }, + /** + * Handler for the "getCachedMessages" request. This method sends the cached + * error messages and the window.console API calls to the client. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response packet to send to the client: it holds the cached + * messages array. + */ + onGetCachedMessages: function WCA_onGetCachedMessages(aRequest) + { + let types = aRequest.messageTypes; + if (!types) { + return { + error: "missingParameter", + message: "The messageTypes parameter is missing.", + }; + } + + let messages = []; + + while (types.length > 0) { + let type = types.shift(); + switch (type) { + case "ConsoleAPI": + if (this.consoleAPIListener) { + let cache = this.consoleAPIListener.getCachedMessages(); + cache.forEach(function(aMessage) { + let message = this.prepareConsoleMessageForRemote(aMessage); + message._type = type; + messages.push(message); + }, this); + } + break; + case "PageError": + if (this.pageErrorListener) { + let cache = this.pageErrorListener.getCachedMessages(); + cache.forEach(function(aMessage) { + let message = this.preparePageErrorForRemote(aMessage); + message._type = type; + messages.push(message); + }, this); + } + break; + } + } + + messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; }); + + return { + from: this.actorID, + messages: messages, + }; + }, + + /** + * Handler for the "evaluateJS" request. This method evaluates the given + * JavaScript string and sends back the result. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The evaluation response packet. + */ + onEvaluateJS: function WCA_onEvaluateJS(aRequest) + { + let input = aRequest.text; + let result, error = null; + let timestamp; + + this.helperResult = null; + this.evalInput = input; + try { + timestamp = Date.now(); + result = this.evalInSandbox(input); + } + catch (ex) { + error = ex; + } + + let helperResult = this.helperResult; + delete this.helperResult; + delete this.evalInput; + + return { + from: this.actorID, + input: input, + result: this.createValueGrip(result), + timestamp: timestamp, + error: error, + errorMessage: error ? String(error) : null, + helperResult: helperResult, + }; + }, + + /** + * The Autocomplete request handler. + * + * @param object aRequest + * The request message - what input to autocomplete. + * @return object + * The response message - matched properties. + */ + onAutocomplete: function WCA_onAutocomplete(aRequest) + { + let result = JSPropertyProvider(this.window, aRequest.text) || {}; + return { + from: this.actorID, + matches: result.matches || [], + matchProp: result.matchProp, + }; + }, + + /** + * The "clearMessagesCache" request handler. + */ + onClearMessagesCache: function WCA_onClearMessagesCache() + { + // TODO: Bug 717611 - Web Console clear button does not clear cached errors + let windowId = WebConsoleUtils.getInnerWindowId(this.window); + ConsoleAPIStorage.clearEvents(windowId); + return {}; + }, + + /** + * Create the JavaScript sandbox where user input is evaluated. + * @private + */ + _createSandbox: function WCA__createSandbox() + { + this._sandboxLocation = this.window.location; + this.sandbox = new Cu.Sandbox(this.window, { + sandboxPrototype: this.window, + wantXrays: false, + }); + + this.sandbox.console = this.window.console; + + JSTermHelpers(this); + }, + + /** + * Evaluates a string in the sandbox. + * + * @param string aString + * String to evaluate in the sandbox. + * @return mixed + * The result of the evaluation. + */ + evalInSandbox: function WCA_evalInSandbox(aString) + { + // If the user changed to a different location, we need to update the + // sandbox. + if (this._sandboxLocation !== this.window.location) { + this._createSandbox(); + } + + // The help function needs to be easy to guess, so we make the () optional + if (aString.trim() == "help" || aString.trim() == "?") { + aString = "help()"; + } + + let window = WebConsoleUtils.unwrap(this.sandbox.window); + let $ = null, $$ = null; + + // We prefer to execute the page-provided implementations for the $() and + // $$() functions. + if (typeof window.$ == "function") { + $ = this.sandbox.$; + delete this.sandbox.$; + } + if (typeof window.$$ == "function") { + $$ = this.sandbox.$$; + delete this.sandbox.$$; + } + + let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", + "Web Console", 1); + + if ($) { + this.sandbox.$ = $; + } + if ($$) { + this.sandbox.$$ = $$; + } + + return result; + }, + /** * Handler for page errors received from the PageErrorListener. This method * sends the nsIScriptError to the remote Web Console client. @@ -183,11 +516,165 @@ WebConsoleActor.prototype = strict: !!(aPageError.flags & aPageError.strictFlag), }; }, + + /** + * Handler for window.console API calls received from the ConsoleAPIListener. + * This method sends the object to the remote Web Console client. + * + * @param object aMessage + * The console API call we need to send to the remote client. + */ + onConsoleAPICall: function WCA_onConsoleAPICall(aMessage) + { + let packet = { + from: this.actorID, + type: "consoleAPICall", + message: this.prepareConsoleMessageForRemote(aMessage), + }; + this.conn.send(packet); + }, + + /** + * Prepare a message from the console API to be sent to the remote Web Console + * instance. + * + * @param object aMessage + * The original message received from console-api-log-event. + * @return object + * The object that can be sent to the remote client. + */ + prepareConsoleMessageForRemote: + function WCA_prepareConsoleMessageForRemote(aMessage) + { + let result = { + level: aMessage.level, + filename: aMessage.filename, + lineNumber: aMessage.lineNumber, + functionName: aMessage.functionName, + timeStamp: aMessage.timeStamp, + }; + + switch (result.level) { + case "trace": + case "group": + case "groupCollapsed": + case "time": + case "timeEnd": + result.arguments = aMessage.arguments; + break; + default: + result.arguments = Array.map(aMessage.arguments || [], + function(aObj) { + return this.createValueGrip(aObj); + }, this); + + if (result.level == "dir") { + result.objectProperties = []; + let first = result.arguments[0]; + if (typeof first == "object" && first && first.inspectable) { + let actor = this.getObjectActorByID(first.actor); + result.objectProperties = actor.onInspectProperties().properties; + } + } + break; + } + + return result; + }, + + /** + * Find the XUL window that owns the content window. + * + * @return Window + * The XUL window that owns the content window. + */ + chromeWindow: function WCA_chromeWindow() + { + return this.window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument.defaultView; + }, }; WebConsoleActor.prototype.requestTypes = { startListeners: WebConsoleActor.prototype.onStartListeners, stopListeners: WebConsoleActor.prototype.onStopListeners, + getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages, + evaluateJS: WebConsoleActor.prototype.onEvaluateJS, + autocomplete: WebConsoleActor.prototype.onAutocomplete, + clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache, +}; + +/** + * Creates an actor for the specified object. + * + * @constructor + * @param object aObj + * The object you want. + * @param object aWebConsoleActor + * The parent WebConsoleActor instance for this object. + */ +function WebConsoleObjectActor(aObj, aWebConsoleActor) +{ + this.obj = aObj; + this.parent = aWebConsoleActor; +} + +WebConsoleObjectActor.prototype = +{ + actorPrefix: "consoleObj", + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function WCOA_grip() + { + let grip = WebConsoleUtils.getObjectGrip(this.obj); + grip.actor = this.actorID; + return grip; + }, + + /** + * Releases this actor from the pool. + */ + release: function WCOA_release() + { + this.parent.releaseObject(this); + this.parent = this.obj = null; + }, + + /** + * Handle a protocol request to inspect the properties of the object. + * + * @return object + * Message to send to the client. This holds the 'properties' property + * - an array with a descriptor for each property in the object. + */ + onInspectProperties: function WCOA_onInspectProperties() + { + // TODO: Bug 787981 - use LongStringActor for strings that are too long. + let createObjectActor = this.parent.createObjectActor.bind(this.parent); + let props = WebConsoleUtils.inspectObject(this.obj, createObjectActor); + return { + from: this.actorID, + properties: props, + }; + }, + + /** + * Handle a protocol request to release a grip. + */ + onRelease: function WCOA_onRelease() + { + this.release(); + return {}; + }, +}; + +WebConsoleObjectActor.prototype.requestTypes = +{ + "inspectProperties": WebConsoleObjectActor.prototype.onInspectProperties, + "release": WebConsoleObjectActor.prototype.onRelease, };