browser(firefox): make Runtime a global object shared between sessions (#1458)

Review URL: 88261ea669

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
This commit is contained in:
Andrey Lushnikov 2020-03-23 16:21:39 -07:00 committed by GitHub
parent c0c9b7f137
commit c28c5a6455
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 291 additions and 225 deletions

View file

@ -1 +1 @@
1051
1052

View file

@ -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)