diff --git a/packages/playwright-core/src/server/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts index 364f81b3c4..57506a4571 100644 --- a/packages/playwright-core/src/server/bidi/bidiChromium.ts +++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts @@ -21,6 +21,7 @@ import { BrowserReadyState, BrowserType, kNoXServerRunningError } from '../brows import { BidiBrowser } from './bidiBrowser'; import { kBrowserCloseMessageId } from './bidiConnection'; import { chromiumSwitches } from '../chromium/chromiumSwitches'; +import { RecentLogsCollector } from '../utils/debugLogger'; import type { BrowserOptions } from '../browser'; import type { SdkObject } from '../instrumentation'; @@ -35,13 +36,23 @@ export class BidiChromium extends BrowserType { super(parent, 'bidi'); } - override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise { + override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise { // Chrome doesn't support Bidi, we create Bidi over CDP which is used by Chrome driver. // bidiOverCdp depends on chromium-bidi which we only have in devDependencies, so // we load bidiOverCdp dynamically. const bidiTransport = await require('./bidiOverCdp').connectBidiOverCdp(transport); (transport as any)[kBidiOverCdpWrapper] = bidiTransport; - return BidiBrowser.connect(this.attribution.playwright, bidiTransport, options); + try { + return BidiBrowser.connect(this.attribution.playwright, bidiTransport, options); + } catch (e) { + if (browserLogsCollector.recentLogs().some(log => log.includes('Failed to create a ProcessSingleton for your profile directory.'))) { + throw new Error( + 'Failed to create a ProcessSingleton for your profile directory. ' + + 'This usually means that the profile is already in use by another instance of Chromium.' + ); + } + throw e; + } } override doRewriteStartupLog(error: ProtocolError): ProtocolError { @@ -155,6 +166,10 @@ export class BidiChromium extends BrowserType { class ChromiumReadyState extends BrowserReadyState { override onBrowserOutput(message: string): void { + if (message.includes('Failed to create a ProcessSingleton for your profile directory.')) { + this._wsEndpoint.reject(new Error('Failed to create a ProcessSingleton for your profile directory. ' + + 'This usually means that the profile is already in use by another instance of Chromium.')); + } const match = message.match(/DevTools listening on (.*)/); if (match) this._wsEndpoint.resolve(match[1]); diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 45e99ce75d..4a8600c557 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -157,7 +157,7 @@ export abstract class BrowserType extends SdkObject { if (persistent) validateBrowserContextOptions(persistent, browserOptions); copyTestHooks(options, browserOptions); - const browser = await this.connectToTransport(transport, browserOptions); + const browser = await this.connectToTransport(transport, browserOptions, browserLogsCollector); (browser as any)._userDataDirForTest = userDataDir; // We assume no control when using custom arguments, and do not prepare the default context in that case. if (persistent && !options.ignoreAllDefaultArgs) @@ -342,7 +342,7 @@ export abstract class BrowserType extends SdkObject { } abstract defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[]; - abstract connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise; + abstract connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise; abstract amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env; abstract doRewriteStartupLog(error: ProtocolError): ProtocolError; abstract attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void; diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 6adef73807..91f4fd6f9b 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -126,13 +126,23 @@ export class Chromium extends BrowserType { return directory ? new CRDevTools(path.join(directory, 'devtools-preferences.json')) : undefined; } - override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise { + override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise { let devtools = this._devtools; if ((options as any).__testHookForDevTools) { devtools = this._createDevTools(); await (options as any).__testHookForDevTools(devtools); } - return CRBrowser.connect(this.attribution.playwright, transport, options, devtools); + try { + return await CRBrowser.connect(this.attribution.playwright, transport, options, devtools); + } catch (e) { + if (browserLogsCollector.recentLogs().some(log => log.includes('Failed to create a ProcessSingleton for your profile directory.'))) { + throw new Error( + 'Failed to create a ProcessSingleton for your profile directory. ' + + 'This usually means that the profile is already in use by another instance of Chromium.' + ); + } + throw e; + } } override doRewriteStartupLog(error: ProtocolError): ProtocolError { @@ -358,6 +368,10 @@ export class Chromium extends BrowserType { class ChromiumReadyState extends BrowserReadyState { override onBrowserOutput(message: string): void { + if (message.includes('Failed to create a ProcessSingleton for your profile directory.')) { + this._wsEndpoint.reject(new Error('Failed to create a ProcessSingleton for your profile directory. ' + + 'This usually means that the profile is already in use by another instance of Chromium.')); + } const match = message.match(/DevTools listening on (.*)/); if (match) this._wsEndpoint.resolve(match[1]); diff --git a/tests/library/chromium/chromium.spec.ts b/tests/library/chromium/chromium.spec.ts index 910e0830ff..f254ad3cf0 100644 --- a/tests/library/chromium/chromium.spec.ts +++ b/tests/library/chromium/chromium.spec.ts @@ -630,3 +630,33 @@ test.describe('PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', () => { expect(req.headers['x-custom-header']).toBe('custom!'); }); }); + +test('should throw when connecting twice to an already running persistent context (--remote-debugging-port)', async ({ browserType, createUserDataDir, platform, isHeadlessShell }) => { + test.skip(isHeadlessShell, 'Headless shell does not create a ProcessSingleton'); + test.fixme(platform === 'win32', 'Windows does not print something to the console when the profile is already in use by another instance of Chromium.'); + const userDataDir = await createUserDataDir(); + const browser = await browserType.launchPersistentContext(userDataDir, { + cdpPort: 9222, + } as any); + try { + const error = await browserType.launchPersistentContext(userDataDir, { + cdpPort: 9223, + } as any).catch(e => e); + expect(error.message).toContain('This usually means that the profile is already in use by another instance of Chromium.'); + } finally { + await browser.close(); + } +}); + +test('should throw when connecting twice to an already running persistent context (--remote-debugging-pipe)', async ({ browserType, createUserDataDir, platform, isHeadlessShell }) => { + test.skip(isHeadlessShell, 'Headless shell does not create a ProcessSingleton'); + test.fixme(platform === 'win32', 'Windows does not print something to the console when the profile is already in use by another instance of Chromium.'); + const userDataDir = await createUserDataDir(); + const browser = await browserType.launchPersistentContext(userDataDir); + try { + const error = await browserType.launchPersistentContext(userDataDir).catch(e => e); + expect(error.message).toContain('This usually means that the profile is already in use by another instance of Chromium.'); + } finally { + await browser.close(); + } +});