chore: extract ws server util (#29247)
This commit is contained in:
parent
aeafd44726
commit
aff6cf3c83
|
|
@ -17,7 +17,7 @@
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { maybeFindValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
|
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 type { ExpectZone } from '../utils/stackTrace';
|
||||||
import { captureRawStack, captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
|
import { captureRawStack, captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
|
||||||
import { isUnderTest } from '../utils';
|
import { isUnderTest } from '../utils';
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { Electron, ElectronApplication } from './electron';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { Stream } from './stream';
|
import { Stream } from './stream';
|
||||||
import { WritableStream } from './writableStream';
|
import { WritableStream } from './writableStream';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import { SelectorsOwner } from './selectors';
|
import { SelectorsOwner } from './selectors';
|
||||||
import { Android, AndroidSocket, AndroidDevice } from './android';
|
import { Android, AndroidSocket, AndroidDevice } from './android';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import type { BrowserContext } from './browserContext';
|
import type { BrowserContext } from './browserContext';
|
||||||
import type { LocalUtils } from './localUtils';
|
import type { LocalUtils } from './localUtils';
|
||||||
import type { Route } from './network';
|
import type { Route } from './network';
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import { debugLogger } from './debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import { createSocket } from '../utils/happy-eyeballs';
|
import { createSocket } from '../utils/happy-eyeballs';
|
||||||
import { assert, createGuid, } from '../utils';
|
import { assert, createGuid, } from '../utils';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import { AndroidDevice } from '../server/android/android';
|
||||||
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
|
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
|
||||||
import { startProfiling, stopProfiling } from '../utils';
|
import { startProfiling, stopProfiling } from '../utils';
|
||||||
import { monotonicTime } 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';
|
export type ClientType = 'controller' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser-or-android';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,18 @@
|
||||||
* limitations under the License.
|
* 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 { Browser } from '../server/browser';
|
||||||
import type { Playwright } from '../server/playwright';
|
import type { Playwright } from '../server/playwright';
|
||||||
import { createPlaywright } from '../server/playwright';
|
import { createPlaywright } from '../server/playwright';
|
||||||
import { PlaywrightConnection } from './playwrightConnection';
|
import { PlaywrightConnection } from './playwrightConnection';
|
||||||
import type { ClientType } from './playwrightConnection';
|
import type { ClientType } from './playwrightConnection';
|
||||||
import type { LaunchOptions } from '../server/types';
|
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 { AndroidDevice } from '../server/android/android';
|
||||||
import type { SocksProxy } from '../common/socksProxy';
|
import type { SocksProxy } from '../common/socksProxy';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import { createHttpServer, userAgentVersionMatchesErrorMessage } from '../utils';
|
import { userAgentVersionMatchesErrorMessage } from '../utils';
|
||||||
import { perMessageDeflate } from '../server/transport';
|
import { WSServer } from '../utils/wsServer';
|
||||||
|
|
||||||
let lastConnectionId = 0;
|
|
||||||
const kConnectionSymbol = Symbol('kConnection');
|
|
||||||
|
|
||||||
type ServerOptions = {
|
type ServerOptions = {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -44,9 +38,8 @@ type ServerOptions = {
|
||||||
|
|
||||||
export class PlaywrightServer {
|
export class PlaywrightServer {
|
||||||
private _preLaunchedPlaywright: Playwright | undefined;
|
private _preLaunchedPlaywright: Playwright | undefined;
|
||||||
private _wsServer: WebSocketServer | undefined;
|
|
||||||
private _server: http.Server | undefined;
|
|
||||||
private _options: ServerOptions;
|
private _options: ServerOptions;
|
||||||
|
private _wsServer: WSServer;
|
||||||
|
|
||||||
constructor(options: ServerOptions) {
|
constructor(options: ServerOptions) {
|
||||||
this._options = options;
|
this._options = options;
|
||||||
|
|
@ -54,183 +47,85 @@ export class PlaywrightServer {
|
||||||
this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright;
|
this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright;
|
||||||
if (options.preLaunchedAndroidDevice)
|
if (options.preLaunchedAndroidDevice)
|
||||||
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright;
|
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright;
|
||||||
}
|
|
||||||
|
|
||||||
async listen(port: number = 0, hostname?: string): Promise<string> {
|
|
||||||
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<string>((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 browserSemaphore = new Semaphore(this._options.maxConnections);
|
||||||
const controllerSemaphore = new Semaphore(1);
|
const controllerSemaphore = new Semaphore(1);
|
||||||
const reuseBrowserSemaphore = 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'] || '');
|
this._wsServer = new WSServer({
|
||||||
if (uaError) {
|
onUpgrade: (request, socket) => {
|
||||||
socket.write(`HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}`);
|
const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || '');
|
||||||
socket.destroy();
|
if (uaError)
|
||||||
return;
|
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'] || '';
|
async listen(port: number = 0, hostname?: string): Promise<string> {
|
||||||
const launchOptionsHeaderValue = Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader;
|
return this._wsServer.listen(port, hostname, this._options.path);
|
||||||
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 close() {
|
async close() {
|
||||||
const server = this._wsServer;
|
await this._wsServer.close();
|
||||||
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<void> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import { ProgressController } from '../progress';
|
||||||
import { CRBrowser } from '../chromium/crBrowser';
|
import { CRBrowser } from '../chromium/crBrowser';
|
||||||
import { helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
import { PipeTransport } from '../../protocol/transport';
|
import { PipeTransport } from '../../protocol/transport';
|
||||||
import { RecentLogsCollector } from '../../common/debugLogger';
|
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||||
import { TimeoutSettings } from '../../common/timeoutSettings';
|
import { TimeoutSettings } from '../../common/timeoutSettings';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { Page } from './page';
|
||||||
import { Download } from './download';
|
import { Download } from './download';
|
||||||
import type { ProxySettings } from './types';
|
import type { ProxySettings } from './types';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
import type { RecentLogsCollector } from '../common/debugLogger';
|
import type { RecentLogsCollector } from '../utils/debugLogger';
|
||||||
import type { CallMetadata } from './instrumentation';
|
import type { CallMetadata } from './instrumentation';
|
||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
|
||||||
import { debugMode } from '../utils';
|
import { debugMode } from '../utils';
|
||||||
import { existsAsync } from '../utils/fileUtils';
|
import { existsAsync } from '../utils/fileUtils';
|
||||||
import { helper } from './helper';
|
import { helper } from './helper';
|
||||||
import { RecentLogsCollector } from '../common/debugLogger';
|
import { RecentLogsCollector } from '../utils/debugLogger';
|
||||||
import type { CallMetadata } from './instrumentation';
|
import type { CallMetadata } from './instrumentation';
|
||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
import { ManualPromise } from '../utils/manualPromise';
|
import { ManualPromise } from '../utils/manualPromise';
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import { getUserAgent } from '../../utils/userAgent';
|
||||||
import { wrapInASCIIBox } from '../../utils/ascii';
|
import { wrapInASCIIBox } from '../../utils/ascii';
|
||||||
import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils';
|
import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils';
|
||||||
import { removeFolders } from '../../utils/fileUtils';
|
import { removeFolders } from '../../utils/fileUtils';
|
||||||
import { RecentLogsCollector } from '../../common/debugLogger';
|
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import type { Progress } from '../progress';
|
import type { Progress } from '../progress';
|
||||||
import { ProgressController } from '../progress';
|
import { ProgressController } from '../progress';
|
||||||
import { TimeoutSettings } from '../../common/timeoutSettings';
|
import { TimeoutSettings } from '../../common/timeoutSettings';
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import { type RegisteredListener, assert, eventsHelper } from '../../utils';
|
||||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||||
import type { Protocol } from './protocol';
|
import type { Protocol } from './protocol';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type { RecentLogsCollector } from '../../common/debugLogger';
|
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import { debugLogger } from '../../common/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
import type { ProtocolLogger } from '../types';
|
import type { ProtocolLogger } from '../types';
|
||||||
import { helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
import { ProtocolError } from '../protocolError';
|
import { ProtocolError } from '../protocolError';
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ import type { BrowserOptions, BrowserProcess } from '../browser';
|
||||||
import type { Playwright } from '../playwright';
|
import type { Playwright } from '../playwright';
|
||||||
import type * as childProcess from 'child_process';
|
import type * as childProcess from 'child_process';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import { RecentLogsCollector } from '../../common/debugLogger';
|
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import { serverSideCallMetadata, SdkObject } from '../instrumentation';
|
import { serverSideCallMetadata, SdkObject } from '../instrumentation';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||||
import type { Protocol } from './protocol';
|
import type { Protocol } from './protocol';
|
||||||
import type { RecentLogsCollector } from '../../common/debugLogger';
|
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import { debugLogger } from '../../common/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
import type { ProtocolLogger } from '../types';
|
import type { ProtocolLogger } from '../types';
|
||||||
import { helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
import { ProtocolError } from '../protocolError';
|
import { ProtocolError } from '../protocolError';
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { FFNetworkManager } from './ffNetworkManager';
|
||||||
import type { Protocol } from './protocol';
|
import type { Protocol } from './protocol';
|
||||||
import type { Progress } from '../progress';
|
import type { Progress } from '../progress';
|
||||||
import { splitErrorMessage } from '../../utils/stackTrace';
|
import { splitErrorMessage } from '../../utils/stackTrace';
|
||||||
import { debugLogger } from '../../common/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
import { ManualPromise } from '../../utils/manualPromise';
|
import { ManualPromise } from '../../utils/manualPromise';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import { TargetClosedError } from '../errors';
|
import { TargetClosedError } from '../errors';
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import type { Progress } from './progress';
|
||||||
import { ProgressController } from './progress';
|
import { ProgressController } from './progress';
|
||||||
import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils';
|
import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils';
|
||||||
import { ManualPromise } from '../utils/manualPromise';
|
import { ManualPromise } from '../utils/manualPromise';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import type { CallMetadata } from './instrumentation';
|
import type { CallMetadata } from './instrumentation';
|
||||||
import { serverSideCallMetadata, SdkObject } from './instrumentation';
|
import { serverSideCallMetadata, SdkObject } from './instrumentation';
|
||||||
import type { InjectedScript, ElementStateWithoutStable, FrameExpectParams } from './injected/injectedScript';
|
import type { InjectedScript, ElementStateWithoutStable, FrameExpectParams } from './injected/injectedScript';
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type { Progress } from './progress';
|
import type { Progress } from './progress';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import type { RegisteredListener } from '../utils/eventsHelper';
|
import type { RegisteredListener } from '../utils/eventsHelper';
|
||||||
import { eventsHelper } from '../utils/eventsHelper';
|
import { eventsHelper } from '../utils/eventsHelper';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import type { Progress } from './progress';
|
||||||
import { ProgressController } from './progress';
|
import { ProgressController } from './progress';
|
||||||
import { LongStandingScope, assert, isError } from '../utils';
|
import { LongStandingScope, assert, isError } from '../utils';
|
||||||
import { ManualPromise } from '../utils/manualPromise';
|
import { ManualPromise } from '../utils/manualPromise';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import type { ImageComparatorOptions } from '../utils/comparators';
|
import type { ImageComparatorOptions } from '../utils/comparators';
|
||||||
import { getComparator } from '../utils/comparators';
|
import { getComparator } from '../utils/comparators';
|
||||||
import type { CallMetadata } from './instrumentation';
|
import type { CallMetadata } from './instrumentation';
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from './transport';
|
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from './transport';
|
||||||
import { makeWaitForNextTask } from '../utils';
|
import { makeWaitForNextTask } from '../utils';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
|
|
||||||
export class PipeTransport implements ConnectionTransport {
|
export class PipeTransport implements ConnectionTransport {
|
||||||
private _pipeRead: NodeJS.ReadableStream;
|
private _pipeRead: NodeJS.ReadableStream;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { Selectors } from './selectors';
|
||||||
import { WebKit } from './webkit/webkit';
|
import { WebKit } from './webkit/webkit';
|
||||||
import type { CallMetadata } from './instrumentation';
|
import type { CallMetadata } from './instrumentation';
|
||||||
import { createInstrumentation, SdkObject } from './instrumentation';
|
import { createInstrumentation, SdkObject } from './instrumentation';
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import type { Page } from './page';
|
import type { Page } from './page';
|
||||||
import { DebugController } from './debugController';
|
import { DebugController } from './debugController';
|
||||||
import type { Language } from '../utils/isomorphic/locatorGenerators';
|
import type { Language } from '../utils/isomorphic/locatorGenerators';
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { TimeoutError } from './errors';
|
import { TimeoutError } from './errors';
|
||||||
import { assert, monotonicTime } from '../utils';
|
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 { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||||
import type { ElementHandle } from './dom';
|
import type { ElementHandle } from './dom';
|
||||||
import { ManualPromise } from '../utils/manualPromise';
|
import { ManualPromise } from '../utils/manualPromise';
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import childProcess from 'child_process';
|
import childProcess from 'child_process';
|
||||||
import { existsAsync } from '../../utils/fileUtils';
|
import { existsAsync } from '../../utils/fileUtils';
|
||||||
import { debugLogger } from '../../common/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
import { ManualPromise } from '../../utils/manualPromise';
|
import { ManualPromise } from '../../utils/manualPromise';
|
||||||
import { colors, progress as ProgressBar } from '../../utilsBundle';
|
import { colors, progress as ProgressBar } from '../../utilsBundle';
|
||||||
import { browserDirectoryToMarkerFilePath } from '.';
|
import { browserDirectoryToMarkerFilePath } from '.';
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import { transformCommandsForRoot, dockerVersion, readDockerVersionSync } from '
|
||||||
import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies';
|
import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies';
|
||||||
import { downloadBrowserWithProgressBar, logPolitely } from './browserFetcher';
|
import { downloadBrowserWithProgressBar, logPolitely } from './browserFetcher';
|
||||||
export { writeDockerVersion } from './dependencies';
|
export { writeDockerVersion } from './dependencies';
|
||||||
import { debugLogger } from '../../common/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
|
|
||||||
const PACKAGE_PATH = path.join(__dirname, '..', '..', '..');
|
const PACKAGE_PATH = path.join(__dirname, '..', '..', '..');
|
||||||
const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
|
const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { BrowserContext } from '../../browserContext';
|
||||||
import { Page } from '../../page';
|
import { Page } from '../../page';
|
||||||
import type { RegisteredListener } from '../../../utils/eventsHelper';
|
import type { RegisteredListener } from '../../../utils/eventsHelper';
|
||||||
import { eventsHelper } 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 { Frame } from '../../frames';
|
||||||
import type { SnapshotData } from './snapshotterInjected';
|
import type { SnapshotData } from './snapshotterInjected';
|
||||||
import { frameSnapshotStreamer } from './snapshotterInjected';
|
import { frameSnapshotStreamer } from './snapshotterInjected';
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import { EventEmitter } from 'events';
|
||||||
import { assert } from '../../utils';
|
import { assert } from '../../utils';
|
||||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||||
import type { Protocol } from './protocol';
|
import type { Protocol } from './protocol';
|
||||||
import type { RecentLogsCollector } from '../../common/debugLogger';
|
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import { debugLogger } from '../../common/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
import type { ProtocolLogger } from '../types';
|
import type { ProtocolLogger } from '../types';
|
||||||
import { helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
import { ProtocolError } from '../protocolError';
|
import { ProtocolError } from '../protocolError';
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput';
|
||||||
import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
|
import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
|
||||||
import { WKProvisionalPage } from './wkProvisionalPage';
|
import { WKProvisionalPage } from './wkProvisionalPage';
|
||||||
import { WKWorkers } from './wkWorkers';
|
import { WKWorkers } from './wkWorkers';
|
||||||
import { debugLogger } from '../../common/debugLogger';
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
import { ManualPromise } from '../../utils/manualPromise';
|
import { ManualPromise } from '../../utils/manualPromise';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import { TargetClosedError } from '../errors';
|
import { TargetClosedError } from '../errors';
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export * from './network';
|
||||||
export * from './processLauncher';
|
export * from './processLauncher';
|
||||||
export * from './profiler';
|
export * from './profiler';
|
||||||
export * from './rtti';
|
export * from './rtti';
|
||||||
|
export * from './semaphore';
|
||||||
export * from './spawnAsync';
|
export * from './spawnAsync';
|
||||||
export * from './stackTrace';
|
export * from './stackTrace';
|
||||||
export * from './task';
|
export * from './task';
|
||||||
|
|
|
||||||
50
packages/playwright-core/src/utils/semaphore.ts
Normal file
50
packages/playwright-core/src/utils/semaphore.ts
Normal file
|
|
@ -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<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
packages/playwright-core/src/utils/wsServer.ts
Normal file
146
packages/playwright-core/src/utils/wsServer.ts
Normal file
|
|
@ -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<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<string> {
|
||||||
|
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<string>((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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue