Skip to content

Commit 8211287

Browse files
authored
fix(session): use isolated root session for client page sessions (#1271)
1 parent 3fa000f commit 8211287

File tree

3 files changed

+62
-28
lines changed

3 files changed

+62
-28
lines changed

src/chromium/crBrowser.ts

+28-18
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ import { Events } from './events';
3232
import { Protocol } from './protocol';
3333

3434
export class CRBrowser extends platform.EventEmitter implements Browser {
35-
_connection: CRConnection;
36-
_client: CRSession;
35+
readonly _connection: CRConnection;
36+
_session: CRSession;
37+
private _clientRootSessionPromise: Promise<CRSession> | null = null;
3738
readonly _defaultContext: CRBrowserContext;
3839
readonly _contexts = new Map<string, CRBrowserContext>();
3940
_targets = new Map<string, CRTarget>();
@@ -70,23 +71,23 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
7071
constructor(connection: CRConnection) {
7172
super();
7273
this._connection = connection;
73-
this._client = this._connection.rootSession;
74+
this._session = this._connection.rootSession;
7475

7576
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({}));
7677
this._connection.on(ConnectionEvents.Disconnected, () => {
7778
for (const context of this._contexts.values())
7879
context._browserClosed();
7980
this.emit(CommonEvents.Browser.Disconnected);
8081
});
81-
this._client.on('Target.targetCreated', this._targetCreated.bind(this));
82-
this._client.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
83-
this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
84-
this._client.on('Target.attachedToTarget', this._onAttachedToTarget.bind(this));
82+
this._session.on('Target.targetCreated', this._targetCreated.bind(this));
83+
this._session.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
84+
this._session.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
85+
this._session.on('Target.attachedToTarget', this._onAttachedToTarget.bind(this));
8586
}
8687

8788
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
8889
options = validateBrowserContextOptions(options);
89-
const { browserContextId } = await this._client.send('Target.createBrowserContext', { disposeOnDetach: true });
90+
const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true });
9091
const context = new CRBrowserContext(this, browserContextId, options);
9192
await context._initialize();
9293
this._contexts.set(browserContextId, context);
@@ -159,7 +160,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
159160
}
160161

161162
async _closePage(page: Page) {
162-
await this._client.send('Target.closeTarget', { targetId: CRTarget.fromPage(page)._targetId });
163+
await this._session.send('Target.closeTarget', { targetId: CRTarget.fromPage(page)._targetId });
163164
}
164165

165166
_allTargets(): CRTarget[] {
@@ -179,7 +180,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
179180

180181
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
181182
assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.');
182-
this._tracingClient = page ? (page._delegate as CRPage)._client : this._client;
183+
this._tracingClient = page ? (page._delegate as CRPage)._client : this._session;
183184

184185
const defaultCategories = [
185186
'-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline',
@@ -220,6 +221,12 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
220221
return !this._connection._closed;
221222
}
222223

224+
async _clientRootSession(): Promise<CRSession> {
225+
if (!this._clientRootSessionPromise)
226+
this._clientRootSessionPromise = this._connection.createBrowserSession();
227+
return this._clientRootSessionPromise;
228+
}
229+
223230
_setDebugFunction(debugFunction: (message: string) => void) {
224231
this._connection._debugProtocol = debugFunction;
225232
}
@@ -265,7 +272,7 @@ export class CRBrowserContext extends BrowserContextBase {
265272

266273
async newPage(): Promise<Page> {
267274
assertBrowserContextIsNotOwned(this);
268-
const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
275+
const { targetId } = await this._browser._session.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
269276
const target = this._browser._targets.get(targetId)!;
270277
const result = await target.pageOrError();
271278
if (result instanceof Page) {
@@ -277,7 +284,7 @@ export class CRBrowserContext extends BrowserContextBase {
277284
}
278285

279286
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> {
280-
const { cookies } = await this._browser._client.send('Storage.getCookies', { browserContextId: this._browserContextId || undefined });
287+
const { cookies } = await this._browser._session.send('Storage.getCookies', { browserContextId: this._browserContextId || undefined });
281288
return network.filterCookies(cookies.map(c => {
282289
const copy: any = { sameSite: 'None', ...c };
283290
delete copy.size;
@@ -287,11 +294,11 @@ export class CRBrowserContext extends BrowserContextBase {
287294
}
288295

289296
async setCookies(cookies: network.SetNetworkCookieParam[]) {
290-
await this._browser._client.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined });
297+
await this._browser._session.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined });
291298
}
292299

293300
async clearCookies() {
294-
await this._browser._client.send('Storage.clearCookies', { browserContextId: this._browserContextId || undefined });
301+
await this._browser._session.send('Storage.clearCookies', { browserContextId: this._browserContextId || undefined });
295302
}
296303

297304
async setPermissions(origin: string, permissions: string[]): Promise<void> {
@@ -319,11 +326,11 @@ export class CRBrowserContext extends BrowserContextBase {
319326
throw new Error('Unknown permission: ' + permission);
320327
return protocolPermission;
321328
});
322-
await this._browser._client.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId || undefined, permissions: filtered });
329+
await this._browser._session.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId || undefined, permissions: filtered });
323330
}
324331

325332
async clearPermissions() {
326-
await this._browser._client.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined });
333+
await this._browser._session.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined });
327334
}
328335

329336
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
@@ -376,7 +383,7 @@ export class CRBrowserContext extends BrowserContextBase {
376383
if (this._closed)
377384
return;
378385
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
379-
await this._browser._client.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
386+
await this._browser._session.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
380387
this._browser._contexts.delete(this._browserContextId);
381388
this._didCloseInternal();
382389
}
@@ -388,6 +395,9 @@ export class CRBrowserContext extends BrowserContextBase {
388395
}
389396

390397
async createSession(page: Page): Promise<CRSession> {
391-
return CRTarget.fromPage(page).sessionFactory();
398+
const targetId = CRTarget.fromPage(page)._targetId;
399+
const rootSession = await this._browser._clientRootSession();
400+
const { sessionId } = await rootSession.send('Target.attachToTarget', { targetId, flatten: true });
401+
return this._browser._connection.session(sessionId)!;
392402
}
393403
}

src/chromium/crConnection.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { assert } from '../helper';
1819
import * as platform from '../platform';
1920
import { ConnectionTransport } from '../transport';
20-
import { assert } from '../helper';
2121
import { Protocol } from './protocol';
2222

2323
export const ConnectionEvents = {
@@ -30,8 +30,8 @@ export const kBrowserCloseMessageId = -9999;
3030

3131
export class CRConnection extends platform.EventEmitter {
3232
private _lastId = 0;
33-
private _transport: ConnectionTransport;
34-
private _sessions = new Map<string, CRSession>();
33+
private readonly _transport: ConnectionTransport;
34+
private readonly _sessions = new Map<string, CRSession>();
3535
readonly rootSession: CRSession;
3636
_closed = false;
3737
_debugProtocol: (message: string) => void;
@@ -41,7 +41,7 @@ export class CRConnection extends platform.EventEmitter {
4141
this._transport = transport;
4242
this._transport.onmessage = this._onMessage.bind(this);
4343
this._transport.onclose = this._onClose.bind(this);
44-
this.rootSession = new CRSession(this, 'browser', '');
44+
this.rootSession = new CRSession(this, '', 'browser', '');
4545
this._sessions.set('', this.rootSession);
4646
this._debugProtocol = platform.debug('pw:protocol');
4747
}
@@ -72,7 +72,8 @@ export class CRConnection extends platform.EventEmitter {
7272
return;
7373
if (object.method === 'Target.attachedToTarget') {
7474
const sessionId = object.params.sessionId;
75-
const session = new CRSession(this, object.params.targetInfo.type, sessionId);
75+
const rootSessionId = object.sessionId || '';
76+
const session = new CRSession(this, rootSessionId, object.params.targetInfo.type, sessionId);
7677
this._sessions.set(sessionId, session);
7778
} else if (object.method === 'Target.detachedFromTarget') {
7879
const session = this._sessions.get(object.params.sessionId);
@@ -118,18 +119,20 @@ export const CRSessionEvents = {
118119

119120
export class CRSession extends platform.EventEmitter {
120121
_connection: CRConnection | null;
121-
private _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
122-
private _targetType: string;
123-
private _sessionId: string;
122+
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
123+
private readonly _targetType: string;
124+
private readonly _sessionId: string;
125+
private readonly _rootSessionId: string;
124126
on: <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;
125127
addListener: <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;
126128
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;
127129
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;
128130
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;
129131

130-
constructor(connection: CRConnection, targetType: string, sessionId: string) {
132+
constructor(connection: CRConnection, rootSessionId: string, targetType: string, sessionId: string) {
131133
super();
132134
this._connection = connection;
135+
this._rootSessionId = rootSessionId;
133136
this._targetType = targetType;
134137
this._sessionId = sessionId;
135138

@@ -169,7 +172,10 @@ export class CRSession extends platform.EventEmitter {
169172
async detach() {
170173
if (!this._connection)
171174
throw new Error(`Session already detached. Most likely the ${this._targetType} has been closed.`);
172-
await this._connection.rootSession.send('Target.detachFromTarget', { sessionId: this._sessionId });
175+
const rootSession = this._connection.session(this._rootSessionId);
176+
if (!rootSession)
177+
throw new Error('Root session has been closed');
178+
await rootSession.send('Target.detachFromTarget', { sessionId: this._sessionId });
173179
}
174180

175181
_onClosed() {

test/chromium/session.spec.js

+18
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,24 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
8282
await client.send('ThisCommand.DoesNotExist');
8383
}
8484
});
85+
it('should not break page.close()', async function({browser, server}) {
86+
const context = await browser.newContext();
87+
const page = await context.newPage();
88+
const session = await page.context().createSession(page);
89+
await session.detach();
90+
await page.close();
91+
await context.close();
92+
});
93+
it('should detach when page closes', async function({browser, server}) {
94+
const context = await browser.newContext();
95+
const page = await context.newPage();
96+
const session = await context.createSession(page);
97+
await page.close();
98+
let error;
99+
await session.detach().catch(e => error = e);
100+
expect(error).toBeTruthy('Calling detach on a closed page\'s session should throw');
101+
await context.close();
102+
});
85103
});
86104
describe('ChromiumBrowser.createBrowserSession', function() {
87105
it('should work', async function({page, browser, server}) {

0 commit comments

Comments
 (0)