@rushstack/playwright-browser-tunnel 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/playwright-browser-tunnel._phase_build.chunks.jsonl +3 -3
- package/.rush/temp/operation/_phase_build/all.log +3 -3
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +3 -3
- package/.rush/temp/operation/_phase_build/state.json +1 -1
- package/.rush/temp/{rushstack+playwright-browser-tunnel-_phase_build-dfbca9926e3d1813fecbd371ea641326bf46c00a.tar.log → rushstack+playwright-browser-tunnel-_phase_build-3f67426e34dc3685ce06e290853fe1361c9ed401.tar.log} +2 -2
- package/CHANGELOG.json +17 -0
- package/CHANGELOG.md +8 -1
- package/lib/PlaywrightBrowserTunnel.d.ts.map +1 -1
- package/lib/PlaywrightBrowserTunnel.js +54 -15
- package/lib/PlaywrightBrowserTunnel.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/lib/tunneledBrowserConnection.d.ts.map +1 -1
- package/lib/tunneledBrowserConnection.js +25 -7
- package/lib/tunneledBrowserConnection.js.map +1 -1
- package/lib/utilities.d.ts +49 -0
- package/lib/utilities.d.ts.map +1 -1
- package/lib/utilities.js +63 -1
- package/lib/utilities.js.map +1 -1
- package/package.json +4 -4
- package/rush-logs/playwright-browser-tunnel._phase_build.cache.log +1 -1
- package/rush-logs/playwright-browser-tunnel._phase_build.log +3 -3
- package/src/PlaywrightBrowserTunnel.ts +76 -16
- package/src/tunneledBrowserConnection.ts +38 -7
- package/src/utilities.ts +93 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +3 -3
- package/temp/build/lint/lint.sarif +8 -8
- package/temp/build/typescript/ts_l9Fw4VUO.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunneledBrowserConnection.js","sourceRoot":"","sources":["../src/tunneledBrowserConnection.ts"],"names":[],"mappings":";AAAA,4FAA4F;AAC5F,2DAA2D;;;;;AAqD3D,8DA+KC;AAsBD,gEAgCC;AAxRD,sEAAyC;AAEzC,2BAA8D;AAC9D,gFAAiE;AAEjE,kDAAwF;AAGxF,6CAA0C;AAE1C,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,GAAG,sBAAqB,CAAC;AAE7D,MAAM,uBAAuB,GAAgB,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;AAaxF,MAAM,mBAAmB,GAAW,KAAK,CAAC;AAsB1C;;;GAGG;AACI,KAAK,UAAU,yBAAyB,CAC7C,MAAiB,EACjB,OAAe,mBAAmB;IAElC,4DAA4D;IAC5D,MAAM,cAAc,GAAoB,IAAI,oBAAe,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACtE,0FAA0F;IAC1F,MAAM,UAAU,GAAe,IAAI,uBAAU,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,UAAU,CAAC,WAAW,EAAE,CAAC;IAC/B,MAAM,CAAC,SAAS,CAAC,uDAAuD,IAAI,EAAE,CAAC,CAAC;IAEhF,MAAM,YAAY,GAAoB,UAAU,CAAC,QAAQ,CAAC;IAC1D,MAAM,oBAAoB,GAAW,UAAU,CAAC,QAAQ,CAAC;IAEzD,IAAI,WAAoC,CAAC;IACzC,IAAI,aAAwC,CAAC;IAC7C,IAAI,YAAmC,CAAC;IACxC,IAAI,YAAY,GAAY,KAAK,CAAC;IAClC,IAAI,aAAa,GAAY,KAAK,CAAC;IAEnC,SAAS,kBAAkB;QACzB,IAAI,CAAC,aAAa,IAAI,YAAY,IAAI,WAAW,IAAI,aAAa,EAAE,CAAC;YACnE,MAAM,SAAS,GAAe;gBAC5B,MAAM,EAAE,WAAW;gBACnB,WAAW;gBACX,aAAa;gBACb,iBAAiB;aAClB,CAAC;YACF,wFAAwF;YACxF,6DAA6D;YAC7D,MAAM,EAAE,QAAQ,EAAE,GAAG,UAAU,EAAE,GAAG,aAAa,CAAC;YAClD,MAAM,YAAY,GAEd;gBACF,GAAG,SAAS;gBACZ,aAAa,EAAE,UAAU;aAC1B,CAAC;YACF,MAAM,CAAC,SAAS,CAAC,gCAAgC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YACjF,aAAa,GAAG,IAAI,CAAC;YACrB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,OAAO,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QACnC,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACnC,MAAM,CAAC,cAAc,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;QAEH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAC9B,MAAM,CAAC,SAAS,CAAC,gCAAgC,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;QAEH,MAAM,qBAAqB,GAAmB,EAAE,CAAC;QAEjD,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,EAAE,EAAE,EAAE;YACrC,MAAM,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;YAC/C,YAAY,GAAG,EAAE,CAAC;YAClB,YAAY,GAAG,KAAK,CAAC;YACrB,kBAAkB,EAAE,CAAC;YAErB,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;gBAC3B,IAAI,CAAC,YAAY,EAAE,CAAC;oBAClB,IAAI,CAAC;wBACH,MAAM,iBAAiB,GAAkB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;wBACxE,IAAI,iBAAiB,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;4BAChD,YAAY,GAAG,IAAI,CAAC;4BACpB,MAAM,CAAC,SAAS,CAAC,mCAAmC,CAAC,CAAC;wBACxD,CAAC;6BAAM,CAAC;4BACN,MAAM,CAAC,cAAc,CAAC,+BAA+B,CAAC,CAAC;4BACvD,EAAE,CAAC,KAAK,EAAE,CAAC;4BACX,OAAO;wBACT,CAAC;oBACH,CAAC;oBAAC,OAAO,CAAC,EAAE,CAAC;wBACX,MAAM,CAAC,cAAc,CAAC,iCAAiC,CAAC,EAAE,CAAC,CAAC;wBAC5D,EAAE,CAAC,KAAK,EAAE,CAAC;wBACX,OAAO;oBACT,CAAC;oBACD,qEAAqE;oBACrE,IAAI,YAAY,EAAE,CAAC;wBACjB,8DAA8D;wBAC9D,MAAM,YAAY,GAA0B,YAAY,CAAC;wBACzD,IAAI,YAAY,IAAI,YAAY,CAAC,UAAU,KAAK,cAAS,CAAC,IAAI,EAAE,CAAC;4BAC/D,OAAO,qBAAqB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gCACxC,MAAM,CAAC,GAAyD,qBAAqB,CAAC,KAAK,EAAE,CAAC;gCAC9F,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;oCACpB,MAAM,CAAC,SAAS,CAAC,8CAA8C,CAAC,EAAE,CAAC,CAAC;oCACpE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gCACvB,CAAC;4BACH,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,2CAA2C;oBAC3C,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;wBACtC,IAAI,MAAM,CAAC,UAAU,KAAK,cAAS,CAAC,IAAI,EAAE,CAAC;4BACzC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;wBACvB,CAAC;oBACH,CAAC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAC,CAAC;YAClE,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC,CAAC;QACnF,CAAC,CAAC,CAAC;QAEH,YAAY,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;YACjD,IAAI,CAAC;gBACH,MAAM,SAAS,GAAuB,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,CAAC;gBACnD,IAAI,SAAS,EAAE,CAAC;oBACd,MAAM,MAAM,GAAQ,IAAI,GAAG,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;oBAC3D,MAAM,CAAC,SAAS,CAAC,6CAA6C,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;oBAChG,MAAM,KAAK,GAAkB,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;oBAChE,IAAI,KAAK,IAAI,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;wBAChD,WAAW,GAAG,KAAoB,CAAC;oBACrC,CAAC;oBACD,MAAM,kBAAkB,GAAkB,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;oBACnF,IAAI,kBAAkB,EAAE,CAAC;wBACvB,IAAI,CAAC;4BACH,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;wBACjD,CAAC;wBAAC,OAAO,CAAC,EAAE,CAAC;4BACX,MAAM,CAAC,cAAc,CAAC,qCAAqC,CAAC,CAAC;wBAC/D,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,CAAC,cAAc,CAAC,gDAAgD,CAAC,EAAE,CAAC,CAAC;YAC7E,CAAC;YAED,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,uBAAuB,GAAW,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACtF,MAAM,CAAC,cAAc,CAAC,iCAAiC,uBAAuB,GAAG,CAAC,CAAC;gBACnF,OAAO,CAAC,KAAK,EAAE,CAAC;gBAChB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,aAAa,GAAG,EAAmB,CAAC,CAAC,gCAAgC;YACvE,CAAC;YAED,kBAAkB,EAAE,CAAC;YAErB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;gBAChC,IAAI,YAAY,IAAI,CAAA,YAAY,aAAZ,YAAY,uBAAZ,YAAY,CAAE,UAAU,MAAK,cAAS,CAAC,IAAI,EAAE,CAAC;oBAChE,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC7B,CAAC;qBAAM,CAAC;oBACN,oFAAoF;oBACpF,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACtC,CAAC;YACH,CAAC,CAAC,CAAC;YACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC,CAAC;YAC7E,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,iCAAiC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC9F,CAAC,CAAC,CAAC;QAEH,4GAA4G;QAC5G,OAAO,CAAC;YACN,cAAc,EAAE,oBAAoB;YACpC,CAAC,MAAM,CAAC,OAAO,CAAC;gBACd,IAAI,CAAC;oBACH,cAAc,CAAC,KAAK,EAAE,CAAC;gBACzB,CAAC;gBAAC,WAAM,CAAC;oBACP,wDAAwD;gBAC1D,CAAC;gBACD,IAAI,CAAC;oBACH,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/B,CAAC;gBAAC,WAAM,CAAC;oBACP,sDAAsD;gBACxD,CAAC;YACH,CAAC;YACD,+CAA+C;YAC/C,YAAY,EAAE,IAAI,OAAO,CAAO,CAAC,QAAQ,EAAE,EAAE;gBAC3C,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;oBAChC,QAAQ,EAAE,CAAC;gBACb,CAAC,CAAC,CAAC;YACL,CAAC,CAAC;SACH,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAkBD;;;GAGG;AACI,KAAK,UAAU,0BAA0B,CAC9C,WAAwB,EACxB,aAA4B,EAC5B,MAAkB,EAClB,OAAe,mBAAmB;IAElC,gGAAgG;IAEhG,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,gBAAgB,GAA4B,IAAI,kCAAuB,EAAE,CAAC;QAChF,MAAM,GAAG,IAAI,mBAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC1C,CAAC;IAED,MAAM,UAAU,GAAyC,MAAM,yBAAyB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACvG,MAAM,EAAE,cAAc,EAAE,GAAG,UAAU,CAAC;IACtC,oDAAoD;IACpD,MAAM,MAAM,GAAQ,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC;IAC5C,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAChD,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC;IAC9E,MAAM,eAAe,GAAW,MAAM,CAAC,QAAQ,EAAE,CAAC;IAClD,MAAM,OAAO,GAAY,MAAM,yBAAU,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAChF,MAAM,CAAC,SAAS,CAAC,kCAAkC,eAAe,EAAE,CAAC,CAAC;IAEtE,OAAO;QACL,OAAO;QACP,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;YACzB,MAAM,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;YACtC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACtB,wDAAwD;YACxD,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n\nimport playwright from 'playwright-core';\nimport type { Browser, LaunchOptions } from 'playwright-core';\nimport { WebSocketServer, WebSocket, type RawData } from 'ws';\nimport playwrightPackageJson from 'playwright-core/package.json';\n\nimport { type ITerminal, Terminal, ConsoleTerminalProvider } from '@rushstack/terminal';\n\nimport type { BrowserName } from './PlaywrightBrowserTunnel';\nimport { HttpServer } from './HttpServer';\n\nconst { version: playwrightVersion } = playwrightPackageJson;\n\nconst SUPPORTED_BROWSER_NAMES: Set<string> = new Set(['chromium', 'firefox', 'webkit']);\n\ninterface IHandshake {\n action: 'handshake';\n browserName: BrowserName;\n launchOptions: LaunchOptions;\n playwrightVersion: string;\n}\n\ninterface IHandshakeAck {\n action: 'handshakeAck';\n}\n\nconst DEFAULT_LISTEN_PORT: number = 56767;\n\n/**\n * Disposable handle returned by {@link tunneledBrowserConnection}.\n * @beta\n */\nexport interface IDisposableTunneledBrowserConnection {\n /**\n * The WebSocket endpoint URL that the local Playwright client should connect to.\n */\n remoteEndpoint: string;\n /**\n * Dispose method that closes the WebSocket servers.\n * Called automatically when using `using` syntax.\n */\n [Symbol.dispose]: () => void;\n /**\n * Promise that resolves when the remote WebSocket server closes.\n */\n closePromise: Promise<void>;\n}\n\n/**\n * Creates a tunneled WebSocket endpoint that a local Playwright client can connect to.\n * @beta\n */\nexport async function tunneledBrowserConnection(\n logger: ITerminal,\n port: number = DEFAULT_LISTEN_PORT\n): Promise<IDisposableTunneledBrowserConnection> {\n // Server that remote peer (actual browser host) connects to\n const remoteWsServer: WebSocketServer = new WebSocketServer({ port });\n // Local HTTP + WebSocket server where the playwright client will connect providing params\n const httpServer: HttpServer = new HttpServer(logger);\n await httpServer.listenAsync();\n logger.writeLine(`Remote WebSocket server listening on ws://localhost:${port}`);\n\n const localProxyWs: WebSocketServer = httpServer.wsServer;\n const localProxyWsEndpoint: string = httpServer.endpoint;\n\n let browserName: BrowserName | undefined;\n let launchOptions: LaunchOptions | undefined;\n let remoteSocket: WebSocket | undefined;\n let handshakeAck: boolean = false;\n let handshakeSent: boolean = false;\n\n function maybeSendHandshake(): void {\n if (!handshakeSent && remoteSocket && browserName && launchOptions) {\n const handshake: IHandshake = {\n action: 'handshake',\n browserName,\n launchOptions,\n playwrightVersion\n };\n // Log handshake without 'headless' to avoid confusion (tunnel enforces headless: false)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { headless, ...logOptions } = launchOptions;\n const logHandshake: Omit<IHandshake, 'launchOptions'> & {\n launchOptions: Omit<LaunchOptions, 'headless'>;\n } = {\n ...handshake,\n launchOptions: logOptions\n };\n logger.writeLine(`Sending handshake to remote: ${JSON.stringify(logHandshake)}`);\n handshakeSent = true;\n remoteSocket.send(JSON.stringify(handshake));\n }\n }\n\n return await new Promise((resolve) => {\n remoteWsServer.on('error', (error) => {\n logger.writeErrorLine(`Remote WebSocket server error: ${error}`);\n });\n\n remoteWsServer.on('close', () => {\n logger.writeLine('Remote WebSocket server closed');\n });\n\n const bufferedLocalMessages: Array<RawData> = [];\n\n remoteWsServer.on('connection', (ws) => {\n logger.writeLine('Remote websocket connected');\n remoteSocket = ws;\n handshakeAck = false;\n maybeSendHandshake();\n\n ws.on('message', (message) => {\n if (!handshakeAck) {\n try {\n const receivedHandshake: IHandshakeAck = JSON.parse(message.toString());\n if (receivedHandshake.action === 'handshakeAck') {\n handshakeAck = true;\n logger.writeLine('Received handshakeAck from remote');\n } else {\n logger.writeErrorLine('Invalid handshake ack message');\n ws.close();\n return;\n }\n } catch (e) {\n logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);\n ws.close();\n return;\n }\n // Resolve only once local proxy available and handshake acknowledged\n if (handshakeAck) {\n // Flush any buffered local messages now that tunnel is active\n const activeRemote: WebSocket | undefined = remoteSocket;\n if (activeRemote && activeRemote.readyState === WebSocket.OPEN) {\n while (bufferedLocalMessages.length > 0) {\n const m: Buffer | ArrayBuffer | Buffer[] | string | undefined = bufferedLocalMessages.shift();\n if (m !== undefined) {\n logger.writeLine(`Flushing buffered local message to remote: ${m}`);\n activeRemote.send(m);\n }\n }\n }\n }\n } else {\n // Forward from remote to all local clients\n localProxyWs.clients.forEach((client) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n });\n }\n });\n\n ws.on('close', () => logger.writeLine('Remote websocket closed'));\n ws.on('error', (err) => logger.writeErrorLine(`Remote websocket error: ${err}`));\n });\n\n localProxyWs.on('connection', (localWs, request) => {\n try {\n const urlString: string | undefined = request?.url;\n if (urlString) {\n const parsed: URL = new URL(urlString, 'http://localhost');\n logger.writeLine(`Local client connected with query params: ${parsed.searchParams.toString()}`);\n const bName: string | null = parsed.searchParams.get('browser');\n if (bName && SUPPORTED_BROWSER_NAMES.has(bName)) {\n browserName = bName as BrowserName;\n }\n const launchOptionsParam: string | null = parsed.searchParams.get('launchOptions');\n if (launchOptionsParam) {\n try {\n launchOptions = JSON.parse(launchOptionsParam);\n } catch (e) {\n logger.writeErrorLine('Invalid launchOptions JSON provided');\n }\n }\n }\n } catch (e) {\n logger.writeErrorLine(`Error parsing local connection query params: ${e}`);\n }\n\n if (!browserName) {\n const supportedBrowsersString: string = Array.from(SUPPORTED_BROWSER_NAMES).join('|');\n logger.writeErrorLine(`browser query param required (${supportedBrowsersString})`);\n localWs.close();\n return;\n }\n if (!launchOptions) {\n launchOptions = {} as LaunchOptions; // default empty if not provided\n }\n\n maybeSendHandshake();\n\n localWs.on('message', (message) => {\n if (handshakeAck && remoteSocket?.readyState === WebSocket.OPEN) {\n remoteSocket.send(message);\n } else {\n // Buffer until handshakeAck to avoid losing early protocol messages from Playwright\n bufferedLocalMessages.push(message);\n }\n });\n localWs.on('close', () => logger.writeLine('Local client websocket closed'));\n localWs.on('error', (err) => logger.writeErrorLine(`Local client websocket error: ${err}`));\n });\n\n // Resolve immediately so caller can initiate local connection with query params (handshake completes later)\n resolve({\n remoteEndpoint: localProxyWsEndpoint,\n [Symbol.dispose]() {\n try {\n remoteWsServer.close();\n } catch {\n // ignore errors during remote WebSocket server shutdown\n }\n try {\n httpServer[Symbol.dispose]();\n } catch {\n // ignore errors during HTTP/WebSocket server shutdown\n }\n },\n // eslint-disable-next-line promise/param-names\n closePromise: new Promise<void>((resolve2) => {\n remoteWsServer.once('close', () => {\n resolve2();\n });\n })\n });\n });\n}\n\n/**\n * Disposable handle returned by {@link createTunneledBrowserAsync}.\n * @beta\n */\nexport interface IDisposableTunneledBrowser {\n /**\n * The connected Playwright Browser instance.\n */\n browser: Browser;\n /**\n * Async dispose method that closes the browser connection.\n * Called automatically when using `await using` syntax.\n */\n [Symbol.asyncDispose]: () => Promise<void>;\n}\n\n/**\n * Creates a Playwright Browser instance connected via a tunneled WebSocket connection.\n * @beta\n */\nexport async function createTunneledBrowserAsync(\n browserName: BrowserName,\n launchOptions: LaunchOptions,\n logger?: ITerminal,\n port: number = DEFAULT_LISTEN_PORT\n): Promise<IDisposableTunneledBrowser> {\n // Establish the tunnel first (remoteEndpoint here refers to local proxy endpoint for connect())\n\n if (!logger) {\n const terminalProvider: ConsoleTerminalProvider = new ConsoleTerminalProvider();\n logger = new Terminal(terminalProvider);\n }\n\n const connection: IDisposableTunneledBrowserConnection = await tunneledBrowserConnection(logger, port);\n const { remoteEndpoint } = connection;\n // Append query params for browser and launchOptions\n const urlObj: URL = new URL(remoteEndpoint);\n urlObj.searchParams.set('browser', browserName);\n urlObj.searchParams.set('launchOptions', JSON.stringify(launchOptions || {}));\n const connectEndpoint: string = urlObj.toString();\n const browser: Browser = await playwright[browserName].connect(connectEndpoint);\n logger.writeLine(`Connected to remote browser at ${connectEndpoint}`);\n\n return {\n browser,\n async [Symbol.asyncDispose]() {\n logger.writeLine('Disposing browser');\n await browser.close();\n // Dispose the tunnel connection after browser is closed\n connection[Symbol.dispose]();\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tunneledBrowserConnection.js","sourceRoot":"","sources":["../src/tunneledBrowserConnection.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n\nimport playwright from 'playwright-core';\nimport type { Browser, LaunchOptions } from 'playwright-core';\nimport { WebSocketServer, WebSocket, type RawData } from 'ws';\nimport playwrightPackageJson from 'playwright-core/package.json';\n\nimport { type ITerminal, Terminal, ConsoleTerminalProvider } from '@rushstack/terminal';\n\nimport type { BrowserName } from './PlaywrightBrowserTunnel';\nimport { HttpServer } from './HttpServer';\nimport {\n getNormalizedErrorString,\n getWebSocketCloseReason,\n getWebSocketReadyStateString,\n WebSocketCloseCode\n} from './utilities';\n\nconst { version: playwrightVersion } = playwrightPackageJson;\n\nconst SUPPORTED_BROWSER_NAMES: Set<string> = new Set(['chromium', 'firefox', 'webkit']);\n\ninterface IHandshake {\n action: 'handshake';\n browserName: BrowserName;\n launchOptions: LaunchOptions;\n playwrightVersion: string;\n}\n\ninterface IHandshakeAck {\n action: 'handshakeAck';\n}\n\nconst DEFAULT_LISTEN_PORT: number = 56767;\n\n/**\n * Disposable handle returned by {@link tunneledBrowserConnection}.\n * @beta\n */\nexport interface IDisposableTunneledBrowserConnection {\n /**\n * The WebSocket endpoint URL that the local Playwright client should connect to.\n */\n remoteEndpoint: string;\n /**\n * Dispose method that closes the WebSocket servers.\n * Called automatically when using `using` syntax.\n */\n [Symbol.dispose]: () => void;\n /**\n * Promise that resolves when the remote WebSocket server closes.\n */\n closePromise: Promise<void>;\n}\n\n/**\n * Creates a tunneled WebSocket endpoint that a local Playwright client can connect to.\n * @beta\n */\nexport async function tunneledBrowserConnection(\n logger: ITerminal,\n port: number = DEFAULT_LISTEN_PORT\n): Promise<IDisposableTunneledBrowserConnection> {\n // Server that remote peer (actual browser host) connects to\n const remoteWsServer: WebSocketServer = new WebSocketServer({ port });\n // Local HTTP + WebSocket server where the playwright client will connect providing params\n const httpServer: HttpServer = new HttpServer(logger);\n await httpServer.listenAsync();\n logger.writeLine(`Remote WebSocket server listening on ws://localhost:${port}`);\n\n const localProxyWs: WebSocketServer = httpServer.wsServer;\n const localProxyWsEndpoint: string = httpServer.endpoint;\n\n let browserName: BrowserName | undefined;\n let launchOptions: LaunchOptions | undefined;\n let remoteSocket: WebSocket | undefined;\n let handshakeAck: boolean = false;\n let handshakeSent: boolean = false;\n\n function maybeSendHandshake(): void {\n if (!handshakeSent && remoteSocket && browserName && launchOptions) {\n const handshake: IHandshake = {\n action: 'handshake',\n browserName,\n launchOptions,\n playwrightVersion\n };\n // Log handshake without 'headless' to avoid confusion (tunnel enforces headless: false)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { headless, ...logOptions } = launchOptions;\n const logHandshake: Omit<IHandshake, 'launchOptions'> & {\n launchOptions: Omit<LaunchOptions, 'headless'>;\n } = {\n ...handshake,\n launchOptions: logOptions\n };\n logger.writeLine(`Sending handshake to remote: ${JSON.stringify(logHandshake)}`);\n handshakeSent = true;\n remoteSocket.send(JSON.stringify(handshake));\n }\n }\n\n return await new Promise((resolve) => {\n remoteWsServer.on('error', (error) => {\n logger.writeErrorLine(`Remote WebSocket server error: ${error}`);\n });\n\n remoteWsServer.on('close', () => {\n logger.writeLine('Remote WebSocket server closed');\n });\n\n const bufferedLocalMessages: Array<RawData> = [];\n\n remoteWsServer.on('connection', (ws) => {\n logger.writeLine('Remote websocket connected');\n remoteSocket = ws;\n handshakeAck = false;\n maybeSendHandshake();\n\n ws.on('message', (message) => {\n if (!handshakeAck) {\n try {\n const receivedHandshake: IHandshakeAck = JSON.parse(message.toString());\n if (receivedHandshake.action === 'handshakeAck') {\n handshakeAck = true;\n logger.writeLine('Received handshakeAck from remote');\n } else {\n logger.writeErrorLine('Invalid handshake ack message');\n ws.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Invalid handshake ack');\n return;\n }\n } catch (e) {\n logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);\n ws.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Failed parsing handshake');\n return;\n }\n // Resolve only once local proxy available and handshake acknowledged\n if (handshakeAck) {\n // Flush any buffered local messages now that tunnel is active\n const activeRemote: WebSocket | undefined = remoteSocket;\n if (activeRemote && activeRemote.readyState === WebSocket.OPEN) {\n while (bufferedLocalMessages.length > 0) {\n const m: Buffer | ArrayBuffer | Buffer[] | string | undefined = bufferedLocalMessages.shift();\n if (m !== undefined) {\n logger.writeLine(`Flushing buffered local message to remote: ${m}`);\n activeRemote.send(m);\n }\n }\n }\n }\n } else {\n // Forward from remote to all local clients\n localProxyWs.clients.forEach((client) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n });\n }\n });\n\n ws.on('close', (code: number, reason: Buffer) => {\n const reasonStr: string = reason.toString() || 'no reason provided';\n const codeDescription: string = getWebSocketCloseReason(code);\n logger.writeDebugLine(\n `Remote websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`\n );\n logger.writeDebugLine(\n ` Connection state at close: handshakeSent=${handshakeSent}, handshakeAck=${handshakeAck}`\n );\n logger.writeDebugLine(` Buffered messages pending: ${bufferedLocalMessages.length}`);\n });\n ws.on('error', (err: Error) => {\n logger.writeErrorLine(`Remote websocket error: ${getNormalizedErrorString(err)}`);\n logger.writeErrorLine(` Socket readyState: ${getWebSocketReadyStateString(ws.readyState)}`);\n });\n });\n\n localProxyWs.on('connection', (localWs, request) => {\n try {\n const urlString: string | undefined = request?.url;\n if (urlString) {\n const parsed: URL = new URL(urlString, 'http://localhost');\n logger.writeLine(`Local client connected with query params: ${parsed.searchParams.toString()}`);\n const bName: string | null = parsed.searchParams.get('browser');\n if (bName && SUPPORTED_BROWSER_NAMES.has(bName)) {\n browserName = bName as BrowserName;\n }\n const launchOptionsParam: string | null = parsed.searchParams.get('launchOptions');\n if (launchOptionsParam) {\n try {\n launchOptions = JSON.parse(launchOptionsParam);\n } catch (e) {\n logger.writeErrorLine('Invalid launchOptions JSON provided');\n }\n }\n }\n } catch (e) {\n logger.writeErrorLine(`Error parsing local connection query params: ${e}`);\n }\n\n if (!browserName) {\n const supportedBrowsersString: string = Array.from(SUPPORTED_BROWSER_NAMES).join('|');\n logger.writeErrorLine(`browser query param required (${supportedBrowsersString})`);\n localWs.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Missing browser param');\n return;\n }\n if (!launchOptions) {\n launchOptions = {} as LaunchOptions; // default empty if not provided\n }\n\n maybeSendHandshake();\n\n localWs.on('message', (message) => {\n if (handshakeAck && remoteSocket?.readyState === WebSocket.OPEN) {\n remoteSocket.send(message);\n } else {\n // Buffer until handshakeAck to avoid losing early protocol messages from Playwright\n bufferedLocalMessages.push(message);\n }\n });\n localWs.on('close', (code: number, reason: Buffer) => {\n const reasonStr: string = reason.toString() || 'no reason provided';\n const codeDescription: string = getWebSocketCloseReason(code);\n logger.writeDebugLine(\n `Local client websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`\n );\n logger.writeDebugLine(\n ` Remote socket state: ${remoteSocket ? getWebSocketReadyStateString(remoteSocket.readyState) : 'undefined'}`\n );\n logger.writeDebugLine(` handshakeAck: ${handshakeAck}`);\n });\n localWs.on('error', (err: Error) => {\n logger.writeErrorLine(`Local client websocket error: ${getNormalizedErrorString(err)}`);\n });\n });\n\n // Resolve immediately so caller can initiate local connection with query params (handshake completes later)\n resolve({\n remoteEndpoint: localProxyWsEndpoint,\n [Symbol.dispose]() {\n try {\n remoteWsServer.close();\n } catch {\n // ignore errors during remote WebSocket server shutdown\n }\n try {\n httpServer[Symbol.dispose]();\n } catch {\n // ignore errors during HTTP/WebSocket server shutdown\n }\n },\n // eslint-disable-next-line promise/param-names\n closePromise: new Promise<void>((resolve2) => {\n remoteWsServer.once('close', () => {\n resolve2();\n });\n })\n });\n });\n}\n\n/**\n * Disposable handle returned by {@link createTunneledBrowserAsync}.\n * @beta\n */\nexport interface IDisposableTunneledBrowser {\n /**\n * The connected Playwright Browser instance.\n */\n browser: Browser;\n /**\n * Async dispose method that closes the browser connection.\n * Called automatically when using `await using` syntax.\n */\n [Symbol.asyncDispose]: () => Promise<void>;\n}\n\n/**\n * Creates a Playwright Browser instance connected via a tunneled WebSocket connection.\n * @beta\n */\nexport async function createTunneledBrowserAsync(\n browserName: BrowserName,\n launchOptions: LaunchOptions,\n logger?: ITerminal,\n port: number = DEFAULT_LISTEN_PORT\n): Promise<IDisposableTunneledBrowser> {\n // Establish the tunnel first (remoteEndpoint here refers to local proxy endpoint for connect())\n\n if (!logger) {\n const terminalProvider: ConsoleTerminalProvider = new ConsoleTerminalProvider();\n logger = new Terminal(terminalProvider);\n }\n\n const connection: IDisposableTunneledBrowserConnection = await tunneledBrowserConnection(logger, port);\n const { remoteEndpoint } = connection;\n // Append query params for browser and launchOptions\n const urlObj: URL = new URL(remoteEndpoint);\n urlObj.searchParams.set('browser', browserName);\n urlObj.searchParams.set('launchOptions', JSON.stringify(launchOptions || {}));\n const connectEndpoint: string = urlObj.toString();\n const browser: Browser = await playwright[browserName].connect(connectEndpoint);\n logger.writeLine(`Connected to remote browser at ${connectEndpoint}`);\n\n return {\n browser,\n async [Symbol.asyncDispose]() {\n logger.writeLine('Disposing browser');\n await browser.close();\n // Dispose the tunnel connection after browser is closed\n connection[Symbol.dispose]();\n }\n };\n}\n"]}
|
package/lib/utilities.d.ts
CHANGED
|
@@ -14,4 +14,53 @@ export declare function isExtensionInstalledAsync(): Promise<boolean>;
|
|
|
14
14
|
* @beta
|
|
15
15
|
*/
|
|
16
16
|
export declare function getNormalizedErrorString(error: unknown): string;
|
|
17
|
+
/**
|
|
18
|
+
* WebSocket close codes as defined by RFC 6455.
|
|
19
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
export declare const WebSocketCloseCode: {
|
|
23
|
+
/** Normal closure; the connection successfully completed. */
|
|
24
|
+
readonly NORMAL_CLOSURE: 1000;
|
|
25
|
+
/** Endpoint is going away (e.g., server shutting down, browser navigating away). */
|
|
26
|
+
readonly GOING_AWAY: 1001;
|
|
27
|
+
/** Protocol error encountered. */
|
|
28
|
+
readonly PROTOCOL_ERROR: 1002;
|
|
29
|
+
/** Received data type that cannot be accepted (e.g., text-only endpoint received binary). */
|
|
30
|
+
readonly UNSUPPORTED_DATA: 1003;
|
|
31
|
+
/** No status code was provided even though one was expected. */
|
|
32
|
+
readonly NO_STATUS_RECEIVED: 1005;
|
|
33
|
+
/** Connection was closed abnormally (e.g., without sending a close frame). */
|
|
34
|
+
readonly ABNORMAL_CLOSURE: 1006;
|
|
35
|
+
/** Received message data inconsistent with the message type. */
|
|
36
|
+
readonly INVALID_PAYLOAD: 1007;
|
|
37
|
+
/** Received a message that violates policy. */
|
|
38
|
+
readonly POLICY_VIOLATION: 1008;
|
|
39
|
+
/** Received a message that is too big to process. */
|
|
40
|
+
readonly MESSAGE_TOO_BIG: 1009;
|
|
41
|
+
/** Server encountered an unexpected condition that prevented it from fulfilling the request. */
|
|
42
|
+
readonly INTERNAL_ERROR: 1011;
|
|
43
|
+
/** Connection was closed due to TLS handshake failure. */
|
|
44
|
+
readonly TLS_HANDSHAKE_FAILED: 1015;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Type for WebSocket close code values.
|
|
48
|
+
* @beta
|
|
49
|
+
*/
|
|
50
|
+
export type WebSocketCloseCodeValue = (typeof WebSocketCloseCode)[keyof typeof WebSocketCloseCode];
|
|
51
|
+
/**
|
|
52
|
+
* Human-readable descriptions for WebSocket close codes.
|
|
53
|
+
* @beta
|
|
54
|
+
*/
|
|
55
|
+
export declare const WebSocketCloseCodeDescriptions: Record<WebSocketCloseCodeValue, string>;
|
|
56
|
+
/**
|
|
57
|
+
* Returns a human-readable description for a WebSocket close code.
|
|
58
|
+
* @beta
|
|
59
|
+
*/
|
|
60
|
+
export declare function getWebSocketCloseReason(code: number): string;
|
|
61
|
+
/**
|
|
62
|
+
* Returns a human-readable string for a WebSocket ready state.
|
|
63
|
+
* @beta
|
|
64
|
+
*/
|
|
65
|
+
export declare function getWebSocketReadyStateString(readyState: number): string;
|
|
17
66
|
//# sourceMappingURL=utilities.d.ts.map
|
package/lib/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,MACiB,CAAC;AAE7D;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CASlE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQ/D"}
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,MACiB,CAAC;AAE7D;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CASlE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQ/D;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE;IAC/B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,kCAAkC;IAClC,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;IAC9B,6FAA6F;IAC7F,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,kBAAkB,EAAE,IAAI,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAChC,qDAAqD;IACrD,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,oBAAoB,EAAE,IAAI,CAAC;CAarC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAEnG;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAYlF,CAAC;AAEF;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAavE"}
|
package/lib/utilities.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
3
3
|
// See LICENSE in the project root for license information.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.EXTENSION_INSTALLED_FILENAME = void 0;
|
|
5
|
+
exports.WebSocketCloseCodeDescriptions = exports.WebSocketCloseCode = exports.EXTENSION_INSTALLED_FILENAME = void 0;
|
|
6
6
|
exports.isExtensionInstalledAsync = isExtensionInstalledAsync;
|
|
7
7
|
exports.getNormalizedErrorString = getNormalizedErrorString;
|
|
8
|
+
exports.getWebSocketCloseReason = getWebSocketCloseReason;
|
|
9
|
+
exports.getWebSocketReadyStateString = getWebSocketReadyStateString;
|
|
8
10
|
const node_os_1 = require("node:os");
|
|
9
11
|
const node_core_library_1 = require("@rushstack/node-core-library");
|
|
10
12
|
/**
|
|
@@ -38,4 +40,64 @@ function getNormalizedErrorString(error) {
|
|
|
38
40
|
}
|
|
39
41
|
return String(error);
|
|
40
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* WebSocket close codes as defined by RFC 6455.
|
|
45
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}
|
|
46
|
+
* @beta
|
|
47
|
+
*/
|
|
48
|
+
exports.WebSocketCloseCode = {
|
|
49
|
+
NORMAL_CLOSURE: 1000,
|
|
50
|
+
GOING_AWAY: 1001,
|
|
51
|
+
PROTOCOL_ERROR: 1002,
|
|
52
|
+
UNSUPPORTED_DATA: 1003,
|
|
53
|
+
NO_STATUS_RECEIVED: 1005,
|
|
54
|
+
ABNORMAL_CLOSURE: 1006,
|
|
55
|
+
INVALID_PAYLOAD: 1007,
|
|
56
|
+
POLICY_VIOLATION: 1008,
|
|
57
|
+
MESSAGE_TOO_BIG: 1009,
|
|
58
|
+
INTERNAL_ERROR: 1011,
|
|
59
|
+
TLS_HANDSHAKE_FAILED: 1015
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Human-readable descriptions for WebSocket close codes.
|
|
63
|
+
* @beta
|
|
64
|
+
*/
|
|
65
|
+
exports.WebSocketCloseCodeDescriptions = {
|
|
66
|
+
[exports.WebSocketCloseCode.NORMAL_CLOSURE]: 'Normal Closure',
|
|
67
|
+
[exports.WebSocketCloseCode.GOING_AWAY]: 'Going Away',
|
|
68
|
+
[exports.WebSocketCloseCode.PROTOCOL_ERROR]: 'Protocol Error',
|
|
69
|
+
[exports.WebSocketCloseCode.UNSUPPORTED_DATA]: 'Unsupported Data',
|
|
70
|
+
[exports.WebSocketCloseCode.NO_STATUS_RECEIVED]: 'No Status Received',
|
|
71
|
+
[exports.WebSocketCloseCode.ABNORMAL_CLOSURE]: 'Abnormal Closure (connection lost)',
|
|
72
|
+
[exports.WebSocketCloseCode.INVALID_PAYLOAD]: 'Invalid Payload',
|
|
73
|
+
[exports.WebSocketCloseCode.POLICY_VIOLATION]: 'Policy Violation',
|
|
74
|
+
[exports.WebSocketCloseCode.MESSAGE_TOO_BIG]: 'Message Too Big',
|
|
75
|
+
[exports.WebSocketCloseCode.INTERNAL_ERROR]: 'Internal Error',
|
|
76
|
+
[exports.WebSocketCloseCode.TLS_HANDSHAKE_FAILED]: 'TLS Handshake Failed'
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Returns a human-readable description for a WebSocket close code.
|
|
80
|
+
* @beta
|
|
81
|
+
*/
|
|
82
|
+
function getWebSocketCloseReason(code) {
|
|
83
|
+
return exports.WebSocketCloseCodeDescriptions[code] || 'Unknown';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns a human-readable string for a WebSocket ready state.
|
|
87
|
+
* @beta
|
|
88
|
+
*/
|
|
89
|
+
function getWebSocketReadyStateString(readyState) {
|
|
90
|
+
switch (readyState) {
|
|
91
|
+
case 0:
|
|
92
|
+
return 'CONNECTING';
|
|
93
|
+
case 1:
|
|
94
|
+
return 'OPEN';
|
|
95
|
+
case 2:
|
|
96
|
+
return 'CLOSING';
|
|
97
|
+
case 3:
|
|
98
|
+
return 'CLOSED';
|
|
99
|
+
default:
|
|
100
|
+
return `UNKNOWN(${readyState})`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
41
103
|
//# sourceMappingURL=utilities.js.map
|
package/lib/utilities.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":";AAAA,4FAA4F;AAC5F,2DAA2D;;;AAkB3D,8DASC;AAMD,4DAQC;
|
|
1
|
+
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":";AAAA,4FAA4F;AAC5F,2DAA2D;;;AAkB3D,8DASC;AAMD,4DAQC;AAwED,0DAEC;AAMD,oEAaC;AApID,qCAAiC;AAEjC,oEAA0D;AAE1D;;;GAGG;AACU,QAAA,4BAA4B,GACvC,0DAA0D,CAAC;AAE7D;;;;GAIG;AACI,KAAK,UAAU,yBAAyB;IAC7C,wFAAwF;IACxF,MAAM,OAAO,GAAW,IAAA,gBAAM,GAAE,CAAC;IAEjC,MAAM,0BAA0B,GAAW,GAAG,OAAO,IAAI,oCAA4B,EAAE,CAAC;IACxF,MAAM,SAAS,GAAY,8BAAU,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAAC;IAEzE,uBAAuB;IACvB,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,KAAc;IACrD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC,KAAK,CAAC;QACrB,CAAC;QACD,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACU,QAAA,kBAAkB,GAuB3B;IACF,cAAc,EAAE,IAAI;IACpB,UAAU,EAAE,IAAI;IAChB,cAAc,EAAE,IAAI;IACpB,gBAAgB,EAAE,IAAI;IACtB,kBAAkB,EAAE,IAAI;IACxB,gBAAgB,EAAE,IAAI;IACtB,eAAe,EAAE,IAAI;IACrB,gBAAgB,EAAE,IAAI;IACtB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,IAAI;IACpB,oBAAoB,EAAE,IAAI;CAC3B,CAAC;AAQF;;;GAGG;AACU,QAAA,8BAA8B,GAA4C;IACrF,CAAC,0BAAkB,CAAC,cAAc,CAAC,EAAE,gBAAgB;IACrD,CAAC,0BAAkB,CAAC,UAAU,CAAC,EAAE,YAAY;IAC7C,CAAC,0BAAkB,CAAC,cAAc,CAAC,EAAE,gBAAgB;IACrD,CAAC,0BAAkB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB;IACzD,CAAC,0BAAkB,CAAC,kBAAkB,CAAC,EAAE,oBAAoB;IAC7D,CAAC,0BAAkB,CAAC,gBAAgB,CAAC,EAAE,oCAAoC;IAC3E,CAAC,0BAAkB,CAAC,eAAe,CAAC,EAAE,iBAAiB;IACvD,CAAC,0BAAkB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB;IACzD,CAAC,0BAAkB,CAAC,eAAe,CAAC,EAAE,iBAAiB;IACvD,CAAC,0BAAkB,CAAC,cAAc,CAAC,EAAE,gBAAgB;IACrD,CAAC,0BAAkB,CAAC,oBAAoB,CAAC,EAAE,sBAAsB;CAClE,CAAC;AAEF;;;GAGG;AACH,SAAgB,uBAAuB,CAAC,IAAY;IAClD,OAAO,sCAA8B,CAAC,IAA+B,CAAC,IAAI,SAAS,CAAC;AACtF,CAAC;AAED;;;GAGG;AACH,SAAgB,4BAA4B,CAAC,UAAkB;IAC7D,QAAQ,UAAU,EAAE,CAAC;QACnB,KAAK,CAAC;YACJ,OAAO,YAAY,CAAC;QACtB,KAAK,CAAC;YACJ,OAAO,MAAM,CAAC;QAChB,KAAK,CAAC;YACJ,OAAO,SAAS,CAAC;QACnB,KAAK,CAAC;YACJ,OAAO,QAAQ,CAAC;QAClB;YACE,OAAO,WAAW,UAAU,GAAG,CAAC;IACpC,CAAC;AACH,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n\nimport { tmpdir } from 'node:os';\n\nimport { FileSystem } from '@rushstack/node-core-library';\n\n/**\n * The filename used to indicate that the Playwright Local Browser Server extension is installed.\n * @beta\n */\nexport const EXTENSION_INSTALLED_FILENAME: string =\n '.playwright-local-browser-server-extension-installed.txt';\n\n/**\n * Helper to determine if the Playwright Local Browser Server extension is installed. This checks for the\n * existence of a well-known file in the OS temp directory.\n * @beta\n */\nexport async function isExtensionInstalledAsync(): Promise<boolean> {\n // Read file from os.tempdir() + '/.playwright-local-browser-server-extension-installed'\n const tempDir: string = tmpdir();\n\n const extensionInstalledFilePath: string = `${tempDir}/${EXTENSION_INSTALLED_FILENAME}`;\n const doesExist: boolean = FileSystem.exists(extensionInstalledFilePath);\n\n // check if file exists\n return doesExist;\n}\n\n/**\n * Normalizes an error to a string for logging purposes.\n * @beta\n */\nexport function getNormalizedErrorString(error: unknown): string {\n if (error instanceof Error) {\n if (error.stack) {\n return error.stack;\n }\n return error.message;\n }\n return String(error);\n}\n\n/**\n * WebSocket close codes as defined by RFC 6455.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}\n * @beta\n */\nexport const WebSocketCloseCode: {\n /** Normal closure; the connection successfully completed. */\n readonly NORMAL_CLOSURE: 1000;\n /** Endpoint is going away (e.g., server shutting down, browser navigating away). */\n readonly GOING_AWAY: 1001;\n /** Protocol error encountered. */\n readonly PROTOCOL_ERROR: 1002;\n /** Received data type that cannot be accepted (e.g., text-only endpoint received binary). */\n readonly UNSUPPORTED_DATA: 1003;\n /** No status code was provided even though one was expected. */\n readonly NO_STATUS_RECEIVED: 1005;\n /** Connection was closed abnormally (e.g., without sending a close frame). */\n readonly ABNORMAL_CLOSURE: 1006;\n /** Received message data inconsistent with the message type. */\n readonly INVALID_PAYLOAD: 1007;\n /** Received a message that violates policy. */\n readonly POLICY_VIOLATION: 1008;\n /** Received a message that is too big to process. */\n readonly MESSAGE_TOO_BIG: 1009;\n /** Server encountered an unexpected condition that prevented it from fulfilling the request. */\n readonly INTERNAL_ERROR: 1011;\n /** Connection was closed due to TLS handshake failure. */\n readonly TLS_HANDSHAKE_FAILED: 1015;\n} = {\n NORMAL_CLOSURE: 1000,\n GOING_AWAY: 1001,\n PROTOCOL_ERROR: 1002,\n UNSUPPORTED_DATA: 1003,\n NO_STATUS_RECEIVED: 1005,\n ABNORMAL_CLOSURE: 1006,\n INVALID_PAYLOAD: 1007,\n POLICY_VIOLATION: 1008,\n MESSAGE_TOO_BIG: 1009,\n INTERNAL_ERROR: 1011,\n TLS_HANDSHAKE_FAILED: 1015\n};\n\n/**\n * Type for WebSocket close code values.\n * @beta\n */\nexport type WebSocketCloseCodeValue = (typeof WebSocketCloseCode)[keyof typeof WebSocketCloseCode];\n\n/**\n * Human-readable descriptions for WebSocket close codes.\n * @beta\n */\nexport const WebSocketCloseCodeDescriptions: Record<WebSocketCloseCodeValue, string> = {\n [WebSocketCloseCode.NORMAL_CLOSURE]: 'Normal Closure',\n [WebSocketCloseCode.GOING_AWAY]: 'Going Away',\n [WebSocketCloseCode.PROTOCOL_ERROR]: 'Protocol Error',\n [WebSocketCloseCode.UNSUPPORTED_DATA]: 'Unsupported Data',\n [WebSocketCloseCode.NO_STATUS_RECEIVED]: 'No Status Received',\n [WebSocketCloseCode.ABNORMAL_CLOSURE]: 'Abnormal Closure (connection lost)',\n [WebSocketCloseCode.INVALID_PAYLOAD]: 'Invalid Payload',\n [WebSocketCloseCode.POLICY_VIOLATION]: 'Policy Violation',\n [WebSocketCloseCode.MESSAGE_TOO_BIG]: 'Message Too Big',\n [WebSocketCloseCode.INTERNAL_ERROR]: 'Internal Error',\n [WebSocketCloseCode.TLS_HANDSHAKE_FAILED]: 'TLS Handshake Failed'\n};\n\n/**\n * Returns a human-readable description for a WebSocket close code.\n * @beta\n */\nexport function getWebSocketCloseReason(code: number): string {\n return WebSocketCloseCodeDescriptions[code as WebSocketCloseCodeValue] || 'Unknown';\n}\n\n/**\n * Returns a human-readable string for a WebSocket ready state.\n * @beta\n */\nexport function getWebSocketReadyStateString(readyState: number): string {\n switch (readyState) {\n case 0:\n return 'CONNECTING';\n case 1:\n return 'OPEN';\n case 2:\n return 'CLOSING';\n case 3:\n return 'CLOSED';\n default:\n return `UNKNOWN(${readyState})`;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rushstack/playwright-browser-tunnel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Run a remote Playwright Browser Tunnel. Useful in remote development environments.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"ws": "~8.14.1",
|
|
21
21
|
"playwright": "1.56.1",
|
|
22
22
|
"@rushstack/node-core-library": "5.19.1",
|
|
23
|
-
"@rushstack/
|
|
24
|
-
"@rushstack/
|
|
23
|
+
"@rushstack/terminal": "0.21.0",
|
|
24
|
+
"@rushstack/ts-command-line": "5.2.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"eslint": "~9.37.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"playwright-core": "~1.56.1",
|
|
31
31
|
"@playwright/test": "~1.56.1",
|
|
32
32
|
"@types/node": "20.17.19",
|
|
33
|
-
"@rushstack/heft": "1.1.
|
|
33
|
+
"@rushstack/heft": "1.1.13",
|
|
34
34
|
"local-node-rig": "1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
Caching build output folders: dist, lib, temp, .rush/temp/operation/_phase_build
|
|
2
2
|
Successfully set cache entry.
|
|
3
|
-
Cache key: rushstack+playwright-browser-tunnel-_phase_build-
|
|
3
|
+
Cache key: rushstack+playwright-browser-tunnel-_phase_build-3f67426e34dc3685ce06e290853fe1361c9ed401
|
|
@@ -2,7 +2,7 @@ Invoking: heft run --only build -- --clean --production
|
|
|
2
2
|
---- build started ----
|
|
3
3
|
[build:typescript] Using TypeScript version 5.8.2
|
|
4
4
|
[build:lint] Using ESLint version 9.37.0
|
|
5
|
-
[build:api-extractor] Using API Extractor version 7.56.
|
|
5
|
+
[build:api-extractor] Using API Extractor version 7.56.1
|
|
6
6
|
[build:api-extractor] Analysis will use the bundled TypeScript version 5.8.2
|
|
7
|
-
---- build finished (21.
|
|
8
|
-
-------------------- Finished (21.
|
|
7
|
+
---- build finished (21.890s) ----
|
|
8
|
+
-------------------- Finished (21.899s) --------------------
|
|
@@ -11,7 +11,12 @@ import semver from 'semver';
|
|
|
11
11
|
import { TerminalProviderSeverity, TerminalStreamWritable, type ITerminal } from '@rushstack/terminal';
|
|
12
12
|
import { Executable, FileSystem, Async } from '@rushstack/node-core-library';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getNormalizedErrorString,
|
|
16
|
+
getWebSocketCloseReason,
|
|
17
|
+
getWebSocketReadyStateString,
|
|
18
|
+
WebSocketCloseCode
|
|
19
|
+
} from './utilities';
|
|
15
20
|
import { LaunchOptionsValidator, type ILaunchOptionsValidationResult } from './LaunchOptionsValidator';
|
|
16
21
|
|
|
17
22
|
/**
|
|
@@ -170,7 +175,7 @@ export class PlaywrightTunnel {
|
|
|
170
175
|
this._pollInterval = undefined;
|
|
171
176
|
}
|
|
172
177
|
await this._initWsPromise?.finally(() => {
|
|
173
|
-
this._ws?.close();
|
|
178
|
+
this._ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Tunnel stopped');
|
|
174
179
|
});
|
|
175
180
|
}
|
|
176
181
|
|
|
@@ -448,37 +453,68 @@ export class PlaywrightTunnel {
|
|
|
448
453
|
// ws1 is the tunnel websocket, ws2 is the browser server websocket
|
|
449
454
|
private async _setupForwardingAsync(ws1: WebSocket, ws2: WebSocket): Promise<void> {
|
|
450
455
|
this._terminal.writeLine('Setting up message forwarding between ws1 and ws2');
|
|
456
|
+
this._terminal.writeLine(` ws1 (tunnel) readyState: ${getWebSocketReadyStateString(ws1.readyState)}`);
|
|
457
|
+
this._terminal.writeLine(` ws2 (browser) readyState: ${getWebSocketReadyStateString(ws2.readyState)}`);
|
|
458
|
+
|
|
459
|
+
const messageCount: { ws1ToWs2: number; ws2ToWs1: number } = { ws1ToWs2: 0, ws2ToWs1: 0 };
|
|
460
|
+
|
|
451
461
|
ws1.on('message', (data) => {
|
|
462
|
+
messageCount.ws1ToWs2++;
|
|
452
463
|
if (ws2.readyState === WebSocket.OPEN) {
|
|
453
464
|
ws2.send(data);
|
|
454
465
|
} else {
|
|
455
|
-
this._terminal.writeLine(
|
|
466
|
+
this._terminal.writeLine(
|
|
467
|
+
`ws2 not open (state: ${getWebSocketReadyStateString(ws2.readyState)}). Dropping message #${messageCount.ws1ToWs2}`
|
|
468
|
+
);
|
|
456
469
|
}
|
|
457
470
|
});
|
|
458
471
|
ws2.on('message', (data) => {
|
|
472
|
+
messageCount.ws2ToWs1++;
|
|
459
473
|
if (ws1.readyState === WebSocket.OPEN) {
|
|
460
474
|
ws1.send(data);
|
|
461
475
|
} else {
|
|
462
|
-
this._terminal.writeLine(
|
|
476
|
+
this._terminal.writeLine(
|
|
477
|
+
`ws1 not open (state: ${getWebSocketReadyStateString(ws1.readyState)}). Dropping message #${messageCount.ws2ToWs1}`
|
|
478
|
+
);
|
|
463
479
|
}
|
|
464
480
|
});
|
|
465
481
|
|
|
466
|
-
ws1.once('close', () => {
|
|
482
|
+
ws1.once('close', (code: number, reason: Buffer) => {
|
|
483
|
+
const reasonStr: string = reason.toString() || 'no reason provided';
|
|
484
|
+
const codeDescription: string = getWebSocketCloseReason(code);
|
|
485
|
+
this._terminal.writeLine(
|
|
486
|
+
`ws1 (tunnel) closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
|
|
487
|
+
);
|
|
488
|
+
this._terminal.writeLine(
|
|
489
|
+
` Messages forwarded: ws1->ws2: ${messageCount.ws1ToWs2}, ws2->ws1: ${messageCount.ws2ToWs1}`
|
|
490
|
+
);
|
|
467
491
|
if (ws2.readyState === WebSocket.OPEN) {
|
|
468
|
-
|
|
492
|
+
this._terminal.writeLine(' Closing ws2 (browser) in response');
|
|
493
|
+
ws2.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Tunnel closed');
|
|
469
494
|
}
|
|
470
495
|
});
|
|
471
|
-
ws2.once('close', () => {
|
|
496
|
+
ws2.once('close', (code: number, reason: Buffer) => {
|
|
497
|
+
const reasonStr: string = reason.toString() || 'no reason provided';
|
|
498
|
+
const codeDescription: string = getWebSocketCloseReason(code);
|
|
499
|
+
this._terminal.writeLine(
|
|
500
|
+
`ws2 (browser) closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
|
|
501
|
+
);
|
|
502
|
+
this._terminal.writeLine(
|
|
503
|
+
` Messages forwarded: ws1->ws2: ${messageCount.ws1ToWs2}, ws2->ws1: ${messageCount.ws2ToWs1}`
|
|
504
|
+
);
|
|
472
505
|
if (ws1.readyState === WebSocket.OPEN) {
|
|
473
|
-
|
|
506
|
+
this._terminal.writeLine(' Closing ws1 (tunnel) in response');
|
|
507
|
+
ws1.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Browser closed');
|
|
474
508
|
}
|
|
475
509
|
});
|
|
476
510
|
|
|
477
511
|
ws1.once('error', (error) => {
|
|
478
|
-
this._terminal.
|
|
512
|
+
this._terminal.writeErrorLine(`ws1 (tunnel) WebSocket error: ${getNormalizedErrorString(error)}`);
|
|
513
|
+
this._terminal.writeErrorLine(` ws1 readyState: ${getWebSocketReadyStateString(ws1.readyState)}`);
|
|
479
514
|
});
|
|
480
515
|
ws2.once('error', (error) => {
|
|
481
|
-
this._terminal.
|
|
516
|
+
this._terminal.writeErrorLine(`ws2 (browser) WebSocket error: ${getNormalizedErrorString(error)}`);
|
|
517
|
+
this._terminal.writeErrorLine(` ws2 readyState: ${getWebSocketReadyStateString(ws2.readyState)}`);
|
|
482
518
|
});
|
|
483
519
|
}
|
|
484
520
|
|
|
@@ -507,11 +543,21 @@ export class PlaywrightTunnel {
|
|
|
507
543
|
this._terminal.writeLine(`WebSocket error occurred: ${getNormalizedErrorString(error)}`);
|
|
508
544
|
});
|
|
509
545
|
|
|
510
|
-
ws.on('close', async () => {
|
|
546
|
+
ws.on('close', async (code: number, reason: Buffer) => {
|
|
547
|
+
const reasonStr: string = reason.toString() || 'no reason provided';
|
|
548
|
+
const codeDescription: string = getWebSocketCloseReason(code);
|
|
511
549
|
this._initWsPromise = undefined;
|
|
512
550
|
this.status = 'stopped';
|
|
513
|
-
this._terminal.writeLine(
|
|
514
|
-
|
|
551
|
+
this._terminal.writeLine(
|
|
552
|
+
`WebSocket connection closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
|
|
553
|
+
);
|
|
554
|
+
this._terminal.writeLine(` handshake received: ${handshake !== undefined}`);
|
|
555
|
+
this._terminal.writeLine(` browserServer active: ${browserServer !== undefined}`);
|
|
556
|
+
if (browserServer) {
|
|
557
|
+
this._terminal.writeLine(' Closing browser server...');
|
|
558
|
+
await browserServer.close();
|
|
559
|
+
this._terminal.writeLine(' Browser server closed');
|
|
560
|
+
}
|
|
515
561
|
});
|
|
516
562
|
|
|
517
563
|
return await new Promise<WebSocket>((resolve, reject) => {
|
|
@@ -531,7 +577,7 @@ export class PlaywrightTunnel {
|
|
|
531
577
|
if (!shouldProceed) {
|
|
532
578
|
terminal.writeLine('Browser server launch cancelled by user.');
|
|
533
579
|
ws.off('message', onMessageHandler);
|
|
534
|
-
ws.close();
|
|
580
|
+
ws.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Launch cancelled by user');
|
|
535
581
|
reject(new Error('Browser server launch cancelled by user'));
|
|
536
582
|
return;
|
|
537
583
|
}
|
|
@@ -544,6 +590,20 @@ export class PlaywrightTunnel {
|
|
|
544
590
|
client = browserServerProxy.client;
|
|
545
591
|
browserServer = browserServerProxy.browserServer;
|
|
546
592
|
|
|
593
|
+
// Monitor browser server process for crashes
|
|
594
|
+
const browserProcess: ChildProcess | null = browserServer.process();
|
|
595
|
+
if (browserProcess) {
|
|
596
|
+
browserProcess.on('exit', (code: number | null, signal: string | null) => {
|
|
597
|
+
terminal.writeErrorLine(`Browser server process exited - code: ${code}, signal: ${signal}`);
|
|
598
|
+
});
|
|
599
|
+
browserProcess.on('error', (err: Error) => {
|
|
600
|
+
terminal.writeErrorLine(`Browser server process error: ${getNormalizedErrorString(err)}`);
|
|
601
|
+
});
|
|
602
|
+
terminal.writeDebugLine(`Browser server process started with PID: ${browserProcess.pid}`);
|
|
603
|
+
} else {
|
|
604
|
+
terminal.writeDebugLine('Warning: Browser server process handle not available for monitoring');
|
|
605
|
+
}
|
|
606
|
+
|
|
547
607
|
this.status = 'browser-server-running';
|
|
548
608
|
|
|
549
609
|
// Send ack so that the counterpart also knows to start forwarding messages.
|
|
@@ -571,7 +631,7 @@ export class PlaywrightTunnel {
|
|
|
571
631
|
|
|
572
632
|
// Cleanup and close connection on error
|
|
573
633
|
ws.off('message', onMessageHandler);
|
|
574
|
-
ws.close();
|
|
634
|
+
ws.close(WebSocketCloseCode.INTERNAL_ERROR, 'Handshake error');
|
|
575
635
|
reject(error);
|
|
576
636
|
return;
|
|
577
637
|
}
|
|
@@ -579,7 +639,7 @@ export class PlaywrightTunnel {
|
|
|
579
639
|
if (!client) {
|
|
580
640
|
terminal.writeLine('Browser WebSocket client is not initialized.');
|
|
581
641
|
ws.off('message', onMessageHandler);
|
|
582
|
-
ws.close();
|
|
642
|
+
ws.close(WebSocketCloseCode.INTERNAL_ERROR, 'Browser client not initialized');
|
|
583
643
|
return;
|
|
584
644
|
}
|
|
585
645
|
}
|
|
@@ -10,6 +10,12 @@ import { type ITerminal, Terminal, ConsoleTerminalProvider } from '@rushstack/te
|
|
|
10
10
|
|
|
11
11
|
import type { BrowserName } from './PlaywrightBrowserTunnel';
|
|
12
12
|
import { HttpServer } from './HttpServer';
|
|
13
|
+
import {
|
|
14
|
+
getNormalizedErrorString,
|
|
15
|
+
getWebSocketCloseReason,
|
|
16
|
+
getWebSocketReadyStateString,
|
|
17
|
+
WebSocketCloseCode
|
|
18
|
+
} from './utilities';
|
|
13
19
|
|
|
14
20
|
const { version: playwrightVersion } = playwrightPackageJson;
|
|
15
21
|
|
|
@@ -121,12 +127,12 @@ export async function tunneledBrowserConnection(
|
|
|
121
127
|
logger.writeLine('Received handshakeAck from remote');
|
|
122
128
|
} else {
|
|
123
129
|
logger.writeErrorLine('Invalid handshake ack message');
|
|
124
|
-
ws.close();
|
|
130
|
+
ws.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Invalid handshake ack');
|
|
125
131
|
return;
|
|
126
132
|
}
|
|
127
133
|
} catch (e) {
|
|
128
134
|
logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);
|
|
129
|
-
ws.close();
|
|
135
|
+
ws.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Failed parsing handshake');
|
|
130
136
|
return;
|
|
131
137
|
}
|
|
132
138
|
// Resolve only once local proxy available and handshake acknowledged
|
|
@@ -153,8 +159,21 @@ export async function tunneledBrowserConnection(
|
|
|
153
159
|
}
|
|
154
160
|
});
|
|
155
161
|
|
|
156
|
-
ws.on('close', () =>
|
|
157
|
-
|
|
162
|
+
ws.on('close', (code: number, reason: Buffer) => {
|
|
163
|
+
const reasonStr: string = reason.toString() || 'no reason provided';
|
|
164
|
+
const codeDescription: string = getWebSocketCloseReason(code);
|
|
165
|
+
logger.writeDebugLine(
|
|
166
|
+
`Remote websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
|
|
167
|
+
);
|
|
168
|
+
logger.writeDebugLine(
|
|
169
|
+
` Connection state at close: handshakeSent=${handshakeSent}, handshakeAck=${handshakeAck}`
|
|
170
|
+
);
|
|
171
|
+
logger.writeDebugLine(` Buffered messages pending: ${bufferedLocalMessages.length}`);
|
|
172
|
+
});
|
|
173
|
+
ws.on('error', (err: Error) => {
|
|
174
|
+
logger.writeErrorLine(`Remote websocket error: ${getNormalizedErrorString(err)}`);
|
|
175
|
+
logger.writeErrorLine(` Socket readyState: ${getWebSocketReadyStateString(ws.readyState)}`);
|
|
176
|
+
});
|
|
158
177
|
});
|
|
159
178
|
|
|
160
179
|
localProxyWs.on('connection', (localWs, request) => {
|
|
@@ -183,7 +202,7 @@ export async function tunneledBrowserConnection(
|
|
|
183
202
|
if (!browserName) {
|
|
184
203
|
const supportedBrowsersString: string = Array.from(SUPPORTED_BROWSER_NAMES).join('|');
|
|
185
204
|
logger.writeErrorLine(`browser query param required (${supportedBrowsersString})`);
|
|
186
|
-
localWs.close();
|
|
205
|
+
localWs.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Missing browser param');
|
|
187
206
|
return;
|
|
188
207
|
}
|
|
189
208
|
if (!launchOptions) {
|
|
@@ -200,8 +219,20 @@ export async function tunneledBrowserConnection(
|
|
|
200
219
|
bufferedLocalMessages.push(message);
|
|
201
220
|
}
|
|
202
221
|
});
|
|
203
|
-
localWs.on('close', (
|
|
204
|
-
|
|
222
|
+
localWs.on('close', (code: number, reason: Buffer) => {
|
|
223
|
+
const reasonStr: string = reason.toString() || 'no reason provided';
|
|
224
|
+
const codeDescription: string = getWebSocketCloseReason(code);
|
|
225
|
+
logger.writeDebugLine(
|
|
226
|
+
`Local client websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
|
|
227
|
+
);
|
|
228
|
+
logger.writeDebugLine(
|
|
229
|
+
` Remote socket state: ${remoteSocket ? getWebSocketReadyStateString(remoteSocket.readyState) : 'undefined'}`
|
|
230
|
+
);
|
|
231
|
+
logger.writeDebugLine(` handshakeAck: ${handshakeAck}`);
|
|
232
|
+
});
|
|
233
|
+
localWs.on('error', (err: Error) => {
|
|
234
|
+
logger.writeErrorLine(`Local client websocket error: ${getNormalizedErrorString(err)}`);
|
|
235
|
+
});
|
|
205
236
|
});
|
|
206
237
|
|
|
207
238
|
// Resolve immediately so caller can initiate local connection with query params (handshake completes later)
|
package/src/utilities.ts
CHANGED
|
@@ -41,3 +41,96 @@ export function getNormalizedErrorString(error: unknown): string {
|
|
|
41
41
|
}
|
|
42
42
|
return String(error);
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* WebSocket close codes as defined by RFC 6455.
|
|
47
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}
|
|
48
|
+
* @beta
|
|
49
|
+
*/
|
|
50
|
+
export const WebSocketCloseCode: {
|
|
51
|
+
/** Normal closure; the connection successfully completed. */
|
|
52
|
+
readonly NORMAL_CLOSURE: 1000;
|
|
53
|
+
/** Endpoint is going away (e.g., server shutting down, browser navigating away). */
|
|
54
|
+
readonly GOING_AWAY: 1001;
|
|
55
|
+
/** Protocol error encountered. */
|
|
56
|
+
readonly PROTOCOL_ERROR: 1002;
|
|
57
|
+
/** Received data type that cannot be accepted (e.g., text-only endpoint received binary). */
|
|
58
|
+
readonly UNSUPPORTED_DATA: 1003;
|
|
59
|
+
/** No status code was provided even though one was expected. */
|
|
60
|
+
readonly NO_STATUS_RECEIVED: 1005;
|
|
61
|
+
/** Connection was closed abnormally (e.g., without sending a close frame). */
|
|
62
|
+
readonly ABNORMAL_CLOSURE: 1006;
|
|
63
|
+
/** Received message data inconsistent with the message type. */
|
|
64
|
+
readonly INVALID_PAYLOAD: 1007;
|
|
65
|
+
/** Received a message that violates policy. */
|
|
66
|
+
readonly POLICY_VIOLATION: 1008;
|
|
67
|
+
/** Received a message that is too big to process. */
|
|
68
|
+
readonly MESSAGE_TOO_BIG: 1009;
|
|
69
|
+
/** Server encountered an unexpected condition that prevented it from fulfilling the request. */
|
|
70
|
+
readonly INTERNAL_ERROR: 1011;
|
|
71
|
+
/** Connection was closed due to TLS handshake failure. */
|
|
72
|
+
readonly TLS_HANDSHAKE_FAILED: 1015;
|
|
73
|
+
} = {
|
|
74
|
+
NORMAL_CLOSURE: 1000,
|
|
75
|
+
GOING_AWAY: 1001,
|
|
76
|
+
PROTOCOL_ERROR: 1002,
|
|
77
|
+
UNSUPPORTED_DATA: 1003,
|
|
78
|
+
NO_STATUS_RECEIVED: 1005,
|
|
79
|
+
ABNORMAL_CLOSURE: 1006,
|
|
80
|
+
INVALID_PAYLOAD: 1007,
|
|
81
|
+
POLICY_VIOLATION: 1008,
|
|
82
|
+
MESSAGE_TOO_BIG: 1009,
|
|
83
|
+
INTERNAL_ERROR: 1011,
|
|
84
|
+
TLS_HANDSHAKE_FAILED: 1015
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Type for WebSocket close code values.
|
|
89
|
+
* @beta
|
|
90
|
+
*/
|
|
91
|
+
export type WebSocketCloseCodeValue = (typeof WebSocketCloseCode)[keyof typeof WebSocketCloseCode];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Human-readable descriptions for WebSocket close codes.
|
|
95
|
+
* @beta
|
|
96
|
+
*/
|
|
97
|
+
export const WebSocketCloseCodeDescriptions: Record<WebSocketCloseCodeValue, string> = {
|
|
98
|
+
[WebSocketCloseCode.NORMAL_CLOSURE]: 'Normal Closure',
|
|
99
|
+
[WebSocketCloseCode.GOING_AWAY]: 'Going Away',
|
|
100
|
+
[WebSocketCloseCode.PROTOCOL_ERROR]: 'Protocol Error',
|
|
101
|
+
[WebSocketCloseCode.UNSUPPORTED_DATA]: 'Unsupported Data',
|
|
102
|
+
[WebSocketCloseCode.NO_STATUS_RECEIVED]: 'No Status Received',
|
|
103
|
+
[WebSocketCloseCode.ABNORMAL_CLOSURE]: 'Abnormal Closure (connection lost)',
|
|
104
|
+
[WebSocketCloseCode.INVALID_PAYLOAD]: 'Invalid Payload',
|
|
105
|
+
[WebSocketCloseCode.POLICY_VIOLATION]: 'Policy Violation',
|
|
106
|
+
[WebSocketCloseCode.MESSAGE_TOO_BIG]: 'Message Too Big',
|
|
107
|
+
[WebSocketCloseCode.INTERNAL_ERROR]: 'Internal Error',
|
|
108
|
+
[WebSocketCloseCode.TLS_HANDSHAKE_FAILED]: 'TLS Handshake Failed'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns a human-readable description for a WebSocket close code.
|
|
113
|
+
* @beta
|
|
114
|
+
*/
|
|
115
|
+
export function getWebSocketCloseReason(code: number): string {
|
|
116
|
+
return WebSocketCloseCodeDescriptions[code as WebSocketCloseCodeValue] || 'Unknown';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns a human-readable string for a WebSocket ready state.
|
|
121
|
+
* @beta
|
|
122
|
+
*/
|
|
123
|
+
export function getWebSocketReadyStateString(readyState: number): string {
|
|
124
|
+
switch (readyState) {
|
|
125
|
+
case 0:
|
|
126
|
+
return 'CONNECTING';
|
|
127
|
+
case 1:
|
|
128
|
+
return 'OPEN';
|
|
129
|
+
case 2:
|
|
130
|
+
return 'CLOSING';
|
|
131
|
+
case 3:
|
|
132
|
+
return 'CLOSED';
|
|
133
|
+
default:
|
|
134
|
+
return `UNKNOWN(${readyState})`;
|
|
135
|
+
}
|
|
136
|
+
}
|