"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.launchGridServer = launchGridServer; var _debug = _interopRequireDefault(require("debug")); var _path = _interopRequireDefault(require("path")); var _assert = _interopRequireDefault(require("assert")); var _events = require("events"); var _url = require("url"); var _httpServer = require("../utils/httpServer"); var _utils = require("../utils/utils"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * 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. */ const WSErrors = { NO_ERROR: { code: 1000, reason: '' }, AUTH_FAILED: { code: 1008, reason: 'Grid authentication failed' }, AGENT_CREATION_FAILED: { code: 1013, reason: 'Grid agent creationg failed' }, AGENT_NOT_FOUND: { code: 1013, reason: 'Grid agent registration failed - agent with given ID not found' }, AGENT_NOT_CONNECTED: { code: 1013, reason: 'Grid worker registration failed - agent has unsupported status' }, AGENT_CREATION_TIMED_OUT: { code: 1013, reason: 'Grid agent creationg timed out' }, AGENT_RETIRED: { code: 1000, reason: 'Grid agent was retired' }, CLIENT_SOCKET_ERROR: { code: 1011, reason: 'Grid client socket error' }, WORKER_SOCKET_ERROR: { code: 1011, reason: 'Grid worker socket error' }, CLIENT_PLAYWRIGHT_VERSION_MISMATCH: { code: 1013, reason: 'Grid Playwright and grid client versions are different' }, AGENT_PLAYWRIGHT_VERSION_MISMATCH: { code: 1013, reason: 'Grid Playwright and grid agent versions are different' }, GRID_SHUTDOWN: { code: 1000, reason: 'Grid was shutdown' }, AGENT_MANUALLY_STOPPED: { code: 1000, reason: 'Grid agent was manually stopped' } }; class GridWorker extends _events.EventEmitter { constructor(clientSocket) { super(); this.workerId = (0, _utils.createGuid)(); this._workerSocket = void 0; this._clientSocket = void 0; this._log = void 0; this._log = (0, _debug.default)(`[worker ${this.workerId}]`); this._clientSocket = clientSocket; clientSocket.on('close', (code, reason) => this.closeWorker(WSErrors.NO_ERROR)); clientSocket.on('error', error => this.closeWorker(WSErrors.CLIENT_SOCKET_ERROR)); } workerConnected(workerSocket) { this._log('connected'); this._workerSocket = workerSocket; workerSocket.on('close', (code, reason) => this.closeWorker(WSErrors.NO_ERROR)); workerSocket.on('error', error => this.closeWorker(WSErrors.WORKER_SOCKET_ERROR)); this._clientSocket.on('message', data => workerSocket.send(data)); workerSocket.on('message', data => this._clientSocket.send(data)); this._clientSocket.send('run'); } closeWorker(errorCode) { var _this$_workerSocket; this._log('close'); (_this$_workerSocket = this._workerSocket) === null || _this$_workerSocket === void 0 ? void 0 : _this$_workerSocket.close(errorCode.code, errorCode.reason); this._clientSocket.close(errorCode.code, errorCode.reason); this.emit('close'); } debugInfo() { return { worker: !!this._workerSocket, client: !!this._clientSocket }; } } class GridAgent extends _events.EventEmitter { constructor(capacity = Infinity, creationTimeout = 5 * 60_000) { super(); this._capacity = void 0; this.agentId = (0, _utils.createGuid)(); this._ws = void 0; this._workers = new Map(); this._status = 'none'; this._workersWaitingForAgentConnected = []; this._retireTimeout = void 0; this._log = void 0; this._agentCreationTimeout = void 0; this._capacity = capacity; this._log = (0, _debug.default)(`[agent ${this.agentId}]`); this.setStatus('created'); this._agentCreationTimeout = setTimeout(() => { this.closeAgent(WSErrors.AGENT_CREATION_TIMED_OUT); }, creationTimeout); } status() { return this._status; } setStatus(status) { this._log(`status ${this._status} => ${status}`); this._status = status; } agentConnected(ws) { clearTimeout(this._agentCreationTimeout); this.setStatus('connected'); this._ws = ws; for (const worker of this._workersWaitingForAgentConnected) { this._log(`send worker id: ${worker.workerId}`); ws.send(worker.workerId); } this._workersWaitingForAgentConnected = []; } canCreateWorker() { return this._workers.size < this._capacity; } async createWorker(clientSocket) { if (this._retireTimeout) clearTimeout(this._retireTimeout); if (this._ws) this.setStatus('connected'); const worker = new GridWorker(clientSocket); this._log(`create worker: ${worker.workerId}`); this._workers.set(worker.workerId, worker); worker.on('close', () => { this._workers.delete(worker.workerId); if (!this._workers.size) { this.setStatus('retiring'); if (this._retireTimeout) clearTimeout(this._retireTimeout); this._retireTimeout = setTimeout(() => this.closeAgent(WSErrors.AGENT_RETIRED), 30000); } }); if (this._ws) { this._log(`send worker id: ${worker.workerId}`); this._ws.send(worker.workerId); } else { this._workersWaitingForAgentConnected.push(worker); } } workerConnected(workerId, ws) { this._log(`worker connected: ${workerId}`); const worker = this._workers.get(workerId); worker.workerConnected(ws); } closeAgent(errorCode) { var _this$_ws; for (const worker of this._workersWaitingForAgentConnected) worker.closeWorker(errorCode); for (const worker of this._workers.values()) worker.closeWorker(errorCode); this._log('close'); (_this$_ws = this._ws) === null || _this$_ws === void 0 ? void 0 : _this$_ws.close(errorCode.code, errorCode.reason); this.emit('close'); } } class GridServer { constructor(factory, authToken = '') { this._server = void 0; this._wsServer = void 0; this._agents = new Map(); this._log = void 0; this._authToken = void 0; this._factory = void 0; this._pwVersion = void 0; this._log = (0, _debug.default)(`[grid]`); this._authToken = authToken || ''; this._server = new _httpServer.HttpServer(); this._factory = factory; this._pwVersion = (0, _utils.getPlaywrightVersion)(true /* majorMinorOnly */ ); this._server.routePath(this._securePath('/'), (request, response) => { response.statusCode = 200; response.setHeader('Content-Type', 'text/html'); response.end(this._state()); return true; }); this._server.routePath(this._securePath('/stopAll'), (request, response) => { for (const agent of this._agents.values()) agent.closeAgent(WSErrors.AGENT_MANUALLY_STOPPED); response.statusCode = 302; response.setHeader('Location', this._securePath('/')); response.end(); return true; }); this._wsServer = this._server.createWebSocketServer(); this._wsServer.shouldHandle = request => { this._log(request.url); if (request.url.startsWith(this._securePath('/claimWorker'))) { // shouldHandle claims it accepts promise, except it doesn't. return true; } if (request.url.startsWith('/registerAgent') || request.url.startsWith('/registerWorker')) { const params = new _url.URL('http://localhost/' + request.url).searchParams; const agentId = params.get('agentId'); return !!agentId && this._agents.has(agentId); } return false; }; this._wsServer.on('connection', async (ws, request) => { var _request$url, _request$url2, _request$url3; if ((_request$url = request.url) !== null && _request$url !== void 0 && _request$url.startsWith(this._securePath('/claimWorker'))) { const params = new _url.URL('http://localhost/' + request.url).searchParams; if (params.get('pwVersion') !== this._pwVersion) { ws.close(WSErrors.CLIENT_PLAYWRIGHT_VERSION_MISMATCH.code, WSErrors.CLIENT_PLAYWRIGHT_VERSION_MISMATCH.reason); return; } const agent = [...this._agents.values()].find(w => w.canCreateWorker()) || this._createAgent(); if (!agent) { ws.close(WSErrors.AGENT_CREATION_FAILED.code, WSErrors.AGENT_CREATION_FAILED.reason); return; } agent.createWorker(ws); return; } if ((_request$url2 = request.url) !== null && _request$url2 !== void 0 && _request$url2.startsWith('/registerAgent')) { const params = new _url.URL('http://localhost/' + request.url).searchParams; if (params.get('pwVersion') !== this._pwVersion) { ws.close(WSErrors.AGENT_PLAYWRIGHT_VERSION_MISMATCH.code, WSErrors.AGENT_PLAYWRIGHT_VERSION_MISMATCH.reason); return; } const agentId = params.get('agentId'); const agent = this._agents.get(agentId); if (!agent) { ws.close(WSErrors.AGENT_NOT_FOUND.code, WSErrors.AGENT_NOT_FOUND.reason); return; } agent.agentConnected(ws); return; } if ((_request$url3 = request.url) !== null && _request$url3 !== void 0 && _request$url3.startsWith('/registerWorker')) { const params = new _url.URL('http://localhost/' + request.url).searchParams; const agentId = params.get('agentId'); const workerId = params.get('workerId'); const agent = this._agents.get(agentId); if (!agent) ws.close(WSErrors.AGENT_NOT_FOUND.code, WSErrors.AGENT_NOT_FOUND.reason);else if (agent.status() !== 'connected') ws.close(WSErrors.AGENT_NOT_CONNECTED.code, WSErrors.AGENT_NOT_CONNECTED.reason);else agent.workerConnected(workerId, ws); return; } }); } _createAgent() { const agent = new GridAgent(this._factory.capacity, this._factory.timeout); this._agents.set(agent.agentId, agent); agent.on('close', () => { this._agents.delete(agent.agentId); }); Promise.resolve().then(() => this._factory.launch({ agentId: agent.agentId, gridURL: this._server.urlPrefix(), playwrightVersion: (0, _utils.getPlaywrightVersion)() })).then(() => { this._log('created'); }).catch(e => { this._log('failed to launch agent ' + agent.agentId); console.error(e); agent.closeAgent(WSErrors.AGENT_CREATION_FAILED); }); return agent; } _securePath(suffix) { return this._authToken ? '/' + this._authToken + suffix : suffix; } _state() { return `
Grid Playwright Version: Agent Factory: Agents:
${this._pwVersion} ${this._factory.name} ${this._agents.size} (Stop All)

`; } async start(port) { await this._server.start(port); } urlPrefix() { return this._server.urlPrefix() + this._securePath('/'); } async stop() { for (const agent of this._agents.values()) agent.closeAgent(WSErrors.GRID_SHUTDOWN); (0, _assert.default)(this._agents.size === 0); await this._server.stop(); } } function mangle(sessionId) { return sessionId.replace(/\w{28}/, 'x'.repeat(28)); } async function launchGridServer(factoryPathOrPackageName, port, authToken) { if (!factoryPathOrPackageName) factoryPathOrPackageName = './simpleGridFactory'; let factory; try { factory = require(_path.default.resolve(factoryPathOrPackageName)); } catch (e) { factory = require(factoryPathOrPackageName); } if (!factory || !factory.launch || typeof factory.launch !== 'function') throw new Error('factory does not export `launch` method'); factory.name = factory.name || factoryPathOrPackageName; const gridServer = new GridServer(factory, authToken); await gridServer.start(port); /* eslint-disable no-console */ console.log('Grid server is running at ' + gridServer.urlPrefix()); }