chore: remove PlaywrightClient and Docker factory (#13571)

This commit is contained in:
Yury Semikhatsky 2022-04-15 12:11:38 -07:00 committed by GitHub
parent 02aa4fe617
commit 7ffce1da53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 21 additions and 347 deletions

View file

@ -19,8 +19,6 @@
"./cli": "./cli.js",
"./package.json": "./package.json",
"./lib/grid/gridServer": "./lib/grid/gridServer.js",
"./lib/grid/gridClient": "./lib/grid/gridClient.js",
"./lib/grid/dockerGridFactory": "./lib/grid/dockerGridFactory.js",
"./lib/outofprocess": "./lib/outofprocess.js",
"./lib/utils": "./lib/utils/index.js",
"./lib/utils/comparators": "./lib/utils/comparators.js",
@ -35,7 +33,6 @@
"./lib/utils/stackTrace": "./lib/utils/stackTrace.js",
"./lib/utils/timeoutRunner": "./lib/utils/timeoutRunner.js",
"./lib/remote/playwrightServer": "./lib/remote/playwrightServer.js",
"./lib/remote/playwrightClient": "./lib/remote/playwrightClient.js",
"./lib/server": "./lib/server/index.js",
"./types/protocol": "./types/protocol.d.ts",
"./types/structs": "./types/structs.d.ts"

View file

@ -16,7 +16,7 @@
import type * as channels from '../protocol/channels';
import { TimeoutError } from '../common/errors';
import * as socks from '../common/socksProxy';
import type * as socks from '../common/socksProxy';
import { Android } from './android';
import { BrowserType } from './browserType';
import { ChannelOwner } from './channelOwner';
@ -87,23 +87,6 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this.selectors._addChannel(selectorsOwner);
}
// TODO: remove this methods together with PlaywrightClient.
_enablePortForwarding(redirectPortForTest?: number) {
const socksSupport = this._initializer.socksSupport;
if (!socksSupport)
return;
const handler = new socks.SocksProxyHandler(redirectPortForTest);
this._socksProxyHandler = handler;
handler.on(socks.SocksProxyHandler.Events.SocksConnected, (payload: socks.SocksSocketConnectedPayload) => socksSupport.socksConnected(payload).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksData, (payload: socks.SocksSocketDataPayload) => socksSupport.socksData({ uid: payload.uid, data: payload.data.toString('base64') }).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksError, (payload: socks.SocksSocketErrorPayload) => socksSupport.socksError(payload).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksFailed, (payload: socks.SocksSocketFailedPayload) => socksSupport.socksFailed(payload).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksEnd, (payload: socks.SocksSocketEndPayload) => socksSupport.socksEnd(payload).catch(() => {}));
socksSupport.on('socksRequested', payload => handler.socketRequested(payload));
socksSupport.on('socksClosed', payload => handler.socketClosed(payload));
socksSupport.on('socksData', payload => handler.sendSocketData({ uid: payload.uid, data: Buffer.from(payload.data, 'base64') }));
}
static from(channel: channels.PlaywrightChannel): Playwright {
return (channel as any)._object;
}

View file

@ -1,165 +0,0 @@
/**
* 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 http from 'http';
import os from 'os';
import type { GridAgentLaunchOptions, GridFactory } from './gridServer';
import * as utils from '../utils';
const dockerFactory: GridFactory = {
name: 'Agents launched inside Docker container',
capacity: Infinity,
launchTimeout: 30000,
retireTimeout: Infinity,
launch: async (options: GridAgentLaunchOptions) => {
const { vncUrl } = await launchDockerGridAgent(options.agentId, options.gridURL);
/* eslint-disable no-console */
console.log(``);
console.log(`✨ Running browsers inside docker container: ${vncUrl}`);
}
};
export default dockerFactory;
interface DockerImage {
Containers: number;
Created: number;
Id: string;
Labels: null | Record<string, string>;
ParentId: string;
RepoDigests: null | string[];
RepoTags: null | string[];
SharedSize: number;
Size: number;
VirtualSize: number;
}
async function launchDockerGridAgent(agentId: string, gridURL: string): Promise<{vncUrl: string }> {
const gridPort = new URL(gridURL).port || '80';
const images: DockerImage[] | null = await getJSON('/images/json');
if (!images) {
throw new Error(`\n` + utils.wrapInASCIIBox([
`Failed to list docker images`,
`Please ensure docker is running.`,
``,
`<3 Playwright Team`,
].join('\n'), 1));
}
const imageName = process.env.PWTEST_IMAGE_NAME ?? `mcr.microsoft.com/playwright:v${require('../../package.json').version}-focal`;
const pwImage = images.find(image => image.RepoTags?.includes(imageName));
if (!pwImage) {
throw new Error(`\n` + utils.wrapInASCIIBox([
`Failed to find ${imageName} docker image.`,
`Please pull docker image with the following command:`,
``,
` npx playwright install docker-image`,
``,
`<3 Playwright Team`,
].join('\n'), 1));
}
const Env = [
'PW_SOCKS_PROXY_PORT=1', // Enable port forwarding over PlaywrightClient
];
const forwardIfDefined = (envName: string) => {
if (process.env[envName])
Env.push(`CI=${process.env[envName]}`);
};
forwardIfDefined('CI');
forwardIfDefined('PWDEBUG');
forwardIfDefined('DEBUG');
forwardIfDefined('DEBUG_FILE');
forwardIfDefined('SELENIUM_REMOTE_URL');
const container = await postJSON('/containers/create', {
Env,
WorkingDir: '/ms-playwright-agent',
Cmd: [ 'bash', 'start_agent.sh', agentId, `http://host.docker.internal:${gridPort}` ],
AttachStdout: true,
AttachStderr: true,
Image: pwImage.Id,
ExposedPorts: {
'7900/tcp': { }
},
HostConfig: {
Init: true,
AutoRemove: true,
ShmSize: 2 * 1024 * 1024 * 1024,
ExtraHosts: process.platform === 'linux' ? [
'host.docker.internal:host-gateway', // Enable host.docker.internal on Linux.
] : [],
PortBindings: {
'7900/tcp': [{ HostPort: '0' }]
},
},
});
await postJSON(`/containers/${container.Id}/start`);
const info = await getJSON(`/containers/${container.Id}/json`);
const vncPort = info?.NetworkSettings?.Ports['7900/tcp'];
return {
vncUrl: `http://localhost:${vncPort[0].HostPort}`,
};
}
async function getJSON(url: string): Promise<any> {
const result = await callDockerAPI('get', url);
if (!result)
return result;
return JSON.parse(result);
}
async function postJSON(url: string, json: any = undefined) {
const result = await callDockerAPI('post', url, json ? JSON.stringify(json) : undefined);
if (!result)
return result;
return JSON.parse(result);
}
function callDockerAPI(method: 'post'|'get', url: string, body: Buffer|string|undefined = undefined): Promise<string|null> {
const dockerSocket = os.platform() === 'win32' ? '\\\\.\\pipe\\docker_engine' : '/var/run/docker.sock';
return new Promise((resolve, reject) => {
const request = http.request({
socketPath: dockerSocket,
path: url,
method,
}, (response: http.IncomingMessage) => {
let body = '';
response.on('data', function(chunk){
body += chunk;
});
response.on('end', function(){
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
console.error(`ERROR ${method} ${url}`, response.statusCode, body);
resolve(null);
} else {
resolve(body);
}
});
});
request.on('error', function(e){
console.error('Error fetching json: ' + e);
resolve(null);
});
if (body) {
request.setHeader('Content-Type', 'application/json');
request.setHeader('Content-Length', body.length);
request.write(body);
}
request.end();
});
}

View file

@ -31,10 +31,15 @@ export function launchGridAgent(agentId: string, gridURL: string, runId: string
ws.on('message', (message: string) => {
log('worker requested ' + message);
const { workerId, browserAlias } = JSON.parse(message);
if (browserAlias)
fork(require.resolve('./gridBrowserWorker.js'), [gridURL, agentId, workerId, browserAlias], { detached: true });
else
fork(require.resolve('./gridWorker.js'), [gridURL, agentId, workerId], { detached: true });
if (!workerId) {
log('workerId not specified');
return;
}
if (!browserAlias) {
log('browserAlias not specified');
return;
}
fork(require.resolve('./gridBrowserWorker.js'), [gridURL, agentId, workerId, browserAlias], { detached: true });
});
ws.on('close', () => process.exit(0));
}

View file

@ -1,48 +0,0 @@
/**
* 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 WebSocket from 'ws';
import debug from 'debug';
import { createPlaywright, PlaywrightDispatcher, DispatcherConnection, Root } from '../server';
import { gracefullyCloseAll } from '../utils/processLauncher';
import { SocksProxy } from '../common/socksProxy';
function launchGridWorker(gridURL: string, agentId: string, workerId: string) {
const log = debug(`pw:grid:worker:${workerId}`);
log('created');
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
const dispatcherConnection = new DispatcherConnection();
dispatcherConnection.onmessage = message => ws.send(JSON.stringify(message));
ws.once('open', () => {
new Root(dispatcherConnection, async rootScope => {
const playwright = createPlaywright('javascript');
const socksProxy = new SocksProxy();
playwright.options.socksProxyPort = await socksProxy.listen(0);
return new PlaywrightDispatcher(rootScope, playwright, socksProxy);
});
});
ws.on('message', message => dispatcherConnection.dispatch(JSON.parse(message.toString())));
ws.on('close', async () => {
// Drop any messages during shutdown on the floor.
dispatcherConnection.onmessage = () => {};
setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers.
await gracefullyCloseAll();
process.exit(0);
});
}
launchGridWorker(process.argv[2], process.argv[3], process.argv[4]);

View file

@ -1,85 +0,0 @@
/**
* 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 WebSocket from 'ws';
import { Connection } from '../client/connection';
import type { Playwright } from '../client/playwright';
import { makeWaitForNextTask } from '../utils';
// TODO: this file should be removed because it uses the old protocol.
export type PlaywrightClientConnectOptions = {
wsEndpoint: string;
timeout?: number;
followRedirects?: boolean;
};
export class PlaywrightClient {
private _playwright: Playwright;
private _ws: WebSocket;
private _closePromise: Promise<void>;
static async connect(options: PlaywrightClientConnectOptions): Promise<PlaywrightClient> {
const { wsEndpoint, timeout = 30000, followRedirects = true } = options;
const connection = new Connection();
connection.markAsRemote();
const ws = new WebSocket(wsEndpoint, { followRedirects });
const waitForNextTask = makeWaitForNextTask();
connection.onmessage = message => {
if (ws.readyState === 2 /** CLOSING */ || ws.readyState === 3 /** CLOSED */)
throw new Error('PlaywrightClient: writing to closed WebSocket connection');
ws.send(JSON.stringify(message));
};
ws.on('message', message => waitForNextTask(() => connection.dispatch(JSON.parse(message.toString()))));
const errorPromise = new Promise((_, reject) => ws.on('error', error => reject(error)));
const closePromise = new Promise((_, reject) => ws.on('close', () => reject(new Error('Connection closed'))));
const playwrightClientPromise = new Promise<PlaywrightClient>((resolve, reject) => {
let playwright: Playwright;
ws.on('open', async () => {
playwright = await connection.initializePlaywright();
resolve(new PlaywrightClient(playwright, ws));
});
ws.on('close', (code, reason) => connection.close(reason.toString()));
});
let timer: NodeJS.Timeout;
try {
await Promise.race([
playwrightClientPromise,
errorPromise,
closePromise,
new Promise((_, reject) => timer = setTimeout(() => reject(`Timeout of ${timeout}ms exceeded while connecting.`), timeout))
]);
return await playwrightClientPromise;
} finally {
clearTimeout(timer!);
}
}
constructor(playwright: Playwright, ws: WebSocket) {
this._playwright = playwright;
this._ws = ws;
this._closePromise = new Promise(f => ws.on('close', f));
}
playwright(): Playwright {
return this._playwright;
}
async close() {
this._ws.close();
await this._closePromise;
}
}

View file

@ -27,9 +27,6 @@ import { Runner, builtInReporters, kDefaultConfigFiles } from './runner';
import { stopProfiling, startProfiling } from './profiler';
import type { FilePatternFilter } from './util';
import { showHTMLReport } from './reporters/html';
import { GridServer } from 'playwright-core/lib/grid/gridServer';
import dockerFactory from 'playwright-core/lib/grid/dockerGridFactory';
import { createGuid } from 'playwright-core/lib/utils';
import { hostPlatform } from 'playwright-core/lib/utils/hostPlatform';
import { fileIsModule } from './loader';
@ -166,8 +163,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
};
});
if (process.env.PLAYWRIGHT_DOCKER)
runner.addInternalGlobalSetup(launchDockerContainer);
const result = await runner.runAllTests({
listOnly: !!opts.list,
filePatternFilter,
@ -235,16 +230,6 @@ function resolveReporter(id: string) {
return require.resolve(id, { paths: [ process.cwd() ] });
}
async function launchDockerContainer(): Promise<() => Promise<void>> {
const gridServer = new GridServer(dockerFactory, createGuid());
await gridServer.start();
// Start docker container in advance.
const { error } = await gridServer.createAgent();
if (error)
throw error;
return async () => await gridServer.stop();
}
function restartWithExperimentalTsEsm(configFile: string | null): boolean {
const nodeVersion = +process.versions.node.split('.')[0];
// New experimental loader is only supported on Node 16+.

View file

@ -230,7 +230,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
});
},
_snapshotSuffix: [process.env.PLAYWRIGHT_DOCKER ? 'docker' : process.platform, { scope: 'worker' }],
_snapshotSuffix: [process.platform, { scope: 'worker' }],
_setupContextOptionsAndArtifacts: [async ({ playwright, _snapshotSuffix, _combinedContextOptions, _browserOptions, _artifactsDir, trace, screenshot, actionTimeout, navigationTimeout }, use, testInfo) => {
testInfo.snapshotSuffix = _snapshotSuffix;

View file

@ -83,7 +83,7 @@ if (mode === 'service') {
config.projects = [{
name: 'Chromium page tests',
testMatch: /page\/.*spec.ts$/,
testIgnore: 'screenshot',
testIgnore: '**/*screenshot*',
use: {
browserName: 'chromium',
mode

View file

@ -31,8 +31,7 @@ class OutOfProcessPlaywrightServer {
stdio: 'pipe',
detached: true,
env: {
...process.env,
PW_SOCKS_PROXY_PORT: '1'
...process.env
}
});
this._driverProcess.unref();

View file

@ -61,7 +61,8 @@ it('should type', async ({ page }) => {
expect(await page.$eval('input', input => input.value)).toBe('hello');
});
it('should take screenshot', async ({ page, server, browserName, headless, isAndroid }) => {
it('should take screenshot', async ({ page, server, browserName, headless, isAndroid, mode }) => {
it.skip(mode === 'service');
it.skip(browserName === 'firefox' && !headless);
it.skip(isAndroid, 'Different dpr. Remove after using 1x scale for screenshots.');
await page.setViewportSize({ width: 500, height: 500 });

View file

@ -76,7 +76,8 @@ it('should work with status code 422', async ({ page, server }) => {
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
});
it('should allow mocking binary responses', async ({ page, server, browserName, headless, asset, isAndroid }) => {
it('should allow mocking binary responses', async ({ page, server, browserName, headless, asset, isAndroid, mode }) => {
it.skip(mode === 'service');
it.skip(browserName === 'firefox' && !headless, 'Firefox headed produces a different image.');
it.skip(isAndroid);
@ -97,7 +98,8 @@ it('should allow mocking binary responses', async ({ page, server, browserName,
expect(await img.screenshot()).toMatchSnapshot('mock-binary-response.png');
});
it('should allow mocking svg with charset', async ({ page, server, browserName, headless, isAndroid }) => {
it('should allow mocking svg with charset', async ({ page, server, browserName, headless, isAndroid, mode }) => {
it.skip(mode === 'service');
it.skip(browserName === 'firefox' && !headless, 'Firefox headed produces a different image.');
it.skip(isAndroid);
@ -117,7 +119,8 @@ it('should allow mocking svg with charset', async ({ page, server, browserName,
expect(await img.screenshot()).toMatchSnapshot('mock-svg.png');
});
it('should work with file path', async ({ page, server, asset, isAndroid }) => {
it('should work with file path', async ({ page, server, asset, isAndroid, mode }) => {
it.skip(mode === 'service');
it.skip(isAndroid);
await page.route('**/*', route => route.fulfill({ contentType: 'shouldBeIgnored', path: asset('pptr.png') }));

View file

@ -141,7 +141,6 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
GITHUB_SHA: undefined,
// END: Reserved CI
PW_TEST_HTML_REPORT_OPEN: undefined,
PLAYWRIGHT_DOCKER: undefined,
PW_TEST_REPORTER: undefined,
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
PW_TEST_SOURCE_TRANSFORM: undefined,