2022-09-09 15:25:42 -07:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
/* eslint-disable no-console */
|
|
|
|
|
|
|
|
import path from 'path';
|
|
|
|
import fs from 'fs';
|
|
|
|
import { spawnAsync } from 'playwright-core/lib/utils/spawnAsync';
|
|
|
|
import * as utils from 'playwright-core/lib/utils';
|
|
|
|
import { getPlaywrightVersion } from 'playwright-core/lib/common/userAgent';
|
2022-09-13 10:55:11 -07:00
|
|
|
import * as dockerApi from './dockerApi';
|
2022-09-09 15:25:42 -07:00
|
|
|
|
|
|
|
const VRT_IMAGE_DISTRO = 'focal';
|
|
|
|
const VRT_IMAGE_NAME = `playwright:local-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
|
|
|
const VRT_CONTAINER_NAME = `playwright-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
|
|
|
|
|
|
|
export async function deleteImage() {
|
|
|
|
const dockerImage = await findDockerImage(VRT_IMAGE_NAME);
|
|
|
|
if (!dockerImage)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (await containerInfo())
|
|
|
|
await stopContainer();
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.removeImage(dockerImage.imageId);
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function buildImage() {
|
|
|
|
const isDevelopmentMode = getPlaywrightVersion().includes('next');
|
|
|
|
let baseImageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
|
|
|
// 1. Build or pull base image.
|
|
|
|
if (isDevelopmentMode) {
|
|
|
|
// Use our docker build scripts in development mode!
|
|
|
|
if (!process.env.PWTEST_DOCKER_BASE_IMAGE) {
|
|
|
|
const arch = process.arch === 'arm64' ? '--arm64' : '--amd64';
|
|
|
|
console.error(utils.wrapInASCIIBox([
|
|
|
|
`You are in DEVELOPMENT mode!`,
|
|
|
|
``,
|
|
|
|
`1. Build local base image`,
|
|
|
|
` ./utils/docker/build.sh ${arch} ${VRT_IMAGE_DISTRO} playwright:localbuild`,
|
|
|
|
`2. Use the local base to build VRT image:`,
|
|
|
|
` PWTEST_DOCKER_BASE_IMAGE=playwright:localbuild npx playwright docker build`,
|
|
|
|
].join('\n'), 1));
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
baseImageName = process.env.PWTEST_DOCKER_BASE_IMAGE;
|
|
|
|
} else {
|
|
|
|
const { code } = await spawnAsync('docker', ['pull', baseImageName], { stdio: 'inherit' });
|
|
|
|
if (code !== 0)
|
|
|
|
throw new Error('Failed to pull docker image!');
|
|
|
|
}
|
|
|
|
// 2. Find pulled docker image
|
|
|
|
const dockerImage = await findDockerImage(baseImageName);
|
|
|
|
if (!dockerImage)
|
|
|
|
throw new Error(`Failed to pull ${baseImageName}`);
|
|
|
|
// 3. Launch container and install VNC in it
|
|
|
|
console.log(`Building ${VRT_IMAGE_NAME}...`);
|
|
|
|
const buildScriptText = await fs.promises.readFile(path.join(__dirname, 'build_docker_image.sh'), 'utf8');
|
2022-09-13 10:55:11 -07:00
|
|
|
const containerId = await dockerApi.launchContainer({
|
|
|
|
imageId: dockerImage.imageId,
|
2022-09-09 15:25:42 -07:00
|
|
|
autoRemove: false,
|
|
|
|
command: ['/bin/bash', '-c', buildScriptText],
|
2022-09-13 10:55:11 -07:00
|
|
|
waitUntil: 'not-running',
|
2022-09-09 15:25:42 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
// 4. Commit a new image based on the launched container with installed VNC & noVNC.
|
|
|
|
const [vrtRepo, vrtTag] = VRT_IMAGE_NAME.split(':');
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.commitContainer({
|
|
|
|
containerId,
|
|
|
|
repo: vrtRepo,
|
|
|
|
tag: vrtTag,
|
|
|
|
entrypoint: '/entrypoint.sh',
|
|
|
|
env: {
|
|
|
|
'DISPLAY_NUM': '99',
|
|
|
|
'DISPLAY': ':99',
|
|
|
|
},
|
2022-09-09 15:25:42 -07:00
|
|
|
});
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.removeContainer(containerId);
|
2022-09-09 15:25:42 -07:00
|
|
|
console.log(`Done!`);
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ContainerInfo {
|
|
|
|
wsEndpoint: string;
|
|
|
|
vncSession: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function containerInfo(): Promise<ContainerInfo|undefined> {
|
2022-09-13 13:23:04 -07:00
|
|
|
const container = await findRunningDockerContainer();
|
|
|
|
if (!container)
|
2022-09-09 15:25:42 -07:00
|
|
|
return undefined;
|
2022-09-13 13:23:04 -07:00
|
|
|
const logLines = await dockerApi.getContainerLogs(container.containerId);
|
|
|
|
|
|
|
|
const containerUrlToHostUrl = (address: string) => {
|
|
|
|
const url = new URL(address);
|
|
|
|
const portBinding = container.portBindings.find(binding => binding.containerPort === +url.port);
|
|
|
|
if (!portBinding)
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
url.host = portBinding.ip;
|
|
|
|
url.port = portBinding.hostPort + '';
|
|
|
|
return url.toString();
|
|
|
|
};
|
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
const WS_LINE_PREFIX = 'Listening on ws://';
|
|
|
|
const webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX));
|
|
|
|
const NOVNC_LINE_PREFIX = 'novnc is listening on ';
|
|
|
|
const novncLine = logLines.find(line => line.startsWith(NOVNC_LINE_PREFIX));
|
2022-09-13 13:23:04 -07:00
|
|
|
if (!novncLine || !webSocketLine)
|
|
|
|
return undefined;
|
|
|
|
const wsEndpoint = containerUrlToHostUrl('ws://' + webSocketLine.substring(WS_LINE_PREFIX.length));
|
|
|
|
const vncSession = containerUrlToHostUrl(novncLine.substring(NOVNC_LINE_PREFIX.length));
|
|
|
|
return wsEndpoint && vncSession ? { wsEndpoint, vncSession } : undefined;
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function ensureContainerOrDie(): Promise<ContainerInfo> {
|
|
|
|
const pwImage = await findDockerImage(VRT_IMAGE_NAME);
|
|
|
|
if (!pwImage) {
|
|
|
|
console.error('\n' + utils.wrapInASCIIBox([
|
|
|
|
`Failed to find local docker image.`,
|
|
|
|
`Please build local docker image with the following command:`,
|
|
|
|
``,
|
|
|
|
` npx playwright docker build`,
|
|
|
|
``,
|
|
|
|
`<3 Playwright Team`,
|
|
|
|
].join('\n'), 1));
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
let info = await containerInfo();
|
|
|
|
if (info)
|
|
|
|
return info;
|
|
|
|
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.launchContainer({
|
|
|
|
imageId: pwImage.imageId,
|
2022-09-09 15:25:42 -07:00
|
|
|
name: VRT_CONTAINER_NAME,
|
|
|
|
autoRemove: true,
|
|
|
|
ports: [5400, 7900],
|
|
|
|
});
|
|
|
|
|
|
|
|
// Wait for the service to become available.
|
|
|
|
const startTime = Date.now();
|
|
|
|
const timeouts = [0, 100, 100, 200, 500, 1000];
|
|
|
|
do {
|
|
|
|
await new Promise(x => setTimeout(x, timeouts.shift() ?? 1000));
|
|
|
|
info = await containerInfo();
|
|
|
|
} while (!info && Date.now() < startTime + 60000);
|
|
|
|
|
|
|
|
if (!info)
|
|
|
|
throw new Error('Failed to launch docker container!');
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function stopContainer() {
|
2022-09-13 13:23:04 -07:00
|
|
|
const container = await findRunningDockerContainer();
|
|
|
|
if (!container)
|
2022-09-09 15:25:42 -07:00
|
|
|
return;
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.stopContainer({
|
2022-09-13 13:23:04 -07:00
|
|
|
containerId: container.containerId,
|
2022-09-13 10:55:11 -07:00
|
|
|
waitUntil: 'removed',
|
|
|
|
});
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function ensureDockerEngineIsRunningOrDie() {
|
2022-09-13 10:55:11 -07:00
|
|
|
if (await dockerApi.checkEngineRunning())
|
|
|
|
return;
|
|
|
|
console.error(utils.wrapInASCIIBox([
|
|
|
|
`Docker is not running!`,
|
|
|
|
`Please install and launch docker:`,
|
|
|
|
``,
|
|
|
|
` https://docs.docker.com/get-docker`,
|
|
|
|
``,
|
|
|
|
].join('\n'), 1));
|
|
|
|
process.exit(1);
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 10:55:11 -07:00
|
|
|
async function findDockerImage(imageName: string): Promise<dockerApi.DockerImage|undefined> {
|
|
|
|
const images = await dockerApi.listImages();
|
|
|
|
return images.find(image => image.names.includes(imageName));
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 13:23:04 -07:00
|
|
|
async function findRunningDockerContainer(): Promise<dockerApi.DockerContainer|undefined> {
|
2022-09-13 10:55:11 -07:00
|
|
|
const containers = await dockerApi.listContainers();
|
2022-09-09 15:25:42 -07:00
|
|
|
const dockerImage = await findDockerImage(VRT_IMAGE_NAME);
|
2022-09-13 10:55:11 -07:00
|
|
|
const container = dockerImage ? containers.find(container => container.imageId === dockerImage.imageId) : undefined;
|
2022-09-13 13:23:04 -07:00
|
|
|
return container?.state === 'running' ? container : undefined;
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|