browser(firefox-stable): simplify isolated world structures (#6542)

This commit is contained in:
Yury Semikhatsky 2021-05-12 17:46:19 +00:00 committed by GitHub
parent 2697f8380f
commit d540b4478b
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 @@
1249 1250
Changed: yurys@chromium.org Tue 11 May 2021 09:29:40 AM PDT Changed: yurys@chromium.org Wed 12 May 2021 08:53:32 AM PDT

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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