feat: introduce docker integration for Playwright Test (#9599)
This commit is contained in:
parent
ba57be99a9
commit
983cfde4d4
|
|
@ -20,7 +20,9 @@
|
|||
"require": "./index.js"
|
||||
},
|
||||
"./cli": "./cli.js",
|
||||
"./src/grid/gridServer": "./lib/grid/gridServer.js",
|
||||
"./src/grid/gridClient": "./lib/grid/gridClient.js",
|
||||
"./src/grid/dockerGridFactory": "./lib/grid/dockerGridFactory.js",
|
||||
"./src/utils/async": "./lib/utils/async.js",
|
||||
"./src/utils/httpServer": "./lib/utils/httpServer.js",
|
||||
"./src/utils/processLauncher": "./lib/utils/processLauncher.js",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { spawn } from 'child_process';
|
|||
import { registry, Executable } from '../utils/registry';
|
||||
import { spawnAsync, getPlaywrightVersion } from '../utils/utils';
|
||||
import { launchGridAgent } from '../grid/gridAgent';
|
||||
import { launchGridServer } from '../grid/gridServer';
|
||||
import { GridServer, GridFactory } from '../grid/gridServer';
|
||||
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
|
|
@ -571,3 +571,22 @@ function commandWithOpenOptions(command: string, description: string, options: a
|
|||
.option('--user-agent <ua string>', 'specify user agent string')
|
||||
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"');
|
||||
}
|
||||
|
||||
async function launchGridServer(factoryPathOrPackageName: string, port: number, authToken: string|undefined): Promise<void> {
|
||||
if (!factoryPathOrPackageName)
|
||||
factoryPathOrPackageName = path.join('..', 'grid', 'simpleGridFactory');
|
||||
let factory;
|
||||
try {
|
||||
factory = require(path.resolve(factoryPathOrPackageName));
|
||||
} catch (e) {
|
||||
factory = require(factoryPathOrPackageName);
|
||||
}
|
||||
if (factory && typeof factory === 'object' && ('default' in factory))
|
||||
factory = factory['default'];
|
||||
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 as GridFactory, authToken);
|
||||
await gridServer.start(port);
|
||||
console.log('Grid server is running at ' + gridServer.urlPrefix());
|
||||
}
|
||||
|
|
|
|||
146
packages/playwright-core/src/grid/dockerGridFactory.ts
Normal file
146
packages/playwright-core/src/grid/dockerGridFactory.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 http from 'http';
|
||||
import os from 'os';
|
||||
import { GridAgentLaunchOptions, GridFactory } from './gridServer';
|
||||
import * as utils from '../utils/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 ✨`);
|
||||
console.log(`- look inside: ${vncUrl}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default dockerFactory;
|
||||
|
||||
async function launchDockerGridAgent(agentId: string, gridURL: string): Promise<{vncUrl: string }> {
|
||||
const gridPort = new URL(gridURL).port || '80';
|
||||
const images = await getJSON('/images/json');
|
||||
let imageName = process.env.PWTEST_IMAGE_NAME;
|
||||
if (!imageName) {
|
||||
const packageJson = require('../../package.json');
|
||||
imageName = `mcr.microsoft.com/playwright:v${packageJson.version}-focal`;
|
||||
}
|
||||
const pwImage = images.find((image: any) => 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 playwight 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();
|
||||
});
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
|
||||
import debug from 'debug';
|
||||
import path from 'path';
|
||||
import assert from 'assert';
|
||||
import { EventEmitter } from 'events';
|
||||
import { URL } from 'url';
|
||||
|
|
@ -32,7 +31,8 @@ export type GridAgentLaunchOptions = {
|
|||
export type GridFactory = {
|
||||
name?: string,
|
||||
capacity?: number,
|
||||
timeout?: number,
|
||||
launchTimeout?: number,
|
||||
retireTimeout?: number,
|
||||
launch: (launchOptions: GridAgentLaunchOptions) => Promise<void>,
|
||||
};
|
||||
|
||||
|
|
@ -44,10 +44,10 @@ type ErrorCode = {
|
|||
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_CREATION_FAILED: { code: 1013, reason: 'Grid agent creation 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_CREATION_TIMED_OUT: { code: 1013, reason: 'Grid agent creation 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' },
|
||||
|
|
@ -102,16 +102,18 @@ class GridAgent extends EventEmitter {
|
|||
readonly _workers = new Map<string, GridWorker>();
|
||||
private _status: AgentStatus = 'none';
|
||||
private _workersWaitingForAgentConnected: GridWorker[] = [];
|
||||
private _retireTimeout: NodeJS.Timeout | undefined;
|
||||
private _retireTimeout = 30000;
|
||||
private _retireTimeoutId: NodeJS.Timeout | undefined;
|
||||
private _log: debug.Debugger;
|
||||
private _agentCreationTimeout: NodeJS.Timeout;
|
||||
private _agentCreationTimeoutId: NodeJS.Timeout;
|
||||
|
||||
constructor(capacity = Infinity, creationTimeout = 5 * 60000) {
|
||||
constructor(capacity = Infinity, creationTimeout = 5 * 60000, retireTimeout = 30000) {
|
||||
super();
|
||||
this._capacity = capacity;
|
||||
this._log = debug(`[agent ${this.agentId}]`);
|
||||
this.setStatus('created');
|
||||
this._agentCreationTimeout = setTimeout(() => {
|
||||
this._retireTimeout = retireTimeout;
|
||||
this._agentCreationTimeoutId = setTimeout(() => {
|
||||
this.closeAgent(WSErrors.AGENT_CREATION_TIMED_OUT);
|
||||
}, creationTimeout);
|
||||
}
|
||||
|
|
@ -126,7 +128,7 @@ class GridAgent extends EventEmitter {
|
|||
}
|
||||
|
||||
agentConnected(ws: WebSocket) {
|
||||
clearTimeout(this._agentCreationTimeout);
|
||||
clearTimeout(this._agentCreationTimeoutId);
|
||||
this.setStatus('connected');
|
||||
this._ws = ws;
|
||||
for (const worker of this._workersWaitingForAgentConnected) {
|
||||
|
|
@ -141,8 +143,8 @@ class GridAgent extends EventEmitter {
|
|||
}
|
||||
|
||||
async createWorker(clientSocket: WebSocket) {
|
||||
if (this._retireTimeout)
|
||||
clearTimeout(this._retireTimeout);
|
||||
if (this._retireTimeoutId)
|
||||
clearTimeout(this._retireTimeoutId);
|
||||
if (this._ws)
|
||||
this.setStatus('connected');
|
||||
const worker = new GridWorker(clientSocket);
|
||||
|
|
@ -152,9 +154,10 @@ class GridAgent extends EventEmitter {
|
|||
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._retireTimeoutId)
|
||||
clearTimeout(this._retireTimeoutId);
|
||||
if (this._retireTimeout && isFinite(this._retireTimeout))
|
||||
this._retireTimeoutId = setTimeout(() => this.closeAgent(WSErrors.AGENT_RETIRED), this._retireTimeout);
|
||||
}
|
||||
});
|
||||
if (this._ws) {
|
||||
|
|
@ -182,7 +185,7 @@ class GridAgent extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
class GridServer {
|
||||
export class GridServer {
|
||||
private _server: HttpServer;
|
||||
private _wsServer: WebSocketServer;
|
||||
private _agents = new Map<string, GridAgent>();
|
||||
|
|
@ -239,7 +242,7 @@ class GridServer {
|
|||
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();
|
||||
const agent = [...this._agents.values()].find(w => w.canCreateWorker()) || this._createAgent()?.agent;
|
||||
if (!agent) {
|
||||
ws.close(WSErrors.AGENT_CREATION_FAILED.code, WSErrors.AGENT_CREATION_FAILED.reason);
|
||||
return;
|
||||
|
|
@ -282,25 +285,32 @@ class GridServer {
|
|||
});
|
||||
}
|
||||
|
||||
private _createAgent(): GridAgent {
|
||||
const agent = new GridAgent(this._factory.capacity, this._factory.timeout);
|
||||
public async createAgent() {
|
||||
const { initPromise } = this._createAgent();
|
||||
await initPromise;
|
||||
}
|
||||
|
||||
private _createAgent(): {agent: GridAgent, initPromise: Promise<{success: boolean, error: any}>} {
|
||||
const agent = new GridAgent(this._factory.capacity, this._factory.launchTimeout, this._factory.retireTimeout);
|
||||
this._agents.set(agent.agentId, agent);
|
||||
agent.on('close', () => {
|
||||
this._agents.delete(agent.agentId);
|
||||
});
|
||||
Promise.resolve()
|
||||
const initPromise = Promise.resolve()
|
||||
.then(() => this._factory.launch({
|
||||
agentId: agent.agentId,
|
||||
gridURL: this._server.urlPrefix(),
|
||||
playwrightVersion: getPlaywrightVersion(),
|
||||
})).then(() => {
|
||||
this._log('created');
|
||||
}).catch(e => {
|
||||
return { success: true, error: undefined };
|
||||
}).catch(error => {
|
||||
this._log('failed to launch agent ' + agent.agentId);
|
||||
console.error(e);
|
||||
console.error(error);
|
||||
agent.closeAgent(WSErrors.AGENT_CREATION_FAILED);
|
||||
return { success: false, error };
|
||||
});
|
||||
return agent;
|
||||
return { agent, initPromise };
|
||||
}
|
||||
|
||||
_securePath(suffix: string): string {
|
||||
|
|
@ -338,7 +348,7 @@ class GridServer {
|
|||
`;
|
||||
}
|
||||
|
||||
async start(port: number) {
|
||||
async start(port?: number) {
|
||||
await this._server.start(port);
|
||||
}
|
||||
|
||||
|
|
@ -357,21 +367,3 @@ class GridServer {
|
|||
function mangle(sessionId: string) {
|
||||
return sessionId.replace(/\w{28}/, 'x'.repeat(28));
|
||||
}
|
||||
|
||||
export async function launchGridServer(factoryPathOrPackageName: string, port: number, authToken: string|undefined): Promise<void> {
|
||||
if (!factoryPathOrPackageName)
|
||||
factoryPathOrPackageName = './simpleGridFactory';
|
||||
let factory;
|
||||
try {
|
||||
factory = require(path.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 as GridFactory, authToken);
|
||||
await gridServer.start(port);
|
||||
/* eslint-disable no-console */
|
||||
console.log('Grid server is running at ' + gridServer.urlPrefix());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,22 +15,26 @@
|
|||
*/
|
||||
|
||||
import child_process from 'child_process';
|
||||
import { GridAgentLaunchOptions } from './gridServer';
|
||||
import { GridAgentLaunchOptions, GridFactory } from './gridServer';
|
||||
import path from 'path';
|
||||
|
||||
export const name = 'Agents co-located with grid';
|
||||
export const capacity = Infinity;
|
||||
export const timeout = 10000;
|
||||
export function launch({ agentId, gridURL }: GridAgentLaunchOptions) {
|
||||
const simpleFactory: GridFactory = {
|
||||
name: 'Agents co-located with grid',
|
||||
capacity: Infinity,
|
||||
launchTimeout: 10000,
|
||||
retireTimeout: 10000,
|
||||
launch: async (options: GridAgentLaunchOptions) => {
|
||||
child_process.spawn(process.argv[0], [
|
||||
path.join(__dirname, '..', 'cli', 'cli.js'),
|
||||
'experimental-grid-agent',
|
||||
'--grid-url', gridURL,
|
||||
'--agent-id', agentId,
|
||||
'--grid-url', options.gridURL,
|
||||
'--agent-id', options.agentId,
|
||||
], {
|
||||
cwd: __dirname,
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default simpleFactory;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ import { stopProfiling, startProfiling } from './profiler';
|
|||
import { FilePatternFilter } from './util';
|
||||
import { Loader } from './loader';
|
||||
import { showHTMLReport } from './reporters/html';
|
||||
import { GridServer } from 'playwright-core/src/grid/gridServer';
|
||||
import dockerFactory from 'playwright-core/src/grid/dockerGridFactory';
|
||||
import { createGuid } from 'playwright-core/src/utils/utils';
|
||||
|
||||
const defaultTimeout = 30000;
|
||||
const defaultReporter: BuiltInReporter = process.env.CI ? 'dot' : 'list';
|
||||
|
|
@ -181,6 +184,8 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
});
|
||||
|
||||
const runner = new Runner(loader);
|
||||
if (process.env.PLAYWRIGHT_DOCKER)
|
||||
runner.addInternalGlobalSetup(launchDockerContainer);
|
||||
const result = await runner.run(!!opts.list, filePatternFilters, opts.project || undefined);
|
||||
await stopProfiling(undefined);
|
||||
|
||||
|
|
@ -225,3 +230,12 @@ function resolveReporter(id: string) {
|
|||
return localPath;
|
||||
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.
|
||||
await gridServer.createAgent();
|
||||
process.env.PW_GRID = gridServer.urlPrefix().substring(0, gridServer.urlPrefix().length - 1);
|
||||
return async () => await gridServer.stop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,10 +56,13 @@ type RunResult = {
|
|||
clashingTests: Map<string, TestCase[]>
|
||||
};
|
||||
|
||||
type InternalGlobalSetupFunction = () => Promise<() => Promise<void>>;
|
||||
|
||||
export class Runner {
|
||||
private _loader: Loader;
|
||||
private _reporter!: Reporter;
|
||||
private _didBegin = false;
|
||||
private _internalGlobalSetups: Array<InternalGlobalSetupFunction> = [];
|
||||
|
||||
constructor(loader: Loader) {
|
||||
this._loader = loader;
|
||||
|
|
@ -96,6 +99,10 @@ export class Runner {
|
|||
return new Multiplexer(reporters);
|
||||
}
|
||||
|
||||
addInternalGlobalSetup(internalGlobalSetup: InternalGlobalSetupFunction) {
|
||||
this._internalGlobalSetups.push(internalGlobalSetup);
|
||||
}
|
||||
|
||||
async run(list: boolean, filePatternFilters: FilePatternFilter[], projectNames?: string[]): Promise<RunResultStatus> {
|
||||
this._reporter = await this._createReporter(list);
|
||||
const config = this._loader.fullConfig();
|
||||
|
|
@ -187,6 +194,11 @@ export class Runner {
|
|||
testFiles.forEach(file => allTestFiles.add(file));
|
||||
}
|
||||
|
||||
const internalGlobalTeardowns: (() => Promise<void>)[] = [];
|
||||
if (!list) {
|
||||
for (const internalGlobalSetup of this._internalGlobalSetups)
|
||||
internalGlobalTeardowns.push(await internalGlobalSetup());
|
||||
}
|
||||
const webServer = (!list && config.webServer) ? await WebServer.create(config.webServer) : undefined;
|
||||
let globalSetupResult: any;
|
||||
if (config.globalSetup && !list)
|
||||
|
|
@ -338,6 +350,8 @@ export class Runner {
|
|||
if (config.globalTeardown && !list)
|
||||
await (await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown'))(this._loader.fullConfig());
|
||||
await webServer?.kill();
|
||||
for (const internalGlobalTeardown of internalGlobalTeardowns)
|
||||
await internalGlobalTeardown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
FROM ubuntu:bionic
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG TZ=America/Los_Angeles
|
||||
|
||||
# === INSTALL Node.js ===
|
||||
|
||||
# Install node14
|
||||
|
|
@ -24,23 +27,43 @@ RUN apt-get update && apt-get install -y python3.8 python3-pip && \
|
|||
update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \
|
||||
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1
|
||||
|
||||
# Install VNC & noVNC
|
||||
|
||||
ARG NOVNC_REF="1.2.0"
|
||||
ARG WEBSOCKIFY_REF="0.10.0"
|
||||
ENV DISPLAY_NUM=99
|
||||
ENV DISPLAY=":${DISPLAY_NUM}"
|
||||
|
||||
RUN mkdir -p /opt/bin && chmod +x /dev/shm \
|
||||
&& apt-get update && apt-get install -y unzip fluxbox x11vnc \
|
||||
&& curl -L -o noVNC.zip "https://github.com/novnc/noVNC/archive/v${NOVNC_REF}.zip" \
|
||||
&& unzip -x noVNC.zip \
|
||||
&& mv noVNC-${NOVNC_REF} /opt/bin/noVNC \
|
||||
&& cp /opt/bin/noVNC/vnc.html /opt/bin/noVNC/index.html \
|
||||
&& rm noVNC.zip \
|
||||
&& curl -L -o websockify.zip "https://github.com/novnc/websockify/archive/v${WEBSOCKIFY_REF}.zip" \
|
||||
&& unzip -x websockify.zip \
|
||||
&& rm websockify.zip \
|
||||
&& mv websockify-${WEBSOCKIFY_REF} /opt/bin/noVNC/utils/websockify
|
||||
|
||||
# === BAKE BROWSERS INTO IMAGE ===
|
||||
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 1. Add tip-of-tree Playwright package to install its browsers.
|
||||
# The package should be built beforehand from tip-of-tree Playwright.
|
||||
COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz
|
||||
|
||||
# 2. Install playwright and then delete the installation.
|
||||
# Browsers will remain downloaded in `/ms-playwright`.
|
||||
# 2. Install playwright agent.
|
||||
# Browsers will be downloaded in `/ms-playwright`.
|
||||
# Note: make sure to set 777 to the registry so that any user can access
|
||||
# registry.
|
||||
RUN mkdir /ms-playwright && \
|
||||
mkdir /tmp/pw && cd /tmp/pw && npm init -y && \
|
||||
mkdir /ms-playwright-agent && \
|
||||
cd /ms-playwright-agent && npm init -y && \
|
||||
npm i /tmp/playwright-core.tar.gz && \
|
||||
npx playwright install && \
|
||||
DEBIAN_FRONTEND=noninteractive npx playwright install-deps && \
|
||||
rm -rf /tmp/pw && rm /tmp/playwright-core.tar.gz && \
|
||||
npx playwright install --with-deps && \
|
||||
rm /tmp/playwright-core.tar.gz && \
|
||||
chmod -R 777 /ms-playwright
|
||||
|
||||
COPY start_agent.sh /ms-playwright-agent/start_agent.sh
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
FROM ubuntu:focal
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG TZ=America/Los_Angeles
|
||||
|
||||
# === INSTALL Node.js ===
|
||||
|
||||
# Install node14
|
||||
|
|
@ -24,6 +27,25 @@ RUN apt-get update && apt-get install -y python3.8 python3-pip && \
|
|||
update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \
|
||||
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1
|
||||
|
||||
# Install VNC & noVNC
|
||||
|
||||
ARG NOVNC_REF="1.2.0"
|
||||
ARG WEBSOCKIFY_REF="0.10.0"
|
||||
ENV DISPLAY_NUM=99
|
||||
ENV DISPLAY=":${DISPLAY_NUM}"
|
||||
|
||||
RUN mkdir -p /opt/bin && chmod +x /dev/shm \
|
||||
&& apt-get update && apt-get install -y unzip fluxbox x11vnc \
|
||||
&& curl -L -o noVNC.zip "https://github.com/novnc/noVNC/archive/v${NOVNC_REF}.zip" \
|
||||
&& unzip -x noVNC.zip \
|
||||
&& mv noVNC-${NOVNC_REF} /opt/bin/noVNC \
|
||||
&& cp /opt/bin/noVNC/vnc.html /opt/bin/noVNC/index.html \
|
||||
&& rm noVNC.zip \
|
||||
&& curl -L -o websockify.zip "https://github.com/novnc/websockify/archive/v${WEBSOCKIFY_REF}.zip" \
|
||||
&& unzip -x websockify.zip \
|
||||
&& rm websockify.zip \
|
||||
&& mv websockify-${WEBSOCKIFY_REF} /opt/bin/noVNC/utils/websockify
|
||||
|
||||
# === BAKE BROWSERS INTO IMAGE ===
|
||||
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
|
|
@ -32,14 +54,16 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
|||
# The package should be built beforehand from tip-of-tree Playwright.
|
||||
COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz
|
||||
|
||||
# 2. Install playwright and then delete the installation.
|
||||
# Browsers will remain downloaded in `/ms-playwright`.
|
||||
# 2. Install playwright agent.
|
||||
# Browsers will be downloaded in `/ms-playwright`.
|
||||
# Note: make sure to set 777 to the registry so that any user can access
|
||||
# registry.
|
||||
RUN mkdir /ms-playwright && \
|
||||
mkdir /tmp/pw && cd /tmp/pw && npm init -y && \
|
||||
mkdir /ms-playwright-agent && \
|
||||
cd /ms-playwright-agent && npm init -y && \
|
||||
npm i /tmp/playwright-core.tar.gz && \
|
||||
npx playwright install && \
|
||||
DEBIAN_FRONTEND=noninteractive npx playwright install-deps && \
|
||||
rm -rf /tmp/pw && rm /tmp/playwright-core.tar.gz && \
|
||||
npx playwright install --with-deps && \
|
||||
rm /tmp/playwright-core.tar.gz && \
|
||||
chmod -R 777 /ms-playwright
|
||||
|
||||
COPY start_agent.sh /ms-playwright-agent/start_agent.sh
|
||||
|
|
|
|||
28
utils/docker/start_agent.sh
Normal file
28
utils/docker/start_agent.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
set +x
|
||||
|
||||
SCREEN_WIDTH=1360
|
||||
SCREEN_HEIGHT=1020
|
||||
SCREEN_DEPTH=24
|
||||
SCREEN_DPI=96
|
||||
GEOMETRY="${SCREEN_WIDTH}""x""${SCREEN_HEIGHT}""x""${SCREEN_DEPTH}"
|
||||
|
||||
nohup /usr/bin/xvfb-run --server-num=${DISPLAY_NUM} \
|
||||
--listen-tcp \
|
||||
--server-args="-screen 0 ${GEOMETRY} -fbdir /var/tmp -dpi ${SCREEN_DPI} -listen tcp -noreset -ac +extension RANDR" \
|
||||
/usr/bin/fluxbox -display ${DISPLAY} >/dev/null 2>&1 &
|
||||
|
||||
for i in $(seq 1 50)
|
||||
do
|
||||
if xdpyinfo -display ${DISPLAY} >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting for Xvfb..."
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
nohup x11vnc -forever -shared -rfbport 5900 -rfbportv6 5900 -display ${DISPLAY} >/dev/null 2>&1 &
|
||||
nohup /opt/bin/noVNC/utils/launch.sh --listen 7900 --vnc localhost:5900 >/dev/null 2>&1 &
|
||||
|
||||
npx playwright experimental-grid-agent --agent-id "$1" --grid-url "$2"
|
||||
Loading…
Reference in a new issue