From 9da62b2a617381838858d82325095f39a84f9e1f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 5 Mar 2026 12:24:22 -0800 Subject: [PATCH 01/10] chore: switch devtools to browser registry --- packages/devtools/src/grid.tsx | 25 +- packages/devtools/src/sessionModel.ts | 37 +- .../playwright-core/src/cli/client/DEPS.list | 3 + .../src/cli/client/devtoolsController.ts | 338 +++++++++++++++ .../src/cli/client/registry.ts | 1 + .../src/cli/client/socketConnection.ts | 8 +- .../playwright-core/src/cli/daemon/program.ts | 3 + .../src/client/browserContext.ts | 4 - .../src/client/eventEmitter.ts | 9 +- .../playwright-core/src/devtools/DEPS.list | 4 +- .../src/devtools/devtoolsApp.ts | 153 +++++-- .../playwright-core/src/protocol/validator.ts | 4 - .../playwright-core/src/server/browser.ts | 5 +- .../src/server/browserContext.ts | 10 - .../src/server/devtoolsController.ts | 404 ------------------ .../dispatchers/browserContextDispatcher.ts | 5 - .../src/utils/isomorphic/protocolMetainfo.ts | 1 - packages/protocol/src/channels.d.ts | 6 - packages/protocol/src/protocol.yml | 5 - 19 files changed, 499 insertions(+), 526 deletions(-) create mode 100644 packages/playwright-core/src/cli/client/devtoolsController.ts delete mode 100644 packages/playwright-core/src/server/devtoolsController.ts diff --git a/packages/devtools/src/grid.tsx b/packages/devtools/src/grid.tsx index 4266690313de0..c26066e2d2a38 100644 --- a/packages/devtools/src/grid.tsx +++ b/packages/devtools/src/grid.tsx @@ -21,7 +21,7 @@ import { navigate } from './index'; import { Screencast } from './screencast'; import { SettingsButton } from './settingsView'; -import type { SessionFile } from '../../playwright-core/src/cli/client/registry'; +import type { BrowserDescriptor } from '../../playwright-core/src/serverRegistry'; import type { Tab } from './devtoolsChannel'; import type { SessionModel, SessionStatus } from './sessionModel'; @@ -44,7 +44,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => { const workspaceGroups = React.useMemo(() => { const groups = new Map(); for (const session of sessions) { - const key = session.file.config.workspaceDir || 'Global'; + const key = session.browserDescriptor.workspaceDir || 'Global'; let list = groups.get(key); if (!list) { list = []; @@ -53,7 +53,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => { list.push(session); } for (const list of groups.values()) - list.sort((a, b) => a.file.config.name.localeCompare(b.file.config.name)); + list.sort((a, b) => a.browserDescriptor.title.localeCompare(b.browserDescriptor.title)); // Current workspace first, then alphabetical. const entries = [...groups.entries()]; @@ -91,7 +91,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => { {isExpanded && (
- {entries.map(({ file, canConnect }) => )} + {entries.map(session => )}
)} @@ -102,10 +102,9 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => { ); }; -const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; visible: boolean; model: SessionModel }> = ({ sessionFile, canConnect, visible, model }) => { - const { config } = sessionFile; - const href = '#session=' + encodeURIComponent(config.socketPath); - const wsUrl = model.wsUrls.get(config.socketPath); +const SessionChip: React.FC<{ descriptor: BrowserDescriptor; canConnect: boolean; visible: boolean; model: SessionModel }> = ({ descriptor, canConnect, visible, model }) => { + const href = '#session=' + encodeURIComponent(descriptor.pipeName!); + const wsUrl = model.wsUrls.get(descriptor.pipeName!); const channel = React.useMemo(() => { if (!canConnect || !visible || !wsUrl) @@ -129,7 +128,7 @@ const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; vis }; }, [channel]); - const chipTitle = selectedTab ? `[${config.name}] ${selectedTab.url} \u2014 ${selectedTab.title}` : config.name; + const chipTitle = selectedTab ? `[${descriptor.title}] ${selectedTab.url} \u2014 ${selectedTab.title}` : descriptor.title; const clickable = canConnect && wsUrl !== null; return ( @@ -141,7 +140,7 @@ const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; vis
- {selectedTab ? <>[{config.name}] {selectedTab.url} — {selectedTab.title} : config.name} + {selectedTab ? <>[{descriptor.title}] {selectedTab.url} — {selectedTab.title} : descriptor.title} {canConnect && (
{isExpanded && (
- {entries.map(session => )} + {entries.map(session => )}
)}
@@ -102,9 +102,8 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => { ); }; -const SessionChip: React.FC<{ descriptor: BrowserDescriptor; canConnect: boolean; visible: boolean; model: SessionModel }> = ({ descriptor, canConnect, visible, model }) => { +const SessionChip: React.FC<{ descriptor: BrowserDescriptor; canConnect: boolean; wsUrl: string | null | undefined; visible: boolean; model: SessionModel }> = ({ descriptor, canConnect, wsUrl, visible, model }) => { const href = '#session=' + encodeURIComponent(descriptor.pipeName!); - const wsUrl = model.wsUrls.get(descriptor.pipeName!); const channel = React.useMemo(() => { if (!canConnect || !visible || !wsUrl) diff --git a/packages/devtools/src/index.tsx b/packages/devtools/src/index.tsx index 664fbbf93560b..99bd7acc6eaed 100644 --- a/packages/devtools/src/index.tsx +++ b/packages/devtools/src/index.tsx @@ -60,7 +60,7 @@ const App: React.FC = () => { }, []); if (socketPath) { - const wsUrl = model.wsUrls.get(socketPath); + const wsUrl = model.sessionBySocketPath(socketPath)?.wsUrl; return ; } return ; diff --git a/packages/devtools/src/sessionModel.ts b/packages/devtools/src/sessionModel.ts index 70ea4634fc427..0a4f9baad6a46 100644 --- a/packages/devtools/src/sessionModel.ts +++ b/packages/devtools/src/sessionModel.ts @@ -20,6 +20,7 @@ import type { BrowserDescriptor } from '../../playwright-core/src/serverRegistry export type SessionStatus = { browserDescriptor: BrowserDescriptor; canConnect: boolean; + wsUrl?: string | null; }; @@ -27,12 +28,10 @@ type Listener = () => void; export class SessionModel { sessions: SessionStatus[] = []; - readonly wsUrls: Map = new Map(); clientInfo: ClientInfo | undefined; error: string | undefined; loading = true; - private _knownPipes = new Set(); private _pollActive = false; private _pollTimeout: ReturnType | undefined; private _lastJson = ''; @@ -86,10 +85,7 @@ export class SessionModel { this.clientInfo = data.clientInfo; this._notify(); - for (const session of this.sessions) { - if (session.canConnect) - this._obtainDevtoolsUrl(session.browserDescriptor); - } + } this.error = undefined; } catch (e: any) { @@ -122,29 +118,6 @@ export class SessionModel { await this._fetchSessions(); } - private _obtainDevtoolsUrl(descriptor: BrowserDescriptor) { - const pipeName = descriptor.pipeName!; - if (this._knownPipes.has(pipeName)) - return; - this._knownPipes.add(pipeName); - - fetch('/api/sessions/devtools-start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ browserDescriptor: descriptor }), - }).then(async resp => { - if (resp.ok) { - const { url } = await resp.json(); - this.wsUrls.set(pipeName, url); - } else { - this.wsUrls.set(pipeName, null); - } - this._notify(); - }).catch(() => { - this._knownPipes.delete(pipeName); - }); - } - dispose() { this.stopPolling(); this._listeners.clear(); diff --git a/packages/playwright-core/src/devtools/devtoolsApp.ts b/packages/playwright-core/src/devtools/devtoolsApp.ts index bc111a456bc2c..cae53c2546eb7 100644 --- a/packages/playwright-core/src/devtools/devtoolsApp.ts +++ b/packages/playwright-core/src/devtools/devtoolsApp.ts @@ -25,8 +25,8 @@ import { chromium } from '../..'; import { HttpServer } from '../server/utils/httpServer'; import { gracefullyProcessExitDoNotHang } from '../server/utils/processLauncher'; import { findChromiumChannelBestEffort, registryDirectory } from '../server/registry/index'; -import { calculateSha1 } from '../utils'; -import { CDPConnection, DevToolsConnection } from './devtoolsController'; +import { calculateSha1, createGuid } from '../utils'; +import { CDPConnection, connectToBrowserSocket, DevToolsConnection } from './devtoolsController'; import { serverRegistry } from '../serverRegistry'; import type * as api from '../..'; @@ -67,23 +67,23 @@ async function loadBrowserDescriptorSessions(): Promise { const sessions: SessionStatus[] = []; for (const [, browsers] of servers) { for (const browser of browsers) { + const wsUrl = new URL('/browser-channel', 'http://placeholder'); + wsUrl.searchParams.set('socketPath', browser.pipeName!); + wsUrl.searchParams.set('playwrightLib', browser.playwrightLib); + if (browser.browser.launchOptions.cdpPort) + wsUrl.searchParams.set('cdpPort', String(browser.browser.launchOptions.cdpPort)); sessions.push({ browserDescriptor: browser, // TODO: do not gc descriptors in registry list(). canConnect: true, + wsUrl: wsUrl.pathname + wsUrl.search, }); } } return sessions; } -type ConnectionInfo = { - browserDescriptor: BrowserDescriptor; - browser: api.Browser; - connection?: DevToolsConnection; -}; - -const socketPathToConnectionInfo = new Map(); +const socketPathToDevToolsConnection = new Map(); async function handleApiRequest(request: http.IncomingMessage, response: http.ServerResponse) { const url = new URL(request.url!, `http://${request.headers.host}`); @@ -97,21 +97,16 @@ async function handleApiRequest(request: http.IncomingMessage, response: http.Se if (apiPath === '/api/sessions/close' && request.method === 'POST') { const { browserDescriptor } = await parseRequest(request); - const socketPath = browserDescriptor.pipeName!; - let browser = socketPathToConnectionInfo.get(socketPath)?.browser; - if (!browser) { - try { - browser = await connectToBrowserSocket(socketPath, browserDescriptor.playwrightLib); - socketPathToConnectionInfo.set(socketPath, { browserDescriptor, browser }); - } catch (e) { - sendJSON(response, { error: 'Failed to connect to browser socket: ' + e.message }, 500); - return; - } + let browser: api.Browser; + try { + browser = await connectToBrowserSocket(browserDescriptor.pipeName!, browserDescriptor.playwrightLib); + } catch (e) { + sendJSON(response, { error: 'Failed to connect to browser socket: ' + e.message }, 500); + return; } try { await Promise.all(browser.contexts().map(context => context.close())); await browser.close(); - socketPathToConnectionInfo.delete(socketPath); sendJSON(response, { success: true }); return; } catch (e) { @@ -125,32 +120,10 @@ async function handleApiRequest(request: http.IncomingMessage, response: http.Se return; } - if (apiPath === '/api/sessions/devtools-start' && request.method === 'POST') { - const { browserDescriptor } = await parseRequest(request); - const socketPath = browserDescriptor.pipeName!; - let browser = socketPathToConnectionInfo.get(socketPath)?.browser; - if (!browser) { - try { - browser = await connectToBrowserSocket(socketPath, browserDescriptor.playwrightLib); - socketPathToConnectionInfo.set(socketPath, { browserDescriptor, browser }); - } catch (e) { - sendJSON(response, { error: 'Failed to connect to browser socket: ' + e.message }, 500); - return; - } - } - sendJSON(response, { url: '/browser-channel?socketPath=' + encodeURIComponent(socketPath) }); - return; - } - response.statusCode = 404; response.end(JSON.stringify({ error: 'Not found' })); } -async function connectToBrowserSocket(socketPath: string, playwrightLib: string): Promise { - const otherPlaywright = (await import(pathToFileURL(playwrightLib).toString())).default as typeof import('../..'); - return await otherPlaywright.chromium.connect(socketPath); -} - async function openDevToolsApp(): Promise { const httpServer = new HttpServer(); const libDir = require.resolve('playwright-core/package.json'); @@ -168,7 +141,7 @@ async function openDevToolsApp(): Promise { const socketPath = url.searchParams.get('socketPath'); const cdpPageId = url.searchParams.get('cdpPageId'); if (cdpPageId && socketPath) { - const connection = socketPathToConnectionInfo.get(socketPath)?.connection; + const connection = socketPathToDevToolsConnection.get(socketPath); if (!connection) throw new Error('CDP connection not found for socket path: ' + socketPath); const page = connection.pageForId(cdpPageId); @@ -177,16 +150,14 @@ async function openDevToolsApp(): Promise { return new CDPConnection(page); } if (socketPath) { - const connectionInfo = socketPathToConnectionInfo.get(socketPath)!; - if (connectionInfo.connection) - return connectionInfo.connection; - const context = connectionInfo.browser.contexts()[0]; - const serverUrl = httpServer.urlPrefix('human-readable'); - const url = new URL(serverUrl); - url.pathname = '/browser-channel'; - url.searchParams.set('socketPath', socketPath); - connectionInfo.connection = new DevToolsConnection(context, url, connectionInfo.browserDescriptor.browser.launchOptions.cdpPort); - return connectionInfo.connection; + const playwrightLib = url.searchParams.get('playwrightLib')!; + const cdpPort = url.searchParams.get('cdpPort') ? Number(url.searchParams.get('cdpPort')) : undefined; + const controllerUrl = new URL(httpServer.urlPrefix('human-readable')); + controllerUrl.pathname = '/browser-channel'; + controllerUrl.searchParams.set('socketPath', socketPath); + const connection = new DevToolsConnection(socketPath, playwrightLib, controllerUrl, cdpPort, () => socketPathToDevToolsConnection.delete(socketPath)); + socketPathToDevToolsConnection.set(socketPath, connection); + return connection; } throw new Error('Unsupported URL: ' + url.toString()); }, 'browser-channel'); diff --git a/packages/playwright-core/src/devtools/devtoolsController.ts b/packages/playwright-core/src/devtools/devtoolsController.ts index 9a05c7224e18a..8a124e5cda400 100644 --- a/packages/playwright-core/src/devtools/devtoolsController.ts +++ b/packages/playwright-core/src/devtools/devtoolsController.ts @@ -14,13 +14,18 @@ * limitations under the License. */ +import { pathToFileURL } from 'url'; + import { eventsHelper } from '../client/eventEmitter'; import type * as api from '../../types/types'; import type { Transport } from '../server/utils/httpServer'; import type { DevToolsChannel, DevToolsChannelEvents, Tab } from '@devtools/devtoolsChannel'; -const pageIdSymbol = Symbol('pageId'); +export async function connectToBrowserSocket(socketPath: string, playwrightLib: string): Promise { + const otherPlaywright = (await import(pathToFileURL(playwrightLib).toString())).default as typeof import('../..'); + return await otherPlaywright.chromium.connect(socketPath); +} export class DevToolsConnection implements Transport, DevToolsChannel { readonly version = 1; @@ -34,16 +39,23 @@ export class DevToolsConnection implements Transport, DevToolsChannel { private _pageListeners: { dispose: () => Promise }[] = []; private _contextListeners: { dispose: () => Promise }[] = []; private _eventListeners = new Map>(); - private _context: api.BrowserContext; + + private _socketPath: string; + private _playwrightLib: string; private _controllerUrl: URL; private _browserCdpPort?: number; + private _onclose: () => void; - private _nextPageId = 1; + private _initPromise?: Promise; + private _context!: api.BrowserContext; + private _browser?: api.Browser; - constructor(context: api.BrowserContext, controllerUrl: URL, cdpPort?: number) { - this._context = context; + constructor(socketPath: string, playwrightLib: string, controllerUrl: URL, cdpPort: number | undefined, onclose: () => void) { + this._socketPath = socketPath; + this._playwrightLib = playwrightLib; this._controllerUrl = controllerUrl; this._browserCdpPort = cdpPort; + this._onclose = onclose; } on(event: K, listener: (params: DevToolsChannelEvents[K]) => void): void { @@ -69,10 +81,16 @@ export class DevToolsConnection implements Transport, DevToolsChannel { } onconnect() { - const context = this._context; + this._initPromise = this._init(); + this._initPromise.catch(() => this.close?.()); + } + + private async _init() { + this._browser = await connectToBrowserSocket(this._socketPath, this._playwrightLib); + this._context = this._browser.contexts()[0]; this._contextListeners.push( - eventsHelper.addEventListener(context, 'page', page => { + eventsHelper.addEventListener(this._context, 'page', page => { this._sendTabList(); if (!this.selectedPage) this._selectPage(page); @@ -80,7 +98,7 @@ export class DevToolsConnection implements Transport, DevToolsChannel { ); // Auto-select first page. - const pages = context.pages(); + const pages = this._context.pages(); if (pages.length > 0) this._selectPage(pages[0]); @@ -91,9 +109,11 @@ export class DevToolsConnection implements Transport, DevToolsChannel { this._deselectPage(); this._contextListeners.forEach(d => d.dispose()); this._contextListeners = []; + this._onclose(); } async dispatch(method: string, params: any): Promise { + await this._initPromise; return (this as any)[method]?.(params); } @@ -240,14 +260,11 @@ export class DevToolsConnection implements Transport, DevToolsChannel { } pageForId(pageId: string) { - return this._context.pages().find(p => this._pageId(p) === pageId); + return this._context?.pages().find(p => this._pageId(p) === pageId); } private _pageId(p: api.Page): string { - const page = p as any; - if (!page[pageIdSymbol]) - page[pageIdSymbol] = 'page_' + this._nextPageId++; - return page[pageIdSymbol]; + return (p as any)._guid; } private async _devtoolsUrl(page: api.Page) { From b972a4b54524d6151f44aa3ca8cb0ce7fc756a25 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 9 Mar 2026 16:34:48 -0700 Subject: [PATCH 07/10] use guid for ws --- .../playwright-core/src/devtools/devtoolsApp.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/playwright-core/src/devtools/devtoolsApp.ts b/packages/playwright-core/src/devtools/devtoolsApp.ts index cae53c2546eb7..4dd1c64e90391 100644 --- a/packages/playwright-core/src/devtools/devtoolsApp.ts +++ b/packages/playwright-core/src/devtools/devtoolsApp.ts @@ -62,12 +62,12 @@ function sendJSON(response: http.ServerResponse, data: any, statusCode = 200) { response.end(JSON.stringify(data)); } -async function loadBrowserDescriptorSessions(): Promise { +async function loadBrowserDescriptorSessions(wsPath: string): Promise { const servers = await serverRegistry.list(); const sessions: SessionStatus[] = []; for (const [, browsers] of servers) { for (const browser of browsers) { - const wsUrl = new URL('/browser-channel', 'http://placeholder'); + const wsUrl = new URL(wsPath, 'http://localhost'); wsUrl.searchParams.set('socketPath', browser.pipeName!); wsUrl.searchParams.set('playwrightLib', browser.playwrightLib); if (browser.browser.launchOptions.cdpPort) @@ -85,12 +85,12 @@ async function loadBrowserDescriptorSessions(): Promise { const socketPathToDevToolsConnection = new Map(); -async function handleApiRequest(request: http.IncomingMessage, response: http.ServerResponse) { - const url = new URL(request.url!, `http://${request.headers.host}`); +async function handleApiRequest(httpServer: HttpServer, request: http.IncomingMessage, response: http.ServerResponse) { + const url = new URL(request.url!, httpServer.urlPrefix('human-readable')); const apiPath = url.pathname; if (apiPath === '/api/sessions/list' && request.method === 'GET') { - const sessions = await loadBrowserDescriptorSessions(); + const sessions = await loadBrowserDescriptorSessions(httpServer.wsGuid()!); sendJSON(response, { sessions }); return; } @@ -130,7 +130,7 @@ async function openDevToolsApp(): Promise { const devtoolsDir = path.join(path.dirname(libDir), 'lib/vite/devtools'); httpServer.routePrefix('/api/', (request: http.IncomingMessage, response: http.ServerResponse) => { - handleApiRequest(request, response).catch(e => { + handleApiRequest(httpServer, request, response).catch(e => { response.statusCode = 500; response.end(JSON.stringify({ error: e.message })); }); @@ -153,14 +153,14 @@ async function openDevToolsApp(): Promise { const playwrightLib = url.searchParams.get('playwrightLib')!; const cdpPort = url.searchParams.get('cdpPort') ? Number(url.searchParams.get('cdpPort')) : undefined; const controllerUrl = new URL(httpServer.urlPrefix('human-readable')); - controllerUrl.pathname = '/browser-channel'; + controllerUrl.pathname = httpServer.wsGuid()!; controllerUrl.searchParams.set('socketPath', socketPath); const connection = new DevToolsConnection(socketPath, playwrightLib, controllerUrl, cdpPort, () => socketPathToDevToolsConnection.delete(socketPath)); socketPathToDevToolsConnection.set(socketPath, connection); return connection; } throw new Error('Unsupported URL: ' + url.toString()); - }, 'browser-channel'); + }); httpServer.routePrefix('/', (request: http.IncomingMessage, response: http.ServerResponse) => { const pathname = new URL(request.url!, `http://${request.headers.host}`).pathname; From 423cf5e5db859edba35dbc5cc2cf9a95a472aefe Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 9 Mar 2026 16:48:18 -0700 Subject: [PATCH 08/10] switch to browser descriptor --- .../src/devtools/devtoolsApp.ts | 33 +++++++++---------- .../src/devtools/devtoolsController.ts | 18 +++++----- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/playwright-core/src/devtools/devtoolsApp.ts b/packages/playwright-core/src/devtools/devtoolsApp.ts index 4dd1c64e90391..05e528108f093 100644 --- a/packages/playwright-core/src/devtools/devtoolsApp.ts +++ b/packages/playwright-core/src/devtools/devtoolsApp.ts @@ -68,10 +68,7 @@ async function loadBrowserDescriptorSessions(wsPath: string): Promise { }); httpServer.createWebSocket(url => { - const socketPath = url.searchParams.get('socketPath'); + const descriptorJson = url.searchParams.get('browserDescriptor'); + if (!descriptorJson) + throw new Error('Unsupported WebSocket URL: ' + url.toString()); + const browserDescriptor = JSON.parse(descriptorJson) as BrowserDescriptor; + const cdpPageId = url.searchParams.get('cdpPageId'); - if (cdpPageId && socketPath) { + if (cdpPageId) { + const socketPath = browserDescriptor.pipeName!; const connection = socketPathToDevToolsConnection.get(socketPath); if (!connection) throw new Error('CDP connection not found for socket path: ' + socketPath); @@ -149,17 +151,14 @@ async function openDevToolsApp(): Promise { throw new Error('Page not found for page ID: ' + cdpPageId); return new CDPConnection(page); } - if (socketPath) { - const playwrightLib = url.searchParams.get('playwrightLib')!; - const cdpPort = url.searchParams.get('cdpPort') ? Number(url.searchParams.get('cdpPort')) : undefined; - const controllerUrl = new URL(httpServer.urlPrefix('human-readable')); - controllerUrl.pathname = httpServer.wsGuid()!; - controllerUrl.searchParams.set('socketPath', socketPath); - const connection = new DevToolsConnection(socketPath, playwrightLib, controllerUrl, cdpPort, () => socketPathToDevToolsConnection.delete(socketPath)); - socketPathToDevToolsConnection.set(socketPath, connection); - return connection; - } - throw new Error('Unsupported URL: ' + url.toString()); + + const socketPath = browserDescriptor.pipeName!; + const cdpUrl = new URL(httpServer.urlPrefix('human-readable')); + cdpUrl.pathname = httpServer.wsGuid()!; + cdpUrl.searchParams.set('browserDescriptor', descriptorJson); + const connection = new DevToolsConnection(browserDescriptor, cdpUrl, () => socketPathToDevToolsConnection.delete(socketPath)); + socketPathToDevToolsConnection.set(socketPath, connection); + return connection; }); httpServer.routePrefix('/', (request: http.IncomingMessage, response: http.ServerResponse) => { diff --git a/packages/playwright-core/src/devtools/devtoolsController.ts b/packages/playwright-core/src/devtools/devtoolsController.ts index 8a124e5cda400..39fae4be2deaf 100644 --- a/packages/playwright-core/src/devtools/devtoolsController.ts +++ b/packages/playwright-core/src/devtools/devtoolsController.ts @@ -21,6 +21,7 @@ import { eventsHelper } from '../client/eventEmitter'; import type * as api from '../../types/types'; import type { Transport } from '../server/utils/httpServer'; import type { DevToolsChannel, DevToolsChannelEvents, Tab } from '@devtools/devtoolsChannel'; +import type { BrowserDescriptor } from '../serverRegistry'; export async function connectToBrowserSocket(socketPath: string, playwrightLib: string): Promise { const otherPlaywright = (await import(pathToFileURL(playwrightLib).toString())).default as typeof import('../..'); @@ -40,21 +41,17 @@ export class DevToolsConnection implements Transport, DevToolsChannel { private _contextListeners: { dispose: () => Promise }[] = []; private _eventListeners = new Map>(); - private _socketPath: string; - private _playwrightLib: string; + private _browserDescriptor: BrowserDescriptor; private _controllerUrl: URL; - private _browserCdpPort?: number; private _onclose: () => void; private _initPromise?: Promise; private _context!: api.BrowserContext; private _browser?: api.Browser; - constructor(socketPath: string, playwrightLib: string, controllerUrl: URL, cdpPort: number | undefined, onclose: () => void) { - this._socketPath = socketPath; - this._playwrightLib = playwrightLib; + constructor(browserDescriptor: BrowserDescriptor, controllerUrl: URL, onclose: () => void) { + this._browserDescriptor = browserDescriptor; this._controllerUrl = controllerUrl; - this._browserCdpPort = cdpPort; this._onclose = onclose; } @@ -86,7 +83,7 @@ export class DevToolsConnection implements Transport, DevToolsChannel { } private async _init() { - this._browser = await connectToBrowserSocket(this._socketPath, this._playwrightLib); + this._browser = await connectToBrowserSocket(this._browserDescriptor.pipeName!, this._browserDescriptor.playwrightLib); this._context = this._browser.contexts()[0]; this._contextListeners.push( @@ -268,8 +265,9 @@ export class DevToolsConnection implements Transport, DevToolsChannel { } private async _devtoolsUrl(page: api.Page) { - if (this._browserCdpPort) - return new URL(`http://localhost:${this._browserCdpPort}/devtools/`); + const cdpPort = this._browserDescriptor.browser.launchOptions.cdpPort; + if (cdpPort) + return new URL(`http://localhost:${cdpPort}/devtools/`); const browserRevision = await getBrowserRevision(page); if (!browserRevision) From b00e1b6f0f3f188b60e4a53660257b65c285bcaa Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 9 Mar 2026 16:51:11 -0700 Subject: [PATCH 09/10] use across versions --- packages/playwright-core/src/devtools/DEPS.list | 3 +-- .../playwright-core/src/devtools/devtoolsApp.ts | 8 ++++---- .../src/devtools/devtoolsController.ts | 16 ++++++---------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/playwright-core/src/devtools/DEPS.list b/packages/playwright-core/src/devtools/DEPS.list index fdd7654d146b1..5ef7c7d71c32a 100644 --- a/packages/playwright-core/src/devtools/DEPS.list +++ b/packages/playwright-core/src/devtools/DEPS.list @@ -4,6 +4,5 @@ ../server/utils/ ../serverRegistry.ts ../utils/ - -[devtoolsController.ts] +../client/connect ../client/eventEmitter.ts diff --git a/packages/playwright-core/src/devtools/devtoolsApp.ts b/packages/playwright-core/src/devtools/devtoolsApp.ts index 05e528108f093..25805f30677a8 100644 --- a/packages/playwright-core/src/devtools/devtoolsApp.ts +++ b/packages/playwright-core/src/devtools/devtoolsApp.ts @@ -19,19 +19,19 @@ import path from 'path'; import os from 'os'; import net from 'net'; import http from 'http'; -import { pathToFileURL } from 'url'; import { chromium } from '../..'; import { HttpServer } from '../server/utils/httpServer'; import { gracefullyProcessExitDoNotHang } from '../server/utils/processLauncher'; import { findChromiumChannelBestEffort, registryDirectory } from '../server/registry/index'; -import { calculateSha1, createGuid } from '../utils'; -import { CDPConnection, connectToBrowserSocket, DevToolsConnection } from './devtoolsController'; +import { calculateSha1 } from '../utils'; +import { CDPConnection, DevToolsConnection } from './devtoolsController'; import { serverRegistry } from '../serverRegistry'; import type * as api from '../..'; import type { SessionStatus } from '@devtools/sessionModel'; import type { BrowserDescriptor } from '../serverRegistry'; +import { connectToBrowserAcrossVersions } from '../client/connect'; function readBody(request: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { @@ -96,7 +96,7 @@ async function handleApiRequest(httpServer: HttpServer, request: http.IncomingMe const { browserDescriptor } = await parseRequest(request); let browser: api.Browser; try { - browser = await connectToBrowserSocket(browserDescriptor.pipeName!, browserDescriptor.playwrightLib); + browser = await connectToBrowserAcrossVersions(browserDescriptor); } catch (e) { sendJSON(response, { error: 'Failed to connect to browser socket: ' + e.message }, 500); return; diff --git a/packages/playwright-core/src/devtools/devtoolsController.ts b/packages/playwright-core/src/devtools/devtoolsController.ts index 39fae4be2deaf..6bcc89698413f 100644 --- a/packages/playwright-core/src/devtools/devtoolsController.ts +++ b/packages/playwright-core/src/devtools/devtoolsController.ts @@ -22,11 +22,7 @@ import type * as api from '../../types/types'; import type { Transport } from '../server/utils/httpServer'; import type { DevToolsChannel, DevToolsChannelEvents, Tab } from '@devtools/devtoolsChannel'; import type { BrowserDescriptor } from '../serverRegistry'; - -export async function connectToBrowserSocket(socketPath: string, playwrightLib: string): Promise { - const otherPlaywright = (await import(pathToFileURL(playwrightLib).toString())).default as typeof import('../..'); - return await otherPlaywright.chromium.connect(socketPath); -} +import { connectToBrowserAcrossVersions } from '../client/connect'; export class DevToolsConnection implements Transport, DevToolsChannel { readonly version = 1; @@ -42,16 +38,16 @@ export class DevToolsConnection implements Transport, DevToolsChannel { private _eventListeners = new Map>(); private _browserDescriptor: BrowserDescriptor; - private _controllerUrl: URL; + private _cdpUrl: URL; private _onclose: () => void; private _initPromise?: Promise; private _context!: api.BrowserContext; private _browser?: api.Browser; - constructor(browserDescriptor: BrowserDescriptor, controllerUrl: URL, onclose: () => void) { + constructor(browserDescriptor: BrowserDescriptor, cdpUrl: URL, onclose: () => void) { this._browserDescriptor = browserDescriptor; - this._controllerUrl = controllerUrl; + this._cdpUrl = cdpUrl; this._onclose = onclose; } @@ -83,7 +79,7 @@ export class DevToolsConnection implements Transport, DevToolsChannel { } private async _init() { - this._browser = await connectToBrowserSocket(this._browserDescriptor.pipeName!, this._browserDescriptor.playwrightLib); + this._browser = await connectToBrowserAcrossVersions(this._browserDescriptor); this._context = this._browser.contexts()[0]; this._contextListeners.push( @@ -277,7 +273,7 @@ export class DevToolsConnection implements Transport, DevToolsChannel { private async _pageInspectorUrl(page: api.Page, devtoolsUrl: URL): Promise { const inspector = new URL('./devtools_app.html', devtoolsUrl); - const cdp = new URL(this._controllerUrl); + const cdp = new URL(this._cdpUrl); cdp.searchParams.set('cdpPageId', this._pageId(page)); inspector.searchParams.set('ws', `${cdp.host}${cdp.pathname}${cdp.search}`); const url = inspector.toString(); From acba660585803077643f8d171a06e77fd3720cb1 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 9 Mar 2026 16:59:59 -0700 Subject: [PATCH 10/10] canConnect --- packages/devtools/src/grid.tsx | 27 +++++++------------ packages/devtools/src/sessionModel.ts | 3 +-- .../playwright-core/src/devtools/DEPS.list | 2 +- .../src/devtools/devtoolsApp.ts | 19 +++++++------ .../src/devtools/devtoolsController.ts | 4 +-- .../playwright-core/src/serverRegistry.ts | 13 +++++---- 6 files changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/devtools/src/grid.tsx b/packages/devtools/src/grid.tsx index b5da7e0dd73e5..2dd8de1601429 100644 --- a/packages/devtools/src/grid.tsx +++ b/packages/devtools/src/grid.tsx @@ -91,7 +91,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => { {isExpanded && (
- {entries.map(session => )} + {entries.map(session => )}
)} @@ -102,14 +102,14 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => { ); }; -const SessionChip: React.FC<{ descriptor: BrowserDescriptor; canConnect: boolean; wsUrl: string | null | undefined; visible: boolean; model: SessionModel }> = ({ descriptor, canConnect, wsUrl, visible, model }) => { +const SessionChip: React.FC<{ descriptor: BrowserDescriptor; wsUrl: string | undefined; visible: boolean; model: SessionModel }> = ({ descriptor, wsUrl, visible, model }) => { const href = '#session=' + encodeURIComponent(descriptor.pipeName!); const channel = React.useMemo(() => { - if (!canConnect || !visible || !wsUrl) + if (!wsUrl || !visible) return undefined; return DevToolsClient.create(wsUrl); - }, [canConnect, visible, wsUrl]); + }, [wsUrl, visible]); const [selectedTab, setSelectedTab] = React.useState(); @@ -128,20 +128,19 @@ const SessionChip: React.FC<{ descriptor: BrowserDescriptor; canConnect: boolean }, [channel]); const chipTitle = selectedTab ? `[${descriptor.title}] ${selectedTab.url} \u2014 ${selectedTab.title}` : descriptor.title; - const clickable = canConnect && wsUrl !== null; return ( - { + { e.preventDefault(); - if (clickable) + if (wsUrl) navigate(href); }}>
-
+
{selectedTab ? <>[{descriptor.title}] {selectedTab.url} — {selectedTab.title} : descriptor.title} - {canConnect && ( + {wsUrl && ( )} - {!canConnect && ( + {!wsUrl && (