diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index 02b1081e2a..7dbf1b3a12 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -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", diff --git a/packages/playwright-core/src/cli/cli.ts b/packages/playwright-core/src/cli/cli.ts index 65c0e30ead..c1c99ade79 100755 --- a/packages/playwright-core/src/cli/cli.ts +++ b/packages/playwright-core/src/cli/cli.ts @@ -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 ', 'specify user agent string') .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"'); } + +async function launchGridServer(factoryPathOrPackageName: string, port: number, authToken: string|undefined): Promise { + 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()); +} diff --git a/packages/playwright-core/src/grid/dockerGridFactory.ts b/packages/playwright-core/src/grid/dockerGridFactory.ts new file mode 100644 index 0000000000..b230575f87 --- /dev/null +++ b/packages/playwright-core/src/grid/dockerGridFactory.ts @@ -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 { + 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 { + 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(); + }); +} diff --git a/packages/playwright-core/src/grid/gridServer.ts b/packages/playwright-core/src/grid/gridServer.ts index 443c734fbc..018afc1367 100644 --- a/packages/playwright-core/src/grid/gridServer.ts +++ b/packages/playwright-core/src/grid/gridServer.ts @@ -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, }; @@ -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(); 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(); @@ -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 { - 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()); -} diff --git a/packages/playwright-core/src/grid/simpleGridFactory.ts b/packages/playwright-core/src/grid/simpleGridFactory.ts index 268ddaa30c..cfc7253cce 100644 --- a/packages/playwright-core/src/grid/simpleGridFactory.ts +++ b/packages/playwright-core/src/grid/simpleGridFactory.ts @@ -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) { - child_process.spawn(process.argv[0], [ - path.join(__dirname, '..', 'cli', 'cli.js'), - 'experimental-grid-agent', - '--grid-url', gridURL, - '--agent-id', agentId, - ], { - cwd: __dirname, - shell: true, - stdio: 'inherit', - }); -} +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', options.gridURL, + '--agent-id', options.agentId, + ], { + cwd: __dirname, + shell: true, + stdio: 'inherit', + }); + }, +}; +export default simpleFactory; diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index e4a8be01b9..f4d6c69fba 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -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> { + 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(); +} diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index efcdc80467..eacf7d1698 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -56,10 +56,13 @@ type RunResult = { clashingTests: Map }; +type InternalGlobalSetupFunction = () => Promise<() => Promise>; + export class Runner { private _loader: Loader; private _reporter!: Reporter; private _didBegin = false; + private _internalGlobalSetups: Array = []; 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 { 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)[] = []; + 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(); } } } diff --git a/utils/docker/Dockerfile.bionic b/utils/docker/Dockerfile.bionic index c30e4b4aaf..58d743aa81 100644 --- a/utils/docker/Dockerfile.bionic +++ b/utils/docker/Dockerfile.bionic @@ -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 diff --git a/utils/docker/Dockerfile.focal b/utils/docker/Dockerfile.focal index 1b8f3d2486..20ac37cc17 100644 --- a/utils/docker/Dockerfile.focal +++ b/utils/docker/Dockerfile.focal @@ -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 diff --git a/utils/docker/start_agent.sh b/utils/docker/start_agent.sh new file mode 100644 index 0000000000..35a0c2f999 --- /dev/null +++ b/utils/docker/start_agent.sh @@ -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"