diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index b493e7d5d5..d12fb41dc5 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'events'; import type * as channels from '@protocol/channels'; import { maybeFindValidator, ValidationError, type ValidatorContext } from '../protocol/validator'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import type { ExpectZone } from '../utils/stackTrace'; import { captureRawStack, captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace'; import { isUnderTest } from '../utils'; diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index bc124d91fa..f468e3ff41 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -32,7 +32,7 @@ import { Electron, ElectronApplication } from './electron'; import type * as channels from '@protocol/channels'; import { Stream } from './stream'; import { WritableStream } from './writableStream'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import { SelectorsOwner } from './selectors'; import { Android, AndroidSocket, AndroidDevice } from './android'; import { Artifact } from './artifact'; diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index a05945d80c..5604ddc6eb 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import type { BrowserContext } from './browserContext'; import type { LocalUtils } from './localUtils'; import type { Route } from './network'; diff --git a/packages/playwright-core/src/common/socksProxy.ts b/packages/playwright-core/src/common/socksProxy.ts index 0c78611e93..ebdf7298f2 100644 --- a/packages/playwright-core/src/common/socksProxy.ts +++ b/packages/playwright-core/src/common/socksProxy.ts @@ -17,7 +17,7 @@ import EventEmitter from 'events'; import type { AddressInfo } from 'net'; import net from 'net'; -import { debugLogger } from './debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import { createSocket } from '../utils/happy-eyeballs'; import { assert, createGuid, } from '../utils'; diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts index c6898f7148..cce31207a8 100644 --- a/packages/playwright-core/src/remote/playwrightConnection.ts +++ b/packages/playwright-core/src/remote/playwrightConnection.ts @@ -27,7 +27,7 @@ import { AndroidDevice } from '../server/android/android'; import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher'; import { startProfiling, stopProfiling } from '../utils'; import { monotonicTime } from '../utils'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; export type ClientType = 'controller' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser-or-android'; diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index 664a5fce77..121cc2d83a 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -14,24 +14,18 @@ * limitations under the License. */ -import { wsServer } from '../utilsBundle'; -import type { WebSocketServer } from '../utilsBundle'; -import type http from 'http'; import type { Browser } from '../server/browser'; import type { Playwright } from '../server/playwright'; import { createPlaywright } from '../server/playwright'; import { PlaywrightConnection } from './playwrightConnection'; import type { ClientType } from './playwrightConnection'; import type { LaunchOptions } from '../server/types'; -import { ManualPromise } from '../utils/manualPromise'; +import { Semaphore } from '../utils/semaphore'; import type { AndroidDevice } from '../server/android/android'; import type { SocksProxy } from '../common/socksProxy'; -import { debugLogger } from '../common/debugLogger'; -import { createHttpServer, userAgentVersionMatchesErrorMessage } from '../utils'; -import { perMessageDeflate } from '../server/transport'; - -let lastConnectionId = 0; -const kConnectionSymbol = Symbol('kConnection'); +import { debugLogger } from '../utils/debugLogger'; +import { userAgentVersionMatchesErrorMessage } from '../utils'; +import { WSServer } from '../utils/wsServer'; type ServerOptions = { path: string; @@ -44,9 +38,8 @@ type ServerOptions = { export class PlaywrightServer { private _preLaunchedPlaywright: Playwright | undefined; - private _wsServer: WebSocketServer | undefined; - private _server: http.Server | undefined; private _options: ServerOptions; + private _wsServer: WSServer; constructor(options: ServerOptions) { this._options = options; @@ -54,183 +47,85 @@ export class PlaywrightServer { this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright; if (options.preLaunchedAndroidDevice) this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright; - } - async listen(port: number = 0, hostname?: string): Promise { - debugLogger.log('server', `Server started at ${new Date()}`); - - const server = createHttpServer((request: http.IncomingMessage, response: http.ServerResponse) => { - if (request.method === 'GET' && request.url === '/json') { - response.setHeader('Content-Type', 'application/json'); - response.end(JSON.stringify({ - wsEndpointPath: this._options.path, - })); - return; - } - response.end('Running'); - }); - server.on('error', error => debugLogger.log('server', String(error))); - this._server = server; - - const wsEndpoint = await new Promise((resolve, reject) => { - server.listen(port, hostname, () => { - const address = server.address(); - if (!address) { - reject(new Error('Could not bind server socket')); - return; - } - const wsEndpoint = typeof address === 'string' ? `${address}${this._options.path}` : `ws://${hostname || 'localhost'}:${address.port}${this._options.path}`; - resolve(wsEndpoint); - }).on('error', reject); - }); - - debugLogger.log('server', 'Listening at ' + wsEndpoint); - this._wsServer = new wsServer({ - noServer: true, - perMessageDeflate, - }); const browserSemaphore = new Semaphore(this._options.maxConnections); const controllerSemaphore = new Semaphore(1); const reuseBrowserSemaphore = new Semaphore(1); - if (process.env.PWTEST_SERVER_WS_HEADERS) { - this._wsServer.on('headers', (headers, request) => { - headers.push(process.env.PWTEST_SERVER_WS_HEADERS!); - }); - } - server.on('upgrade', (request, socket, head) => { - const pathname = new URL('http://localhost' + request.url!).pathname; - if (pathname !== this._options.path) { - socket.write(`HTTP/${request.httpVersion} 400 Bad Request\r\n\r\n`); - socket.destroy(); - return; - } - const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || ''); - if (uaError) { - socket.write(`HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}`); - socket.destroy(); - return; - } + this._wsServer = new WSServer({ + onUpgrade: (request, socket) => { + const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || ''); + if (uaError) + return { error: `HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}` }; + }, - this._wsServer?.handleUpgrade(request, socket, head, ws => this._wsServer?.emit('connection', ws, request)); + onHeaders: headers => { + if (process.env.PWTEST_SERVER_WS_HEADERS) + headers.push(process.env.PWTEST_SERVER_WS_HEADERS!); + }, + + onConnection: (request, url, ws, id) => { + const browserHeader = request.headers['x-playwright-browser']; + const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null; + const proxyHeader = request.headers['x-playwright-proxy']; + const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader); + + const launchOptionsHeader = request.headers['x-playwright-launch-options'] || ''; + const launchOptionsHeaderValue = Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader; + const launchOptionsParam = url.searchParams.get('launch-options'); + let launchOptions: LaunchOptions = {}; + try { + launchOptions = JSON.parse(launchOptionsParam || launchOptionsHeaderValue); + } catch (e) { + } + + // Instantiate playwright for the extension modes. + const isExtension = this._options.mode === 'extension'; + if (isExtension) { + if (!this._preLaunchedPlaywright) + this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true }); + } + + let clientType: ClientType = 'launch-browser'; + let semaphore: Semaphore = browserSemaphore; + if (isExtension && url.searchParams.has('debug-controller')) { + clientType = 'controller'; + semaphore = controllerSemaphore; + } else if (isExtension) { + clientType = 'reuse-browser'; + semaphore = reuseBrowserSemaphore; + } else if (this._options.mode === 'launchServer') { + clientType = 'pre-launched-browser-or-android'; + semaphore = browserSemaphore; + } + + return new PlaywrightConnection( + semaphore.acquire(), + clientType, ws, + { socksProxyPattern: proxyValue, browserName, launchOptions }, + { + playwright: this._preLaunchedPlaywright, + browser: this._options.preLaunchedBrowser, + androidDevice: this._options.preLaunchedAndroidDevice, + socksProxy: this._options.preLaunchedSocksProxy, + }, + id, () => semaphore.release()); + }, + + onClose: async () => { + debugLogger.log('server', 'closing browsers'); + if (this._preLaunchedPlaywright) + await Promise.all(this._preLaunchedPlaywright.allBrowsers().map(browser => browser.close({ reason: 'Playwright Server stopped' }))); + debugLogger.log('server', 'closed browsers'); + } }); - this._wsServer.on('connection', (ws, request) => { - debugLogger.log('server', 'Connected client ws.extension=' + ws.extensions); - const url = new URL('http://localhost' + (request.url || '')); - const browserHeader = request.headers['x-playwright-browser']; - const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null; - const proxyHeader = request.headers['x-playwright-proxy']; - const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader); + } - const launchOptionsHeader = request.headers['x-playwright-launch-options'] || ''; - const launchOptionsHeaderValue = Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader; - const launchOptionsParam = url.searchParams.get('launch-options'); - let launchOptions: LaunchOptions = {}; - try { - launchOptions = JSON.parse(launchOptionsParam || launchOptionsHeaderValue); - } catch (e) { - } - - const id = String(++lastConnectionId); - debugLogger.log('server', `[${id}] serving connection: ${request.url}`); - - // Instantiate playwright for the extension modes. - const isExtension = this._options.mode === 'extension'; - if (isExtension) { - if (!this._preLaunchedPlaywright) - this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true }); - } - - let clientType: ClientType = 'launch-browser'; - let semaphore: Semaphore = browserSemaphore; - if (isExtension && url.searchParams.has('debug-controller')) { - clientType = 'controller'; - semaphore = controllerSemaphore; - } else if (isExtension) { - clientType = 'reuse-browser'; - semaphore = reuseBrowserSemaphore; - } else if (this._options.mode === 'launchServer') { - clientType = 'pre-launched-browser-or-android'; - semaphore = browserSemaphore; - } - - const connection = new PlaywrightConnection( - semaphore.acquire(), - clientType, ws, - { socksProxyPattern: proxyValue, browserName, launchOptions }, - { - playwright: this._preLaunchedPlaywright, - browser: this._options.preLaunchedBrowser, - androidDevice: this._options.preLaunchedAndroidDevice, - socksProxy: this._options.preLaunchedSocksProxy, - }, - id, () => semaphore.release()); - (ws as any)[kConnectionSymbol] = connection; - }); - - return wsEndpoint; + async listen(port: number = 0, hostname?: string): Promise { + return this._wsServer.listen(port, hostname, this._options.path); } async close() { - const server = this._wsServer; - if (!server) - return; - debugLogger.log('server', 'closing websocket server'); - const waitForClose = new Promise(f => server.close(f)); - // First disconnect all remaining clients. - await Promise.all(Array.from(server.clients).map(async ws => { - const connection = (ws as any)[kConnectionSymbol] as PlaywrightConnection | undefined; - if (connection) - await connection.close(); - try { - ws.terminate(); - } catch (e) { - } - })); - await waitForClose; - debugLogger.log('server', 'closing http server'); - if (this._server) - await new Promise(f => this._server!.close(f)); - this._wsServer = undefined; - this._server = undefined; - debugLogger.log('server', 'closed server'); - - debugLogger.log('server', 'closing browsers'); - if (this._preLaunchedPlaywright) - await Promise.all(this._preLaunchedPlaywright.allBrowsers().map(browser => browser.close({ reason: 'Playwright Server stopped' }))); - debugLogger.log('server', 'closed browsers'); - } -} - -export class Semaphore { - private _max: number; - private _acquired = 0; - private _queue: ManualPromise[] = []; - - constructor(max: number) { - this._max = max; - } - - setMax(max: number) { - this._max = max; - } - - acquire(): Promise { - const lock = new ManualPromise(); - this._queue.push(lock); - this._flush(); - return lock; - } - - release() { - --this._acquired; - this._flush(); - } - - private _flush() { - while (this._acquired < this._max && this._queue.length) { - ++this._acquired; - this._queue.shift()!.resolve(); - } + await this._wsServer.close(); } } diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index fd54088ecf..0b4cb331b0 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -30,7 +30,7 @@ import { ProgressController } from '../progress'; import { CRBrowser } from '../chromium/crBrowser'; import { helper } from '../helper'; import { PipeTransport } from '../../protocol/transport'; -import { RecentLogsCollector } from '../../common/debugLogger'; +import { RecentLogsCollector } from '../../utils/debugLogger'; import { gracefullyCloseSet } from '../../utils/processLauncher'; import { TimeoutSettings } from '../../common/timeoutSettings'; import type * as channels from '@protocol/channels'; diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 425b812b31..15d8e3b792 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -21,7 +21,7 @@ import { Page } from './page'; import { Download } from './download'; import type { ProxySettings } from './types'; import type { ChildProcess } from 'child_process'; -import type { RecentLogsCollector } from '../common/debugLogger'; +import type { RecentLogsCollector } from '../utils/debugLogger'; import type { CallMetadata } from './instrumentation'; import { SdkObject } from './instrumentation'; import { Artifact } from './artifact'; diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 7540e40090..cfa8862b84 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -35,7 +35,7 @@ import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings'; import { debugMode } from '../utils'; import { existsAsync } from '../utils/fileUtils'; import { helper } from './helper'; -import { RecentLogsCollector } from '../common/debugLogger'; +import { RecentLogsCollector } from '../utils/debugLogger'; import type { CallMetadata } from './instrumentation'; import { SdkObject } from './instrumentation'; import { ManualPromise } from '../utils/manualPromise'; diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index c3988d4648..9c1fa53253 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -37,7 +37,7 @@ import { getUserAgent } from '../../utils/userAgent'; import { wrapInASCIIBox } from '../../utils/ascii'; import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils'; import { removeFolders } from '../../utils/fileUtils'; -import { RecentLogsCollector } from '../../common/debugLogger'; +import { RecentLogsCollector } from '../../utils/debugLogger'; import type { Progress } from '../progress'; import { ProgressController } from '../progress'; import { TimeoutSettings } from '../../common/timeoutSettings'; diff --git a/packages/playwright-core/src/server/chromium/crConnection.ts b/packages/playwright-core/src/server/chromium/crConnection.ts index bcf883f238..ccec4663d1 100644 --- a/packages/playwright-core/src/server/chromium/crConnection.ts +++ b/packages/playwright-core/src/server/chromium/crConnection.ts @@ -19,8 +19,8 @@ import { type RegisteredListener, assert, eventsHelper } from '../../utils'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { Protocol } from './protocol'; import { EventEmitter } from 'events'; -import type { RecentLogsCollector } from '../../common/debugLogger'; -import { debugLogger } from '../../common/debugLogger'; +import type { RecentLogsCollector } from '../../utils/debugLogger'; +import { debugLogger } from '../../utils/debugLogger'; import type { ProtocolLogger } from '../types'; import { helper } from '../helper'; import { ProtocolError } from '../protocolError'; diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 80ce04d809..ee0e8608f0 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -40,7 +40,7 @@ import type { BrowserOptions, BrowserProcess } from '../browser'; import type { Playwright } from '../playwright'; import type * as childProcess from 'child_process'; import * as readline from 'readline'; -import { RecentLogsCollector } from '../../common/debugLogger'; +import { RecentLogsCollector } from '../../utils/debugLogger'; import { serverSideCallMetadata, SdkObject } from '../instrumentation'; import type * as channels from '@protocol/channels'; diff --git a/packages/playwright-core/src/server/firefox/ffConnection.ts b/packages/playwright-core/src/server/firefox/ffConnection.ts index 3d5070440b..f36dba4b23 100644 --- a/packages/playwright-core/src/server/firefox/ffConnection.ts +++ b/packages/playwright-core/src/server/firefox/ffConnection.ts @@ -18,8 +18,8 @@ import { EventEmitter } from 'events'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { Protocol } from './protocol'; -import type { RecentLogsCollector } from '../../common/debugLogger'; -import { debugLogger } from '../../common/debugLogger'; +import type { RecentLogsCollector } from '../../utils/debugLogger'; +import { debugLogger } from '../../utils/debugLogger'; import type { ProtocolLogger } from '../types'; import { helper } from '../helper'; import { ProtocolError } from '../protocolError'; diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 8b3876036c..d2d2a03412 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -32,7 +32,7 @@ import { FFNetworkManager } from './ffNetworkManager'; import type { Protocol } from './protocol'; import type { Progress } from '../progress'; import { splitErrorMessage } from '../../utils/stackTrace'; -import { debugLogger } from '../../common/debugLogger'; +import { debugLogger } from '../../utils/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; import { BrowserContext } from '../browserContext'; import { TargetClosedError } from '../errors'; diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 1a7f9d77ec..f7d0f5cbf7 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -31,7 +31,7 @@ import type { Progress } from './progress'; import { ProgressController } from './progress'; import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import type { CallMetadata } from './instrumentation'; import { serverSideCallMetadata, SdkObject } from './instrumentation'; import type { InjectedScript, ElementStateWithoutStable, FrameExpectParams } from './injected/injectedScript'; diff --git a/packages/playwright-core/src/server/helper.ts b/packages/playwright-core/src/server/helper.ts index 38246c9fd6..84fd0174f6 100644 --- a/packages/playwright-core/src/server/helper.ts +++ b/packages/playwright-core/src/server/helper.ts @@ -18,7 +18,7 @@ import type { EventEmitter } from 'events'; import type * as types from './types'; import type { Progress } from './progress'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import type { RegisteredListener } from '../utils/eventsHelper'; import { eventsHelper } from '../utils/eventsHelper'; diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 0b043d1ff4..661ca8a50a 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -33,7 +33,7 @@ import type { Progress } from './progress'; import { ProgressController } from './progress'; import { LongStandingScope, assert, isError } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import type { ImageComparatorOptions } from '../utils/comparators'; import { getComparator } from '../utils/comparators'; import type { CallMetadata } from './instrumentation'; diff --git a/packages/playwright-core/src/server/pipeTransport.ts b/packages/playwright-core/src/server/pipeTransport.ts index cd8e31cb94..a84ca67023 100644 --- a/packages/playwright-core/src/server/pipeTransport.ts +++ b/packages/playwright-core/src/server/pipeTransport.ts @@ -17,7 +17,7 @@ import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from './transport'; import { makeWaitForNextTask } from '../utils'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; export class PipeTransport implements ConnectionTransport { private _pipeRead: NodeJS.ReadableStream; diff --git a/packages/playwright-core/src/server/playwright.ts b/packages/playwright-core/src/server/playwright.ts index cfa3e699ac..c4ffd518ce 100644 --- a/packages/playwright-core/src/server/playwright.ts +++ b/packages/playwright-core/src/server/playwright.ts @@ -24,7 +24,7 @@ import { Selectors } from './selectors'; import { WebKit } from './webkit/webkit'; import type { CallMetadata } from './instrumentation'; import { createInstrumentation, SdkObject } from './instrumentation'; -import { debugLogger } from '../common/debugLogger'; +import { debugLogger } from '../utils/debugLogger'; import type { Page } from './page'; import { DebugController } from './debugController'; import type { Language } from '../utils/isomorphic/locatorGenerators'; diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index 6f99322935..70de43b944 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -16,7 +16,7 @@ import { TimeoutError } from './errors'; import { assert, monotonicTime } from '../utils'; -import type { LogName } from '../common/debugLogger'; +import type { LogName } from '../utils/debugLogger'; import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation'; import type { ElementHandle } from './dom'; import { ManualPromise } from '../utils/manualPromise'; diff --git a/packages/playwright-core/src/server/registry/browserFetcher.ts b/packages/playwright-core/src/server/registry/browserFetcher.ts index 154edf1114..6f8da6e825 100644 --- a/packages/playwright-core/src/server/registry/browserFetcher.ts +++ b/packages/playwright-core/src/server/registry/browserFetcher.ts @@ -20,7 +20,7 @@ import os from 'os'; import path from 'path'; import childProcess from 'child_process'; import { existsAsync } from '../../utils/fileUtils'; -import { debugLogger } from '../../common/debugLogger'; +import { debugLogger } from '../../utils/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; import { colors, progress as ProgressBar } from '../../utilsBundle'; import { browserDirectoryToMarkerFilePath } from '.'; diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 8c3fe31287..fa3a1bbf78 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -31,7 +31,7 @@ import { transformCommandsForRoot, dockerVersion, readDockerVersionSync } from ' import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies'; import { downloadBrowserWithProgressBar, logPolitely } from './browserFetcher'; export { writeDockerVersion } from './dependencies'; -import { debugLogger } from '../../common/debugLogger'; +import { debugLogger } from '../../utils/debugLogger'; const PACKAGE_PATH = path.join(__dirname, '..', '..', '..'); const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin'); diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts index 30bb86c8bf..cb09ea5652 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts @@ -18,7 +18,7 @@ import { BrowserContext } from '../../browserContext'; import { Page } from '../../page'; import type { RegisteredListener } from '../../../utils/eventsHelper'; import { eventsHelper } from '../../../utils/eventsHelper'; -import { debugLogger } from '../../../common/debugLogger'; +import { debugLogger } from '../../../utils/debugLogger'; import type { Frame } from '../../frames'; import type { SnapshotData } from './snapshotterInjected'; import { frameSnapshotStreamer } from './snapshotterInjected'; diff --git a/packages/playwright-core/src/server/webkit/wkConnection.ts b/packages/playwright-core/src/server/webkit/wkConnection.ts index ff92cf5770..3eb777fd73 100644 --- a/packages/playwright-core/src/server/webkit/wkConnection.ts +++ b/packages/playwright-core/src/server/webkit/wkConnection.ts @@ -19,8 +19,8 @@ import { EventEmitter } from 'events'; import { assert } from '../../utils'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { Protocol } from './protocol'; -import type { RecentLogsCollector } from '../../common/debugLogger'; -import { debugLogger } from '../../common/debugLogger'; +import type { RecentLogsCollector } from '../../utils/debugLogger'; +import { debugLogger } from '../../utils/debugLogger'; import type { ProtocolLogger } from '../types'; import { helper } from '../helper'; import { ProtocolError } from '../protocolError'; diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index d3a95fec70..038010f456 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -42,7 +42,7 @@ import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput'; import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest'; import { WKProvisionalPage } from './wkProvisionalPage'; import { WKWorkers } from './wkWorkers'; -import { debugLogger } from '../../common/debugLogger'; +import { debugLogger } from '../../utils/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; import { BrowserContext } from '../browserContext'; import { TargetClosedError } from '../errors'; diff --git a/packages/playwright-core/src/common/debugLogger.ts b/packages/playwright-core/src/utils/debugLogger.ts similarity index 100% rename from packages/playwright-core/src/common/debugLogger.ts rename to packages/playwright-core/src/utils/debugLogger.ts diff --git a/packages/playwright-core/src/utils/index.ts b/packages/playwright-core/src/utils/index.ts index a33e73ba01..2595c40ff2 100644 --- a/packages/playwright-core/src/utils/index.ts +++ b/packages/playwright-core/src/utils/index.ts @@ -32,6 +32,7 @@ export * from './network'; export * from './processLauncher'; export * from './profiler'; export * from './rtti'; +export * from './semaphore'; export * from './spawnAsync'; export * from './stackTrace'; export * from './task'; diff --git a/packages/playwright-core/src/utils/semaphore.ts b/packages/playwright-core/src/utils/semaphore.ts new file mode 100644 index 0000000000..538e44fd9c --- /dev/null +++ b/packages/playwright-core/src/utils/semaphore.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ManualPromise } from './manualPromise'; + +export class Semaphore { + private _max: number; + private _acquired = 0; + private _queue: ManualPromise[] = []; + + constructor(max: number) { + this._max = max; + } + + setMax(max: number) { + this._max = max; + } + + acquire(): Promise { + const lock = new ManualPromise(); + this._queue.push(lock); + this._flush(); + return lock; + } + + release() { + --this._acquired; + this._flush(); + } + + private _flush() { + while (this._acquired < this._max && this._queue.length) { + ++this._acquired; + this._queue.shift()!.resolve(); + } + } +} diff --git a/packages/playwright-core/src/utils/wsServer.ts b/packages/playwright-core/src/utils/wsServer.ts new file mode 100644 index 0000000000..e510a7ae8e --- /dev/null +++ b/packages/playwright-core/src/utils/wsServer.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type http from 'http'; +import type stream from 'stream'; +import { createHttpServer } from '../utils'; +import type { WebSocketServer, WebSocket } from '../utilsBundle'; +import { wsServer } from '../utilsBundle'; +import { debugLogger } from './debugLogger'; + +let lastConnectionId = 0; +const kConnectionSymbol = Symbol('kConnection'); + +export const perMessageDeflate = { + zlibDeflateOptions: { + level: 3, + }, + zlibInflateOptions: { + chunkSize: 10 * 1024 + }, + threshold: 10 * 1024, +}; + +export type WSConnection = { + close: () => Promise; +}; + +export type WSServerDelegate = { + onHeaders: (headers: string[]) => void; + onUpgrade: (request: http.IncomingMessage, socket: stream.Duplex) => { error: string } | undefined; + onConnection: (request: http.IncomingMessage, url: URL, ws: WebSocket, id: string) => WSConnection; + onClose(): Promise; +}; + +export class WSServer { + private _wsServer: WebSocketServer | undefined; + server: http.Server | undefined; + private _delegate: WSServerDelegate; + + constructor(delegate: WSServerDelegate) { + this._delegate = delegate; + } + + async listen(port: number = 0, hostname: string | undefined, path: string): Promise { + debugLogger.log('server', `Server started at ${new Date()}`); + + const server = createHttpServer((request: http.IncomingMessage, response: http.ServerResponse) => { + if (request.method === 'GET' && request.url === '/json') { + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify({ + wsEndpointPath: path, + })); + return; + } + response.end('Running'); + }); + server.on('error', error => debugLogger.log('server', String(error))); + this.server = server; + + const wsEndpoint = await new Promise((resolve, reject) => { + server.listen(port, hostname, () => { + const address = server.address(); + if (!address) { + reject(new Error('Could not bind server socket')); + return; + } + const wsEndpoint = typeof address === 'string' ? `${address}${path}` : `ws://${hostname || 'localhost'}:${address.port}${path}`; + resolve(wsEndpoint); + }).on('error', reject); + }); + + debugLogger.log('server', 'Listening at ' + wsEndpoint); + + this._wsServer = new wsServer({ + noServer: true, + perMessageDeflate, + }); + + this._wsServer.on('headers', headers => this._delegate.onHeaders(headers)); + + server.on('upgrade', (request, socket, head) => { + const pathname = new URL('http://localhost' + request.url!).pathname; + if (pathname !== path) { + socket.write(`HTTP/${request.httpVersion} 400 Bad Request\r\n\r\n`); + socket.destroy(); + return; + } + const upgradeResult = this._delegate.onUpgrade(request, socket); + if (upgradeResult) { + socket.write(upgradeResult.error); + socket.destroy(); + return; + } + this._wsServer?.handleUpgrade(request, socket, head, ws => this._wsServer?.emit('connection', ws, request)); + }); + + this._wsServer.on('connection', (ws, request) => { + debugLogger.log('server', 'Connected client ws.extension=' + ws.extensions); + const url = new URL('http://localhost' + (request.url || '')); + const id = String(++lastConnectionId); + debugLogger.log('server', `[${id}] serving connection: ${request.url}`); + const connection = this._delegate.onConnection(request, url, ws, id); + (ws as any)[kConnectionSymbol] = connection; + }); + + return wsEndpoint; + } + + async close() { + const server = this._wsServer; + if (!server) + return; + debugLogger.log('server', 'closing websocket server'); + const waitForClose = new Promise(f => server.close(f)); + // First disconnect all remaining clients. + await Promise.all(Array.from(server.clients).map(async ws => { + const connection = (ws as any)[kConnectionSymbol] as WSConnection | undefined; + if (connection) + await connection.close(); + try { + ws.terminate(); + } catch (e) { + } + })); + await waitForClose; + debugLogger.log('server', 'closing http server'); + if (this.server) + await new Promise(f => this.server!.close(f)); + this._wsServer = undefined; + this.server = undefined; + debugLogger.log('server', 'closed server'); + } +}