From 88261ea669d1dac0c15304ba7121edb9f431af73 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Thu, 19 Mar 2020 20:20:54 -0700 Subject: [PATCH] refactor: make Runtime a global object shared between sessions --- juggler/content/FrameTree.js | 109 +++++++--- juggler/content/PageAgent.js | 124 ++++++----- .../content/{RuntimeAgent.js => Runtime.js} | 197 ++++++++---------- juggler/content/WorkerMain.js | 48 ++++- juggler/content/main.js | 8 +- juggler/jar.mn | 2 +- 6 files changed, 277 insertions(+), 211 deletions(-) rename juggler/content/{RuntimeAgent.js => Runtime.js} (85%) diff --git a/juggler/content/FrameTree.js b/juggler/content/FrameTree.js index 679b5851c4270..5f2b6b5de4faa 100644 --- a/juggler/content/FrameTree.js +++ b/juggler/content/FrameTree.js @@ -6,6 +6,7 @@ const Cu = Components.utils; 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(); @@ -17,8 +18,10 @@ class FrameTree { 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(); @@ -31,7 +34,6 @@ class FrameTree { Ci.nsIWebProgressListener2, Ci.nsISupportsWeakReference, ]); - this._scriptsToEvaluateOnNewDocument = []; this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager); this._wdmListener = { @@ -43,13 +45,12 @@ class FrameTree { 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), ]; } @@ -58,6 +59,10 @@ class FrameTree { return [...this._workers.values()]; } + runtime() { + return this._runtime; + } + _frameForWorker(workerDebugger) { if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED) return null; @@ -65,6 +70,14 @@ class FrameTree { 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) @@ -106,30 +119,19 @@ class FrameTree { } 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) { @@ -159,6 +161,7 @@ class FrameTree { dispose() { this._browsingContextGroup.__jugglerFrameTrees.delete(this); this._wdm.removeListener(this._wdmListener); + this._runtime.dispose(); helper.removeListeners(this._eventListeners); } @@ -236,12 +239,14 @@ class FrameTree { _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; } @@ -251,16 +256,6 @@ class FrameTree { 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) @@ -270,6 +265,7 @@ class FrameTree { if (frame._parentFrame) frame._parentFrame._children.delete(frame); frame._parentFrame = null; + frame.dispose(); this.emit(FrameTree.Events.FrameDetached, frame); } } @@ -289,8 +285,9 @@ FrameTree.Events = { }; 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(); @@ -308,6 +305,50 @@ class Frame { 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() { diff --git a/juggler/content/PageAgent.js b/juggler/content/PageAgent.js index 6a001d9f51c81..8b4202213ad6f 100644 --- a/juggler/content/PageAgent.js +++ b/juggler/content/PageAgent.js @@ -23,7 +23,6 @@ class WorkerData { 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), @@ -44,37 +43,21 @@ class WorkerData { } 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) @@ -92,7 +75,7 @@ class FrameData { 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, }); @@ -101,35 +84,37 @@ class FrameData { } 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', { @@ -159,6 +144,12 @@ class PageAgent { 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; @@ -166,17 +157,6 @@ class PageAgent { 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}) { @@ -212,17 +192,24 @@ class PageAgent { } _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}) { @@ -271,12 +258,40 @@ class PageAgent { 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); @@ -331,8 +346,8 @@ class PageAgent { 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) }); } @@ -429,7 +444,7 @@ class PageAgent { 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) { @@ -440,9 +455,8 @@ class PageAgent { } _onBindingCalled({frame, name, payload}) { - const frameData = this._frameData.get(frame); this._browserPage.emit('pageBindingCalled', { - executionContextId: frameData.mainContext.id(), + executionContextId: frame.executionContext().id(), name, payload }); diff --git a/juggler/content/RuntimeAgent.js b/juggler/content/Runtime.js similarity index 85% rename from juggler/content/RuntimeAgent.js rename to juggler/content/Runtime.js index b10cc8d29bbff..bd5345b1fab48 100644 --- a/juggler/content/RuntimeAgent.js +++ b/juggler/content/Runtime.js @@ -49,41 +49,72 @@ const disallowedMessageCategories = new Set([ '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) { @@ -98,8 +129,7 @@ class RuntimeAgent { } 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); @@ -111,7 +141,7 @@ class RuntimeAgent { [Ci.nsIConsoleMessage.warn]: 'warn', [Ci.nsIConsoleMessage.error]: 'error', }; - this._session.emit('runtimeConsole', { + emitEvent(this.events.onConsoleMessage, { args: [{ value: message.message, }], @@ -131,11 +161,6 @@ class RuntimeAgent { _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; @@ -148,33 +173,20 @@ class RuntimeAgent { 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(), @@ -186,25 +198,7 @@ class RuntimeAgent { }); } - _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 = []; @@ -254,7 +248,7 @@ class RuntimeAgent { this._executionContexts.set(context._id, context); if (domWindow) this._windowToExecutionContext.set(domWindow, context); - this._notifyExecutionContextCreated(context); + emitEvent(this.events.onExecutionContextCreated, context); return context; } @@ -278,47 +272,7 @@ class RuntimeAgent { 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); } } @@ -555,5 +509,26 @@ class ExecutionContext { } } -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/WorkerMain.js b/juggler/content/WorkerMain.js index b1a33558c6028..a6ed6200364b8 100644 --- a/juggler/content/WorkerMain.js +++ b/juggler/content/WorkerMain.js @@ -1,5 +1,5 @@ "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(); @@ -12,12 +12,52 @@ channel.transport = { 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}) => { diff --git a/juggler/content/main.js b/juggler/content/main.js index 9bb5c2bff8eb3..97d45c054ab9c 100644 --- a/juggler/content/main.js +++ b/juggler/content/main.js @@ -4,7 +4,6 @@ const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTr 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 = [ @@ -23,11 +22,8 @@ const messageManager = this; 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(); } diff --git a/juggler/jar.mn b/juggler/jar.mn index e8a057109be8b..164060acebeaf 100644 --- a/juggler/jar.mn +++ b/juggler/jar.mn @@ -21,7 +21,7 @@ juggler.jar: 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)