browser(firefox): remove multisession logic (#4039)

This patch:
1. Changes `SimpleChannel` to buffer messages to the namespace that
   hasn't been registered yet. This allows us to create `SimpleChannel`
   per target on the browser side right away.
2. Removes multisession support. Now there's only one `PageAgent` in the
   content process, which talks to a single `PageHandler` on the browser
   side. Both ends can be created as-soon-as-needed; thanks to
   `SimpleChannel` bufferring, no messages will be lost and all messages
   will be delivered in proper order. (This is currently the reason why
   build 1178 flakes on windows).
3. Straightens up the target reporting. Targets are reported as soon
   as they appear on the browser side.

**NOTE:** this doesn't yet remove sessions from protocol.

References #3995
This commit is contained in:
Andrey Lushnikov 2020-10-02 04:13:42 -07:00 committed by GitHub
parent 5e42029fce
commit 2c11b10598
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 83 additions and 141 deletions

View File

@ -1,2 +1,2 @@
1178
Changed: lushnikov@chromium.org Wed Sep 30 23:36:27 PDT 2020
1179
Changed: lushnikov@chromium.org Fri Oct 2 03:14:15 PDT 2020

View File

@ -30,6 +30,7 @@ class SimpleChannel {
this._connectorId = 0;
this._pendingMessages = new Map();
this._handlers = new Map();
this._bufferedRequests = [];
this.transport = {
sendMessage: null,
dispose: null,
@ -70,6 +71,14 @@ class SimpleChannel {
if (this._handlers.has(namespace))
throw new Error('ERROR: double-register for namespace ' + namespace);
this._handlers.set(namespace, handler);
// Try to re-deliver all pending messages.
Promise.resolve().then(() => {
const bufferedRequests = this._bufferedRequests;
this._bufferedRequests = [];
for (const data of bufferedRequests) {
this._onMessage(data);
}
});
return () => this.unregister(namespace);
}
@ -107,7 +116,7 @@ class SimpleChannel {
const namespace = data.namespace;
const handler = this._handlers.get(namespace);
if (!handler) {
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No handler for namespace "${namespace}"`});
this._bufferedRequests.push(data);
return;
}
const method = handler[data.methodName];

View File

@ -9,10 +9,6 @@ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
const {NetworkHandler} = ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js");
const {RuntimeHandler} = ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js");
const {AccessibilityHandler} = ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js");
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
const helper = new Helper();
@ -145,15 +141,10 @@ class TargetRegistry {
if (!target)
return;
const sessions = [];
const readyData = { sessions, target };
target.markAsReported();
this.emit(TargetRegistry.Events.TargetCreated, readyData);
return {
scriptsToEvaluateOnNewDocument: target.browserContext().scriptsToEvaluateOnNewDocument,
bindings: target.browserContext().bindings,
settings: target.browserContext().settings,
sessionIds: sessions.map(session => session.sessionId()),
};
},
});
@ -162,7 +153,7 @@ class TargetRegistry {
const tab = event.target;
const userContextId = tab.userContextId;
const browserContext = this._userContextIdToBrowserContext.get(userContextId);
const hasExplicitSize = (appWindow.chromeFlags & Ci.nsIWebBrowserChrome.JUGGLER_WINDOW_EXPLICIT_SIZE) !== 0;
const hasExplicitSize = appWindow && (appWindow.chromeFlags & Ci.nsIWebBrowserChrome.JUGGLER_WINDOW_EXPLICIT_SIZE) !== 0;
const openerContext = tab.linkedBrowser.browsingContext.opener;
let openerTarget;
if (openerContext) {
@ -191,9 +182,14 @@ class TargetRegistry {
const domWindowTabListeners = new Map();
const onOpenWindow = async (appWindow) => {
if (!(appWindow instanceof Ci.nsIAppWindow))
return;
const domWindow = appWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
let domWindow;
if (appWindow instanceof Ci.nsIAppWindow) {
domWindow = appWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
} else {
domWindow = appWindow;
appWindow = null;
}
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
return;
// In persistent mode, window might be opened long ago and might be
@ -317,12 +313,11 @@ class TargetRegistry {
if (await target.hasFailedToOverrideTimezone())
throw new Error('Failed to override timezone');
}
await target.reportedPromise();
return target.id();
}
reportedTargets() {
return Array.from(this._browserToTarget.values()).filter(pageTarget => pageTarget._isReported);
targets() {
return Array.from(this._browserToTarget.values());
}
targetForBrowser(browser) {
@ -364,24 +359,12 @@ class PageTarget {
helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
];
this._isReported = false;
this._reportedPromise = new Promise(resolve => {
this._reportedCallback = resolve;
});
this._disposed = false;
browserContext.pages.add(this);
this._registry._browserToTarget.set(this._linkedBrowser, this);
this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this);
}
reportedPromise() {
return this._reportedPromise;
}
markAsReported() {
this._isReported = true;
this._reportedCallback();
this._registry.emit(TargetRegistry.Events.TargetCreated, this);
}
async windowReady() {
@ -411,7 +394,7 @@ class PageTarget {
// Otherwise, explicitly set page viewport prevales over browser context
// default viewport.
const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize;
const actualSize = setViewportSizeForBrowser(viewportSize, this._linkedBrowser, this._window);
const actualSize = await setViewportSizeForBrowser(viewportSize, this._linkedBrowser, this._window);
await this._channel.connect('').send('awaitViewportDimensions', {
width: actualSize.width,
height: actualSize.height
@ -423,30 +406,14 @@ class PageTarget {
await this.updateViewportSize();
}
connectSession(session) {
this._channel.connect('').send('attach', { sessionId: session.sessionId() });
}
disconnectSession(session) {
if (!this._disposed)
this._channel.connect('').emit('detach', { sessionId: session.sessionId() });
}
async close(runBeforeUnload = false) {
await this._gBrowser.removeTab(this._tab, {
skipPermitUnload: !runBeforeUnload,
});
}
initSession(session) {
const pageHandler = new PageHandler(this, session, this._channel);
const networkHandler = new NetworkHandler(this, session, this._channel);
session.registerHandler('Page', pageHandler);
session.registerHandler('Network', networkHandler);
session.registerHandler('Runtime', new RuntimeHandler(session, this._channel));
session.registerHandler('Accessibility', new AccessibilityHandler(session, this._channel));
pageHandler.enable();
networkHandler.enable();
channel() {
return this._channel;
}
id() {
@ -493,8 +460,7 @@ class PageTarget {
this._registry._browserToTarget.delete(this._linkedBrowser);
this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext);
helper.removeListeners(this._eventListeners);
if (this._isReported)
this._registry.emit(TargetRegistry.Events.TargetDestroyed, this);
this._registry.emit(TargetRegistry.Events.TargetDestroyed, this);
}
}
@ -733,7 +699,8 @@ async function waitForWindowReady(window) {
await helper.awaitEvent(window, 'load');
}
function setViewportSizeForBrowser(viewportSize, browser, window) {
async function setViewportSizeForBrowser(viewportSize, browser, window) {
await waitForWindowReady(window);
if (viewportSize) {
const {width, height} = viewportSize;
const rect = browser.getBoundingClientRect();

View File

@ -20,32 +20,31 @@ const obs = Cc["@mozilla.org/observer-service;1"].getService(
const helper = new Helper();
class WorkerData {
constructor(pageAgent, browserChannel, sessionId, worker) {
this._workerRuntime = worker.channel().connect(sessionId + 'runtime');
this._browserWorker = browserChannel.connect(sessionId + worker.id());
constructor(pageAgent, browserChannel, worker) {
this._workerRuntime = worker.channel().connect('runtime');
this._browserWorker = browserChannel.connect(worker.id());
this._worker = worker;
this._sessionId = sessionId;
const emit = name => {
return (...args) => this._browserWorker.emit(name, ...args);
};
this._eventListeners = [
worker.channel().register(sessionId + 'runtime', {
worker.channel().register('runtime', {
runtimeConsole: emit('runtimeConsole'),
runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'),
runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'),
}),
browserChannel.register(sessionId + worker.id(), {
browserChannel.register(worker.id(), {
evaluate: (options) => this._workerRuntime.send('evaluate', options),
callFunction: (options) => this._workerRuntime.send('callFunction', options),
getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options),
disposeObject: (options) =>this._workerRuntime.send('disposeObject', options),
}),
];
worker.channel().connect('').emit('attach', {sessionId});
worker.channel().connect('').emit('attach');
}
dispose() {
this._worker.channel().connect('').emit('detach', {sessionId: this._sessionId});
this._worker.channel().connect('').emit('detach');
this._workerRuntime.dispose();
this._browserWorker.dispose();
helper.removeListeners(this._eventListeners);
@ -112,12 +111,11 @@ class FrameData {
}
class PageAgent {
constructor(messageManager, browserChannel, sessionId, frameTree) {
constructor(messageManager, browserChannel, frameTree) {
this._messageManager = messageManager;
this._browserChannel = browserChannel;
this._sessionId = sessionId;
this._browserPage = browserChannel.connect(sessionId + 'page');
this._browserRuntime = browserChannel.connect(sessionId + 'runtime');
this._browserPage = browserChannel.connect('page');
this._browserRuntime = browserChannel.connect('runtime');
this._frameTree = frameTree;
this._runtime = frameTree.runtime();
@ -127,7 +125,7 @@ class PageAgent {
this._isolatedWorlds = new Map();
this._eventListeners = [
browserChannel.register(sessionId + 'page', {
browserChannel.register('page', {
addBinding: ({ name, script }) => this._frameTree.addBinding(name, script),
addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this),
adoptNode: this._adoptNode.bind(this),
@ -152,7 +150,7 @@ class PageAgent {
setFileInputFiles: this._setFileInputFiles.bind(this),
setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this),
}),
browserChannel.register(sessionId + 'runtime', {
browserChannel.register('runtime', {
evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime),
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
@ -302,7 +300,7 @@ class PageAgent {
}
_onWorkerCreated(worker) {
const workerData = new WorkerData(this, this._browserChannel, this._sessionId, worker);
const workerData = new WorkerData(this, this._browserChannel, worker);
this._workerData.set(worker.id(), workerData);
this._browserPage.emit('pageWorkerCreated', {
workerId: worker.id(),

View File

@ -6,8 +6,6 @@
loadSubScript('chrome://juggler/content/content/Runtime.js');
loadSubScript('chrome://juggler/content/SimpleChannel.js');
const runtimeAgents = new Map();
const channel = new SimpleChannel('worker::worker');
const eventListener = event => channel._onMessage(JSON.parse(event.data));
this.addEventListener('message', eventListener);
@ -34,11 +32,11 @@ const runtime = new Runtime(true /* isWorker */);
})();
class RuntimeAgent {
constructor(runtime, channel, sessionId) {
constructor(runtime, channel) {
this._runtime = runtime;
this._browserRuntime = channel.connect(sessionId + 'runtime');
this._browserRuntime = channel.connect('runtime');
this._eventListeners = [
channel.register(sessionId + 'runtime', {
channel.register('runtime', {
evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime),
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
@ -72,15 +70,14 @@ class RuntimeAgent {
}
}
let runtimeAgent;
channel.register('', {
attach: ({sessionId}) => {
const runtimeAgent = new RuntimeAgent(runtime, channel, sessionId);
runtimeAgents.set(sessionId, runtimeAgent);
attach: () => {
runtimeAgent = new RuntimeAgent(runtime, channel);
},
detach: ({sessionId}) => {
const runtimeAgent = runtimeAgents.get(sessionId);
runtimeAgents.delete(sessionId);
detach: () => {
runtimeAgent.dispose();
},
});

View File

@ -12,20 +12,7 @@ let frameTree;
const helper = new Helper();
const messageManager = this;
const sessions = new Map();
function createContentSession(channel, sessionId) {
const pageAgent = new PageAgent(messageManager, channel, sessionId, frameTree);
sessions.set(sessionId, [pageAgent]);
pageAgent.enable();
}
function disposeContentSession(sessionId) {
const handlers = sessions.get(sessionId);
sessions.delete(sessionId);
for (const handler of handlers)
handler.dispose();
}
let pageAgent;
let failedToOverrideTimezone = false;
@ -89,6 +76,8 @@ const applySetting = {
},
};
const channel = SimpleChannel.createForMessageManager('content::page', messageManager);
function initialize() {
const response = sendSyncMessage('juggler:content-ready')[0];
// If we didn't get a response, then we don't want to do anything
@ -96,7 +85,6 @@ function initialize() {
if (!response)
return;
const {
sessionIds = [],
scriptsToEvaluateOnNewDocument = [],
bindings = [],
settings = {}
@ -114,20 +102,10 @@ function initialize() {
for (const { name, script } of bindings)
frameTree.addBinding(name, script);
const channel = SimpleChannel.createForMessageManager('content::page', messageManager);
for (const sessionId of sessionIds)
createContentSession(channel, sessionId);
pageAgent = new PageAgent(messageManager, channel, frameTree);
pageAgent.enable();
channel.register('', {
attach({sessionId}) {
createContentSession(channel, sessionId);
},
detach({sessionId}) {
disposeContentSession(sessionId);
},
addScriptToEvaluateOnNewDocument(script) {
frameTree.addScriptToEvaluateOnNewDocument(script);
},
@ -169,12 +147,9 @@ function initialize() {
const gListeners = [
helper.addEventListener(messageManager, 'unload', msg => {
helper.removeListeners(gListeners);
channel.dispose();
for (const sessionId of sessions.keys())
disposeContentSession(sessionId);
pageAgent.dispose();
frameTree.dispose();
channel.dispose();
}),
];
}

View File

@ -4,7 +4,7 @@
class AccessibilityHandler {
constructor(session, contentChannel) {
this._contentPage = contentChannel.connect(session.sessionId() + 'page');
this._contentPage = contentChannel.connect('page');
}
async getFullAXTree(params) {

View File

@ -7,6 +7,10 @@
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
const {NetworkHandler} = ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js");
const {RuntimeHandler} = ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js");
const {AccessibilityHandler} = ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js");
const helper = new Helper();
@ -29,19 +33,6 @@ class BrowserHandler {
this._enabled = true;
this._attachToDefaultContext = attachToDefaultContext;
for (const target of this._targetRegistry.reportedTargets()) {
if (!this._shouldAttachToTarget(target))
continue;
const session = this._dispatcher.createSession();
this._attachedSessions.set(target, session);
this._session.emitEvent('Browser.attachedToTarget', {
sessionId: session.sessionId(),
targetInfo: target.info()
});
target.initSession(session);
target.connectSession(session);
}
this._eventListeners = [
helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
@ -54,6 +45,9 @@ class BrowserHandler {
};
Services.obs.addObserver(onScreencastStopped, 'juggler-screencast-stopped');
this._eventListeners.push(() => Services.obs.removeObserver(onScreencastStopped, 'juggler-screencast-stopped'));
for (const target of this._targetRegistry.targets())
this._onTargetCreated(target);
}
async createBrowserContext({removeOnDetach}) {
@ -73,10 +67,8 @@ class BrowserHandler {
dispose() {
helper.removeListeners(this._eventListeners);
for (const [target, session] of this._attachedSessions) {
target.disconnectSession(session);
for (const [target, session] of this._attachedSessions)
this._dispatcher.destroySession(session);
}
this._attachedSessions.clear();
for (const browserContextId of this._createdBrowserContextIds) {
const browserContext = this._targetRegistry.browserContextForId(browserContextId);
@ -87,24 +79,29 @@ class BrowserHandler {
}
_shouldAttachToTarget(target) {
if (!target._browserContext)
return false;
if (this._createdBrowserContextIds.has(target._browserContext.browserContextId))
return true;
return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext();
}
_onTargetCreated({sessions, target}) {
_onTargetCreated(target) {
if (!this._shouldAttachToTarget(target))
return;
const channel = target.channel();
const session = this._dispatcher.createSession();
this._attachedSessions.set(target, session);
const pageHandler = new PageHandler(target, session, channel);
const networkHandler = new NetworkHandler(target, session, channel);
session.registerHandler('Page', pageHandler);
session.registerHandler('Network', networkHandler);
session.registerHandler('Runtime', new RuntimeHandler(session, channel));
session.registerHandler('Accessibility', new AccessibilityHandler(session, channel));
pageHandler.enable();
networkHandler.enable();
this._session.emitEvent('Browser.attachedToTarget', {
sessionId: session.sessionId(),
targetInfo: target.info()
});
target.initSession(session);
sessions.push(session);
}
_onTargetDestroyed(target) {

View File

@ -17,7 +17,7 @@ const helper = new Helper();
class WorkerHandler {
constructor(session, contentChannel, workerId) {
this._session = session;
this._contentWorker = contentChannel.connect(session.sessionId() + workerId);
this._contentWorker = contentChannel.connect(workerId);
this._workerId = workerId;
const emitWrappedProtocolEvent = eventName => {
@ -30,7 +30,7 @@ class WorkerHandler {
}
this._eventListeners = [
contentChannel.register(session.sessionId() + workerId, {
contentChannel.register(workerId, {
runtimeConsole: emitWrappedProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'),
@ -59,7 +59,7 @@ class PageHandler {
constructor(target, session, contentChannel) {
this._session = session;
this._contentChannel = contentChannel;
this._contentPage = contentChannel.connect(session.sessionId() + 'page');
this._contentPage = contentChannel.connect('page');
this._workers = new Map();
const emitProtocolEvent = eventName => {
@ -67,7 +67,7 @@ class PageHandler {
}
this._eventListeners = [
contentChannel.register(session.sessionId() + 'page', {
contentChannel.register('page', {
pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
pageEventFired: emitProtocolEvent('Page.eventFired'),

View File

@ -14,15 +14,14 @@ const helper = new Helper();
class RuntimeHandler {
constructor(session, contentChannel) {
const sessionId = session.sessionId();
this._contentRuntime = contentChannel.connect(sessionId + 'runtime');
this._contentRuntime = contentChannel.connect('runtime');
const emitProtocolEvent = eventName => {
return (...args) => session.emitEvent(eventName, ...args);
}
this._eventListeners = [
contentChannel.register(sessionId + 'runtime', {
contentChannel.register('runtime', {
runtimeConsole: emitProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),