chore: extract CDPSession on the server side, remove duplicate code (#27349)
This makes sure that protocol calls after target close are rejected correctly.
This commit is contained in:
parent
2a8d6a8207
commit
55c4bb97af
|
|
@ -28,7 +28,7 @@ import type { Dialog } from '../dialog';
|
|||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { CRSession } from './crConnection';
|
||||
import type { CRSession, CDPSession } from './crConnection';
|
||||
import { ConnectionEvents, CRConnection } from './crConnection';
|
||||
import { CRPage } from './crPage';
|
||||
import { saveProtocolStream } from './crProtocolHelper';
|
||||
|
|
@ -41,7 +41,7 @@ import { Artifact } from '../artifact';
|
|||
export class CRBrowser extends Browser {
|
||||
readonly _connection: CRConnection;
|
||||
_session: CRSession;
|
||||
private _clientRootSessionPromise: Promise<CRSession> | null = null;
|
||||
private _clientRootSessionPromise: Promise<CDPSession> | null = null;
|
||||
readonly _contexts = new Map<string, CRBrowserContext>();
|
||||
_crPages = new Map<string, CRPage>();
|
||||
_backgroundPages = new Map<string, CRPage>();
|
||||
|
|
@ -166,12 +166,7 @@ export class CRBrowser extends Browser {
|
|||
const treatOtherAsPage = targetInfo.type === 'other' && process.env.PW_CHROMIUM_ATTACH_TO_OTHER;
|
||||
|
||||
if (!context || (targetInfo.type === 'other' && !treatOtherAsPage)) {
|
||||
if (waitingForDebugger) {
|
||||
// Ideally, detaching should resume any target, but there is a bug in the backend.
|
||||
session._sendMayFail('Runtime.runIfWaitingForDebugger').then(() => {
|
||||
this._session._sendMayFail('Target.detachFromTarget', { sessionId });
|
||||
});
|
||||
}
|
||||
session.detach().catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -204,15 +199,10 @@ export class CRBrowser extends Browser {
|
|||
// One example of a side effect: upon shared worker restart, we receive
|
||||
// Inspector.targetReloadedAfterCrash and backend waits for Runtime.runIfWaitingForDebugger
|
||||
// from any attached client. If we do not resume, shared worker will stall.
|
||||
//
|
||||
// Ideally, detaching should resume any target, but there is a bug in the backend,
|
||||
// so we must Runtime.runIfWaitingForDebugger first.
|
||||
session._sendMayFail('Runtime.runIfWaitingForDebugger').then(() => {
|
||||
this._session._sendMayFail('Target.detachFromTarget', { sessionId });
|
||||
});
|
||||
session.detach().catch(() => {});
|
||||
}
|
||||
|
||||
_onDetachedFromTarget(payload: Protocol.Target.detachFromTargetParameters) {
|
||||
_onDetachedFromTarget(payload: Protocol.Target.detachedFromTargetPayload) {
|
||||
const targetId = payload.targetId!;
|
||||
const crPage = this._crPages.get(targetId);
|
||||
if (crPage) {
|
||||
|
|
@ -286,7 +276,7 @@ export class CRBrowser extends Browser {
|
|||
await this._session.send('Target.closeTarget', { targetId: crPage._targetId });
|
||||
}
|
||||
|
||||
async newBrowserCDPSession(): Promise<CRSession> {
|
||||
async newBrowserCDPSession(): Promise<CDPSession> {
|
||||
return await this._connection.createBrowserSession();
|
||||
}
|
||||
|
||||
|
|
@ -333,7 +323,7 @@ export class CRBrowser extends Browser {
|
|||
return !this._connection._closed;
|
||||
}
|
||||
|
||||
async _clientRootSession(): Promise<CRSession> {
|
||||
async _clientRootSession(): Promise<CDPSession> {
|
||||
if (!this._clientRootSessionPromise)
|
||||
this._clientRootSessionPromise = this._connection.createBrowserSession();
|
||||
return this._clientRootSessionPromise;
|
||||
|
|
@ -592,7 +582,7 @@ export class CRBrowserContext extends BrowserContext {
|
|||
return Array.from(this._browser._serviceWorkers.values()).filter(serviceWorker => serviceWorker._browserContext === this);
|
||||
}
|
||||
|
||||
async newCDPSession(page: Page | Frame): Promise<CRSession> {
|
||||
async newCDPSession(page: Page | Frame): Promise<CDPSession> {
|
||||
let targetId: string | null = null;
|
||||
if (page instanceof Page) {
|
||||
targetId = (page._delegate as CRPage)._targetId;
|
||||
|
|
@ -605,7 +595,6 @@ export class CRBrowserContext extends BrowserContext {
|
|||
}
|
||||
|
||||
const rootSession = await this._browser._clientRootSession();
|
||||
const { sessionId } = await rootSession.send('Target.attachToTarget', { targetId, flatten: true });
|
||||
return rootSession.createChildSession(sessionId);
|
||||
return rootSession.attachToTarget(targetId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assert } from '../../utils';
|
||||
import { type RegisteredListener, assert, eventsHelper } from '../../utils';
|
||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import type { Protocol } from './protocol';
|
||||
import { EventEmitter } from 'events';
|
||||
|
|
@ -90,19 +90,17 @@ export class CRConnection extends EventEmitter {
|
|||
this._transport.close();
|
||||
}
|
||||
|
||||
async createBrowserSession(): Promise<CRSession> {
|
||||
async createBrowserSession(): Promise<CDPSession> {
|
||||
const { sessionId } = await this.rootSession.send('Target.attachToBrowserTarget');
|
||||
return this.rootSession.createChildSession(sessionId);
|
||||
return new CDPSession(this.rootSession, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
export const CRSessionEvents = {
|
||||
Disconnected: Symbol('Events.CDPSession.Disconnected')
|
||||
};
|
||||
type SessionEventListener = (method: string, params?: Object) => void;
|
||||
|
||||
export class CRSession extends EventEmitter {
|
||||
private readonly _connection: CRConnection;
|
||||
_eventListener?: (method: string, params?: Object) => void;
|
||||
private _eventListener?: SessionEventListener;
|
||||
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError, method: string}>();
|
||||
private readonly _sessionId: string;
|
||||
private readonly _parentSession: CRSession | null;
|
||||
|
|
@ -113,15 +111,14 @@ export class CRSession extends EventEmitter {
|
|||
override off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
override removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
override once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
readonly guid: string;
|
||||
|
||||
constructor(connection: CRConnection, parentSession: CRSession | null, sessionId: string) {
|
||||
constructor(connection: CRConnection, parentSession: CRSession | null, sessionId: string, eventListener?: SessionEventListener) {
|
||||
super();
|
||||
this.guid = `cdp-session@${sessionId}`;
|
||||
this.setMaxListeners(0);
|
||||
this._connection = connection;
|
||||
this._parentSession = parentSession;
|
||||
this._sessionId = sessionId;
|
||||
this._eventListener = eventListener;
|
||||
|
||||
this.on = super.on;
|
||||
this.addListener = super.addListener;
|
||||
|
|
@ -134,8 +131,8 @@ export class CRSession extends EventEmitter {
|
|||
this._crashed = true;
|
||||
}
|
||||
|
||||
createChildSession(sessionId: string): CRSession {
|
||||
const session = new CRSession(this._connection, this, sessionId);
|
||||
createChildSession(sessionId: string, eventListener?: SessionEventListener): CRSession {
|
||||
const session = new CRSession(this._connection, this, sessionId, eventListener);
|
||||
this._connection._sessions.set(sessionId, session);
|
||||
return session;
|
||||
}
|
||||
|
|
@ -147,6 +144,8 @@ export class CRSession extends EventEmitter {
|
|||
return `Browser closed.` + this._connection._browserDisconnectedLogs;
|
||||
if (this._closed)
|
||||
return `Target closed`;
|
||||
if (this._connection._closed)
|
||||
return 'Browser closed';
|
||||
}
|
||||
|
||||
async send<T extends keyof Protocol.CommandParameters>(
|
||||
|
|
@ -191,6 +190,9 @@ export class CRSession extends EventEmitter {
|
|||
throw new Error(`Session already detached. Most likely the page has been closed.`);
|
||||
if (!this._parentSession)
|
||||
throw new Error('Root session cannot be closed');
|
||||
// Ideally, detaching should resume any target, but there is a bug in the backend,
|
||||
// so we must Runtime.runIfWaitingForDebugger first.
|
||||
await this._sendMayFail('Runtime.runIfWaitingForDebugger');
|
||||
await this._parentSession.send('Target.detachFromTarget', { sessionId: this._sessionId });
|
||||
this.dispose();
|
||||
}
|
||||
|
|
@ -204,7 +206,46 @@ export class CRSession extends EventEmitter {
|
|||
callback.reject(rewriteErrorMessage(callback.error, errorMessage));
|
||||
}
|
||||
this._callbacks.clear();
|
||||
Promise.resolve().then(() => this.emit(CRSessionEvents.Disconnected));
|
||||
}
|
||||
}
|
||||
|
||||
export class CDPSession extends EventEmitter {
|
||||
static Events = {
|
||||
Event: 'event',
|
||||
Closed: 'close',
|
||||
};
|
||||
|
||||
readonly guid: string;
|
||||
private _session: CRSession;
|
||||
private _listeners: RegisteredListener[] = [];
|
||||
|
||||
constructor(parentSession: CRSession, sessionId: string) {
|
||||
super();
|
||||
this.guid = `cdp-session@${sessionId}`;
|
||||
this._session = parentSession.createChildSession(sessionId, (method, params) => this.emit(CDPSession.Events.Event, { method, params }));
|
||||
this._listeners = [eventsHelper.addEventListener(parentSession, 'Target.detachedFromTarget', (event: Protocol.Target.detachedFromTargetPayload) => {
|
||||
if (event.sessionId === sessionId)
|
||||
this._onClose();
|
||||
})];
|
||||
}
|
||||
|
||||
async send(method: string, params?: any) {
|
||||
return await this._session.send(method as any, params);
|
||||
}
|
||||
|
||||
async detach() {
|
||||
return await this._session.detach();
|
||||
}
|
||||
|
||||
async attachToTarget(targetId: string) {
|
||||
const { sessionId } = await this.send('Target.attachToTarget', { targetId, flatten: true });
|
||||
return new CDPSession(this._session, sessionId);
|
||||
}
|
||||
|
||||
private _onClose() {
|
||||
eventsHelper.removeEventListeners(this._listeners);
|
||||
this._session.dispose();
|
||||
this.emit(CDPSession.Events.Closed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -733,10 +733,7 @@ class FrameSession {
|
|||
}
|
||||
|
||||
if (event.targetInfo.type !== 'worker') {
|
||||
// Ideally, detaching should resume any target, but there is a bug in the backend.
|
||||
session._sendMayFail('Runtime.runIfWaitingForDebugger').then(() => {
|
||||
this._client._sendMayFail('Target.detachFromTarget', { sessionId: event.sessionId });
|
||||
});
|
||||
session.detach().catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,22 +14,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CRSession } from '../chromium/crConnection';
|
||||
import { CRSessionEvents } from '../chromium/crConnection';
|
||||
import { CDPSession } from '../chromium/crConnection';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import type { BrowserDispatcher } from './browserDispatcher';
|
||||
import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
|
||||
export class CDPSessionDispatcher extends Dispatcher<CRSession, channels.CDPSessionChannel, BrowserDispatcher | BrowserContextDispatcher> implements channels.CDPSessionChannel {
|
||||
export class CDPSessionDispatcher extends Dispatcher<CDPSession, channels.CDPSessionChannel, BrowserDispatcher | BrowserContextDispatcher> implements channels.CDPSessionChannel {
|
||||
_type_CDPSession = true;
|
||||
|
||||
constructor(scope: BrowserDispatcher | BrowserContextDispatcher, crSession: CRSession) {
|
||||
super(scope, crSession, 'CDPSession', {});
|
||||
crSession._eventListener = (method, params) => {
|
||||
this._dispatchEvent('event', { method, params });
|
||||
};
|
||||
this.addObjectListener(CRSessionEvents.Disconnected, () => this._dispose());
|
||||
constructor(scope: BrowserDispatcher | BrowserContextDispatcher, cdpSession: CDPSession) {
|
||||
super(scope, cdpSession, 'CDPSession', {});
|
||||
this.addObjectListener(CDPSession.Events.Event, ({ method, params }) => this._dispatchEvent('event', { method, params }));
|
||||
this.addObjectListener(CDPSession.Events.Closed, () => this._dispose());
|
||||
}
|
||||
|
||||
async send(params: channels.CDPSessionSendParams): Promise<channels.CDPSessionSendResult> {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,19 @@ browserTest('should detach when page closes', async function({ browser }) {
|
|||
await context.close();
|
||||
});
|
||||
|
||||
browserTest('should reject protocol calls when page closes', async function({ browser }) {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const session = await context.newCDPSession(page);
|
||||
const promise = session.send('Runtime.evaluate', { expression: 'new Promise(() => {})', awaitPromise: true }).catch(e => e);
|
||||
await page.close();
|
||||
const error1 = await promise;
|
||||
expect(error1.message).toContain('Target closed');
|
||||
const error2 = await session.send('Runtime.evaluate', { expression: 'new Promise(() => {})', awaitPromise: true }).catch(e => e);
|
||||
expect(error2.message).toContain('Target page, context or browser has been closed');
|
||||
await context.close();
|
||||
});
|
||||
|
||||
browserTest('should work with newBrowserCDPSession', async function({ browser }) {
|
||||
const session = await browser.newBrowserCDPSession();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue