fix: resolve ip using grid/api/testsession endpoint (#10196)

For Selenium 4, we use se:cdp ws proxy, pointing it to the hub url.
For Selenium 3, we use grid api to try and get the target node ip.
This commit is contained in:
Dmitry Gozman 2021-11-09 14:41:13 -08:00 committed by GitHub
parent 4e90eb9406
commit 75efeb1e08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 134 additions and 33 deletions

View File

@ -27,7 +27,7 @@ import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../tra
import { CRDevTools } from './crDevTools'; import { CRDevTools } from './crDevTools';
import { Browser, BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import { Browser, BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
import * as types from '../types'; import * as types from '../types';
import { debugMode, fetchData, headersArrayToObject, removeFolders, streamToString } from '../../utils/utils'; import { debugMode, fetchData, headersArrayToObject, HTTPRequestParams, removeFolders, streamToString } from '../../utils/utils';
import { RecentLogsCollector } from '../../utils/debugLogger'; import { RecentLogsCollector } from '../../utils/debugLogger';
import { Progress, ProgressController } from '../progress'; import { Progress, ProgressController } from '../progress';
import { TimeoutSettings } from '../../utils/timeoutSettings'; import { TimeoutSettings } from '../../utils/timeoutSettings';
@ -65,7 +65,7 @@ export class Chromium extends BrowserType {
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER); const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
const wsEndpoint = await urlToWSEndpoint(endpointURL); const wsEndpoint = await urlToWSEndpoint(progress, endpointURL);
progress.throwIfAborted(); progress.throwIfAborted();
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, headersMap); const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, headersMap);
@ -153,7 +153,7 @@ export class Chromium extends BrowserType {
args.push('--remote-debugging-port=0'); args.push('--remote-debugging-port=0');
const desiredCapabilities = { 'browserName': 'chrome', 'goog:chromeOptions': { args } }; const desiredCapabilities = { 'browserName': 'chrome', 'goog:chromeOptions': { args } };
progress.log(`<connecting to selenium> ${hubUrl}`); progress.log(`<selenium> connecting to ${hubUrl}`);
const response = await fetchData({ const response = await fetchData({
url: hubUrl + 'session', url: hubUrl + 'session',
method: 'POST', method: 'POST',
@ -162,41 +162,59 @@ export class Chromium extends BrowserType {
capabilities: { alwaysMatch: desiredCapabilities } capabilities: { alwaysMatch: desiredCapabilities }
}), }),
timeout: progress.timeUntilDeadline(), timeout: progress.timeUntilDeadline(),
}, async response => { }, seleniumErrorHandler);
const body = await streamToString(response);
let message = '';
try {
const json = JSON.parse(body);
message = json.value.localizedMessage || json.value.message;
} catch (e) {
}
return new Error(`Error connecting to Selenium at ${hubUrl}: ${message}`);
});
const value = JSON.parse(response).value; const value = JSON.parse(response).value;
const sessionId = value.sessionId; const sessionId = value.sessionId;
progress.log(`<connected to selenium> sessionId=${sessionId}`); progress.log(`<selenium> connected to sessionId=${sessionId}`);
const disconnectFromSelenium = async () => { const disconnectFromSelenium = async () => {
progress.log(`<disconnecting from selenium> sessionId=${sessionId}`); progress.log(`<selenium> disconnecting from sessionId=${sessionId}`);
await fetchData({ await fetchData({
url: hubUrl + 'session/' + sessionId, url: hubUrl + 'session/' + sessionId,
method: 'DELETE', method: 'DELETE',
}).catch(error => progress.log(`<error disconnecting from selenium>: ${error}`)); }).catch(error => progress.log(`<error disconnecting from selenium>: ${error}`));
progress.log(`<disconnected from selenium> sessionId=${sessionId}`); progress.log(`<selenium> disconnected from sessionId=${sessionId}`);
gracefullyCloseSet.delete(disconnectFromSelenium); gracefullyCloseSet.delete(disconnectFromSelenium);
}; };
gracefullyCloseSet.add(disconnectFromSelenium); gracefullyCloseSet.add(disconnectFromSelenium);
try { try {
const capabilities = value.capabilities; const capabilities = value.capabilities;
const maybeChromeOptions = capabilities['goog:chromeOptions']; let endpointURL: URL;
const chromeOptions = maybeChromeOptions && typeof maybeChromeOptions === 'object' ? maybeChromeOptions : undefined;
const debuggerAddress = chromeOptions && typeof chromeOptions.debuggerAddress === 'string' ? chromeOptions.debuggerAddress : undefined; if (capabilities['se:cdp']) {
const chromeOptionsURL = typeof maybeChromeOptions === 'string' ? maybeChromeOptions : undefined; // Selenium 4 - use built-in CDP websocket proxy.
let endpointURL = capabilities['se:cdp'] || debuggerAddress || chromeOptionsURL; const endpointURLString = addProtocol(capabilities['se:cdp']);
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => endpointURL.startsWith(protocol))) endpointURL = new URL(endpointURLString);
endpointURL = 'http://' + endpointURL; endpointURL.hostname = new URL(hubUrl).hostname;
return this._connectOverCDPInternal(progress, endpointURL, { slowMo: options.slowMo }, disconnectFromSelenium); progress.log(`<selenium> retrieved endpoint ${endpointURL.toString()} for sessionId=${sessionId}`);
} else {
// Selenium 3 - resolve target node IP to use instead of localhost ws url.
const maybeChromeOptions = capabilities['goog:chromeOptions'];
const chromeOptions = maybeChromeOptions && typeof maybeChromeOptions === 'object' ? maybeChromeOptions : undefined;
const debuggerAddress = chromeOptions && typeof chromeOptions.debuggerAddress === 'string' ? chromeOptions.debuggerAddress : undefined;
const chromeOptionsURL = typeof maybeChromeOptions === 'string' ? maybeChromeOptions : undefined;
const endpointURLString = addProtocol(debuggerAddress || chromeOptionsURL);
progress.log(`<selenium> retrieved endpoint ${endpointURLString} for sessionId=${sessionId}`);
endpointURL = new URL(endpointURLString);
if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1') {
const sessionInfoUrl = new URL(hubUrl).origin + '/grid/api/testsession?session=' + sessionId;
try {
const sessionResponse = await fetchData({
url: sessionInfoUrl,
method: 'GET',
timeout: progress.timeUntilDeadline(),
}, seleniumErrorHandler);
const proxyId = JSON.parse(sessionResponse).proxyId;
endpointURL.hostname = new URL(proxyId).hostname;
progress.log(`<selenium> resolved endpoint ip ${endpointURL.toString()} for sessionId=${sessionId}`);
} catch (e) {
progress.log(`<selenium> unable to resolve endpoint ip for sessionId=${sessionId}, running in standalone?`);
}
}
}
return await this._connectOverCDPInternal(progress, endpointURL.toString(), { slowMo: options.slowMo }, disconnectFromSelenium);
} catch (e) { } catch (e) {
await disconnectFromSelenium(); await disconnectFromSelenium();
throw e; throw e;
@ -296,9 +314,10 @@ const DEFAULT_ARGS = [
'--no-service-autorun', '--no-service-autorun',
]; ];
async function urlToWSEndpoint(endpointURL: string) { async function urlToWSEndpoint(progress: Progress, endpointURL: string) {
if (endpointURL.startsWith('ws')) if (endpointURL.startsWith('ws'))
return endpointURL; return endpointURL;
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`; const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
const request = endpointURL.startsWith('https') ? https : http; const request = endpointURL.startsWith('https') ? https : http;
const json = await new Promise<string>((resolve, reject) => { const json = await new Promise<string>((resolve, reject) => {
@ -314,3 +333,20 @@ async function urlToWSEndpoint(endpointURL: string) {
}); });
return JSON.parse(json).webSocketDebuggerUrl; return JSON.parse(json).webSocketDebuggerUrl;
} }
async function seleniumErrorHandler(params: HTTPRequestParams, response: http.IncomingMessage) {
const body = await streamToString(response);
let message = body;
try {
const json = JSON.parse(body);
message = json.value.localizedMessage || json.value.message;
} catch (e) {
}
return new Error(`Error connecting to Selenium at ${params.url}: ${message}`);
}
function addProtocol(url: string) {
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => url.startsWith(protocol)))
return 'http://' + url;
return url;
}

View File

@ -40,7 +40,7 @@ const ProxyAgent = require('https-proxy-agent');
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err))); export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
type HTTPRequestParams = { export type HTTPRequestParams = {
url: string, url: string,
method?: string, method?: string,
headers?: http.OutgoingHttpHeaders, headers?: http.OutgoingHttpHeaders,
@ -97,11 +97,11 @@ function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMes
request.end(params.data); request.end(params.data);
} }
export function fetchData(params: HTTPRequestParams, onError?: (response: http.IncomingMessage) => Promise<Error>): Promise<string> { export function fetchData(params: HTTPRequestParams, onError?: (params: HTTPRequestParams, response: http.IncomingMessage) => Promise<Error>): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
httpRequest(params, async response => { httpRequest(params, async response => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
const error = onError ? await onError(response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`); const error = onError ? await onError(params, response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`);
reject(error); reject(error);
return; return;
} }

View File

@ -22,13 +22,13 @@ import { start } from '../packages/playwright-core/lib/outofprocess';
const chromeDriver = require('chromedriver').path; const chromeDriver = require('chromedriver').path;
const brokenDriver = path.join(__dirname, 'assets', 'selenium-grid', 'broken-selenium-driver.js'); const brokenDriver = path.join(__dirname, 'assets', 'selenium-grid', 'broken-selenium-driver.js');
const seleniumConfigStandalone = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-config-standalone.json');
const standalone_3_141_59 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-standalone-3.141.59.jar'); const standalone_3_141_59 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-standalone-3.141.59.jar');
const selenium_4_0_0_rc1 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-4.0.0-rc-1.jar'); const selenium_4_0_0_rc1 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-4.0.0-rc-1.jar');
function writeSeleniumConfig(testInfo: TestInfo, port: number) { function writeSeleniumConfig(testInfo: TestInfo, port: number) {
const content = fs.readFileSync(seleniumConfigStandalone, 'utf8').replace(/4444/g, String(port)); const template = path.join(__dirname, 'assets', 'selenium-grid', `selenium-config-standalone.json`);
const file = testInfo.outputPath('selenium-config.json'); const content = fs.readFileSync(template, 'utf8').replace(/4444/g, String(port));
const file = testInfo.outputPath(`selenium-config-standalone.json`);
fs.writeFileSync(file, content, 'utf8'); fs.writeFileSync(file, content, 'utf8');
return file; return file;
} }
@ -60,6 +60,39 @@ test('selenium grid 3.141.59 standalone chromium', async ({ browserName, childPr
await grid.waitForOutput('Removing session'); await grid.waitForOutput('Removing session');
}); });
test('selenium grid 3.141.59 hub + node chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
test.skip(browserName !== 'chromium');
const port = testInfo.workerIndex + 15123;
const hub = childProcess({
command: ['java', '-jar', standalone_3_141_59, '-role', 'hub', '-port', String(port)],
cwd: __dirname,
});
await waitForPort(port);
const node = childProcess({
command: ['java', `-Dwebdriver.chrome.driver=${chromeDriver}`, '-jar', standalone_3_141_59, '-role', 'node', '-host', '127.0.0.1', '-hub', `http://localhost:${port}/grid/register`],
cwd: __dirname,
});
await Promise.all([
node.waitForOutput('The node is registered to the hub and ready to use'),
hub.waitForOutput('Registered a node'),
]);
const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`;
const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any);
const page = await browser.newPage();
await page.setContent('<title>Hello world</title><div>Get Started</div>');
await page.click('text=Get Started');
await expect(page).toHaveTitle('Hello world');
await browser.close();
expect(hub.output).toContain('Got a request to create a new session');
expect(node.output).toContain('Starting ChromeDriver');
expect(node.output).toContain('Started new session');
await node.waitForOutput('Removing session');
});
test('selenium grid 4.0.0-rc-1 standalone chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => { test('selenium grid 4.0.0-rc-1 standalone chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
test.skip(browserName !== 'chromium'); test.skip(browserName !== 'chromium');
@ -83,6 +116,38 @@ test('selenium grid 4.0.0-rc-1 standalone chromium', async ({ browserName, child
await grid.waitForOutput('Deleted session'); await grid.waitForOutput('Deleted session');
}); });
test('selenium grid 4.0.0-rc-1 hub + node chromium', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
test.skip(browserName !== 'chromium');
const port = testInfo.workerIndex + 15123;
const hub = childProcess({
command: ['java', '-jar', selenium_4_0_0_rc1, 'hub', '--port', String(port)],
cwd: __dirname,
});
await waitForPort(port);
const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`;
const node = childProcess({
command: ['java', `-Dwebdriver.chrome.driver=${chromeDriver}`, '-jar', selenium_4_0_0_rc1, 'node', '--grid-url', `http://localhost:${port}`, '--port', String(port + 1)],
cwd: __dirname,
});
await Promise.all([
node.waitForOutput('Node has been added'),
hub.waitForOutput('from DOWN to UP'),
]);
const browser = await browserType.launch({ __testHookSeleniumRemoteURL } as any);
const page = await browser.newPage();
await page.setContent('<title>Hello world</title><div>Get Started</div>');
await page.click('text=Get Started');
await expect(page).toHaveTitle('Hello world');
await browser.close();
expect(hub.output).toContain('Session request received by the distributor');
expect(node.output).toContain('Starting ChromeDriver');
await hub.waitForOutput('Deleted session');
});
test('selenium grid 4.0.0-rc-1 standalone chromium broken driver', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => { test('selenium grid 4.0.0-rc-1 standalone chromium broken driver', async ({ browserName, childProcess, waitForPort, browserType }, testInfo) => {
test.skip(browserName !== 'chromium'); test.skip(browserName !== 'chromium');
@ -95,7 +160,7 @@ test('selenium grid 4.0.0-rc-1 standalone chromium broken driver', async ({ brow
const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`; const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`;
const error = await browserType.launch({ __testHookSeleniumRemoteURL } as any).catch(e => e); const error = await browserType.launch({ __testHookSeleniumRemoteURL } as any).catch(e => e);
expect(error.message).toContain(`Error connecting to Selenium at http://localhost:${port}/wd/hub/: Could not start a new session`); expect(error.message).toContain(`Error connecting to Selenium at http://localhost:${port}/wd/hub/session: Could not start a new session`);
expect(grid.output).not.toContain('Starting ChromeDriver'); expect(grid.output).not.toContain('Starting ChromeDriver');
}); });
@ -108,7 +173,7 @@ test('selenium grid 3.141.59 standalone non-chromium', async ({ browserName, bro
expect(error.message).toContain('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium'); expect(error.message).toContain('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium');
}); });
test('selenium grid 3.141.59 standalone chromium through driver', async ({ browserName, childProcess, waitForPort }, testInfo) => { test('selenium grid 3.141.59 standalone chromium through run-driver', async ({ browserName, childProcess, waitForPort }, testInfo) => {
test.skip(browserName !== 'chromium'); test.skip(browserName !== 'chromium');
const port = testInfo.workerIndex + 15123; const port = testInfo.workerIndex + 15123;