Skip to content

Commit 8fe29fe

Browse files
authored
feat(rpc): support more chromium-specific apis (#2883)
This includes page CDPSession, backgroundPages() and serviceWorkers(). This has also revealed an issue with closing order between the context and the service worker.
1 parent b3ca4af commit 8fe29fe

15 files changed

+124
-26
lines changed

src/browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { ProxySettings } from './types';
2626
import { LoggerSink } from './loggerSink';
2727

2828
export type BrowserOptions = {
29+
name: string,
2930
loggers: Loggers,
3031
downloadsPath?: string,
3132
headful?: boolean,

src/browserContext.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
6262
readonly _options: BrowserContextOptions;
6363
_routes: { url: types.URLMatch, handler: network.RouteHandler }[] = [];
6464
private _isPersistentContext: boolean;
65-
private _startedClosing = false;
65+
private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
6666
readonly _closePromise: Promise<Error>;
6767
private _closePromiseFulfill: ((error: Error) => void) | undefined;
6868
readonly _permissions = new Map<string, string[]>();
@@ -109,6 +109,12 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
109109
}
110110

111111
private _didCloseInternal() {
112+
if (this._closedStatus === 'closed') {
113+
// We can come here twice if we close browser context and browser
114+
// at the same time.
115+
return;
116+
}
117+
this._closedStatus = 'closed';
112118
this._downloads.clear();
113119
this._closePromiseFulfill!(new Error('Context closed'));
114120
this.emit(Events.BrowserContext.Close);
@@ -235,8 +241,8 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
235241
await this._browserBase.close();
236242
return;
237243
}
238-
if (!this._startedClosing) {
239-
this._startedClosing = true;
244+
if (this._closedStatus === 'open') {
245+
this._closedStatus = 'closing';
240246
await this._doClose();
241247
await Promise.all([...this._downloads].map(d => d.delete()));
242248
this._didCloseInternal();

src/chromium/crBrowser.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,16 @@ export class CRBrowserContext extends BrowserContextBase {
425425
assert(this._browserContextId);
426426
await this._browser._session.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
427427
this._browser._contexts.delete(this._browserContextId);
428+
for (const [targetId, serviceWorker] of this._browser._serviceWorkers) {
429+
if (serviceWorker._browserContext !== this)
430+
continue;
431+
// When closing a browser context, service workers are shutdown
432+
// asynchronously and we get detached from them later.
433+
// To avoid the wrong order of notifications, we manually fire
434+
// "close" event here and forget about the serivce worker.
435+
serviceWorker.emit(CommonEvents.Worker.Close);
436+
this._browser._serviceWorkers.delete(targetId);
437+
}
428438
}
429439

430440
backgroundPages(): Page[] {

src/rpc/channels.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ export interface BrowserChannel extends Channel {
6363
close(): Promise<void>;
6464
newContext(params: types.BrowserContextOptions): Promise<BrowserContextChannel>;
6565

66-
// Chromium-specific.
67-
newBrowserCDPSession(): Promise<CDPSessionChannel>;
66+
crNewBrowserCDPSession(): Promise<CDPSessionChannel>;
6867
}
6968
export type BrowserInitializer = {};
7069

@@ -92,9 +91,15 @@ export interface BrowserContextChannel extends Channel {
9291
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
9392
setOffline(params: { offline: boolean }): Promise<void>;
9493
waitForEvent(params: { event: string }): Promise<any>;
94+
95+
on(event: 'crBackgroundPage', callback: (params: PageChannel) => void): this;
96+
on(event: 'crServiceWorker', callback: (params: WorkerChannel) => void): this;
97+
crNewCDPSession(params: { page: PageChannel }): Promise<CDPSessionChannel>;
9598
}
9699
export type BrowserContextInitializer = {
97-
pages: PageChannel[]
100+
pages: PageChannel[],
101+
crBackgroundPages: PageChannel[],
102+
crServiceWorkers: WorkerChannel[],
98103
};
99104

100105

src/rpc/client/browser.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
7979
await this._channel.close();
8080
}
8181

82-
// Chromium-specific.
8382
async newBrowserCDPSession(): Promise<CDPSession> {
84-
return CDPSession.from(await this._channel.newBrowserCDPSession());
83+
return CDPSession.from(await this._channel.crNewBrowserCDPSession());
8584
}
8685
}

src/rpc/client/browserContext.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ import { Browser } from './browser';
2626
import { ConnectionScope } from './connection';
2727
import { Events } from '../../events';
2828
import { TimeoutSettings } from '../../timeoutSettings';
29+
import { CDPSession } from './cdpSession';
30+
import { Events as ChromiumEvents } from '../../chromium/events';
31+
import { Worker } from './worker';
2932

3033
export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserContextInitializer> {
3134
_pages = new Set<Page>();
35+
_crBackgroundPages = new Set<Page>();
36+
_crServiceWorkers = new Set<Worker>();
3237
private _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = [];
3338
_browser: Browser | undefined;
3439
readonly _bindings = new Map<string, frames.FunctionWithSource>();
@@ -46,7 +51,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
4651

4752
constructor(scope: ConnectionScope, guid: string, initializer: BrowserContextInitializer) {
4853
super(scope, guid, initializer, true);
49-
initializer.pages.map(p => {
54+
initializer.pages.forEach(p => {
5055
const page = Page.from(p);
5156
this._pages.add(page);
5257
page._setBrowserContext(this);
@@ -55,6 +60,29 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
5560
this._channel.on('close', () => this._onClose());
5661
this._channel.on('page', page => this._onPage(Page.from(page)));
5762
this._channel.on('route', ({ route, request }) => this._onRoute(network.Route.from(route), network.Request.from(request)));
63+
64+
initializer.crBackgroundPages.forEach(p => {
65+
const page = Page.from(p);
66+
this._crBackgroundPages.add(page);
67+
page._setBrowserContext(this);
68+
});
69+
this._channel.on('crBackgroundPage', pageChannel => {
70+
const page = Page.from(pageChannel);
71+
page._setBrowserContext(this);
72+
this._crBackgroundPages.add(page);
73+
this.emit(ChromiumEvents.CRBrowserContext.BackgroundPage, page);
74+
});
75+
initializer.crServiceWorkers.forEach(w => {
76+
const worker = Worker.from(w);
77+
worker._context = this;
78+
this._crServiceWorkers.add(worker);
79+
});
80+
this._channel.on('crServiceWorker', serviceWorkerChannel => {
81+
const worker = Worker.from(serviceWorkerChannel);
82+
worker._context = this;
83+
this._crServiceWorkers.add(worker);
84+
this.emit(ChromiumEvents.CRBrowserContext.ServiceWorker, worker);
85+
});
5886
}
5987

6088
private _onPage(page: Page): void {
@@ -199,4 +227,16 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
199227
async close(): Promise<void> {
200228
await this._channel.close();
201229
}
230+
231+
async newCDPSession(page: Page): Promise<CDPSession> {
232+
return CDPSession.from(await this._channel.crNewCDPSession({ page: page._channel }));
233+
}
234+
235+
backgroundPages(): Page[] {
236+
return [...this._crBackgroundPages];
237+
}
238+
239+
serviceWorkers(): Worker[] {
240+
return [...this._crServiceWorkers];
241+
}
202242
}

src/rpc/client/worker.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import { ConnectionScope } from './connection';
2121
import { ChannelOwner } from './channelOwner';
2222
import { Func1, JSHandle, parseResult, serializeArgument, SmartHandle } from './jsHandle';
2323
import { Page } from './page';
24+
import { BrowserContext } from './browserContext';
2425

2526
export class Worker extends ChannelOwner<WorkerChannel, WorkerInitializer> {
26-
_page: Page | undefined;
27+
_page: Page | undefined; // Set for web workers.
28+
_context: BrowserContext | undefined; // Set for service workers.
2729

2830
static from(worker: WorkerChannel): Worker {
2931
return (worker as any)._object;
@@ -32,7 +34,10 @@ export class Worker extends ChannelOwner<WorkerChannel, WorkerInitializer> {
3234
constructor(scope: ConnectionScope, guid: string, initializer: WorkerInitializer) {
3335
super(scope, guid, initializer);
3436
this._channel.on('close', () => {
35-
this._page!._workers.delete(this);
37+
if (this._page)
38+
this._page._workers.delete(this);
39+
if (this._context)
40+
this._context._crServiceWorkers.delete(this);
3641
this.emit(Events.Worker.Close, this);
3742
});
3843
}

src/rpc/server/browserContextDispatcher.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,31 @@ import * as types from '../../types';
1818
import { BrowserContextBase, BrowserContext } from '../../browserContext';
1919
import { Events } from '../../events';
2020
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, lookupDispatcher } from './dispatcher';
21-
import { PageDispatcher, BindingCallDispatcher } from './pageDispatcher';
22-
import { PageChannel, BrowserContextChannel, BrowserContextInitializer } from '../channels';
21+
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
22+
import { PageChannel, BrowserContextChannel, BrowserContextInitializer, CDPSessionChannel } from '../channels';
2323
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
2424
import { Page } from '../../page';
25+
import { CRBrowserContext } from '../../chromium/crBrowser';
26+
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
27+
import { Events as ChromiumEvents } from '../../chromium/events';
2528

2629
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, BrowserContextInitializer> implements BrowserContextChannel {
2730
private _context: BrowserContextBase;
2831

2932
constructor(scope: DispatcherScope, context: BrowserContextBase) {
33+
let crBackgroundPages: PageDispatcher[] = [];
34+
let crServiceWorkers: WorkerDispatcher[] = [];
35+
if (context._browserBase._options.name === 'chromium') {
36+
crBackgroundPages = (context as CRBrowserContext).backgroundPages().map(p => new PageDispatcher(scope, p));
37+
context.on(ChromiumEvents.CRBrowserContext.BackgroundPage, page => this._dispatchEvent('crBackgroundPage', new PageDispatcher(this._scope, page)));
38+
crServiceWorkers = (context as CRBrowserContext).serviceWorkers().map(w => new WorkerDispatcher(scope, w));
39+
context.on(ChromiumEvents.CRBrowserContext.ServiceWorker, serviceWorker => this._dispatchEvent('crServiceWorker', new WorkerDispatcher(this._scope, serviceWorker)));
40+
}
41+
3042
super(scope, context, 'context', {
31-
pages: context.pages().map(p => new PageDispatcher(scope, p))
43+
pages: context.pages().map(p => new PageDispatcher(scope, p)),
44+
crBackgroundPages,
45+
crServiceWorkers,
3246
}, true);
3347
this._context = context;
3448
context.on(Events.BrowserContext.Page, page => this._dispatchEvent('page', new PageDispatcher(this._scope, page)));
@@ -118,4 +132,9 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
118132
async close(): Promise<void> {
119133
await this._context.close();
120134
}
135+
136+
async crNewCDPSession(params: { page: PageDispatcher }): Promise<CDPSessionChannel> {
137+
const crBrowserContext = this._object as CRBrowserContext;
138+
return new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession(params.page._object));
139+
}
121140
}

src/rpc/server/browserDispatcher.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> i
4141
await this._object.close();
4242
}
4343

44-
// Chromium-specific.
45-
async newBrowserCDPSession(): Promise<CDPSessionChannel> {
44+
async crNewBrowserCDPSession(): Promise<CDPSessionChannel> {
4645
const crBrowser = this._object as CRBrowser;
4746
return new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession());
4847
}

src/server/browserType.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export abstract class BrowserTypeBase implements BrowserType {
106106
if ((options as any).__testHookBeforeCreateBrowser)
107107
await (options as any).__testHookBeforeCreateBrowser();
108108
const browserOptions: BrowserOptions = {
109+
name: this._name,
109110
slowMo: options.slowMo,
110111
persistent,
111112
headful: !options.headless,
@@ -142,7 +143,7 @@ export abstract class BrowserTypeBase implements BrowserType {
142143
progress.cleanupWhenAborted(() => transport.closeAndWait());
143144
if ((options as any).__testHookBeforeCreateBrowser)
144145
await (options as any).__testHookBeforeCreateBrowser();
145-
const browser = await this._connectToTransport(transport, { slowMo: options.slowMo, loggers });
146+
const browser = await this._connectToTransport(transport, { name: this._name, slowMo: options.slowMo, loggers });
146147
return browser;
147148
}, loggers.browser, TimeoutSettings.timeout(options), 'browserType.connect');
148149
}

0 commit comments

Comments
 (0)