Andrey Lushnikov 2b495c9750
browser(firefox): fix SimpleChannel to await initialization (#4311)
As Joel noticed recently, MessageManager in firefox doesn't guarantee
message delivery if the opposite end hasn't been initialized yet. In
this case, message will be silently dropped on the ground.

To fix this, we establish a handshake in SimpleChannel to make sure that
both ends are initialized, end buffer outgoing messages until this
happens.

Drive-by: serialize dialog events to only deliver *after* the
`Page.ready` protocol event. Otherwise, we deliver dialog events to the
unreported page.
2020-11-02 16:21:34 -08:00

376 lines
13 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const helper = new Helper();
class WorkerHandler {
constructor(session, contentChannel, workerId) {
this._session = session;
this._contentWorker = contentChannel.connect(workerId);
this._workerId = workerId;
const emitWrappedProtocolEvent = eventName => {
return params => {
this._session.emitEvent('Page.dispatchMessageFromWorker', {
workerId,
message: JSON.stringify({method: eventName, params}),
});
}
}
this._eventListeners = [
contentChannel.register(workerId, {
runtimeConsole: emitWrappedProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'),
}),
];
}
async sendMessage(message) {
const [domain, method] = message.method.split('.');
if (domain !== 'Runtime')
throw new Error('ERROR: can only dispatch to Runtime domain inside worker');
const result = await this._contentWorker.send(method, message.params);
this._session.emitEvent('Page.dispatchMessageFromWorker', {
workerId: this._workerId,
message: JSON.stringify({result, id: message.id}),
});
}
dispose() {
this._contentWorker.dispose();
helper.removeListeners(this._eventListeners);
}
}
class PageHandler {
constructor(target, session, contentChannel) {
this._session = session;
this._contentChannel = contentChannel;
this._contentPage = contentChannel.connect('page');
this._workers = new Map();
this._pageTarget = target;
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
const emitProtocolEvent = eventName => {
return (...args) => this._session.emitEvent(eventName, ...args);
}
this._reportedFrameIds = new Set();
this._networkEventsForUnreportedFrameIds = new Map();
// `Page.ready` protocol event is emitted whenever page has completed initialization, e.g.
// finished all the transient navigations to the `about:blank`.
//
// We'd like to avoid reporting meaningful events before the `Page.ready` since they are likely
// to be ignored by the protocol clients.
this._isPageReady = false;
if (this._pageTarget.screencastInfo())
this._onScreencastStarted();
this._eventListeners = [
helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)),
helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)),
helper.on(this._pageTarget, PageTarget.Events.Crashed, () => {
this._session.emitEvent('Page.crashed', {});
}),
helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onScreencastStarted.bind(this)),
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')),
contentChannel.register('page', {
pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
pageEventFired: emitProtocolEvent('Page.eventFired'),
pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'),
pageFrameAttached: this._onFrameAttached.bind(this),
pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
pageLinkClicked: emitProtocolEvent('Page.linkClicked'),
pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'),
pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'),
pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'),
pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'),
pageReady: this._onPageReady.bind(this),
pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
pageWorkerCreated: this._onWorkerCreated.bind(this),
pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
runtimeConsole: emitProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
webSocketCreated: emitProtocolEvent('Page.webSocketCreated'),
webSocketOpened: emitProtocolEvent('Page.webSocketOpened'),
webSocketClosed: emitProtocolEvent('Page.webSocketClosed'),
webSocketFrameReceived: emitProtocolEvent('Page.webSocketFrameReceived'),
webSocketFrameSent: emitProtocolEvent('Page.webSocketFrameSent'),
}),
];
}
async dispose() {
this._contentPage.dispose();
helper.removeListeners(this._eventListeners);
}
_onScreencastStarted() {
const info = this._pageTarget.screencastInfo();
this._session.emitEvent('Page.screencastStarted', { screencastId: info.videoSessionId, file: info.file });
}
_onPageReady(event) {
this._isPageReady = true;
this._session.emitEvent('Page.ready');
for (const dialog of this._pageTarget.dialogs())
this._onDialogOpened(dialog);
}
_onDialogOpened(dialog) {
if (!this._isPageReady)
return;
this._session.emitEvent('Page.dialogOpened', {
dialogId: dialog.id(),
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
_onDialogClosed(dialog) {
if (!this._isPageReady)
return;
this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), });
}
_onWorkerCreated({workerId, frameId, url}) {
const worker = new WorkerHandler(this._session, this._contentChannel, workerId);
this._workers.set(workerId, worker);
this._session.emitEvent('Page.workerCreated', {workerId, frameId, url});
}
_onWorkerDestroyed({workerId}) {
const worker = this._workers.get(workerId);
if (!worker)
return;
this._workers.delete(workerId);
worker.dispose();
this._session.emitEvent('Page.workerDestroyed', {workerId});
}
_handleNetworkEvent(protocolEventName, eventDetails, frameId) {
if (!this._reportedFrameIds.has(frameId)) {
let events = this._networkEventsForUnreportedFrameIds.get(frameId);
if (!events) {
events = [];
this._networkEventsForUnreportedFrameIds.set(frameId, events);
}
events.push({eventName: protocolEventName, eventDetails});
} else {
this._session.emitEvent(protocolEventName, eventDetails);
}
}
_onFrameAttached({frameId, parentFrameId}) {
this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId});
this._reportedFrameIds.add(frameId);
const events = this._networkEventsForUnreportedFrameIds.get(frameId) || [];
this._networkEventsForUnreportedFrameIds.delete(frameId);
for (const {eventName, eventDetails} of events)
this._session.emitEvent(eventName, eventDetails);
}
async ['Page.close']({runBeforeUnload}) {
// Postpone target close to deliver response in session.
Services.tm.dispatchToMainThread(() => {
this._pageTarget.close(runBeforeUnload);
});
}
async ['Page.setViewportSize']({viewportSize}) {
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
}
async ['Runtime.evaluate'](options) {
return await this._contentPage.send('evaluate', options);
}
async ['Runtime.callFunction'](options) {
return await this._contentPage.send('callFunction', options);
}
async ['Runtime.getObjectProperties'](options) {
return await this._contentPage.send('getObjectProperties', options);
}
async ['Runtime.disposeObject'](options) {
return await this._contentPage.send('disposeObject', options);
}
async ['Network.getResponseBody']({requestId}) {
return this._pageNetwork.getResponseBody(requestId);
}
async ['Network.setExtraHTTPHeaders']({headers}) {
this._pageNetwork.setExtraHTTPHeaders(headers);
}
async ['Network.setRequestInterception']({enabled}) {
if (enabled)
this._pageNetwork.enableRequestInterception();
else
this._pageNetwork.disableRequestInterception();
}
async ['Network.resumeInterceptedRequest']({requestId, method, headers, postData}) {
this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData);
}
async ['Network.abortInterceptedRequest']({requestId, errorCode}) {
this._pageNetwork.abortInterceptedRequest(requestId, errorCode);
}
async ['Network.fulfillInterceptedRequest']({requestId, status, statusText, headers, base64body}) {
this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body);
}
async ['Accessibility.getFullAXTree'](params) {
return await this._contentPage.send('getFullAXTree', params);
}
async ['Page.setFileInputFiles'](options) {
return await this._contentPage.send('setFileInputFiles', options);
}
async ['Page.setEmulatedMedia'](options) {
return await this._contentPage.send('setEmulatedMedia', options);
}
async ['Page.bringToFront'](options) {
this._pageTarget._window.focus();
}
async ['Page.setCacheDisabled'](options) {
return await this._contentPage.send('setCacheDisabled', options);
}
async ['Page.addBinding'](options) {
return await this._contentPage.send('addBinding', options);
}
async ['Page.adoptNode'](options) {
return await this._contentPage.send('adoptNode', options);
}
async ['Page.screenshot'](options) {
return await this._contentPage.send('screenshot', options);
}
async ['Page.getBoundingBox'](options) {
return await this._contentPage.send('getBoundingBox', options);
}
async ['Page.getContentQuads'](options) {
return await this._contentPage.send('getContentQuads', options);
}
async ['Page.navigate'](options) {
return await this._contentPage.send('navigate', options);
}
async ['Page.goBack'](options) {
return await this._contentPage.send('goBack', options);
}
async ['Page.goForward'](options) {
return await this._contentPage.send('goForward', options);
}
async ['Page.reload'](options) {
return await this._contentPage.send('reload', options);
}
async ['Page.describeNode'](options) {
return await this._contentPage.send('describeNode', options);
}
async ['Page.scrollIntoViewIfNeeded'](options) {
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
}
async ['Page.addScriptToEvaluateOnNewDocument'](options) {
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);
}
async ['Page.dispatchTouchEvent'](options) {
return await this._contentPage.send('dispatchTouchEvent', options);
}
async ['Page.dispatchTapEvent'](options) {
return await this._contentPage.send('dispatchTapEvent', options);
}
async ['Page.dispatchMouseEvent'](options) {
return await this._contentPage.send('dispatchMouseEvent', options);
}
async ['Page.insertText'](options) {
return await this._contentPage.send('insertText', options);
}
async ['Page.crash'](options) {
return await this._contentPage.send('crash', options);
}
async ['Page.handleDialog']({dialogId, accept, promptText}) {
const dialog = this._pageTarget.dialog(dialogId);
if (!dialog)
throw new Error('Failed to find dialog with id = ' + dialogId);
if (accept)
dialog.accept(promptText);
else
dialog.dismiss();
}
async ['Page.setInterceptFileChooserDialog'](options) {
return await this._contentPage.send('setInterceptFileChooserDialog', options);
}
async ['Page.sendMessageToWorker']({workerId, message}) {
const worker = this._workers.get(workerId);
if (!worker)
throw new Error('ERROR: cannot find worker with id ' + workerId);
return await worker.sendMessage(JSON.parse(message));
}
async ['Page.stopVideoRecording']() {
await this._pageTarget.stopVideoRecording();
}
}
var EXPORTED_SYMBOLS = ['PageHandler'];
this.PageHandler = PageHandler;