From 6e064729887f1c53db71e559236b4cfd29477ed8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 9 Jan 2020 11:02:55 -0800 Subject: [PATCH] chore(webkit): move target management to WKPageProxy (#437) This allows to remove WKTargetSession and use WKSession instead. --- src/webkit/wkAccessibility.ts | 4 +- src/webkit/wkConnection.ts | 99 ++++------------------------------ src/webkit/wkInput.ts | 6 +-- src/webkit/wkNetworkManager.ts | 12 ++--- src/webkit/wkPage.ts | 16 +++--- src/webkit/wkPageProxy.ts | 84 ++++++++++++++++++++++------- src/webkit/wkWorkers.ts | 6 +-- 7 files changed, 95 insertions(+), 132 deletions(-) diff --git a/src/webkit/wkAccessibility.ts b/src/webkit/wkAccessibility.ts index 3f72414c6a..d115233420 100644 --- a/src/webkit/wkAccessibility.ts +++ b/src/webkit/wkAccessibility.ts @@ -14,10 +14,10 @@ * limitations under the License. */ import * as accessibility from '../accessibility'; -import { WKTargetSession } from './wkConnection'; +import { WKSession } from './wkConnection'; import { Protocol } from './protocol'; -export async function getAccessibilityTree(session: WKTargetSession) { +export async function getAccessibilityTree(session: WKSession) { const {axNode} = await session.send('Page.accessibilitySnapshot'); return new WKAXNode(axNode); } diff --git a/src/webkit/wkConnection.ts b/src/webkit/wkConnection.ts index c4a6b7f753..4f38d348f7 100644 --- a/src/webkit/wkConnection.ts +++ b/src/webkit/wkConnection.ts @@ -29,12 +29,6 @@ export const WKConnectionEvents = { PageProxyDestroyed: Symbol('Connection.PageProxyDestroyed') }; -export const WKPageProxySessionEvents = { - TargetCreated: Symbol('PageProxyEvents.TargetCreated'), - TargetDestroyed: Symbol('PageProxyEvents.TargetDestroyed'), - DidCommitProvisionalTarget: Symbol('PageProxyEvents.DidCommitProvisionalTarget'), -}; - export const kBrowserCloseMessageId = -9999; export class WKConnection extends platform.EventEmitter { @@ -110,7 +104,7 @@ export class WKConnection extends platform.EventEmitter { Promise.resolve().then(() => this.emit(WKConnectionEvents.PageProxyDestroyed, pageProxyId)); } else if (!object.id && object.pageProxyId) { const pageProxySession = this._pageProxySessions.get(object.pageProxyId); - pageProxySession._dispatchEvent(object, message); + Promise.resolve().then(() => pageProxySession.emit(object.method, object.params)); } } @@ -142,8 +136,7 @@ export const WKSessionEvents = { export class WKPageProxySession extends platform.EventEmitter { _connection: WKConnection; - private readonly _sessions = new Map(); - private readonly _pageProxyId: string; + readonly _pageProxyId: string; private readonly _closePromise: Promise; private _closePromiseCallback: () => void; on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; @@ -171,67 +164,23 @@ export class WKPageProxySession extends platform.EventEmitter { ]); } - _dispatchEvent(object: {method: string, params: any, pageProxyId?: string}, wrappedMessage: string) { - if (object.method === 'Target.targetCreated') { - const targetInfo = object.params.targetInfo as Protocol.Target.TargetInfo; - const session = new WKTargetSession(this, targetInfo); - this._sessions.set(session.sessionId, session); - Promise.resolve().then(() => this.emit(WKPageProxySessionEvents.TargetCreated, session, object.params.targetInfo)); - } else if (object.method === 'Target.targetDestroyed') { - const session = this._sessions.get(object.params.targetId); - if (session) { - session.dispose(); - this._sessions.delete(object.params.targetId); - } - Promise.resolve().then(() => this.emit(WKPageProxySessionEvents.TargetDestroyed, { targetId: object.params.targetId, crashed: object.params.crashed })); - } else if (object.method === 'Target.dispatchMessageFromTarget') { - const {targetId, message} = object.params as Protocol.Target.dispatchMessageFromTargetPayload; - const session = this._sessions.get(targetId); - if (!session) - throw new Error('Unknown target: ' + targetId); - if (session.isProvisional()) - session._addProvisionalMessage(message); - else - session.dispatchMessage(JSON.parse(message)); - } else if (object.method === 'Target.didCommitProvisionalTarget') { - const {oldTargetId, newTargetId} = object.params as Protocol.Target.didCommitProvisionalTargetPayload; - Promise.resolve().then(() => this.emit(WKPageProxySessionEvents.DidCommitProvisionalTarget, { oldTargetId, newTargetId })); - const newSession = this._sessions.get(newTargetId); - if (!newSession) - throw new Error('Unknown new target: ' + newTargetId); - const oldSession = this._sessions.get(oldTargetId); - if (!oldSession) - throw new Error('Unknown old target: ' + oldTargetId); - // TODO: make some calls like screenshot catch swapped out error and retry. - oldSession.errorText = 'Target was swapped out.'; - assert(newSession.isProvisional()); - for (const message of newSession._takeProvisionalMessagesAndCommit()) - newSession.dispatchMessage(JSON.parse(message)); - } else { - Promise.resolve().then(() => this.emit(object.method, object.params)); - } - } - isClosed() { return !this._connection; } dispose() { - for (const session of this._sessions.values()) - session.dispose(); - this._sessions.clear(); - this._closePromiseCallback(); this._connection = null; } } export class WKSession extends platform.EventEmitter { - connection: WKConnection | null; - readonly sessionId: string; - private _rawSend: (message: any) => void; + connection?: WKConnection; errorText: string; - readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); + readonly sessionId: string; + + private readonly _rawSend: (message: any) => void; + private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; @@ -271,8 +220,8 @@ export class WKSession extends platform.EventEmitter { for (const callback of this._callbacks.values()) callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): ${this.errorText}`)); this._callbacks.clear(); - this.connection = null; - Promise.resolve().then(() => this.emit(WKSessionEvents.Disconnected)); + this.connection = undefined; + this.emit(WKSessionEvents.Disconnected); } dispatchMessage(object: any) { @@ -293,36 +242,6 @@ export class WKSession extends platform.EventEmitter { } } -export class WKTargetSession extends WKSession { - private _provisionalMessages?: string[]; - - constructor(pageProxySession: WKPageProxySession, targetInfo: Protocol.Target.TargetInfo) { - super(pageProxySession._connection, targetInfo.targetId, `The ${targetInfo.type} has been closed.`, (message: any) => { - pageProxySession.send('Target.sendMessageToTarget', { - message: JSON.stringify(message), targetId: targetInfo.targetId - }).catch(e => { - this.dispatchMessage({ id: message.id, error: { message: e.message } }); - }); - }); - if (targetInfo.isProvisional) - this._provisionalMessages = []; - } - - isProvisional() : boolean { - return !!this._provisionalMessages; - } - - _addProvisionalMessage(message: string) { - this._provisionalMessages.push(message); - } - - _takeProvisionalMessagesAndCommit() : string[] { - const messages = this._provisionalMessages; - this._provisionalMessages = undefined; - return messages; - } -} - export function createProtocolError(error: Error, method: string, object: { error: { message: string; data: any; }; }): Error { let message = `Protocol error (${method}): ${object.error.message}`; if ('data' in object.error) diff --git a/src/webkit/wkInput.ts b/src/webkit/wkInput.ts index 0fad71c917..fcf1858935 100644 --- a/src/webkit/wkInput.ts +++ b/src/webkit/wkInput.ts @@ -18,7 +18,7 @@ import * as input from '../input'; import { helper } from '../helper'; import { macEditingCommands } from '../usKeyboardLayout'; -import { WKPageProxySession, WKTargetSession } from './wkConnection'; +import { WKPageProxySession, WKSession } from './wkConnection'; function toModifiersMask(modifiers: Set): number { // From Source/WebKit/Shared/WebEvent.h @@ -36,13 +36,13 @@ function toModifiersMask(modifiers: Set): number { export class RawKeyboardImpl implements input.RawKeyboard { private readonly _pageProxySession: WKPageProxySession; - private _session: WKTargetSession; + private _session: WKSession; constructor(session: WKPageProxySession) { this._pageProxySession = session; } - setSession(session: WKTargetSession) { + setSession(session: WKSession) { this._session = session; } diff --git a/src/webkit/wkNetworkManager.ts b/src/webkit/wkNetworkManager.ts index 0cdb64e878..72d2051dab 100644 --- a/src/webkit/wkNetworkManager.ts +++ b/src/webkit/wkNetworkManager.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { WKTargetSession, WKPageProxySession } from './wkConnection'; +import { WKSession, WKPageProxySession } from './wkConnection'; import { Page } from '../page'; import { helper, RegisteredListener, assert } from '../helper'; import { Protocol } from './protocol'; @@ -27,7 +27,7 @@ import * as platform from '../platform'; export class WKNetworkManager { private readonly _page: Page; private readonly _pageProxySession: WKPageProxySession; - private _session: WKTargetSession; + private _session: WKSession; private readonly _requestIdToRequest = new Map(); private _userCacheDisabled = false; private _sessionListeners: RegisteredListener[] = []; @@ -41,7 +41,7 @@ export class WKNetworkManager { await this.authenticate(credentials); } - setSession(session: WKTargetSession) { + setSession(session: WKSession) { helper.removeEventListeners(this._sessionListeners); this._session = session; this._sessionListeners = [ @@ -53,7 +53,7 @@ export class WKNetworkManager { ]; } - async initializeSession(session: WKTargetSession, interceptNetwork: boolean | null, offlineMode: boolean | null) { + async initializeSession(session: WKSession, interceptNetwork: boolean | null, offlineMode: boolean | null) { const promises = []; promises.push(session.send('Network.enable')); if (interceptNetwork) @@ -188,14 +188,14 @@ const errorReasons: { [reason: string]: string } = { }; class InterceptableRequest implements network.RequestDelegate { - private _session: WKTargetSession; + private _session: WKSession; readonly request: network.Request; _requestId: string; _documentId: string | undefined; _interceptedCallback: () => void; private _interceptedPromise: Promise; - constructor(session: WKTargetSession, allowInterception: boolean, frame: frames.Frame | null, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[], documentId: string | undefined) { + constructor(session: WKSession, allowInterception: boolean, frame: frames.Frame | null, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[], documentId: string | undefined) { this._session = session; this._requestId = event.requestId; this._documentId = documentId; diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index aba87fc33a..74d58aa314 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -19,7 +19,7 @@ import * as frames from '../frames'; import { debugError, helper, RegisteredListener } from '../helper'; import * as dom from '../dom'; import * as network from '../network'; -import { WKTargetSession, WKSessionEvents, WKPageProxySession } from './wkConnection'; +import { WKSession, WKSessionEvents, WKPageProxySession } from './wkConnection'; import { Events } from '../events'; import { WKExecutionContext, EVALUATION_SCRIPT_URL } from './wkExecutionContext'; import { WKNetworkManager } from './wkNetworkManager'; @@ -40,7 +40,7 @@ const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; export class WKPage implements PageDelegate { readonly rawMouse: RawMouseImpl; readonly rawKeyboard: RawKeyboardImpl; - _session: WKTargetSession; + _session: WKSession; readonly _page: Page; private readonly _pageProxySession: WKPageProxySession; private readonly _networkManager: WKNetworkManager; @@ -74,7 +74,7 @@ export class WKPage implements PageDelegate { await Promise.all(promises); } - setSession(session: WKTargetSession) { + setSession(session: WKSession) { helper.removeEventListeners(this._sessionListeners); this.disconnectFromTarget(); this._session = session; @@ -91,7 +91,7 @@ export class WKPage implements PageDelegate { // This method is called for provisional targets as well. The session passed as the parameter // may be different from the current session and may be destroyed without becoming current. - async _initializeSession(session: WKTargetSession) { + async _initializeSession(session: WKSession, isProvisional: boolean) { const promises : Promise[] = [ // Page agent must be enabled before Runtime. session.send('Page.enable'), @@ -108,7 +108,7 @@ export class WKPage implements PageDelegate { promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent })); if (this._page._state.mediaType || this._page._state.colorScheme) promises.push(this._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme)); - if (session.isProvisional()) + if (isProvisional) promises.push(this._setBootstrapScripts(session)); if (contextOptions.bypassCSP) promises.push(session.send('Page.setBypassCSP', { enabled: true })); @@ -288,11 +288,11 @@ export class WKPage implements PageDelegate { }); } - private async _setExtraHTTPHeaders(session: WKTargetSession, headers: network.Headers): Promise { + private async _setExtraHTTPHeaders(session: WKSession, headers: network.Headers): Promise { await session.send('Network.setExtraHTTPHeaders', { headers }); } - private async _setEmulateMedia(session: WKTargetSession, mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise { + private async _setEmulateMedia(session: WKSession, mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise { const promises = []; promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' })); if (colorScheme !== null) { @@ -371,7 +371,7 @@ export class WKPage implements PageDelegate { await this._setBootstrapScripts(this._session); } - private async _setBootstrapScripts(session: WKTargetSession) { + private async _setBootstrapScripts(session: WKSession) { const source = this._bootstrapScripts.join(';'); await session.send('Page.setBootstrapScript', { source }); } diff --git a/src/webkit/wkPageProxy.ts b/src/webkit/wkPageProxy.ts index 1503736065..6ae96159f6 100644 --- a/src/webkit/wkPageProxy.ts +++ b/src/webkit/wkPageProxy.ts @@ -5,11 +5,16 @@ import { BrowserContext } from '../browserContext'; import { Page } from '../page'; import { Protocol } from './protocol'; -import { WKPageProxySession, WKPageProxySessionEvents, WKTargetSession } from './wkConnection'; +import { WKPageProxySession, WKSession } from './wkConnection'; import { WKPage } from './wkPage'; import { RegisteredListener, helper, assert, debugError } from '../helper'; import { Events } from '../events'; +// We keep provisional messages on the session instace until provisional +// target is committed. Non-provisional target (there should be just one) +// has undefined instead. +const provisionalMessagesSymbol = Symbol('provisionalMessages'); + export class WKPageProxy { private readonly _pageProxySession: WKPageProxySession; readonly _browserContext: BrowserContext; @@ -17,7 +22,7 @@ export class WKPageProxy { private _wkPage: WKPage | null = null; private readonly _firstTargetPromise: Promise; private _firstTargetCallback: () => void; - private readonly _targetSessions = new Map(); + private readonly _sessions = new Map(); private readonly _eventListeners: RegisteredListener[]; constructor(session: WKPageProxySession, browserContext: BrowserContext) { @@ -25,9 +30,10 @@ export class WKPageProxy { this._browserContext = browserContext; this._firstTargetPromise = new Promise(r => this._firstTargetCallback = r); this._eventListeners = [ - helper.addEventListener(this._pageProxySession, WKPageProxySessionEvents.TargetCreated, this._onTargetCreated.bind(this)), - helper.addEventListener(this._pageProxySession, WKPageProxySessionEvents.TargetDestroyed, this._onTargetDestroyed.bind(this)), - helper.addEventListener(this._pageProxySession, WKPageProxySessionEvents.DidCommitProvisionalTarget, this._onProvisionalTargetCommitted.bind(this)) + helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)), + helper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), + helper.addEventListener(this._pageProxySession, 'Target.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.bind(this)), + helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)), ]; // Intercept provisional targets during cross-process navigation. @@ -41,6 +47,9 @@ export class WKPageProxy { dispose() { helper.removeEventListeners(this._eventListeners); + for (const session of this._sessions.values()) + session.dispose(); + this._sessions.clear(); } async page(): Promise { @@ -59,10 +68,10 @@ export class WKPageProxy { private async _initializeWKPage(): Promise { await this._firstTargetPromise; - let session: WKTargetSession; - for (const targetSession of this._targetSessions.values()) { - if (!targetSession.isProvisional()) { - session = targetSession; + let session: WKSession; + for (const anySession of this._sessions.values()) { + if (!(anySession as any)[provisionalMessagesSymbol]) { + session = anySession; break; } } @@ -71,35 +80,70 @@ export class WKPageProxy { this._wkPage.setSession(session); await Promise.all([ this._wkPage._initializePageProxySession(), - this._wkPage._initializeSession(session) + this._wkPage._initializeSession(session, false), ]); return this._wkPage._page; } - private _onTargetCreated(session: WKTargetSession, targetInfo: Protocol.Target.TargetInfo) { + private _onTargetCreated(event: Protocol.Target.targetCreatedPayload) { + const { targetInfo } = event; + const session = new WKSession(this._pageProxySession._connection, targetInfo.targetId, `The ${targetInfo.type} has been closed.`, (message: any) => { + this._pageProxySession.send('Target.sendMessageToTarget', { + message: JSON.stringify(message), targetId: targetInfo.targetId + }).catch(e => { + session.dispatchMessage({ id: message.id, error: { message: e.message } }); + }); + }); assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type); - this._targetSessions.set(targetInfo.targetId, session); + this._sessions.set(targetInfo.targetId, session); if (this._firstTargetCallback) { this._firstTargetCallback(); this._firstTargetCallback = null; } + if (targetInfo.isProvisional) + (session as any)[provisionalMessagesSymbol] = []; if (targetInfo.isProvisional && this._wkPage) - this._wkPage._initializeSession(session); + this._wkPage._initializeSession(session, true); if (targetInfo.isPaused) this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); } - private _onTargetDestroyed({targetId, crashed}) { - const targetSession = this._targetSessions.get(targetId); - this._targetSessions.delete(targetId); + private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) { + const { targetId, crashed } = event; + const session = this._sessions.get(targetId); + if (session) + session.dispose(); + this._sessions.delete(targetId); if (!this._wkPage) return; - if (this._wkPage._session === targetSession) + if (this._wkPage._session === session) this._wkPage.didClose(crashed); } - private _onProvisionalTargetCommitted({oldTargetId, newTargetId}) { - const newTargetSession = this._targetSessions.get(newTargetId); - this._wkPage.setSession(newTargetSession); + private _onDispatchMessageFromTarget(event: Protocol.Target.dispatchMessageFromTargetPayload) { + const { targetId, message } = event; + const session = this._sessions.get(targetId); + assert(session, 'Unknown target: ' + targetId); + const provisionalMessages = (session as any)[provisionalMessagesSymbol]; + if (provisionalMessages) + provisionalMessages.push(message); + else + session.dispatchMessage(JSON.parse(message)); + } + + private _onDidCommitProvisionalTarget(event: Protocol.Target.didCommitProvisionalTargetPayload) { + const { oldTargetId, newTargetId } = event; + const newSession = this._sessions.get(newTargetId); + assert(newSession, 'Unknown new target: ' + newTargetId); + const oldSession = this._sessions.get(oldTargetId); + assert(oldSession, 'Unknown old target: ' + oldTargetId); + // TODO: make some calls like screenshot catch swapped out error and retry. + oldSession.errorText = 'Target was swapped out.'; + const provisionalMessages = (newSession as any)[provisionalMessagesSymbol]; + assert(provisionalMessages, 'Committing target must be provisional'); + (newSession as any)[provisionalMessagesSymbol] = undefined; + for (const message of provisionalMessages) + newSession.dispatchMessage(JSON.parse(message)); + this._wkPage.setSession(newSession); } } diff --git a/src/webkit/wkWorkers.ts b/src/webkit/wkWorkers.ts index 4cb0c9b843..9b75586425 100644 --- a/src/webkit/wkWorkers.ts +++ b/src/webkit/wkWorkers.ts @@ -17,7 +17,7 @@ import { helper, RegisteredListener } from '../helper'; import { Page, Worker } from '../page'; import { Protocol } from './protocol'; -import { WKSession, WKTargetSession } from './wkConnection'; +import { WKSession } from './wkConnection'; import { WKExecutionContext } from './wkExecutionContext'; export class WKWorkers { @@ -29,7 +29,7 @@ export class WKWorkers { this._page = page; } - setSession(session: WKTargetSession) { + setSession(session: WKSession) { helper.removeEventListeners(this._sessionListeners); this._page._clearWorkers(); this._workerSessions.clear(); @@ -73,7 +73,7 @@ export class WKWorkers { ]; } - async initializeSession(session: WKTargetSession) { + async initializeSession(session: WKSession) { await session.send('Worker.enable'); }