fix: throw pretty error if opening userDataDir twice (#36007)

This commit is contained in:
Max Schmitt 2025-05-28 14:57:49 -07:00 committed by GitHub
parent 9d40340099
commit bc92cb44fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 65 additions and 6 deletions

View File

@ -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<BidiBrowser> {
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise<BidiBrowser> {
// 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;
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]);

View File

@ -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<Browser>;
abstract connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise<Browser>;
abstract amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
abstract doRewriteStartupLog(error: ProtocolError): ProtocolError;
abstract attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void;

View File

@ -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<CRBrowser> {
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise<CRBrowser> {
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]);

View File

@ -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();
}
});