From c8a64b88e1bb3892db1481a11f58e4ba481a34e0 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 6 Oct 2020 00:15:24 -0700 Subject: [PATCH] browser(firefox): enable document channel (#4065) In the current tip-of-tree Firefox, document channel is enabled by default, so we have to enable it in order to roll further. This patch: 1. Removes content disposition sniffing from content process since it crashes renderer with document channel. 2. Merges all page-related handlers in a single `PageHandler` and serializes network events wrt the `Page.frameAttached` event. The serialization mentioned in (2) is necessary: frame attachment is reported from the content process, and network events are reported from the browsers process. This is an inherent race, that becomes exposed by the document channel. On a side note, (2) makes it possible to synchronously report all buffered events in `SimpleChannel` (cc offline discussion with @dgozman that highlighted an unsighty approach that we currently employ there: reporting events in a subsequent microtask.) References #3995 --- browser_patches/firefox/BUILD_NUMBER | 4 +- .../firefox/juggler/NetworkObserver.js | 39 ++-- .../firefox/juggler/TargetRegistry.js | 31 +-- .../firefox/juggler/components/juggler.js | 6 +- .../firefox/juggler/content/FrameTree.js | 21 +- browser_patches/firefox/juggler/jar.mn | 3 - .../juggler/protocol/AccessibilityHandler.js | 20 -- .../juggler/protocol/BrowserHandler.js | 74 +++---- .../firefox/juggler/protocol/Dispatcher.js | 30 ++- .../juggler/protocol/NetworkHandler.js | 86 -------- .../firefox/juggler/protocol/PageHandler.js | 196 ++++++++++++------ .../juggler/protocol/RuntimeHandler.js | 55 ----- .../firefox/preferences/playwright.cfg | 4 - 13 files changed, 216 insertions(+), 353 deletions(-) delete mode 100644 browser_patches/firefox/juggler/protocol/AccessibilityHandler.js delete mode 100644 browser_patches/firefox/juggler/protocol/NetworkHandler.js delete mode 100644 browser_patches/firefox/juggler/protocol/RuntimeHandler.js diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index a981b340e3..02913a0183 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -1,2 +1,2 @@ -1181 -Changed: pavel.feldman@gmail.com Mon, Oct 5, 2020 5:57:35 PM +1182 +Changed: lushnikov@chromium.org Mon Oct 5 23:55:54 PDT 2020 diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js index 62578d9ad7..522a8f2a14 100644 --- a/browser_patches/firefox/juggler/NetworkObserver.js +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -128,11 +128,24 @@ class NetworkRequest { this.httpChannel = httpChannel; this._networkObserver._channelToRequest.set(this.httpChannel, this); + const loadInfo = this.httpChannel.loadInfo; + let browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.browsingContext; + // TODO: Unfortunately, requests from web workers don't have frameBrowsingContext or + // browsingContext. + // + // We fail to attribute them to the original frames on the browser side, but we + // can use load context top frame to attribute them to the top frame at least. + if (!browsingContext) { + const loadContext = helper.getLoadContext(this.httpChannel); + browsingContext = loadContext?.topFrameElement?.browsingContext; + } + + this._frameId = helper.browsingContextToFrameId(browsingContext); + this.requestId = httpChannel.channelId + ''; this.navigationId = httpChannel.isMainDocumentChannel ? this.requestId : undefined; const internalCauseType = this.httpChannel.loadInfo ? this.httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; - this.channelKey = this.httpChannel.channelId + ':' + internalCauseType; this._redirectedIndex = 0; const ignoredRedirect = redirectedFrom && !redirectedFrom._sentOnResponse; @@ -140,13 +153,11 @@ class NetworkRequest { // We just ignore redirect that did not hit the network before being redirected. // This happens, for example, for automatic http->https redirects. this.navigationId = redirectedFrom.navigationId; - this.channelKey = redirectedFrom.channelKey; } else if (redirectedFrom) { this.redirectedFromId = redirectedFrom.requestId; this._redirectedIndex = redirectedFrom._redirectedIndex + 1; this.requestId = this.requestId + '-redirect' + this._redirectedIndex; this.navigationId = redirectedFrom.navigationId; - this.channelKey = redirectedFrom.channelKey; // Finish previous request now. Since we inherit the listener, we could in theory // use onStopRequest, but that will only happen after the last redirect has finished. redirectedFrom._sendOnRequestFinished(); @@ -492,21 +503,9 @@ class NetworkRequest { const loadInfo = this.httpChannel.loadInfo; const causeType = loadInfo?.externalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER; const internalCauseType = loadInfo?.internalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER; - - let browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.browsingContext; - // TODO: Unfortunately, requests from web workers don't have frameBrowsingContext or - // browsingContext. - // - // We fail to attribute them to the original frames on the browser side, but we - // can use load context top frame to attribute them to the top frame at least. - if (!browsingContext) { - const loadContext = helper.getLoadContext(this.httpChannel); - browsingContext = loadContext?.topFrameElement?.browsingContext; - } - pageNetwork.emit(PageNetwork.Events.Request, { url: this.httpChannel.URI.spec, - frameId: helper.browsingContextToFrameId(browsingContext), + frameId: this._frameId, isIntercepted, requestId: this.requestId, redirectedFrom: this.redirectedFromId, @@ -516,7 +515,7 @@ class NetworkRequest { navigationId: this.navigationId, cause: causeTypeToString(causeType), internalCause: causeTypeToString(internalCauseType), - }, this.channelKey); + }, this._frameId); } _sendOnResponse(fromCache) { @@ -563,7 +562,7 @@ class NetworkRequest { remotePort, status, statusText, - }); + }, this._frameId); } _sendOnRequestFailed(error) { @@ -572,7 +571,7 @@ class NetworkRequest { pageNetwork.emit(PageNetwork.Events.RequestFailed, { requestId: this.requestId, errorCode: helper.getNetworkErrorStatusText(error), - }); + }, this._frameId); } this._networkObserver._channelToRequest.delete(this.httpChannel); } @@ -582,7 +581,7 @@ class NetworkRequest { if (pageNetwork) { pageNetwork.emit(PageNetwork.Events.RequestFinished, { requestId: this.requestId, - }); + }, this._frameId); } this._networkObserver._channelToRequest.delete(this.httpChannel); } diff --git a/browser_patches/firefox/juggler/TargetRegistry.js b/browser_patches/firefox/juggler/TargetRegistry.js index b3e2e91886..9387a2a5d1 100644 --- a/browser_patches/firefox/juggler/TargetRegistry.js +++ b/browser_patches/firefox/juggler/TargetRegistry.js @@ -26,29 +26,16 @@ class DownloadInterceptor { constructor(registry) { this._registry = registry this._handlerToUuid = new Map(); - helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'); - } - - _onRequest(httpChannel, topic) { - let loadContext = helper.getLoadContext(httpChannel); - if (!loadContext) - return; - if (!loadContext.topFrameElement) - return; - const target = this._registry.targetForBrowser(loadContext.topFrameElement); - if (!target) - return; - target._channelIds.add(httpChannel.channelId); } // // nsIDownloadInterceptor implementation. // interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) { - let pageTarget = this._registry._browserBrowsingContextToTarget.get(browsingContext); - // New page downloads won't have browsing contex. - if (!pageTarget) - pageTarget = this._registry._targetForChannel(request); + if (!(request instanceof Ci.nsIChannel)) + return false; + const channel = request.QueryInterface(Ci.nsIChannel); + let pageTarget = this._registry._browserBrowsingContextToTarget.get(channel.loadInfo.browsingContext); if (!pageTarget) return false; @@ -324,15 +311,6 @@ class TargetRegistry { targetForBrowser(browser) { return this._browserToTarget.get(browser); } - - _targetForChannel(channel) { - const channelId = channel.channelId; - for (const target of this._browserToTarget.values()) { - if (target._channelIds.has(channelId)) - return target; - } - return null; - } } class PageTarget { @@ -350,7 +328,6 @@ class PageTarget { this._url = 'about:blank'; this._openerId = opener ? opener.id() : undefined; this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager); - this._channelIds = new Set(); this._screencastInfo = undefined; const navigationListener = { diff --git a/browser_patches/firefox/juggler/components/juggler.js b/browser_patches/firefox/juggler/components/juggler.js index e0943bb3c2..bba1f9d491 100644 --- a/browser_patches/firefox/juggler/components/juggler.js +++ b/browser_patches/firefox/juggler/components/juggler.js @@ -73,7 +73,7 @@ CommandLineHandler.prototype = { if (silent) Services.startup.exitLastWindowClosingSurvivalArea(); }); - dispatcher.rootSession().registerHandler('Browser', browserHandler); + dispatcher.rootSession().setHandler(browserHandler); } }); loadFrameScript(); @@ -101,9 +101,9 @@ CommandLineHandler.prototype = { pipe.stop(); }); }); - dispatcher.rootSession().registerHandler('Browser', browserHandler); + dispatcher.rootSession().setHandler(browserHandler); loadFrameScript(); - dump(`Juggler listening to the pipe\n`); + dump(`\nJuggler listening to the pipe\n`); } }, diff --git a/browser_patches/firefox/juggler/content/FrameTree.js b/browser_patches/firefox/juggler/content/FrameTree.js index 21dbbdac4e..a666b2f1fe 100644 --- a/browser_patches/firefox/juggler/content/FrameTree.js +++ b/browser_patches/firefox/juggler/content/FrameTree.js @@ -197,19 +197,12 @@ class FrameTree { const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING; const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; - let isDownload = false; - try { - isDownload = (channel.contentDisposition === Ci.nsIChannel.DISPOSITION_ATTACHMENT); - } catch(e) { - // The method is expected to throw if it's not an attachment. - } - if (isStart) { // Starting a new navigation. frame._pendingNavigationId = this._channelId(channel); frame._pendingNavigationURL = channel.URI.spec; this.emit(FrameTree.Events.NavigationStarted, frame); - } else if (isTransferring || (isStop && frame._pendingNavigationId && !status && !isDownload)) { + } else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) { // Navigation is committed. for (const subframe of frame._children) this._detachFrame(subframe); @@ -221,15 +214,15 @@ class FrameTree { this.emit(FrameTree.Events.NavigationCommitted, frame); if (frame === this._mainFrame) this.forcePageReady(); - } else if (isStop && frame._pendingNavigationId && (status || isDownload)) { + } else if (isStop && frame._pendingNavigationId && status) { // Navigation is aborted. const navigationId = frame._pendingNavigationId; frame._pendingNavigationId = null; frame._pendingNavigationURL = null; // Always report download navigation as failure to match other browsers. - const errorText = isDownload ? 'Will download to file' : helper.getNetworkErrorStatusText(status); + const errorText = helper.getNetworkErrorStatusText(status); this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText); - if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED && !isDownload) + if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED) this.forcePageReady(); } } @@ -245,9 +238,9 @@ class FrameTree { } _channelId(channel) { - if (channel instanceof Ci.nsIHttpChannel) { - const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); - return String(httpChannel.channelId); + if (channel instanceof Ci.nsIIdentChannel) { + const identChannel = channel.QueryInterface(Ci.nsIIdentChannel); + return String(identChannel.channelId); } return helper.generateId(); } diff --git a/browser_patches/firefox/juggler/jar.mn b/browser_patches/firefox/juggler/jar.mn index 419fb2d9f4..adc90edd79 100644 --- a/browser_patches/firefox/juggler/jar.mn +++ b/browser_patches/firefox/juggler/jar.mn @@ -12,10 +12,7 @@ juggler.jar: content/protocol/Protocol.js (protocol/Protocol.js) content/protocol/Dispatcher.js (protocol/Dispatcher.js) content/protocol/PageHandler.js (protocol/PageHandler.js) - content/protocol/RuntimeHandler.js (protocol/RuntimeHandler.js) - content/protocol/NetworkHandler.js (protocol/NetworkHandler.js) content/protocol/BrowserHandler.js (protocol/BrowserHandler.js) - content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js) content/content/main.js (content/main.js) content/content/FrameTree.js (content/FrameTree.js) content/content/PageAgent.js (content/PageAgent.js) diff --git a/browser_patches/firefox/juggler/protocol/AccessibilityHandler.js b/browser_patches/firefox/juggler/protocol/AccessibilityHandler.js deleted file mode 100644 index 8fe3e155fd..0000000000 --- a/browser_patches/firefox/juggler/protocol/AccessibilityHandler.js +++ /dev/null @@ -1,20 +0,0 @@ -/* 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/. */ - -class AccessibilityHandler { - constructor(session, contentChannel) { - this._contentPage = contentChannel.connect('page'); - } - - async getFullAXTree(params) { - return await this._contentPage.send('getFullAXTree', params); - } - - dispose() { - this._contentPage.dispose(); - } -} - -var EXPORTED_SYMBOLS = ['AccessibilityHandler']; -this.AccessibilityHandler = AccessibilityHandler; diff --git a/browser_patches/firefox/juggler/protocol/BrowserHandler.js b/browser_patches/firefox/juggler/protocol/BrowserHandler.js index d979c3765c..cce64d5561 100644 --- a/browser_patches/firefox/juggler/protocol/BrowserHandler.js +++ b/browser_patches/firefox/juggler/protocol/BrowserHandler.js @@ -8,9 +8,6 @@ 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(); @@ -27,7 +24,7 @@ class BrowserHandler { this._onclose = onclose; } - async enable({attachToDefaultContext}) { + async ['Browser.enable']({attachToDefaultContext}) { if (this._enabled) return; this._enabled = true; @@ -50,7 +47,7 @@ class BrowserHandler { this._onTargetCreated(target); } - async createBrowserContext({removeOnDetach}) { + async ['Browser.createBrowserContext']({removeOnDetach}) { if (!this._enabled) throw new Error('Browser domain is not enabled'); const browserContext = this._targetRegistry.createBrowserContext(removeOnDetach); @@ -58,7 +55,7 @@ class BrowserHandler { return {browserContextId: browserContext.browserContextId}; } - async removeBrowserContext({browserContextId}) { + async ['Browser.removeBrowserContext']({browserContextId}) { if (!this._enabled) throw new Error('Browser domain is not enabled'); await this._targetRegistry.browserContextForId(browserContextId).destroy(); @@ -90,18 +87,11 @@ class BrowserHandler { 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() }); + session.setHandler(new PageHandler(target, session, channel)); } _onTargetDestroyed(target) { @@ -124,12 +114,12 @@ class BrowserHandler { this._session.emitEvent('Browser.downloadFinished', downloadInfo); } - async newPage({browserContextId}) { + async ['Browser.newPage']({browserContextId}) { const targetId = await this._targetRegistry.newPage({browserContextId}); return {targetId}; } - async close() { + async ['Browser.close']() { let browserWindow = Services.wm.getMostRecentWindow( "navigator:browser" ); @@ -140,109 +130,109 @@ class BrowserHandler { Services.startup.quit(Ci.nsIAppStartup.eForceQuit); } - async grantPermissions({browserContextId, origin, permissions}) { + async ['Browser.grantPermissions']({browserContextId, origin, permissions}) { await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions); } - resetPermissions({browserContextId}) { + async ['Browser.resetPermissions']({browserContextId}) { this._targetRegistry.browserContextForId(browserContextId).resetPermissions(); } - setExtraHTTPHeaders({browserContextId, headers}) { + ['Browser.setExtraHTTPHeaders']({browserContextId, headers}) { this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers; } - setHTTPCredentials({browserContextId, credentials}) { + ['Browser.setHTTPCredentials']({browserContextId, credentials}) { this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials); } - async setBrowserProxy({type, host, port, bypass, username, password}) { + async ['Browser.setBrowserProxy']({type, host, port, bypass, username, password}) { this._targetRegistry.setBrowserProxy({ type, host, port, bypass, username, password}); } - async setContextProxy({browserContextId, type, host, port, bypass, username, password}) { + async ['Browser.setContextProxy']({browserContextId, type, host, port, bypass, username, password}) { const browserContext = this._targetRegistry.browserContextForId(browserContextId); browserContext.setProxy({ type, host, port, bypass, username, password }); } - setRequestInterception({browserContextId, enabled}) { + ['Browser.setRequestInterception']({browserContextId, enabled}) { this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled; } - setIgnoreHTTPSErrors({browserContextId, ignoreHTTPSErrors}) { + ['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) { this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors)); } - setDownloadOptions({browserContextId, downloadOptions}) { + ['Browser.setDownloadOptions']({browserContextId, downloadOptions}) { this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions); } - async setGeolocationOverride({browserContextId, geolocation}) { + async ['Browser.setGeolocationOverride']({browserContextId, geolocation}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation)); } - async setOnlineOverride({browserContextId, override}) { + async ['Browser.setOnlineOverride']({browserContextId, override}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override)); } - async setColorScheme({browserContextId, colorScheme}) { + async ['Browser.setColorScheme']({browserContextId, colorScheme}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('colorScheme', nullToUndefined(colorScheme)); } - async setScreencastOptions({browserContextId, dir, width, height, scale}) { + async ['Browser.setScreencastOptions']({browserContextId, dir, width, height, scale}) { await this._targetRegistry.browserContextForId(browserContextId).setScreencastOptions({dir, width, height, scale}); } - async setUserAgentOverride({browserContextId, userAgent}) { + async ['Browser.setUserAgentOverride']({browserContextId, userAgent}) { await this._targetRegistry.browserContextForId(browserContextId).setDefaultUserAgent(userAgent); } - async setBypassCSP({browserContextId, bypassCSP}) { + async ['Browser.setBypassCSP']({browserContextId, bypassCSP}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP)); } - async setJavaScriptDisabled({browserContextId, javaScriptDisabled}) { + async ['Browser.setJavaScriptDisabled']({browserContextId, javaScriptDisabled}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled)); } - async setLocaleOverride({browserContextId, locale}) { + async ['Browser.setLocaleOverride']({browserContextId, locale}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale)); } - async setTimezoneOverride({browserContextId, timezoneId}) { + async ['Browser.setTimezoneOverride']({browserContextId, timezoneId}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId)); } - async setTouchOverride({browserContextId, hasTouch}) { + async ['Browser.setTouchOverride']({browserContextId, hasTouch}) { await this._targetRegistry.browserContextForId(browserContextId).applySetting('hasTouch', nullToUndefined(hasTouch)); } - async setDefaultViewport({browserContextId, viewport}) { + async ['Browser.setDefaultViewport']({browserContextId, viewport}) { await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport)); } - async addScriptToEvaluateOnNewDocument({browserContextId, script}) { + async ['Browser.addScriptToEvaluateOnNewDocument']({browserContextId, script}) { await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script); } - async addBinding({browserContextId, name, script}) { + async ['Browser.addBinding']({browserContextId, name, script}) { await this._targetRegistry.browserContextForId(browserContextId).addBinding(name, script); } - setCookies({browserContextId, cookies}) { + ['Browser.setCookies']({browserContextId, cookies}) { this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies); } - clearCookies({browserContextId}) { + ['Browser.clearCookies']({browserContextId}) { this._targetRegistry.browserContextForId(browserContextId).clearCookies(); } - getCookies({browserContextId}) { + ['Browser.getCookies']({browserContextId}) { const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies(); return {cookies}; } - async getInfo() { + async ['Browser.getInfo']() { const version = Components.classes["@mozilla.org/xre/app-info;1"] .getService(Components.interfaces.nsIXULAppInfo) .version; diff --git a/browser_patches/firefox/juggler/protocol/Dispatcher.js b/browser_patches/firefox/juggler/protocol/Dispatcher.js index 1c882dda33..8f13d512ab 100644 --- a/browser_patches/firefox/juggler/protocol/Dispatcher.js +++ b/browser_patches/firefox/juggler/protocol/Dispatcher.js @@ -66,7 +66,7 @@ class Dispatcher { if (!checkScheme(descriptor.params || {}, params, details)) throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`); - const result = await session.dispatch(domain, methodName, params); + const result = await session.dispatch(method, params); details = {}; if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details)) @@ -97,24 +97,21 @@ class ProtocolSession { constructor(dispatcher, sessionId) { this._sessionId = sessionId; this._dispatcher = dispatcher; - this._handlers = new Map(); + this._handler = null; } sessionId() { return this._sessionId; } - registerHandler(domainName, handler) { - this._handlers.set(domainName, handler); + setHandler(handler) { + this._handler = handler; } _dispose() { - for (const [domainName, handler] of this._handlers) { - if (typeof handler.dispose !== 'function') - throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`); - handler.dispose(); - } - this._handlers.clear(); + if (this._handler) + this._handler.dispose(); + this._handler = null; this._dispatcher = null; } @@ -124,13 +121,12 @@ class ProtocolSession { this._dispatcher._emitEvent(this._sessionId, eventName, params); } - async dispatch(domainName, methodName, params) { - const handler = this._handlers.get(domainName); - if (!handler) - throw new Error(`Domain "${domainName}" does not exist`); - if (!handler[methodName]) - throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`); - return await handler[methodName](params); + async dispatch(method, params) { + if (!this._handler) + throw new Error(`Session does not have a handler!`); + if (!this._handler[method]) + throw new Error(`Handler for does not implement method "${method}"`); + return await this._handler[method](params); } } diff --git a/browser_patches/firefox/juggler/protocol/NetworkHandler.js b/browser_patches/firefox/juggler/protocol/NetworkHandler.js deleted file mode 100644 index b2a0c64d2e..0000000000 --- a/browser_patches/firefox/juggler/protocol/NetworkHandler.js +++ /dev/null @@ -1,86 +0,0 @@ -/* 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 {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.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 NetworkHandler { - constructor(target, session, contentChannel) { - this._session = session; - this._enabled = false; - this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); - this._eventListeners = []; - } - - async enable() { - if (this._enabled) - return; - this._enabled = true; - this._eventListeners = [ - helper.on(this._pageNetwork, PageNetwork.Events.Request, this._onRequest.bind(this)), - helper.on(this._pageNetwork, PageNetwork.Events.Response, this._onResponse.bind(this)), - helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._onRequestFinished.bind(this)), - helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._onRequestFailed.bind(this)), - this._pageNetwork.addSession(), - ]; - } - - async getResponseBody({requestId}) { - return this._pageNetwork.getResponseBody(requestId); - } - - async setExtraHTTPHeaders({headers}) { - this._pageNetwork.setExtraHTTPHeaders(headers); - } - - async setRequestInterception({enabled}) { - if (enabled) - this._pageNetwork.enableRequestInterception(); - else - this._pageNetwork.disableRequestInterception(); - } - - async resumeInterceptedRequest({requestId, method, headers, postData}) { - this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData); - } - - async abortInterceptedRequest({requestId, errorCode}) { - this._pageNetwork.abortInterceptedRequest(requestId, errorCode); - } - - async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) { - this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body); - } - - dispose() { - helper.removeListeners(this._eventListeners); - } - - async _onRequest(eventDetails, channelKey) { - this._session.emitEvent('Network.requestWillBeSent', eventDetails); - } - - async _onResponse(eventDetails) { - this._session.emitEvent('Network.responseReceived', eventDetails); - } - - async _onRequestFinished(eventDetails) { - this._session.emitEvent('Network.requestFinished', eventDetails); - } - - async _onRequestFailed(eventDetails) { - this._session.emitEvent('Network.requestFailed', eventDetails); - } -} - -var EXPORTED_SYMBOLS = ['NetworkHandler']; -this.NetworkHandler = NetworkHandler; diff --git a/browser_patches/firefox/juggler/protocol/PageHandler.js b/browser_patches/firefox/juggler/protocol/PageHandler.js index ef853c62d9..e7496af607 100644 --- a/browser_patches/firefox/juggler/protocol/PageHandler.js +++ b/browser_patches/firefox/juggler/protocol/PageHandler.js @@ -6,6 +6,7 @@ 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 Cc = Components.classes; const Ci = Components.interfaces; @@ -59,19 +60,28 @@ class PageHandler { this._session = session; this._contentChannel = contentChannel; this._contentPage = contentChannel.connect('page'); + this._contentRuntime = contentChannel.connect('runtime'); this._workers = new Map(); + this._pageTarget = target; + this._browser = target.linkedBrowser(); + this._dialogs = new Map(); + this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); + const emitProtocolEvent = eventName => { return (...args) => this._session.emitEvent(eventName, ...args); } + this._reportedFrameIds = new Set(); + this._networkEventsForUnreportedFrameIds = new Map(); + this._eventListeners = [ contentChannel.register('page', { pageBindingCalled: emitProtocolEvent('Page.bindingCalled'), pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'), pageEventFired: emitProtocolEvent('Page.eventFired'), pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'), - pageFrameAttached: emitProtocolEvent('Page.frameAttached'), + pageFrameAttached: this._onFrameAttached.bind(this), pageFrameDetached: emitProtocolEvent('Page.frameDetached'), pageLinkClicked: emitProtocolEvent('Page.linkClicked'), pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'), @@ -84,12 +94,35 @@ class PageHandler { pageWorkerCreated: this._onWorkerCreated.bind(this), pageWorkerDestroyed: this._onWorkerDestroyed.bind(this), }), + contentChannel.register('runtime', { + runtimeConsole: emitProtocolEvent('Runtime.console'), + runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), + runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), + }), + helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => { + // wait for the dialog to be actually added to DOM. + await Promise.resolve(); + this._updateModalDialogs(); + }), + helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), + helper.on(this._pageTarget, 'crashed', () => { + this._session.emitEvent('Page.crashed', {}); + }), + helper.on(this._pageTarget, 'screencastStarted', () => { + const info = this._pageTarget.screencastInfo(); + this._session.emitEvent('Page.screencastStarted', { screencastId: '' + info.videoSessionId, file: info.file }); + }), + 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')), + this._pageNetwork.addSession(), ]; - this._pageTarget = target; - this._browser = target.linkedBrowser(); - this._dialogs = new Map(); - this._enabled = false; + this._updateModalDialogs(); + const options = this._pageTarget.browserContext().screencastOptions; + if (options) + this._pageTarget.startVideoRecording(options); } _onWorkerCreated({workerId, frameId, url}) { @@ -107,49 +140,45 @@ class PageHandler { this._session.emitEvent('Page.workerDestroyed', {workerId}); } - async close({runBeforeUnload}) { + _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 enable() { - if (this._enabled) - return; - this._enabled = true; - this._updateModalDialogs(); - - this._eventListeners.push(...[ - helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => { - // wait for the dialog to be actually added to DOM. - await Promise.resolve(); - this._updateModalDialogs(); - }), - helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), - helper.on(this._pageTarget, 'crashed', () => { - this._session.emitEvent('Page.crashed', {}); - }), - helper.on(this._pageTarget, 'screencastStarted', () => { - const info = this._pageTarget.screencastInfo(); - this._session.emitEvent('Page.screencastStarted', { screencastId: '' + info.videoSessionId, file: info.file }); - }), - ]); - - const options = this._pageTarget.browserContext().screencastOptions; - if (options) - await this._pageTarget.startVideoRecording(options); - } - async dispose() { this._contentPage.dispose(); + this._contentRuntime.dispose(); helper.removeListeners(this._eventListeners); if (this._pageTarget.screencastInfo()) await this._pageTarget.stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`)); } - async setViewportSize({viewportSize}) { + async ['Page.setViewportSize']({viewportSize}) { await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize); } @@ -179,107 +208,154 @@ class PageHandler { } } - async setFileInputFiles(options) { + async ['Runtime.evaluate'](options) { + return await this._contentRuntime.send('evaluate', options); + } + + async ['Runtime.callFunction'](options) { + return await this._contentRuntime.send('callFunction', options); + } + + async ['Runtime.getObjectProperties'](options) { + return await this._contentRuntime.send('getObjectProperties', options); + } + + async ['Runtime.disposeObject'](options) { + return await this._contentRuntime.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 setEmulatedMedia(options) { + async ['Page.setEmulatedMedia'](options) { return await this._contentPage.send('setEmulatedMedia', options); } - async bringToFront(options) { + async ['Page.bringToFront'](options) { this._pageTarget._window.focus(); } - async setCacheDisabled(options) { + async ['Page.setCacheDisabled'](options) { return await this._contentPage.send('setCacheDisabled', options); } - async addBinding(options) { + async ['Page.addBinding'](options) { return await this._contentPage.send('addBinding', options); } - async adoptNode(options) { + async ['Page.adoptNode'](options) { return await this._contentPage.send('adoptNode', options); } - async screenshot(options) { + async ['Page.screenshot'](options) { return await this._contentPage.send('screenshot', options); } - async getBoundingBox(options) { + async ['Page.getBoundingBox'](options) { return await this._contentPage.send('getBoundingBox', options); } - async getContentQuads(options) { + async ['Page.getContentQuads'](options) { return await this._contentPage.send('getContentQuads', options); } /** * @param {{frameId: string, url: string}} options */ - async navigate(options) { + async ['Page.navigate'](options) { return await this._contentPage.send('navigate', options); } /** * @param {{frameId: string, url: string}} options */ - async goBack(options) { + async ['Page.goBack'](options) { return await this._contentPage.send('goBack', options); } /** * @param {{frameId: string, url: string}} options */ - async goForward(options) { + async ['Page.goForward'](options) { return await this._contentPage.send('goForward', options); } /** * @param {{frameId: string, url: string}} options */ - async reload(options) { + async ['Page.reload'](options) { return await this._contentPage.send('reload', options); } - async describeNode(options) { + async ['Page.describeNode'](options) { return await this._contentPage.send('describeNode', options); } - async scrollIntoViewIfNeeded(options) { + async ['Page.scrollIntoViewIfNeeded'](options) { return await this._contentPage.send('scrollIntoViewIfNeeded', options); } - async addScriptToEvaluateOnNewDocument(options) { + async ['Page.addScriptToEvaluateOnNewDocument'](options) { return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options); } - async removeScriptToEvaluateOnNewDocument(options) { + async ['Page.removeScriptToEvaluateOnNewDocument'](options) { return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options); } - async dispatchKeyEvent(options) { + async ['Page.dispatchKeyEvent'](options) { return await this._contentPage.send('dispatchKeyEvent', options); } - async dispatchTouchEvent(options) { + async ['Page.dispatchTouchEvent'](options) { return await this._contentPage.send('dispatchTouchEvent', options); } - async dispatchMouseEvent(options) { + async ['Page.dispatchMouseEvent'](options) { return await this._contentPage.send('dispatchMouseEvent', options); } - async insertText(options) { + async ['Page.insertText'](options) { return await this._contentPage.send('insertText', options); } - async crash(options) { + async ['Page.crash'](options) { return await this._contentPage.send('crash', options); } - async handleDialog({dialogId, accept, promptText}) { + async ['Page.handleDialog']({dialogId, accept, promptText}) { const dialog = this._dialogs.get(dialogId); if (!dialog) throw new Error('Failed to find dialog with id = ' + dialogId); @@ -289,18 +365,18 @@ class PageHandler { dialog.dismiss(); } - async setInterceptFileChooserDialog(options) { + async ['Page.setInterceptFileChooserDialog'](options) { return await this._contentPage.send('setInterceptFileChooserDialog', options); } - async sendMessageToWorker({workerId, message}) { + 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 stopVideoRecording() { + async ['Page.stopVideoRecording']() { await this._pageTarget.stopVideoRecording(); } } diff --git a/browser_patches/firefox/juggler/protocol/RuntimeHandler.js b/browser_patches/firefox/juggler/protocol/RuntimeHandler.js deleted file mode 100644 index cffa05933a..0000000000 --- a/browser_patches/firefox/juggler/protocol/RuntimeHandler.js +++ /dev/null @@ -1,55 +0,0 @@ -/* 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 Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; -const helper = new Helper(); - -class RuntimeHandler { - constructor(session, contentChannel) { - this._contentRuntime = contentChannel.connect('runtime'); - - const emitProtocolEvent = eventName => { - return (...args) => session.emitEvent(eventName, ...args); - } - - this._eventListeners = [ - contentChannel.register('runtime', { - runtimeConsole: emitProtocolEvent('Runtime.console'), - runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), - runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), - }), - ]; - } - - async evaluate(options) { - return await this._contentRuntime.send('evaluate', options); - } - - async callFunction(options) { - return await this._contentRuntime.send('callFunction', options); - } - - async getObjectProperties(options) { - return await this._contentRuntime.send('getObjectProperties', options); - } - - async disposeObject(options) { - return await this._contentRuntime.send('disposeObject', options); - } - - dispose() { - this._contentRuntime.dispose(); - helper.removeListeners(this._eventListeners); - } -} - -var EXPORTED_SYMBOLS = ['RuntimeHandler']; -this.RuntimeHandler = RuntimeHandler; diff --git a/browser_patches/firefox/preferences/playwright.cfg b/browser_patches/firefox/preferences/playwright.cfg index bda5094a1b..6da6b15ced 100644 --- a/browser_patches/firefox/preferences/playwright.cfg +++ b/browser_patches/firefox/preferences/playwright.cfg @@ -240,10 +240,6 @@ pref("security.notification_enable_delay", 0); // Ensure blocklist updates do not hit the network pref("services.settings.server", ""); -// Disable DocumentChannel. -// See https://github.com/microsoft/playwright/pull/451 -pref("browser.tabs.documentchannel", false); - // Do not automatically fill sign-in forms with known usernames and // passwords pref("signon.autofillForms", false);