From 788333e142418ce7fe6b176657e37df32c8a11c8 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Fri, 14 Nov 2025 16:47:27 +0100 Subject: [PATCH] Revert "simplify code. remove methods" This reverts commit e362d44a7c7b19d6e434b5bafece65c555c1d14f. --- extension-playwright/src/background.ts | 47 +-- extension-playwright/src/relayConnection.ts | 92 +++--- playwriter/src/extension/cdpRelay.ts | 345 ++++++++++---------- 3 files changed, 253 insertions(+), 231 deletions(-) diff --git a/extension-playwright/src/background.ts b/extension-playwright/src/background.ts index cf7fb5a..26adcee 100644 --- a/extension-playwright/src/background.ts +++ b/extension-playwright/src/background.ts @@ -25,12 +25,12 @@ class SimplifiedExtension { constructor() { debugLog(`Using relay URL: ${RELAY_URL}`); - chrome.tabs.onRemoved.addListener(this._onTabRemoved); - chrome.tabs.onActivated.addListener(this._onTabActivated); - chrome.action.onClicked.addListener(this._onActionClicked); + chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this)); + chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this)); + chrome.action.onClicked.addListener(this._onActionClicked.bind(this)); } - private _onActionClicked = async (tab: chrome.tabs.Tab): Promise => { + private async _onActionClicked(tab: chrome.tabs.Tab): Promise { if (!tab.id) { debugLog('No tab ID available'); return; @@ -41,7 +41,23 @@ class SimplifiedExtension { } else { await this._connectTab(tab.id); } - }; + } + + private async _waitForServer(): Promise { + const httpUrl = 'http://localhost:9988'; + + while (true) { + try { + debugLog('Checking if relay server is available...'); + await fetch(httpUrl, { method: 'HEAD' }); + debugLog('Server is available'); + return; + } catch (error: any) { + debugLog(`Server not available, retrying in 1 second...`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } private async _connectTab(tabId: number): Promise { try { @@ -51,18 +67,7 @@ class SimplifiedExtension { if (!this._connection) { debugLog('No existing connection, creating new relay connection'); debugLog('Waiting for server at http://localhost:9988...'); - - // Wait for server to be available - while (true) { - try { - await fetch('http://localhost:9988', { method: 'HEAD' }); - debugLog('Server is available'); - break; - } catch (error: any) { - debugLog('Server not available, retrying in 1 second...'); - await new Promise(resolve => setTimeout(resolve, 1000)); - } - } + await this._waitForServer(); debugLog('Server is ready, creating WebSocket connection to:', RELAY_URL); const socket = new WebSocket(RELAY_URL); @@ -234,19 +239,19 @@ class SimplifiedExtension { } } - private _onTabRemoved = async (tabId: number): Promise => { + private async _onTabRemoved(tabId: number): Promise { debugLog('Tab removed event for tab:', tabId, 'is connected:', this._connectedTabs.has(tabId)); if (!this._connectedTabs.has(tabId)) return; debugLog(`Connected tab ${tabId} was closed, disconnecting`); await this._disconnectTab(tabId); - }; + } - private _onTabActivated = async (activeInfo: chrome.tabs.TabActiveInfo): Promise => { + private async _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo): Promise { const isConnected = this._connectedTabs.has(activeInfo.tabId); debugLog('Tab activated:', activeInfo.tabId, 'is connected:', isConnected); await this._updateIcon(activeInfo.tabId, isConnected ? 'connected' : 'disconnected'); - }; + } } new SimplifiedExtension(); diff --git a/extension-playwright/src/relayConnection.ts b/extension-playwright/src/relayConnection.ts index d3c2afb..e1b20b8 100644 --- a/extension-playwright/src/relayConnection.ts +++ b/extension-playwright/src/relayConnection.ts @@ -41,42 +41,16 @@ export class RelayConnection { private _attachedTabs: Map = new Map(); private _nextSessionId: number = 1; private _ws: WebSocket; + private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void; + private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void; private _closed = false; onclose?: () => void; constructor(ws: WebSocket) { this._ws = ws; - this._ws.onmessage = async (event: MessageEvent) => { - let message: ExtensionCommandMessage; - try { - message = JSON.parse(event.data); - } catch (error: any) { - debugLog('Error parsing message:', error); - this._sendMessage({ - error: { - code: -32700, - message: `Error parsing message: ${error.message}`, - }, - }); - return; - } - - debugLog('Received message:', message); - - const response: ExtensionResponseMessage = { - id: message.id, - }; - try { - response.result = await this._handleCommand(message); - } catch (error: any) { - debugLog('Error handling command:', error); - response.error = error.message; - } - debugLog('Sending response:', response); - this._sendMessage(response); - }; - this._ws.onclose = (event: CloseEvent) => { + this._ws.onmessage = this._onMessage.bind(this); + this._ws.onclose = (event) => { debugLog('WebSocket onclose event:', { code: event.code, reason: event.reason, @@ -84,11 +58,13 @@ export class RelayConnection { }); this._onClose(); }; - this._ws.onerror = (event: Event) => { + this._ws.onerror = (event) => { debugLog('WebSocket onerror event:', event); }; - chrome.debugger.onEvent.addListener(this._onDebuggerEvent); - chrome.debugger.onDetach.addListener(this._onDebuggerDetach); + this._eventListener = this._onDebuggerEvent.bind(this); + this._detachListener = this._onDebuggerDetach.bind(this); + chrome.debugger.onEvent.addListener(this._eventListener); + chrome.debugger.onDetach.addListener(this._detachListener); debugLog('RelayConnection created, WebSocket readyState:', this._ws.readyState); } @@ -191,8 +167,8 @@ export class RelayConnection { debugLog('Connection closing, attached tabs count:', this._attachedTabs.size); this._closed = true; - chrome.debugger.onEvent.removeListener(this._onDebuggerEvent); - chrome.debugger.onDetach.removeListener(this._onDebuggerDetach); + chrome.debugger.onEvent.removeListener(this._eventListener); + chrome.debugger.onDetach.removeListener(this._detachListener); const tabIds = Array.from(this._attachedTabs.keys()); debugLog('Detaching all tabs:', tabIds); @@ -215,7 +191,7 @@ export class RelayConnection { this.onclose?.(); } - private _onDebuggerEvent = (source: chrome.debugger.DebuggerSession, method: string, params: any): void => { + private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void { const tab = this._attachedTabs.get(source.tabId!); if (!tab) return; @@ -245,9 +221,9 @@ export class RelayConnection { params, }, }); - }; + } - private _onDebuggerDetach = (source: chrome.debugger.Debuggee, reason: string): void => { + private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void { const tabId = source.tabId; debugLog('_onDebuggerDetach called for tab:', tabId, 'reason:', reason, 'isAttached:', tabId ? this._attachedTabs.has(tabId) : false); @@ -258,7 +234,36 @@ export class RelayConnection { debugLog(`Debugger detached from tab ${tabId}: ${reason}`); this.detachTab(tabId); - }; + } + + private _onMessage(event: MessageEvent): void { + this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e)); + } + + private async _onMessageAsync(event: MessageEvent): Promise { + let message: ExtensionCommandMessage; + try { + message = JSON.parse(event.data); + } catch (error: any) { + debugLog('Error parsing message:', error); + this._sendError(-32700, `Error parsing message: ${error.message}`); + return; + } + + debugLog('Received message:', message); + + const response: ExtensionResponseMessage = { + id: message.id, + }; + try { + response.result = await this._handleCommand(message); + } catch (error: any) { + debugLog('Error handling command:', error); + response.error = error.message; + } + debugLog('Sending response:', response); + this._sendMessage(response); + } private async _handleCommand(message: ExtensionCommandMessage): Promise { if (message.method === 'attachToTab') { @@ -334,6 +339,15 @@ export class RelayConnection { } } + private _sendError(code: number, message: string): void { + this._sendMessage({ + error: { + code, + message, + }, + }); + } + private _sendMessage(message: any): void { if (this._ws.readyState === WebSocket.OPEN) { try { diff --git a/playwriter/src/extension/cdpRelay.ts b/playwriter/src/extension/cdpRelay.ts index d63e8f8..d97b660 100644 --- a/playwriter/src/extension/cdpRelay.ts +++ b/playwriter/src/extension/cdpRelay.ts @@ -66,31 +66,7 @@ export class CDPRelayServer { this._resetExtensionConnection(); this._wss = new wsServer({ server }); - this._wss.on('connection', (ws: WebSocket, request: http.IncomingMessage) => { - const url = new URL(`http://localhost${request.url}`); - debugLogger(`New connection to ${url.pathname}`); - if (url.pathname === this._cdpPath) { - this._handlePlaywrightConnection(ws); - } else if (url.pathname === this._extensionPath) { - if (this._extensionConnection) { - ws.close(1000, 'Another extension connection already established'); - return; - } - this._extensionConnection = new ExtensionConnection(ws); - this._extensionConnection.onclose = (c, reason) => { - debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection); - if (this._extensionConnection !== c) - return; - this._resetExtensionConnection(); - this._closePlaywrightConnection(`Extension disconnected: ${reason}`); - }; - this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this); - this._extensionConnectionPromise.resolve(); - } else { - debugLogger(`Invalid path: ${url.pathname}`); - ws.close(4004, 'Invalid path'); - } - }); + this._wss.on('connection', this._onConnection.bind(this)); } cdpEndpoint() { @@ -112,7 +88,18 @@ export class CDPRelayServer { this._closeExtensionConnection(reason); } - + private _onConnection(ws: WebSocket, request: http.IncomingMessage): void { + const url = new URL(`http://localhost${request.url}`); + debugLogger(`New connection to ${url.pathname}`); + if (url.pathname === this._cdpPath) { + this._handlePlaywrightConnection(ws); + } else if (url.pathname === this._extensionPath) { + this._handleExtensionConnection(ws); + } else { + debugLogger(`Invalid path: ${url.pathname}`); + ws.close(4004, 'Invalid path'); + } + } private _handlePlaywrightConnection(ws: WebSocket): void { if (this._playwrightConnection) { @@ -161,12 +148,27 @@ export class CDPRelayServer { this._playwrightConnection = null; } - + private _handleExtensionConnection(ws: WebSocket): void { + if (this._extensionConnection) { + ws.close(1000, 'Another extension connection already established'); + return; + } + this._extensionConnection = new ExtensionConnection(ws); + this._extensionConnection.onclose = (c, reason) => { + debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection); + if (this._extensionConnection !== c) + return; + this._resetExtensionConnection(); + this._closePlaywrightConnection(`Extension disconnected: ${reason}`); + }; + this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this); + this._extensionConnectionPromise.resolve(); + } private _handleExtensionMessage(message: ExtensionEventMessage) { if (message.method === 'forwardCDPEvent') { const { method, params, sessionId } = message.params; - + if (method === 'Target.attachedToTarget') { const targetParams = params as Protocol.Target.AttachedToTargetEvent; this._connectedTargets.set(targetParams.sessionId, { @@ -174,25 +176,25 @@ export class CDPRelayServer { targetId: targetParams.targetInfo.targetId, targetInfo: targetParams.targetInfo }); - + debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.attachedToTarget sessionId=${targetParams.sessionId}, targetId=${targetParams.targetInfo.targetId}`); - + this._sendToPlaywright({ method: 'Target.attachedToTarget', params: targetParams } as CDPEvent); - + } else if (method === 'Target.detachedFromTarget') { const detachParams = params as Protocol.Target.DetachedFromTargetEvent; this._connectedTargets.delete(detachParams.sessionId); - + debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.detachedFromTarget sessionId=${detachParams.sessionId}`); - + this._sendToPlaywright({ method: 'Target.detachedFromTarget', params: detachParams } as CDPEvent); - + } else { this._sendToPlaywright({ sessionId, @@ -206,94 +208,8 @@ export class CDPRelayServer { private async _handlePlaywrightMessage(message: CDPCommand): Promise { debugLogger('\x1b[36m← Playwright:\x1b[0m', `${message.method} (id=${message.id})`); const { id, sessionId, method, params } = message; - - const forwardToExtension = async (method: string, params: any, sessionId: string | undefined): Promise => { - if (!this._extensionConnection) - throw new Error('Extension not connected'); - - return await this._extensionConnection.send({ - method: 'forwardCDPCommand', - params: { sessionId, method, params } - }); - }; - - const handleCDPCommand = async (method: string, params: any, sessionId: string | undefined): Promise => { - switch (method) { - case 'Browser.getVersion': { - return { - protocolVersion: '1.3', - product: 'Chrome/Extension-Bridge', - revision: '1.0.0', - userAgent: 'CDP-Bridge-Server/1.0.0', - jsVersion: 'V8', - } satisfies Protocol.Browser.GetVersionResponse; - } - case 'Browser.setDownloadBehavior': { - return { }; - } - case 'Target.setAutoAttach': { - if (sessionId) { - break; - } - - debugLogger('Target.setAutoAttach received (manual attach mode)'); - debugLogger('Sending Target.attachedToTarget events for existing targets:', this._connectedTargets.size); - - for (const target of this._connectedTargets.values()) { - debugLogger('Sending Target.attachedToTarget for sessionId:', target.sessionId, 'targetId:', target.targetId); - this._sendToPlaywright({ - method: 'Target.attachedToTarget', - params: { - sessionId: target.sessionId, - targetInfo: { - ...target.targetInfo, - attached: true - }, - waitingForDebugger: false - } - } satisfies CDPEvent); - } - - return {}; - } - case 'Target.getTargetInfo': { - const targetId = params?.targetId; - - if (targetId) { - for (const target of this._connectedTargets.values()) { - if (target.targetId === targetId) { - return { targetInfo: target.targetInfo }; - } - } - } - - if (sessionId) { - const target = this._connectedTargets.get(sessionId); - if (target) { - return { targetInfo: target.targetInfo }; - } - } - - const firstTarget = this._connectedTargets.values().next().value; - return { targetInfo: firstTarget?.targetInfo }; - } - case 'Target.getTargets': { - return { - targetInfos: Array.from(this._connectedTargets.values()).map(t => ({ - ...t.targetInfo, - attached: true, - })) - }; - } - case 'Target.closeTarget': { - break; - } - } - return await forwardToExtension(method, params, sessionId); - }; - try { - const result = await handleCDPCommand(method, params, sessionId); + const result = await this._handleCDPCommand(method, params, sessionId); this._sendToPlaywright({ id, sessionId, result }); } catch (e) { debugLogger('\x1b[31mError in the extension:\x1b[0m', e); @@ -305,9 +221,94 @@ export class CDPRelayServer { } } + private async _handleCDPCommand(method: string, params: any, sessionId: string | undefined): Promise { + switch (method) { + case 'Browser.getVersion': { + return { + protocolVersion: '1.3', + product: 'Chrome/Extension-Bridge', + revision: '1.0.0', + userAgent: 'CDP-Bridge-Server/1.0.0', + jsVersion: 'V8', + } satisfies Protocol.Browser.GetVersionResponse; + } + case 'Browser.setDownloadBehavior': { + return { }; + } + case 'Target.setAutoAttach': { + if (sessionId) { + break; + } + + debugLogger('Target.setAutoAttach received (manual attach mode)'); + debugLogger('Sending Target.attachedToTarget events for existing targets:', this._connectedTargets.size); + + for (const target of this._connectedTargets.values()) { + debugLogger('Sending Target.attachedToTarget for sessionId:', target.sessionId, 'targetId:', target.targetId); + this._sendToPlaywright({ + method: 'Target.attachedToTarget', + params: { + sessionId: target.sessionId, + targetInfo: { + ...target.targetInfo, + attached: true + }, + waitingForDebugger: false + } + } as CDPEvent); + } + + return {}; + } + case 'Target.getTargetInfo': { + const targetId = params?.targetId; + + if (targetId) { + for (const target of this._connectedTargets.values()) { + if (target.targetId === targetId) { + return { targetInfo: target.targetInfo }; + } + } + } + + if (sessionId) { + const target = this._connectedTargets.get(sessionId); + if (target) { + return { targetInfo: target.targetInfo }; + } + } + + const firstTarget = this._connectedTargets.values().next().value; + return { targetInfo: firstTarget?.targetInfo }; + } + case 'Target.getTargets': { + return { + targetInfos: Array.from(this._connectedTargets.values()).map(t => ({ + ...t.targetInfo, + attached: true, + })) + }; + } + case 'Target.closeTarget': { + break; + } + } + return await this._forwardToExtension(method, params, sessionId); + } + + private async _forwardToExtension(method: string, params: any, sessionId: string | undefined): Promise { + if (!this._extensionConnection) + throw new Error('Extension not connected'); + + return await this._extensionConnection.send({ + method: 'forwardCDPCommand', + params: { sessionId, method, params } + }); + } + private _sendToPlaywright(message: CDPResponse | CDPEvent): void { - const logMessage = 'method' in message && message.method - ? message.method + const logMessage = 'method' in message && message.method + ? message.method : `response(id=${'id' in message ? message.id : 'unknown'})`; debugLogger('\x1b[32m→ Playwright:\x1b[0m', logMessage); this._playwrightConnection?.send(JSON.stringify(message)); @@ -324,54 +325,9 @@ class ExtensionConnection { constructor(ws: WebSocket) { this._ws = ws; - - this._ws.on('message', (event: websocket.RawData) => { - const eventData = event.toString(); - let parsedJson; - try { - parsedJson = JSON.parse(eventData); - } catch (e: any) { - debugLogger(` Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`); - this._ws.close(); - return; - } - - const handleParsedMessage = (object: ExtensionMessage) => { - if ('id' in object && this._callbacks.has(object.id)) { - const callback = this._callbacks.get(object.id)!; - this._callbacks.delete(object.id); - if (object.error) { - const error = callback.error; - error.message = object.error; - callback.reject(error); - } else { - callback.resolve(object.result); - } - } else if ('id' in object) { - debugLogger('← Extension: unexpected response', object); - } else { - this.onmessage?.(object as ExtensionEventMessage); - } - }; - - try { - handleParsedMessage(parsedJson); - } catch (e: any) { - debugLogger(` Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`); - this._ws.close(); - } - }); - - this._ws.on('close', (event: websocket.CloseEvent) => { - debugLogger(` code=${event.code} reason=${event.reason}`); - this._dispose(); - this.onclose?.(this, event.reason); - }); - - this._ws.on('error', (event: websocket.ErrorEvent) => { - debugLogger(` message=${event.message} type=${event.type} target=${event.target}`); - this._dispose(); - }); + this._ws.on('message', this._onMessage.bind(this)); + this._ws.on('close', this._onClose.bind(this)); + this._ws.on('error', this._onError.bind(this)); } async send(command: Omit): Promise { @@ -391,6 +347,53 @@ class ExtensionConnection { this._ws.close(1000, message); } + private _onMessage(event: websocket.RawData) { + const eventData = event.toString(); + let parsedJson; + try { + parsedJson = JSON.parse(eventData); + } catch (e: any) { + debugLogger(` Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`); + this._ws.close(); + return; + } + try { + this._handleParsedMessage(parsedJson); + } catch (e: any) { + debugLogger(` Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`); + this._ws.close(); + } + } + + private _handleParsedMessage(object: ExtensionMessage) { + if ('id' in object && this._callbacks.has(object.id)) { + const callback = this._callbacks.get(object.id)!; + this._callbacks.delete(object.id); + if (object.error) { + const error = callback.error; + error.message = object.error; + callback.reject(error); + } else { + callback.resolve(object.result); + } + } else if ('id' in object) { + debugLogger('← Extension: unexpected response', object); + } else { + this.onmessage?.(object as ExtensionEventMessage); + } + } + + private _onClose(event: websocket.CloseEvent) { + debugLogger(` code=${event.code} reason=${event.reason}`); + this._dispose(); + this.onclose?.(this, event.reason); + } + + private _onError(event: websocket.ErrorEvent) { + debugLogger(` message=${event.message} type=${event.type} target=${event.target}`); + this._dispose(); + } + private _dispose() { for (const callback of this._callbacks.values()) callback.reject(new Error('WebSocket closed'));