From 60ebe5967ac0a0eec24a217cbbaf91556baad365 Mon Sep 17 00:00:00 2001 From: Andrey Belym Date: Fri, 26 May 2017 16:04:07 +0300 Subject: [PATCH] Implement uniform URL for connecting remote browsers (closes #1476) (#1478) --- src/browser/connection/gateway.js | 28 +++++++++++-- src/browser/connection/remotes-queue.js | 54 +++++++++++++++++++++++++ src/cli/remotes-wizard.js | 32 +++++++++------ 3 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 src/browser/connection/remotes-queue.js diff --git a/src/browser/connection/gateway.js b/src/browser/connection/gateway.js index 1eeaccc78a8..6d5a70397ff 100644 --- a/src/browser/connection/gateway.js +++ b/src/browser/connection/gateway.js @@ -1,5 +1,6 @@ import { readSync as read } from 'read-file-relative'; import { respond404, respond500, respondWithJSON, redirect, preventCaching } from '../../utils/http'; +import RemotesQueue from './remotes-queue'; // Const @@ -7,12 +8,14 @@ const IDLE_PAGE_SCRIPT = read('../../client/browser/idle-page/index.js'); const IDLE_PAGE_STYLE = read('../../client/browser/idle-page/styles.css'); const IDLE_PAGE_LOGO = read('../../client/browser/idle-page/logo.svg', true); - // Gateway export default class BrowserConnectionGateway { constructor (proxy) { - this.connections = {}; - this.domain = proxy.server1Info.domain; + this.connections = {}; + this.remotesQueue = new RemotesQueue(); + this.domain = proxy.server1Info.domain; + + this.connectUrl = `${this.domain}/browser/connect`; this._registerRoutes(proxy); } @@ -38,6 +41,9 @@ export default class BrowserConnectionGateway { this._dispatch('/browser/init-script/{id}', proxy, BrowserConnectionGateway.onInitScriptRequest); this._dispatch('/browser/init-script/{id}', proxy, BrowserConnectionGateway.onInitScriptResponse, 'POST'); + proxy.GET('/browser/connect', (req, res) => this._connectNextRemoteBrowser(req, res)); + proxy.GET('/browser/connect/', (req, res) => this._connectNextRemoteBrowser(req, res)); + proxy.GET('/browser/assets/index.js', { content: IDLE_PAGE_SCRIPT, contentType: 'application/x-javascript' }); proxy.GET('/browser/assets/styles.css', { content: IDLE_PAGE_STYLE, contentType: 'text/css' }); proxy.GET('/browser/assets/logo.svg', { content: IDLE_PAGE_LOGO, contentType: 'image/svg+xml' }); @@ -111,14 +117,30 @@ export default class BrowserConnectionGateway { } } + async _connectNextRemoteBrowser (req, res) { + preventCaching(res); + + var remoteConnection = await this.remotesQueue.shift(); + + if (remoteConnection) + redirect(res, remoteConnection.url); + else + respond500(res, 'There are no available connections to establish.'); + } // API startServingConnection (connection) { this.connections[connection.id] = connection; + + if (connection.browserInfo.providerName === 'remote') + this.remotesQueue.add(connection); } stopServingConnection (connection) { delete this.connections[connection.id]; + + if (connection.browserInfo.providerName === 'remote') + this.remotesQueue.remove(connection); } close () { diff --git a/src/browser/connection/remotes-queue.js b/src/browser/connection/remotes-queue.js new file mode 100644 index 00000000000..c85b149f1cd --- /dev/null +++ b/src/browser/connection/remotes-queue.js @@ -0,0 +1,54 @@ +import Promise from 'pinkie'; +import { EventEmitter } from 'events'; +import promisifyEvent from 'promisify-event'; +import timeLimit from 'time-limit-promise'; + + +const REMOTE_REDIRECT_TIMEOUT = 10000; +const ADDING_CONNECTION_WAITING_TIMEOUT = 10000; + +export default class RemotesQueue { + constructor () { + this.events = new EventEmitter(); + this.shiftingTimeout = Promise.resolve(); + this.pendingConnections = {}; + } + + add (remoteConnection) { + var connectionReadyPromise = promisifyEvent(remoteConnection, 'ready') + .then(() => this.remove(remoteConnection)); + + this.pendingConnections[remoteConnection.id] = { + connection: remoteConnection, + readyPromise: connectionReadyPromise + }; + + this.events.emit('connection-added', remoteConnection.id); + } + + remove (remoteConnection) { + delete this.pendingConnections[remoteConnection.id]; + } + + shift () { + var shiftingPromise = this.shiftingTimeout + .then(async () => { + var headId = Object.keys(this.pendingConnections)[0]; + + if (!headId) + headId = await timeLimit(promisifyEvent(this.events, 'connection-added'), ADDING_CONNECTION_WAITING_TIMEOUT); + + return headId ? this.pendingConnections[headId].connection : null; + }); + + this.shiftingTimeout = shiftingPromise + .then(connection => { + if (!connection) + return Promise.resolve(); + + return timeLimit(this.pendingConnections[connection.id].readyPromise, REMOTE_REDIRECT_TIMEOUT); + }); + + return shiftingPromise; + } +} diff --git a/src/cli/remotes-wizard.js b/src/cli/remotes-wizard.js index 7309186ce35..1657a04100b 100644 --- a/src/cli/remotes-wizard.js +++ b/src/cli/remotes-wizard.js @@ -1,18 +1,20 @@ +import Promise from 'pinkie'; import qrcode from 'qrcode-terminal'; import chalk from 'chalk'; import log from './log'; import promisifyEvent from 'promisify-event'; import dedent from 'dedent'; + export default async function (testCafe, remoteCount, showQRCode) { - var connections = []; + var connectionPromises = []; if (remoteCount) { log.hideSpinner(); var description = dedent(` Connecting ${remoteCount} remote browser(s)... - Navigate to the appropriate URL from each of the remote browsers. + Navigate to the following URL from each remote browser. `); log.write(description); @@ -20,22 +22,28 @@ export default async function (testCafe, remoteCount, showQRCode) { if (showQRCode) log.write('You can either enter the URL or scan the QR-code.'); - for (var i = 0; i < remoteCount; i++) { - var browserConnection = await testCafe.createBrowserConnection(); - - log.write(`Browser #${i + 1}: ${chalk.underline.blue(browserConnection.url)}`); + var connectionUrl = testCafe.browserConnectionGateway.connectUrl; - if (showQRCode) - qrcode.generate(browserConnection.url); + log.write(`Connect URL: ${chalk.underline.blue(connectionUrl)}`); - await promisifyEvent(browserConnection, 'ready'); + if (showQRCode) + qrcode.generate(connectionUrl); - connections.push(browserConnection); - log.write(`${chalk.green('CONNECTED')} ${browserConnection.userAgent}\n`); + for (var i = 0; i < remoteCount; i++) { + connectionPromises.push(testCafe + .createBrowserConnection() + .then(bc => promisifyEvent(bc, 'ready').then(() => bc)) + .then(bc => { + log.hideSpinner(); + log.write(`${chalk.green('CONNECTED')} ${bc.userAgent}`); + log.showSpinner(); + return bc; + }) + ); } log.showSpinner(); } - return connections; + return await Promise.all(connectionPromises); }