From c28c5a64550b5fe8f9e9742c95add9e93bd754ca Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Mon, 23 Mar 2020 16:21:39 -0700 Subject: [PATCH] browser(firefox): make Runtime a global object shared between sessions (#1458) Review URL: https://github.com/aslushnikov/juggler/commit/88261ea669d1dac0c15304ba7121edb9f431af73 Key points: - `Runtime` is now shared between protocol sessions - `RuntimeAgent` does not exist any more and is merged into `PageAgent` for Page - `RuntimeAgent` is re-implemented in a worker --- browser_patches/firefox/BUILD_NUMBER | 2 +- .../firefox/patches/bootstrap.diff | 514 ++++++++++-------- 2 files changed, 291 insertions(+), 225 deletions(-) diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index f1f0ca518c795..bcbb007ab65dc 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -1 +1 @@ -1051 +1052 diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index ac2727ecf7aeb..eb62702df2ad1 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -2364,10 +2364,10 @@ index 0000000000000000000000000000000000000000..268fbc361d8053182bb6c27f626e853d + diff --git a/juggler/content/FrameTree.js b/juggler/content/FrameTree.js new file mode 100644 -index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc45b0de67 +index 0000000000000000000000000000000000000000..5f2b6b5de4faa91e32c14e53064b9484648ef9eb --- /dev/null +++ b/juggler/content/FrameTree.js -@@ -0,0 +1,411 @@ +@@ -0,0 +1,452 @@ +"use strict"; +const Ci = Components.interfaces; +const Cr = Components.results; @@ -2376,6 +2376,7 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); ++const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js'); + +const helper = new Helper(); + @@ -2387,8 +2388,10 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + if (!this._browsingContextGroup.__jugglerFrameTrees) + this._browsingContextGroup.__jugglerFrameTrees = new Set(); + this._browsingContextGroup.__jugglerFrameTrees.add(this); ++ this._scriptsToEvaluateOnNewDocument = new Map(); + + this._bindings = new Map(); ++ this._runtime = new Runtime(false /* isWorker */); + this._workers = new Map(); + this._docShellToFrame = new Map(); + this._frameIdToFrame = new Map(); @@ -2401,7 +2404,6 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + Ci.nsIWebProgressListener2, + Ci.nsISupportsWeakReference, + ]); -+ this._scriptsToEvaluateOnNewDocument = []; + + this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager); + this._wdmListener = { @@ -2413,13 +2415,12 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator()) + this._onWorkerCreated(workerDebugger); + -+ + const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT | + Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION; + this._eventListeners = [ ++ helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'), + helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'), + helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'), -+ helper.addObserver(window => this._onDOMWindowCreated(window), 'content-document-global-created'), + helper.addProgressListener(webProgress, this, flags), + ]; + } @@ -2428,6 +2429,10 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + return [...this._workers.values()]; + } + ++ runtime() { ++ return this._runtime; ++ } ++ + _frameForWorker(workerDebugger) { + if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED) + return null; @@ -2435,6 +2440,14 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + return this._docShellToFrame.get(docShell) || null; + } + ++ _onDOMWindowCreated(window) { ++ const frame = this._docShellToFrame.get(window.docShell) || null; ++ if (!frame) ++ return; ++ frame._onGlobalObjectCleared(); ++ this.emit(FrameTree.Events.GlobalObjectCreated, { frame, window }); ++ } ++ + _onWorkerCreated(workerDebugger) { + // Note: we do not interoperate with firefox devtools. + if (workerDebugger.isInitialized) @@ -2476,30 +2489,19 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + } + + addScriptToEvaluateOnNewDocument(script) { -+ this._scriptsToEvaluateOnNewDocument.push(script); ++ const scriptId = helper.generateId(); ++ this._scriptsToEvaluateOnNewDocument.set(scriptId, script); ++ return scriptId; + } + -+ scriptsToEvaluateOnNewDocument() { -+ return this._scriptsToEvaluateOnNewDocument; ++ removeScriptToEvaluateOnNewDocument(scriptId) { ++ this._scriptsToEvaluateOnNewDocument.delete(scriptId); + } + + addBinding(name, script) { + this._bindings.set(name, script); + for (const frame of this.frames()) -+ this._addBindingToFrame(frame, name, script); -+ } -+ -+ _addBindingToFrame(frame, name, script) { -+ Cu.exportFunction((...args) => { -+ this.emit(FrameTree.Events.BindingCalled, { -+ frame, -+ name, -+ payload: args[0] -+ }); -+ }, frame.domWindow(), { -+ defineAs: name, -+ }); -+ frame.domWindow().eval(script); ++ frame._addBinding(name, script); + } + + frameForDocShell(docShell) { @@ -2529,6 +2531,7 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + dispose() { + this._browsingContextGroup.__jugglerFrameTrees.delete(this); + this._wdm.removeListener(this._wdmListener); ++ this._runtime.dispose(); + helper.removeListeners(this._eventListeners); + } + @@ -2606,12 +2609,14 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + + _createFrame(docShell) { + const parentFrame = this._docShellToFrame.get(docShell.parent) || null; -+ const frame = new Frame(this, docShell, parentFrame); ++ const frame = new Frame(this, this._runtime, docShell, parentFrame); + this._docShellToFrame.set(docShell, frame); + this._frameIdToFrame.set(frame.id(), frame); -+ for (const [name, script] of this._bindings) -+ this._addBindingToFrame(frame, name, script); + this.emit(FrameTree.Events.FrameAttached, frame); ++ // Create execution context **after** reporting frame. ++ // This is our protocol contract. ++ if (frame.domWindow()) ++ frame._onGlobalObjectCleared(); + return frame; + } + @@ -2621,16 +2626,6 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + this._detachFrame(frame); + } + -+ _onDOMWindowCreated(window) { -+ const docShell = window.docShell; -+ const frame = this.frameForDocShell(docShell); -+ if (!frame) -+ return; -+ for (const [name, script] of this._bindings) -+ this._addBindingToFrame(frame, name, script); -+ this.emit(FrameTree.Events.GlobalObjectCreated, { frame, window }); -+ } -+ + _detachFrame(frame) { + // Detach all children first + for (const subframe of frame._children) @@ -2640,6 +2635,7 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + if (frame._parentFrame) + frame._parentFrame._children.delete(frame); + frame._parentFrame = null; ++ frame.dispose(); + this.emit(FrameTree.Events.FrameDetached, frame); + } +} @@ -2659,8 +2655,9 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc +}; + +class Frame { -+ constructor(frameTree, docShell, parentFrame) { ++ constructor(frameTree, runtime, docShell, parentFrame) { + this._frameTree = frameTree; ++ this._runtime = runtime; + this._docShell = docShell; + this._children = new Set(); + this._frameId = helper.generateId(); @@ -2678,6 +2675,50 @@ index 0000000000000000000000000000000000000000..679b5851c427064c636bd1f7793358cc + this._pendingNavigationURL = null; + + this._textInputProcessor = null; ++ this._executionContext = null; ++ } ++ ++ dispose() { ++ if (this._executionContext) ++ this._runtime.destroyExecutionContext(this._executionContext); ++ this._executionContext = null; ++ } ++ ++ _addBinding(name, script) { ++ Cu.exportFunction((...args) => { ++ this._frameTree.emit(FrameTree.Events.BindingCalled, { ++ frame: this, ++ name, ++ payload: args[0] ++ }); ++ }, this.domWindow(), { ++ defineAs: name, ++ }); ++ this.domWindow().eval(script); ++ } ++ ++ _onGlobalObjectCleared() { ++ if (this._executionContext) ++ this._runtime.destroyExecutionContext(this._executionContext); ++ this._executionContext = this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), { ++ frameId: this._frameId, ++ name: '', ++ }); ++ for (const [name, script] of this._frameTree._bindings) ++ this._addBinding(name, script); ++ for (const script of this._frameTree._scriptsToEvaluateOnNewDocument.values()) { ++ try { ++ const result = this._executionContext.evaluateScript(script); ++ if (result && result.objectId) ++ this._executionContext.disposeObject(result.objectId); ++ } catch (e) { ++ dump(`ERROR: ${e.message}\n${e.stack}\n`); ++ } ++ } ++ } ++ ++ executionContext() { ++ return this._executionContext; + } + + textInputProcessor() { @@ -2849,10 +2890,10 @@ index 0000000000000000000000000000000000000000..be70ea364f9534bb3b344f64970366c3 + diff --git a/juggler/content/PageAgent.js b/juggler/content/PageAgent.js new file mode 100644 -index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87d6f85840 +index 0000000000000000000000000000000000000000..8b4202213ad6f663a8e161748ebd23c4c5deddec --- /dev/null +++ b/juggler/content/PageAgent.js -@@ -0,0 +1,921 @@ +@@ -0,0 +1,935 @@ +"use strict"; +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const Ci = Components.interfaces; @@ -2878,7 +2919,6 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + runtimeConsole: emit('runtimeConsole'), + runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'), + runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'), -+ workerConsoleMessage: (hash) => pageAgent._runtime.filterConsoleMessage(hash), + }), + browserChannel.register(sessionId + worker.id(), { + evaluate: (options) => this._workerRuntime.send('evaluate', options), @@ -2899,37 +2939,21 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 +} + +class FrameData { -+ constructor(agent, frame) { ++ constructor(agent, runtime, frame) { + this._agent = agent; ++ this._runtime = runtime; + this._frame = frame; + this._isolatedWorlds = new Map(); + this.reset(); + } + + reset() { -+ if (this.mainContext) -+ this._agent._runtime.destroyExecutionContext(this.mainContext); + for (const world of this._isolatedWorlds.values()) -+ this._agent._runtime.destroyExecutionContext(world); ++ this._runtime.destroyExecutionContext(world); + this._isolatedWorlds.clear(); + -+ this.mainContext = this._agent._runtime.createExecutionContext(this._frame.domWindow(), this._frame.domWindow(), { -+ frameId: this._frame.id(), -+ name: '', -+ }); -+ -+ for (const script of this._agent._frameTree.scriptsToEvaluateOnNewDocument()) { -+ // TODO: this should actually be handled in FrameTree, but first we have to move -+ // execution contexts there. -+ try { -+ let result = this.mainContext.evaluateScript(script); -+ if (result && result.objectId) -+ this.mainContext.disposeObject(result.objectId); -+ } catch (e) { -+ } -+ } -+ for (const {script, worldName} of this._agent._scriptsToEvaluateOnNewDocument.values()) { -+ const context = worldName ? this.createIsolatedWorld(worldName) : this.mainContext; ++ for (const {script, worldName} of this._agent._isolatedWorlds.values()) { ++ const context = worldName ? this.createIsolatedWorld(worldName) : this._frame.executionContext(); + try { + let result = context.evaluateScript(script); + if (result && result.objectId) @@ -2947,7 +2971,7 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + wantExportHelpers: false, + wantXrays: true, + }); -+ const world = this._agent._runtime.createExecutionContext(this._frame.domWindow(), sandbox, { ++ const world = this._runtime.createExecutionContext(this._frame.domWindow(), sandbox, { + frameId: this._frame.id(), + name, + }); @@ -2956,35 +2980,37 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + } + + unsafeObject(objectId) { -+ if (this.mainContext) { -+ const result = this.mainContext.unsafeObject(objectId); -+ if (result) -+ return result.object; -+ } -+ for (const world of this._isolatedWorlds.values()) { -+ const result = world.unsafeObject(objectId); ++ const contexts = [this._frame.executionContext(), ...this._isolatedWorlds.values()]; ++ for (const context of contexts) { ++ const result = context.unsafeObject(objectId); + if (result) + return result.object; + } + throw new Error('Cannot find object with id = ' + objectId); + } + -+ dispose() {} ++ dispose() { ++ for (const world of this._isolatedWorlds.values()) ++ this._runtime.destroyExecutionContext(world); ++ this._isolatedWorlds.clear(); ++ } +} + +class PageAgent { -+ constructor(messageManager, browserChannel, sessionId, runtimeAgent, frameTree, networkMonitor) { ++ constructor(messageManager, browserChannel, sessionId, frameTree, networkMonitor) { + this._messageManager = messageManager; + this._browserChannel = browserChannel; + this._sessionId = sessionId; + this._browserPage = browserChannel.connect(sessionId + 'page'); -+ this._runtime = runtimeAgent; ++ this._browserRuntime = browserChannel.connect(sessionId + 'runtime'); + this._frameTree = frameTree; ++ this._runtime = frameTree.runtime(); + this._networkMonitor = networkMonitor; + + this._frameData = new Map(); + this._workerData = new Map(); + this._scriptsToEvaluateOnNewDocument = new Map(); ++ this._isolatedWorlds = new Map(); + + this._eventListeners = [ + browserChannel.register(sessionId + 'page', { @@ -3014,6 +3040,12 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + setFileInputFiles: this._setFileInputFiles.bind(this), + setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this), + }), ++ browserChannel.register(sessionId + 'runtime', { ++ evaluate: this._runtime.evaluate.bind(this._runtime), ++ callFunction: this._runtime.callFunction.bind(this._runtime), ++ getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), ++ disposeObject: this._runtime.disposeObject.bind(this._runtime), ++ }), + ]; + this._enabled = false; + @@ -3021,17 +3053,6 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + this._docShell = docShell; + this._initialDPPX = docShell.contentViewer.overrideDPPX; + this._customScrollbars = null; -+ -+ this._runtime.setOnErrorFromWorker((domWindow, message, stack) => { -+ const frame = this._frameTree.frameForDocShell(domWindow.docShell); -+ if (!frame) -+ return; -+ this._browserPage.emit('pageUncaughtError', { -+ frameId: frame.id(), -+ message, -+ stack, -+ }); -+ }); + } + + async _awaitViewportDimensions({width, height}) { @@ -3067,17 +3088,24 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + } + + _addScriptToEvaluateOnNewDocument({script, worldName}) { ++ if (worldName) ++ return this._createIsolatedWorld({script, worldName}); ++ return {scriptId: this._frameTree.addScriptToEvaluateOnNewDocument(script)}; ++ } ++ ++ _createIsolatedWorld({script, worldName}) { + const scriptId = helper.generateId(); -+ this._scriptsToEvaluateOnNewDocument.set(scriptId, {script, worldName}); -+ if (worldName) { -+ for (const frameData of this._frameData.values()) -+ frameData.createIsolatedWorld(worldName); -+ } ++ this._isolatedWorlds.set(scriptId, {script, worldName}); ++ for (const frameData of this._frameData.values()) ++ frameData.createIsolatedWorld(worldName); + return {scriptId}; + } + + _removeScriptToEvaluateOnNewDocument({scriptId}) { -+ this._scriptsToEvaluateOnNewDocument.delete(scriptId); ++ if (this._isolatedWorlds.has(scriptId)) ++ this._isolatedWorlds.delete(scriptId); ++ else ++ this._frameTree.removeScriptToEvaluateOnNewDocument(scriptId); + } + + _setCacheDisabled({cacheDisabled}) { @@ -3126,12 +3154,40 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)), + helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)), + helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'), ++ this._runtime.events.onErrorFromWorker((domWindow, message, stack) => { ++ const frame = this._frameTree.frameForDocShell(domWindow.docShell); ++ if (!frame) ++ return; ++ this._browserPage.emit('pageUncaughtError', { ++ frameId: frame.id(), ++ message, ++ stack, ++ }); ++ }), ++ this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)), ++ this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), ++ this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), + ]); ++ for (const context of this._runtime.executionContexts()) ++ this._onExecutionContextCreated(context); + + if (this._frameTree.isPageReady()) + this._browserPage.emit('pageReady', {}); + } + ++ _onExecutionContextCreated(executionContext) { ++ this._browserRuntime.emit('runtimeExecutionContextCreated', { ++ executionContextId: executionContext.id(), ++ auxData: executionContext.auxData(), ++ }); ++ } ++ ++ _onExecutionContextDestroyed(executionContext) { ++ this._browserRuntime.emit('runtimeExecutionContextDestroyed', { ++ executionContextId: executionContext.id(), ++ }); ++ } ++ + _onWorkerCreated(worker) { + const workerData = new WorkerData(this, this._browserChannel, this._sessionId, worker); + this._workerData.set(worker.id(), workerData); @@ -3186,8 +3242,8 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + return; + const frameData = this._findFrameForNode(inputElement); + this._browserPage.emit('pageFileChooserOpened', { -+ executionContextId: frameData.mainContext.id(), -+ element: frameData.mainContext.rawValueToRemoteObject(inputElement) ++ executionContextId: frameData._frame.executionContext().id(), ++ element: frameData._frame.executionContext().rawValueToRemoteObject(inputElement) + }); + } + @@ -3284,7 +3340,7 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + frameId: frame.id(), + parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined, + }); -+ this._frameData.set(frame, new FrameData(this, frame)); ++ this._frameData.set(frame, new FrameData(this, this._runtime, frame)); + } + + _onFrameDetached(frame) { @@ -3295,9 +3351,8 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 + } + + _onBindingCalled({frame, name, payload}) { -+ const frameData = this._frameData.get(frame); + this._browserPage.emit('pageBindingCalled', { -+ executionContextId: frameData.mainContext.id(), ++ executionContextId: frame.executionContext().id(), + name, + payload + }); @@ -3774,12 +3829,12 @@ index 0000000000000000000000000000000000000000..6a001d9f51c819edd3981e090172ac87 +var EXPORTED_SYMBOLS = ['PageAgent']; +this.PageAgent = PageAgent; + -diff --git a/juggler/content/RuntimeAgent.js b/juggler/content/RuntimeAgent.js +diff --git a/juggler/content/Runtime.js b/juggler/content/Runtime.js new file mode 100644 -index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c0f803262 +index 0000000000000000000000000000000000000000..bd5345b1fab48d798b7e628eed67787a4ba952bb --- /dev/null -+++ b/juggler/content/RuntimeAgent.js -@@ -0,0 +1,559 @@ ++++ b/juggler/content/Runtime.js +@@ -0,0 +1,534 @@ +"use strict"; +// Note: this file should be loadabale with eval() into worker environment. +// Avoid Components.*, ChromeUtils and global const variables. @@ -3831,41 +3886,72 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + 'xbl javascript', +]); + -+class RuntimeAgent { -+ constructor(channel, sessionId, isWorker = false) { ++class Runtime { ++ constructor(isWorker = false) { + this._debugger = new Debugger(); + this._pendingPromises = new Map(); + this._executionContexts = new Map(); + this._windowToExecutionContext = new Map(); -+ this._session = channel.connect(sessionId + 'runtime'); -+ this._eventListeners = [ -+ channel.register(sessionId + 'runtime', { -+ evaluate: this._evaluate.bind(this), -+ callFunction: this._callFunction.bind(this), -+ getObjectProperties: this._getObjectProperties.bind(this), -+ disposeObject: this._disposeObject.bind(this), -+ }), -+ ]; -+ this._enabled = false; -+ this._filteredConsoleMessageHashes = new Set(); -+ this._onErrorFromWorker = null; -+ this._isWorker = isWorker; -+ } -+ -+ enable() { -+ if (this._enabled) -+ return; -+ this._enabled = true; -+ for (const executionContext of this._executionContexts.values()) -+ this._notifyExecutionContextCreated(executionContext); -+ -+ if (this._isWorker) { -+ this._registerConsoleEventHandler(); ++ this._eventListeners = []; ++ if (isWorker) { ++ this._registerWorkerConsoleHandler(); + } else { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + this._registerConsoleServiceListener(Services); + this._registerConsoleObserver(Services); + } ++ // We can't use event listener here to be compatible with Worker Global Context. ++ // Use plain callbacks instead. ++ this.events = { ++ onConsoleMessage: createEvent(), ++ onErrorFromWorker: createEvent(), ++ onExecutionContextCreated: createEvent(), ++ onExecutionContextDestroyed: createEvent(), ++ }; ++ } ++ ++ executionContexts() { ++ return [...this._executionContexts.values()]; ++ } ++ ++ async evaluate({executionContextId, expression, returnByValue}) { ++ const executionContext = this.findExecutionContext(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ const exceptionDetails = {}; ++ let result = await executionContext.evaluateScript(expression, exceptionDetails); ++ if (!result) ++ return {exceptionDetails}; ++ if (returnByValue) ++ result = executionContext.ensureSerializedToValue(result); ++ return {result}; ++ } ++ ++ async callFunction({executionContextId, functionDeclaration, args, returnByValue}) { ++ const executionContext = this.findExecutionContext(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ const exceptionDetails = {}; ++ let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails); ++ if (!result) ++ return {exceptionDetails}; ++ if (returnByValue) ++ result = executionContext.ensureSerializedToValue(result); ++ return {result}; ++ } ++ ++ async getObjectProperties({executionContextId, objectId}) { ++ const executionContext = this.findExecutionContext(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ return {properties: executionContext.getObjectProperties(objectId)}; ++ } ++ ++ async disposeObject({executionContextId, objectId}) { ++ const executionContext = this.findExecutionContext(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ return executionContext.disposeObject(objectId); + } + + _registerConsoleServiceListener(Services) { @@ -3880,8 +3966,7 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + } + const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID); + if (message.category === 'Web Worker' && (message.flags & Ci.nsIScriptError.exceptionFlag)) { -+ if (this._onErrorFromWorker) -+ this._onErrorFromWorker(errorWindow, message.message, '' + message.stack); ++ emitEvent(this.events.onErrorFromWorker, errorWindow, message.message, '' + message.stack); + return; + } + const executionContext = this._windowToExecutionContext.get(errorWindow); @@ -3893,7 +3978,7 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + [Ci.nsIConsoleMessage.warn]: 'warn', + [Ci.nsIConsoleMessage.error]: 'error', + }; -+ this._session.emit('runtimeConsole', { ++ emitEvent(this.events.onConsoleMessage, { + args: [{ + value: message.message, + }], @@ -3913,11 +3998,6 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + + _registerConsoleObserver(Services) { + const consoleObserver = ({wrappedJSObject}, topic, data) => { -+ const hash = this._consoleMessageHash(wrappedJSObject); -+ if (this._filteredConsoleMessageHashes.has(hash)) { -+ this._filteredConsoleMessageHashes.delete(hash); -+ return; -+ } + const executionContext = Array.from(this._executionContexts.values()).find(context => { + const domWindow = context._domWindow; + return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID; @@ -3930,33 +4010,20 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + this._eventListeners.push(() => Services.obs.removeObserver(consoleObserver, "console-api-log-event")); + } + -+ _registerConsoleEventHandler() { ++ _registerWorkerConsoleHandler() { + setConsoleEventHandler(message => { -+ this._session.emit('workerConsoleMessage', this._consoleMessageHash(message)); + const executionContext = Array.from(this._executionContexts.values())[0]; + this._onConsoleMessage(executionContext, message); + }); + this._eventListeners.push(() => setConsoleEventHandler(null)); + } + -+ filterConsoleMessage(messageHash) { -+ this._filteredConsoleMessageHashes.add(messageHash); -+ } -+ -+ setOnErrorFromWorker(onErrorFromWorker) { -+ this._onErrorFromWorker = onErrorFromWorker; -+ } -+ -+ _consoleMessageHash(message) { -+ return `${message.timeStamp}/${message.filename}/${message.lineNumber}/${message.columnNumber}/${message.sourceId}/${message.level}`; -+ } -+ + _onConsoleMessage(executionContext, message) { + const type = consoleLevelToProtocolType[message.level]; + if (!type) + return; + const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg)); -+ this._session.emit('runtimeConsole', { ++ emitEvent(this.events.onConsoleMessage, { + args, + type, + executionContextId: executionContext.id(), @@ -3968,25 +4035,7 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + }); + } + -+ _notifyExecutionContextCreated(executionContext) { -+ if (!this._enabled) -+ return; -+ this._session.emit('runtimeExecutionContextCreated', { -+ executionContextId: executionContext._id, -+ auxData: executionContext._auxData, -+ }); -+ } -+ -+ _notifyExecutionContextDestroyed(executionContext) { -+ if (!this._enabled) -+ return; -+ this._session.emit('runtimeExecutionContextDestroyed', { -+ executionContextId: executionContext._id, -+ }); -+ } -+ + dispose() { -+ this._session.dispose(); + for (const tearDown of this._eventListeners) + tearDown.call(null); + this._eventListeners = []; @@ -4036,7 +4085,7 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + this._executionContexts.set(context._id, context); + if (domWindow) + this._windowToExecutionContext.set(domWindow, context); -+ this._notifyExecutionContextCreated(context); ++ emitEvent(this.events.onExecutionContextCreated, context); + return context; + } + @@ -4060,47 +4109,7 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + this._executionContexts.delete(destroyedContext._id); + if (destroyedContext._domWindow) + this._windowToExecutionContext.delete(destroyedContext._domWindow); -+ this._notifyExecutionContextDestroyed(destroyedContext); -+ } -+ -+ async _evaluate({executionContextId, expression, returnByValue}) { -+ const executionContext = this._executionContexts.get(executionContextId); -+ if (!executionContext) -+ throw new Error('Failed to find execution context with id = ' + executionContextId); -+ const exceptionDetails = {}; -+ let result = await executionContext.evaluateScript(expression, exceptionDetails); -+ if (!result) -+ return {exceptionDetails}; -+ if (returnByValue) -+ result = executionContext.ensureSerializedToValue(result); -+ return {result}; -+ } -+ -+ async _callFunction({executionContextId, functionDeclaration, args, returnByValue}) { -+ const executionContext = this._executionContexts.get(executionContextId); -+ if (!executionContext) -+ throw new Error('Failed to find execution context with id = ' + executionContextId); -+ const exceptionDetails = {}; -+ let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails); -+ if (!result) -+ return {exceptionDetails}; -+ if (returnByValue) -+ result = executionContext.ensureSerializedToValue(result); -+ return {result}; -+ } -+ -+ async _getObjectProperties({executionContextId, objectId}) { -+ const executionContext = this._executionContexts.get(executionContextId); -+ if (!executionContext) -+ throw new Error('Failed to find execution context with id = ' + executionContextId); -+ return {properties: executionContext.getObjectProperties(objectId)}; -+ } -+ -+ async _disposeObject({executionContextId, objectId}) { -+ const executionContext = this._executionContexts.get(executionContextId); -+ if (!executionContext) -+ throw new Error('Failed to find execution context with id = ' + executionContextId); -+ return executionContext.disposeObject(objectId); ++ emitEvent(this.events.onExecutionContextDestroyed, destroyedContext); + } +} + @@ -4337,8 +4346,29 @@ index 0000000000000000000000000000000000000000..b10cc8d29bbfff1d3490ee795710bd7c + } +} + -+var EXPORTED_SYMBOLS = ['RuntimeAgent']; -+this.RuntimeAgent = RuntimeAgent; ++const listenersSymbol = Symbol('listeners'); ++ ++function createEvent() { ++ const listeners = new Set(); ++ const subscribeFunction = listener => { ++ listeners.add(listener); ++ return () => listeners.delete(listener); ++ } ++ subscribeFunction[listenersSymbol] = listeners; ++ return subscribeFunction; ++} ++ ++function emitEvent(event, ...args) { ++ let listeners = event[listenersSymbol]; ++ if (!listeners || !listeners.size) ++ return; ++ listeners = new Set(listeners); ++ for (const listener of listeners) ++ listener.call(null, ...args); ++} ++ ++var EXPORTED_SYMBOLS = ['Runtime']; ++this.Runtime = Runtime; diff --git a/juggler/content/ScrollbarManager.js b/juggler/content/ScrollbarManager.js new file mode 100644 index 0000000000000000000000000000000000000000..caee4df323d0a526ed7e38947c41c6430983568d @@ -4432,12 +4462,12 @@ index 0000000000000000000000000000000000000000..caee4df323d0a526ed7e38947c41c643 + diff --git a/juggler/content/WorkerMain.js b/juggler/content/WorkerMain.js new file mode 100644 -index 0000000000000000000000000000000000000000..b1a33558c60289c24f7f58e253a0a617ce35e469 +index 0000000000000000000000000000000000000000..a6ed6200364b871ee21ee2cdfd2c9246c9bf0055 --- /dev/null +++ b/juggler/content/WorkerMain.js -@@ -0,0 +1,29 @@ +@@ -0,0 +1,69 @@ +"use strict"; -+loadSubScript('chrome://juggler/content/content/RuntimeAgent.js'); ++loadSubScript('chrome://juggler/content/content/Runtime.js'); +loadSubScript('chrome://juggler/content/SimpleChannel.js'); + +const runtimeAgents = new Map(); @@ -4450,12 +4480,52 @@ index 0000000000000000000000000000000000000000..b1a33558c60289c24f7f58e253a0a617 + dispose: () => this.removeEventListener('message', eventListener), +}; + ++const runtime = new Runtime(true /* isWorker */); ++runtime.createExecutionContext(null /* domWindow */, global, {}); ++ ++class RuntimeAgent { ++ constructor(runtime, channel, sessionId) { ++ this._runtime = runtime; ++ this._browserRuntime = channel.connect(sessionId + 'runtime'); ++ this._eventListeners = [ ++ channel.register(sessionId + 'runtime', { ++ evaluate: this._runtime.evaluate.bind(this._runtime), ++ callFunction: this._runtime.callFunction.bind(this._runtime), ++ getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), ++ disposeObject: this._runtime.disposeObject.bind(this._runtime), ++ }), ++ this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)), ++ this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), ++ this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), ++ ]; ++ for (const context of this._runtime.executionContexts()) ++ this._onExecutionContextCreated(context); ++ } ++ ++ _onExecutionContextCreated(executionContext) { ++ this._browserRuntime.emit('runtimeExecutionContextCreated', { ++ executionContextId: executionContext.id(), ++ auxData: executionContext.auxData(), ++ }); ++ } ++ ++ _onExecutionContextDestroyed(executionContext) { ++ this._browserRuntime.emit('runtimeExecutionContextDestroyed', { ++ executionContextId: executionContext.id(), ++ }); ++ } ++ ++ dispose() { ++ for (const disposer of this._eventListeners) ++ disposer(); ++ this._eventListeners = []; ++ } ++} ++ +channel.register('', { + attach: ({sessionId}) => { -+ const runtimeAgent = new RuntimeAgent(channel, sessionId, true /* isWorker */); ++ const runtimeAgent = new RuntimeAgent(runtime, channel, sessionId); + runtimeAgents.set(sessionId, runtimeAgent); -+ runtimeAgent.createExecutionContext(null /* domWindow */, global, {}); -+ runtimeAgent.enable(); + }, + + detach: ({sessionId}) => { @@ -4539,17 +4609,16 @@ index 0000000000000000000000000000000000000000..3a386425d3796d0a6786dea193b3402d + diff --git a/juggler/content/main.js b/juggler/content/main.js new file mode 100644 -index 0000000000000000000000000000000000000000..9bb5c2bff8eb3e350203b56a3445e9b200747f8b +index 0000000000000000000000000000000000000000..97d45c054ab9c8d805eb26fdc4b42dc503bc930b --- /dev/null +++ b/juggler/content/main.js -@@ -0,0 +1,178 @@ +@@ -0,0 +1,174 @@ +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js'); +const {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js'); +const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js'); +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); -+const {RuntimeAgent} = ChromeUtils.import('chrome://juggler/content/content/RuntimeAgent.js'); +const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); + +const ALL_PERMISSIONS = [ @@ -4568,11 +4637,8 @@ index 0000000000000000000000000000000000000000..9bb5c2bff8eb3e350203b56a3445e9b2 +const sessions = new Map(); + +function createContentSession(channel, sessionId) { -+ const runtimeAgent = new RuntimeAgent(channel, sessionId); -+ const pageAgent = new PageAgent(messageManager, channel, sessionId, runtimeAgent, frameTree, networkMonitor); -+ sessions.set(sessionId, [runtimeAgent, pageAgent]); -+ -+ runtimeAgent.enable(); ++ const pageAgent = new PageAgent(messageManager, channel, sessionId, frameTree, networkMonitor); ++ sessions.set(sessionId, [pageAgent]); + pageAgent.enable(); +} + @@ -4723,7 +4789,7 @@ index 0000000000000000000000000000000000000000..9bb5c2bff8eb3e350203b56a3445e9b2 +initialize(); diff --git a/juggler/jar.mn b/juggler/jar.mn new file mode 100644 -index 0000000000000000000000000000000000000000..e8a057109be8b328aefc3af26715c00689ecd6d8 +index 0000000000000000000000000000000000000000..164060acebeaf784d0c38cf161f408e5d141a44e --- /dev/null +++ b/juggler/jar.mn @@ -0,0 +1,29 @@ @@ -4750,7 +4816,7 @@ index 0000000000000000000000000000000000000000..e8a057109be8b328aefc3af26715c006 + content/content/FrameTree.js (content/FrameTree.js) + content/content/NetworkMonitor.js (content/NetworkMonitor.js) + content/content/PageAgent.js (content/PageAgent.js) -+ content/content/RuntimeAgent.js (content/RuntimeAgent.js) ++ content/content/Runtime.js (content/Runtime.js) + content/content/WorkerMain.js (content/WorkerMain.js) + content/content/ScrollbarManager.js (content/ScrollbarManager.js) + content/content/floating-scrollbars.css (content/floating-scrollbars.css)