Skip to content

Commit 928dee4

Browse files
authored
chore: allow launchServer with a sharedBrowser (#36054)
1 parent a15e94a commit 928dee4

File tree

8 files changed

+174
-130
lines changed

8 files changed

+174
-130
lines changed

packages/playwright-core/src/browserServerImpl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
3636
this._browserName = browserName;
3737
}
3838

39-
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
39+
async launchServer(options: LaunchServerOptions & { _sharedBrowser?: boolean } = {}): Promise<BrowserServer> {
4040
const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
4141
// TODO: enable socks proxy once ipv6 is supported.
4242
const socksProxy = false ? new SocksProxy() : undefined;
@@ -59,7 +59,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
5959
const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;
6060

6161
// 2. Start the server
62-
const server = new PlaywrightServer({ mode: 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser, preLaunchedSocksProxy: socksProxy });
62+
const server = new PlaywrightServer({ mode: options._sharedBrowser ? 'launchServerShared' : 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser, preLaunchedSocksProxy: socksProxy });
6363
const wsEndpoint = await server.listen(options.port, options.host);
6464

6565
// 3. Return the BrowserServer interface

packages/playwright-core/src/client/types.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -105,31 +105,10 @@ export type ConnectOptions = {
105105
timeout?: number,
106106
logger?: Logger,
107107
};
108-
export type LaunchServerOptions = {
109-
channel?: channels.BrowserTypeLaunchOptions['channel'],
110-
executablePath?: string,
111-
args?: string[],
112-
ignoreDefaultArgs?: boolean | string[],
113-
handleSIGINT?: boolean,
114-
handleSIGTERM?: boolean,
115-
handleSIGHUP?: boolean,
116-
timeout?: number,
117-
env?: Env,
118-
headless?: boolean,
119-
devtools?: boolean,
120-
proxy?: {
121-
server: string,
122-
bypass?: string,
123-
username?: string,
124-
password?: string
125-
},
126-
downloadsPath?: string,
127-
chromiumSandbox?: boolean,
108+
export type LaunchServerOptions = LaunchOptions & {
128109
host?: string,
129110
port?: number,
130111
wsPath?: string,
131-
logger?: Logger,
132-
firefoxUserPrefs?: { [key: string]: string | number | boolean };
133112
};
134113

135114
export type LaunchAndroidServerOptions = {

packages/playwright-core/src/remote/playwrightConnection.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Options = {
3838
socksProxyPattern: string | undefined,
3939
browserName: string | null,
4040
launchOptions: LaunchOptions,
41+
sharedBrowser?: boolean,
4142
};
4243

4344
type PreLaunched = {
@@ -138,7 +139,7 @@ export class PlaywrightConnection {
138139
this.close({ code: 1001, reason: 'Browser closed' });
139140
});
140141

141-
return new PlaywrightDispatcher(scope, playwright, ownedSocksProxy, browser);
142+
return new PlaywrightDispatcher(scope, playwright, { socksProxy: ownedSocksProxy, preLaunchedBrowser: browser });
142143
}
143144

144145
private async _initPreLaunchedBrowserMode(scope: RootDispatcher) {
@@ -154,7 +155,11 @@ export class PlaywrightConnection {
154155
this.close({ code: 1001, reason: 'Browser closed' });
155156
});
156157

157-
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, this._preLaunched.socksProxy, browser);
158+
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, {
159+
socksProxy: this._preLaunched.socksProxy,
160+
preLaunchedBrowser: browser,
161+
sharedBrowser: this._options.sharedBrowser,
162+
});
158163
// In pre-launched mode, keep only the pre-launched browser.
159164
for (const b of playwright.allBrowsers()) {
160165
if (b !== browser)
@@ -172,7 +177,7 @@ export class PlaywrightConnection {
172177
// Underlying browser did close for some reason - force disconnect the client.
173178
this.close({ code: 1001, reason: 'Android device disconnected' });
174179
});
175-
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, undefined, undefined, androidDevice);
180+
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, { preLaunchedAndroidDevice: androidDevice });
176181
this._cleanups.push(() => playwrightDispatcher.cleanup());
177182
return playwrightDispatcher;
178183
}
@@ -233,7 +238,7 @@ export class PlaywrightConnection {
233238
}
234239
});
235240

236-
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, undefined, browser);
241+
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, { preLaunchedBrowser: browser });
237242
return playwrightDispatcher;
238243
}
239244

packages/playwright-core/src/remote/playwrightServer.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import type { LaunchOptions } from '../server/types';
3434
type ServerOptions = {
3535
path: string;
3636
maxConnections: number;
37-
mode: 'default' | 'launchServer' | 'extension';
37+
mode: 'default' | 'launchServer' | 'launchServerShared' | 'extension';
3838
preLaunchedBrowser?: Browser;
3939
preLaunchedAndroidDevice?: AndroidDevice;
4040
preLaunchedSocksProxy?: SocksProxy;
@@ -98,15 +98,21 @@ export class PlaywrightServer {
9898
} else if (isExtension) {
9999
clientType = 'reuse-browser';
100100
semaphore = reuseBrowserSemaphore;
101-
} else if (this._options.mode === 'launchServer') {
101+
} else if (this._options.mode === 'launchServer' || this._options.mode === 'launchServerShared') {
102102
clientType = 'pre-launched-browser-or-android';
103103
semaphore = browserSemaphore;
104104
}
105105

106106
return new PlaywrightConnection(
107107
semaphore.acquire(),
108108
clientType, ws,
109-
{ socksProxyPattern: proxyValue, browserName, launchOptions, allowFSPaths: this._options.mode === 'extension' },
109+
{
110+
socksProxyPattern: proxyValue,
111+
browserName,
112+
launchOptions,
113+
allowFSPaths: this._options.mode === 'extension',
114+
sharedBrowser: this._options.mode === 'launchServerShared',
115+
},
110116
{
111117
playwright: this._preLaunchedPlaywright,
112118
browser: this._options.preLaunchedBrowser,

packages/playwright-core/src/server/dispatchers/browserDispatcher.ts

Lines changed: 48 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,35 @@ import { BrowserContext } from '../browserContext';
2222
import { ArtifactDispatcher } from './artifactDispatcher';
2323

2424
import type { BrowserTypeDispatcher } from './browserTypeDispatcher';
25-
import type { RootDispatcher } from './dispatcher';
2625
import type { PageDispatcher } from './pageDispatcher';
2726
import type { CRBrowser } from '../chromium/crBrowser';
2827
import type { CallMetadata } from '../instrumentation';
2928
import type * as channels from '@protocol/channels';
3029

30+
type BrowserDispatcherOptions = {
31+
// Do not allow to close this browser.
32+
ignoreStopAndKill?: boolean,
33+
// Only expose browser contexts created by this dispatcher. By default, all contexts are exposed.
34+
isolateContexts?: boolean,
35+
};
36+
3137
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChannel, BrowserTypeDispatcher> implements channels.BrowserChannel {
3238
_type_Browser = true;
39+
private _options: BrowserDispatcherOptions;
40+
private _isolatedContexts = new Set<BrowserContext>();
3341

34-
constructor(scope: BrowserTypeDispatcher, browser: Browser) {
42+
constructor(scope: BrowserTypeDispatcher, browser: Browser, options: BrowserDispatcherOptions = {}) {
3543
super(scope, browser, 'Browser', { version: browser.version(), name: browser.options.name });
36-
this.addObjectListener(Browser.Events.Context, (context: BrowserContext) => this._dispatchEvent('context', { context: BrowserContextDispatcher.from(this, context) }));
37-
this.addObjectListener(Browser.Events.Disconnected, () => this._didClose());
38-
if (browser._defaultContext)
39-
this._dispatchEvent('context', { context: BrowserContextDispatcher.from(this, browser._defaultContext) });
40-
for (const context of browser.contexts())
41-
this._dispatchEvent('context', { context: BrowserContextDispatcher.from(this, context) });
44+
this._options = options;
45+
46+
if (!options.isolateContexts) {
47+
this.addObjectListener(Browser.Events.Context, (context: BrowserContext) => this._dispatchEvent('context', { context: BrowserContextDispatcher.from(this, context) }));
48+
this.addObjectListener(Browser.Events.Disconnected, () => this._didClose());
49+
if (browser._defaultContext)
50+
this._dispatchEvent('context', { context: BrowserContextDispatcher.from(this, browser._defaultContext) });
51+
for (const context of browser.contexts())
52+
this._dispatchEvent('context', { context: BrowserContextDispatcher.from(this, context) });
53+
}
4254
}
4355

4456
_didClose() {
@@ -47,24 +59,49 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChann
4759
}
4860

4961
async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<channels.BrowserNewContextResult> {
62+
if (!this._options.isolateContexts) {
63+
const context = await this._object.newContext(metadata, params);
64+
const contextDispatcher = BrowserContextDispatcher.from(this, context);
65+
return { context: contextDispatcher };
66+
}
67+
68+
if (params.recordVideo)
69+
params.recordVideo.dir = this._object.options.artifactsDir;
5070
const context = await this._object.newContext(metadata, params);
51-
return { context: BrowserContextDispatcher.from(this, context) };
71+
this._isolatedContexts.add(context);
72+
context.on(BrowserContext.Events.Close, () => this._isolatedContexts.delete(context));
73+
const contextDispatcher = BrowserContextDispatcher.from(this, context);
74+
this._dispatchEvent('context', { context: contextDispatcher });
75+
return { context: contextDispatcher };
5276
}
5377

5478
async newContextForReuse(params: channels.BrowserNewContextForReuseParams, metadata: CallMetadata): Promise<channels.BrowserNewContextForReuseResult> {
55-
return await newContextForReuse(this._object, this, params, metadata);
79+
const { context, needsReset } = await this._object.newContextForReuse(params, metadata);
80+
if (needsReset) {
81+
const oldContextDispatcher = this.connection.existingDispatcher<BrowserContextDispatcher>(context);
82+
if (oldContextDispatcher)
83+
oldContextDispatcher._dispose();
84+
await context.resetForReuse(metadata, params);
85+
}
86+
const contextDispatcher = BrowserContextDispatcher.from(this, context);
87+
this._dispatchEvent('context', { context: contextDispatcher });
88+
return { context: contextDispatcher };
5689
}
5790

5891
async stopPendingOperations(params: channels.BrowserStopPendingOperationsParams, metadata: CallMetadata): Promise<channels.BrowserStopPendingOperationsResult> {
5992
await this._object.stopPendingOperations(params.reason);
6093
}
6194

6295
async close(params: channels.BrowserCloseParams, metadata: CallMetadata): Promise<void> {
96+
if (this._options.ignoreStopAndKill)
97+
return;
6398
metadata.potentiallyClosesScope = true;
6499
await this._object.close(params);
65100
}
66101

67102
async killForTests(_: any, metadata: CallMetadata): Promise<void> {
103+
if (this._options.ignoreStopAndKill)
104+
return;
68105
metadata.potentiallyClosesScope = true;
69106
await this._object.killForTests();
70107
}
@@ -93,83 +130,8 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChann
93130
const crBrowser = this._object as CRBrowser;
94131
return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) };
95132
}
96-
}
97-
98-
// This class implements multiplexing browser dispatchers over a single Browser instance.
99-
export class ConnectedBrowserDispatcher extends Dispatcher<Browser, channels.BrowserChannel, RootDispatcher> implements channels.BrowserChannel {
100-
_type_Browser = true;
101-
private _contexts = new Set<BrowserContext>();
102-
103-
constructor(scope: RootDispatcher, browser: Browser) {
104-
super(scope, browser, 'Browser', { version: browser.version(), name: browser.options.name });
105-
}
106-
107-
async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<channels.BrowserNewContextResult> {
108-
if (params.recordVideo)
109-
params.recordVideo.dir = this._object.options.artifactsDir;
110-
const context = await this._object.newContext(metadata, params);
111-
this._contexts.add(context);
112-
context.on(BrowserContext.Events.Close, () => this._contexts.delete(context));
113-
const contextDispatcher = BrowserContextDispatcher.from(this, context);
114-
this._dispatchEvent('context', { context: contextDispatcher });
115-
return { context: contextDispatcher };
116-
}
117-
118-
async newContextForReuse(params: channels.BrowserNewContextForReuseParams, metadata: CallMetadata): Promise<channels.BrowserNewContextForReuseResult> {
119-
return await newContextForReuse(this._object, this as any as BrowserDispatcher, params, metadata);
120-
}
121-
122-
async stopPendingOperations(params: channels.BrowserStopPendingOperationsParams, metadata: CallMetadata): Promise<channels.BrowserStopPendingOperationsResult> {
123-
await this._object.stopPendingOperations(params.reason);
124-
}
125-
126-
async close(): Promise<void> {
127-
// Client should not send us Browser.close.
128-
}
129-
130-
async killForTests(): Promise<void> {
131-
// Client should not send us Browser.killForTests.
132-
}
133-
134-
async defaultUserAgentForTest(): Promise<channels.BrowserDefaultUserAgentForTestResult> {
135-
throw new Error('Client should not send us Browser.defaultUserAgentForTest');
136-
}
137-
138-
async newBrowserCDPSession(): Promise<channels.BrowserNewBrowserCDPSessionResult> {
139-
if (!this._object.options.isChromium)
140-
throw new Error(`CDP session is only available in Chromium`);
141-
const crBrowser = this._object as CRBrowser;
142-
return { session: new CDPSessionDispatcher(this as any as BrowserDispatcher, await crBrowser.newBrowserCDPSession()) };
143-
}
144-
145-
async startTracing(params: channels.BrowserStartTracingParams): Promise<void> {
146-
if (!this._object.options.isChromium)
147-
throw new Error(`Tracing is only available in Chromium`);
148-
const crBrowser = this._object as CRBrowser;
149-
await crBrowser.startTracing(params.page ? (params.page as PageDispatcher)._object : undefined, params);
150-
}
151-
152-
async stopTracing(): Promise<channels.BrowserStopTracingResult> {
153-
if (!this._object.options.isChromium)
154-
throw new Error(`Tracing is only available in Chromium`);
155-
const crBrowser = this._object as CRBrowser;
156-
return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) };
157-
}
158133

159134
async cleanupContexts() {
160-
await Promise.all(Array.from(this._contexts).map(context => context.close({ reason: 'Global context cleanup (connection terminated)' })));
161-
}
162-
}
163-
164-
async function newContextForReuse(browser: Browser, scope: BrowserDispatcher, params: channels.BrowserNewContextForReuseParams, metadata: CallMetadata): Promise<channels.BrowserNewContextForReuseResult> {
165-
const { context, needsReset } = await browser.newContextForReuse(params, metadata);
166-
if (needsReset) {
167-
const oldContextDispatcher = scope.connection.existingDispatcher<BrowserContextDispatcher>(context);
168-
if (oldContextDispatcher)
169-
oldContextDispatcher._dispose();
170-
await context.resetForReuse(metadata, params);
135+
await Promise.all(Array.from(this._isolatedContexts).map(context => context.close({ reason: 'Global context cleanup (connection terminated)' })));
171136
}
172-
const contextDispatcher = BrowserContextDispatcher.from(scope, context);
173-
scope._dispatchEvent('context', { context: contextDispatcher });
174-
return { context: contextDispatcher };
175137
}

packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { SocksProxy } from '../utils/socksProxy';
1818
import { GlobalAPIRequestContext } from '../fetch';
1919
import { AndroidDispatcher } from './androidDispatcher';
2020
import { AndroidDeviceDispatcher } from './androidDispatcher';
21-
import { ConnectedBrowserDispatcher } from './browserDispatcher';
21+
import { BrowserDispatcher } from './browserDispatcher';
2222
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
2323
import { Dispatcher } from './dispatcher';
2424
import { ElectronDispatcher } from './electronDispatcher';
@@ -35,27 +35,48 @@ import type { Browser } from '../browser';
3535
import type { Playwright } from '../playwright';
3636
import type * as channels from '@protocol/channels';
3737

38+
type PlaywrightDispatcherOptions = {
39+
socksProxy?: SocksProxy;
40+
preLaunchedBrowser?: Browser;
41+
preLaunchedAndroidDevice?: AndroidDevice;
42+
sharedBrowser?: boolean;
43+
};
44+
3845
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightChannel, RootDispatcher> implements channels.PlaywrightChannel {
3946
_type_Playwright;
40-
private _browserDispatcher: ConnectedBrowserDispatcher | undefined;
47+
private _browserDispatcher: BrowserDispatcher | undefined;
4148

42-
constructor(scope: RootDispatcher, playwright: Playwright, socksProxy?: SocksProxy, preLaunchedBrowser?: Browser, prelaunchedAndroidDevice?: AndroidDevice) {
43-
const browserDispatcher = preLaunchedBrowser ? new ConnectedBrowserDispatcher(scope, preLaunchedBrowser) : undefined;
49+
constructor(scope: RootDispatcher, playwright: Playwright, options: PlaywrightDispatcherOptions = {}) {
50+
const chromium = new BrowserTypeDispatcher(scope, playwright.chromium);
51+
const firefox = new BrowserTypeDispatcher(scope, playwright.firefox);
52+
const webkit = new BrowserTypeDispatcher(scope, playwright.webkit);
4453
const android = new AndroidDispatcher(scope, playwright.android);
45-
const prelaunchedAndroidDeviceDispatcher = prelaunchedAndroidDevice ? new AndroidDeviceDispatcher(android, prelaunchedAndroidDevice) : undefined;
46-
super(scope, playwright, 'Playwright', {
47-
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
48-
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
49-
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
54+
const initializer: channels.PlaywrightInitializer = {
55+
chromium,
56+
firefox,
57+
webkit,
5058
bidiChromium: new BrowserTypeDispatcher(scope, playwright.bidiChromium),
5159
bidiFirefox: new BrowserTypeDispatcher(scope, playwright.bidiFirefox),
5260
android,
5361
electron: new ElectronDispatcher(scope, playwright.electron),
5462
utils: playwright.options.isServer ? undefined : new LocalUtilsDispatcher(scope, playwright),
55-
preLaunchedBrowser: browserDispatcher,
56-
preConnectedAndroidDevice: prelaunchedAndroidDeviceDispatcher,
57-
socksSupport: socksProxy ? new SocksSupportDispatcher(scope, socksProxy) : undefined,
58-
});
63+
socksSupport: options.socksProxy ? new SocksSupportDispatcher(scope, options.socksProxy) : undefined,
64+
};
65+
66+
let browserDispatcher: BrowserDispatcher | undefined;
67+
if (options.preLaunchedBrowser) {
68+
const browserTypeDispatcher = { chromium, firefox, webkit }[options.preLaunchedBrowser.options.name as 'chromium' | 'firefox' | 'webkit'];
69+
browserDispatcher = new BrowserDispatcher(browserTypeDispatcher, options.preLaunchedBrowser, {
70+
ignoreStopAndKill: true,
71+
isolateContexts: !options.sharedBrowser,
72+
});
73+
initializer.preLaunchedBrowser = browserDispatcher;
74+
}
75+
76+
if (options.preLaunchedAndroidDevice)
77+
initializer.preConnectedAndroidDevice = new AndroidDeviceDispatcher(android, options.preLaunchedAndroidDevice);
78+
79+
super(scope, playwright, 'Playwright', initializer);
5980
this._type_Playwright = true;
6081
this._browserDispatcher = browserDispatcher;
6182
}

tests/config/remoteServer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export type RemoteServerOptions = {
6868
inCluster?: boolean;
6969
url?: string;
7070
startStopAndRunHttp?: boolean;
71+
sharedBrowser?: boolean;
7172
};
7273

7374
export class RemoteServer implements PlaywrightServer {
@@ -94,6 +95,8 @@ export class RemoteServer implements PlaywrightServer {
9495
handleSIGHUP: true,
9596
logger: undefined,
9697
};
98+
if (remoteServerOptions.sharedBrowser)
99+
(launchOptions as any)._sharedBrowser = true;
97100
const options = {
98101
browserTypeName: browserType.name(),
99102
channel,

0 commit comments

Comments
 (0)