browser(firefox): simplify isolated world structures (#6521)

This commit is contained in:
Yury Semikhatsky 2021-05-12 15:52:08 +00:00 committed by GitHub
parent 45ee257a26
commit 84031d4a07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 110 deletions

View file

@ -1,2 +1,2 @@
1259
Changed: yurys@chromium.org Tue 11 May 2021 09:09:59 AM PDT
1260
Changed: yurys@chromium.org Wed 12 May 2021 08:49:17 AM PDT

View file

@ -22,14 +22,12 @@ class FrameTree {
if (!this._browsingContextGroup.__jugglerFrameTrees)
this._browsingContextGroup.__jugglerFrameTrees = new Set();
this._browsingContextGroup.__jugglerFrameTrees.add(this);
this._scriptsToEvaluateOnNewDocument = new Map();
this._isolatedWorlds = new Map();
this._webSocketEventService = Cc[
"@mozilla.org/websocketevent/service;1"
].getService(Ci.nsIWebSocketEventService);
this._bindings = new Map();
this._runtime = new Runtime(false /* isWorker */);
this._workers = new Map();
this._docShellToFrame = new Map();
@ -74,22 +72,25 @@ class FrameTree {
}
addScriptToEvaluateOnNewDocument(script, worldName) {
const scriptId = helper.generateId();
if (worldName) {
this._isolatedWorlds.set(scriptId, {script, worldName});
worldName = worldName || '';
const existing = this._isolatedWorlds.has(worldName);
const world = this._ensureWorld(worldName);
world._scriptsToEvaluateOnNewDocument.push(script);
// FIXME: 'should inherit http credentials from browser context' fails without this
if (worldName && !existing) {
for (const frame of this.frames())
frame.createIsolatedWorld(worldName);
} else {
this._scriptsToEvaluateOnNewDocument.set(scriptId, script);
frame._createIsolatedContext(worldName);
}
return {scriptId};
}
removeScriptToEvaluateOnNewDocument(scriptId) {
if (this._isolatedWorlds.has(scriptId))
this._isolatedWorlds.delete(scriptId);
else
this._scriptsToEvaluateOnNewDocument.delete(scriptId);
_ensureWorld(worldName) {
worldName = worldName || '';
let world = this._isolatedWorlds.get(worldName);
if (!world) {
world = new IsolatedWorld(worldName);
this._isolatedWorlds.set(worldName, world);
}
return world;
}
_frameForWorker(workerDebugger) {
@ -149,7 +150,9 @@ class FrameTree {
}
addBinding(worldName, name, script) {
this._bindings.set(worldName + ':' + name, {worldName, name, script});
worldName = worldName || '';
const world = this._ensureWorld(worldName);
world._bindings.set(name, script);
for (const frame of this.frames())
frame._addBinding(worldName, name, script);
}
@ -314,6 +317,14 @@ FrameTree.Events = {
Load: 'load',
};
class IsolatedWorld {
constructor(name) {
this._name = name;
this._scriptsToEvaluateOnNewDocument = [];
this._bindings = new Map();
}
}
class Frame {
constructor(frameTree, runtime, docShell, parentFrame) {
this._frameTree = frameTree;
@ -335,9 +346,8 @@ class Frame {
this._pendingNavigationURL = null;
this._textInputProcessor = null;
this._executionContext = null;
this._isolatedWorlds = new Map();
this._worldNameToContext = new Map();
this._initialNavigationDone = false;
this._webSocketListenerInnerWindowId = 0;
@ -425,7 +435,7 @@ class Frame {
};
}
createIsolatedWorld(name) {
_createIsolatedContext(name) {
const principal = [this.domWindow()]; // extended principal
const sandbox = Cu.Sandbox(principal, {
sandboxPrototype: this.domWindow(),
@ -437,13 +447,12 @@ class Frame {
frameId: this.id(),
name,
});
this._isolatedWorlds.set(world.id(), world);
this._worldNameToContext.set(name, world);
return world;
}
unsafeObject(objectId) {
const contexts = [this.executionContext(), ...this._isolatedWorlds.values()];
for (const context of contexts) {
for (const context of this._worldNameToContext.values()) {
const result = context.unsafeObject(objectId);
if (result)
return result.object;
@ -452,38 +461,19 @@ class Frame {
}
dispose() {
for (const world of this._isolatedWorlds.values())
this._runtime.destroyExecutionContext(world);
this._isolatedWorlds.clear();
if (this._executionContext)
this._runtime.destroyExecutionContext(this._executionContext);
this._executionContext = null;
}
_getOrCreateIsolatedContext(worldName) {
for (let context of this._isolatedWorlds.values()) {
if (context.auxData().name === worldName)
return context;
}
return this.createIsolatedWorld(worldName);
for (const context of this._worldNameToContext.values())
this._runtime.destroyExecutionContext(context);
this._worldNameToContext.clear();
}
_addBinding(worldName, name, script) {
const executionContext = worldName ? this._getOrCreateIsolatedContext(worldName) : this._executionContext;
let executionContext = this._worldNameToContext.get(worldName);
if (worldName && !executionContext)
executionContext = this._createIsolatedContext(worldName);
if (executionContext)
executionContext.addBinding(name, script);
}
_evaluateScriptSafely(executionContext, script) {
try {
let result = executionContext.evaluateScript(script);
if (result && result.objectId)
executionContext.disposeObject(result.objectId);
} catch (e) {
dump(`ERROR: ${e.message}\n${e.stack}\n`);
}
}
_onGlobalObjectCleared() {
const webSocketService = this._frameTree._webSocketEventService;
if (this._webSocketListenerInnerWindowId)
@ -491,33 +481,28 @@ class Frame {
this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId;
webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
if (this._executionContext)
this._runtime.destroyExecutionContext(this._executionContext);
for (const world of this._isolatedWorlds.values())
this._runtime.destroyExecutionContext(world);
this._isolatedWorlds.clear();
for (const context of this._worldNameToContext.values())
this._runtime.destroyExecutionContext(context);
this._worldNameToContext.clear();
this._executionContext = this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), {
this._worldNameToContext.set('', this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), {
frameId: this._frameId,
name: '',
});
for (const {script, worldName} of this._frameTree._isolatedWorlds.values())
this.createIsolatedWorld(worldName);
// Add bindings before evaluating scripts.
for (const [id, {worldName, name, script}] of this._frameTree._bindings)
this._addBinding(worldName, name, script);
for (const script of this._frameTree._scriptsToEvaluateOnNewDocument.values())
this._evaluateScriptSafely(this._executionContext, script);
for (const {script, worldName} of this._frameTree._isolatedWorlds.values()) {
const context = worldName ? this._getOrCreateIsolatedContext(worldName) : this.executionContext();
this._evaluateScriptSafely(context, script);
}));
for (const [name, world] of this._frameTree._isolatedWorlds) {
if (name)
this._createIsolatedContext(name);
const executionContext = this._worldNameToContext.get(name);
// Add bindings before evaluating scripts.
for (const [name, script] of world._bindings)
executionContext.addBinding(name, script);
for (const script of world._scriptsToEvaluateOnNewDocument)
executionContext.evaluateScriptSafely(script);
}
}
executionContext() {
return this._executionContext;
mainExecutionContext() {
return this._worldNameToContext.get('');
}
textInputProcessor() {

View file

@ -150,7 +150,6 @@ class PageAgent {
insertText: this._insertText.bind(this),
navigate: this._navigate.bind(this),
reload: this._reload.bind(this),
removeScriptToEvaluateOnNewDocument: ({scriptId}) => this._frameTree.removeScriptToEvaluateOnNewDocument(scriptId),
screenshot: this._screenshot.bind(this),
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
setCacheDisabled: this._setCacheDisabled.bind(this),
@ -251,8 +250,8 @@ class PageAgent {
return;
const frame = this._findFrameForNode(inputElement);
this._browserPage.emit('pageFileChooserOpened', {
executionContextId: frame.executionContext().id(),
element: frame.executionContext().rawValueToRemoteObject(inputElement)
executionContextId: frame.mainExecutionContext().id(),
element: frame.mainExecutionContext().rawValueToRemoteObject(inputElement)
});
}

View file

@ -217,9 +217,9 @@ class Runtime {
if (obj.promiseState === 'fulfilled')
return {success: true, obj: obj.promiseValue};
if (obj.promiseState === 'rejected') {
const global = executionContext._global;
exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
const debuggee = executionContext._debuggee;
exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
return {success: false, obj: null};
}
let resolve, reject;
@ -245,15 +245,15 @@ class Runtime {
pendingPromise.resolve({success: true, obj: obj.promiseValue});
return;
};
const global = pendingPromise.executionContext._global;
pendingPromise.exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
pendingPromise.exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
const debuggee = pendingPromise.executionContext._debuggee;
pendingPromise.exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
pendingPromise.exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
pendingPromise.resolve({success: false, obj: null});
}
createExecutionContext(domWindow, contextGlobal, auxData) {
// Note: domWindow is null for workers.
const context = new ExecutionContext(this, domWindow, contextGlobal, this._debugger.addDebuggee(contextGlobal), auxData);
const context = new ExecutionContext(this, domWindow, contextGlobal, auxData);
this._executionContexts.set(context._id, context);
if (domWindow)
this._windowToExecutionContext.set(domWindow, context);
@ -286,15 +286,15 @@ class Runtime {
}
class ExecutionContext {
constructor(runtime, domWindow, contextGlobal, global, auxData) {
constructor(runtime, domWindow, contextGlobal, auxData) {
this._runtime = runtime;
this._domWindow = domWindow;
this._contextGlobal = contextGlobal;
this._global = global;
this._debuggee = runtime._debugger.addDebuggee(contextGlobal);
this._remoteObjects = new Map();
this._id = generateId();
this._auxData = auxData;
this._jsonStringifyObject = this._global.executeInGlobal(`((stringify, dateProto, object) => {
this._jsonStringifyObject = this._debuggee.executeInGlobal(`((stringify, dateProto, object) => {
const oldToJson = dateProto.toJSON;
dateProto.toJSON = undefined;
let hasSymbol = false;
@ -325,7 +325,7 @@ class ExecutionContext {
if (this._domWindow && this._domWindow.document)
this._domWindow.document.notifyUserGestureActivation();
let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails);
let {success, obj} = this._getResult(this._debuggee.executeInGlobal(script), exceptionDetails);
userInputHelper && userInputHelper.destruct();
if (!success)
return null;
@ -338,8 +338,16 @@ class ExecutionContext {
return this._createRemoteObject(obj);
}
evaluateScriptSafely(script) {
try {
this._debuggee.executeInGlobal(script);
} catch (e) {
dump(`ERROR: ${e.message}\n${e.stack}\n`);
}
}
async evaluateFunction(functionText, args, exceptionDetails = {}) {
const funEvaluation = this._getResult(this._global.executeInGlobal('(' + functionText + ')'), exceptionDetails);
const funEvaluation = this._getResult(this._debuggee.executeInGlobal('(' + functionText + ')'), exceptionDetails);
if (!funEvaluation.success)
return null;
if (!funEvaluation.obj.callable)
@ -384,11 +392,7 @@ class ExecutionContext {
}, this._contextGlobal, {
defineAs: name,
});
try {
this._global.executeInGlobal(script);
} catch (e) {
dump(`ERROR: ${e.message}\n${e.stack}\n`);
}
this.evaluateScriptSafely(script);
}
unsafeObject(objectId) {
@ -398,14 +402,14 @@ class ExecutionContext {
}
rawValueToRemoteObject(rawValue) {
const debuggerObj = this._global.makeDebuggeeValue(rawValue);
const debuggerObj = this._debuggee.makeDebuggeeValue(rawValue);
return this._createRemoteObject(debuggerObj);
}
_instanceOf(debuggerObj, rawObj, className) {
if (this._domWindow)
return rawObj instanceof this._domWindow[className];
return this._global.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._global.makeDebuggeeValue(className)}).return;
return this._debuggee.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._debuggee.makeDebuggeeValue(className)}).return;
}
_createRemoteObject(debuggerObj) {
@ -489,13 +493,13 @@ class ExecutionContext {
};
}
const baseObject = Array.isArray(obj) ? '([])' : '({})';
const debuggerObj = this._global.executeInGlobal(baseObject).return;
const debuggerObj = this._debuggee.executeInGlobal(baseObject).return;
debuggerObj.defineProperties(properties);
return debuggerObj;
}
_serialize(obj) {
const result = this._global.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject});
const result = this._debuggee.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject});
if (result.throw)
throw new Error('Object is not serializable');
return result.return === undefined ? undefined : JSON.parse(result.return);
@ -530,9 +534,9 @@ class ExecutionContext {
return {success: false, obj: null};
}
if (completionValue.throw) {
if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) {
exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
if (this._debuggee.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) {
exceptionDetails.text = this._debuggee.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
exceptionDetails.stack = this._debuggee.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
} else {
exceptionDetails.value = this._serialize(completionValue.throw);
}

View file

@ -89,10 +89,10 @@ function initialize() {
if (value !== undefined)
applySetting[name](value);
}
for (const script of scriptsToEvaluateOnNewDocument)
frameTree.addScriptToEvaluateOnNewDocument(script);
for (const { worldName, name, script } of bindings)
frameTree.addBinding(worldName, name, script);
for (const script of scriptsToEvaluateOnNewDocument)
frameTree.addScriptToEvaluateOnNewDocument(script);
pageAgent = new PageAgent(messageManager, channel, frameTree);

View file

@ -336,10 +336,6 @@ class PageHandler {
return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
}
async ['Page.removeScriptToEvaluateOnNewDocument'](options) {
return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options);
}
async ['Page.dispatchKeyEvent'](options) {
return await this._contentPage.send('dispatchKeyEvent', options);
}

View file

@ -768,16 +768,8 @@ const Page = {
params: {
script: t.String,
worldName: t.Optional(t.String),
},
returns: {
scriptId: t.String,
}
},
'removeScriptToEvaluateOnNewDocument': {
params: {
scriptId: t.String,
},
},
'navigate': {
params: {
frameId: t.String,