diff --git a/packages/runner/src/commands/test/parse-options.js b/packages/runner/src/commands/test/parse-options.js index 656ec810..beb8389f 100644 --- a/packages/runner/src/commands/test/parse-options.js +++ b/packages/runner/src/commands/test/parse-options.js @@ -56,7 +56,6 @@ function parseOptions(args, config) { targetFilter: argv.targetFilter, configurationFilter: argv.configurationFilter || argv._[1], dockerWithSudo: $('dockerWithSudo'), - chromeDockerUseCopy: $('chromeDockerUseCopy'), chromeDockerWithoutSeccomp: $('chromeDockerWithoutSeccomp'), passWithNoStories: $('passWithNoStories'), device: $('device'), diff --git a/packages/runner/src/commands/test/run-tests.js b/packages/runner/src/commands/test/run-tests.js index a64cc47e..954460c8 100644 --- a/packages/runner/src/commands/test/run-tests.js +++ b/packages/runner/src/commands/test/run-tests.js @@ -256,7 +256,6 @@ async function runTests(flatConfigurations, options) { chromeFlags: options.chromeFlags, dockerNet: options.dockerNet, dockerWithSudo: options.dockerWithSudo, - chromeDockerUseCopy: options.chromeDockerUseCopy, chromeDockerWithoutSeccomp: options.chromeDockerWithoutSeccomp, }), configurations, diff --git a/packages/target-chrome-docker/package.json b/packages/target-chrome-docker/package.json index 372cddbb..d32ad2e9 100644 --- a/packages/target-chrome-docker/package.json +++ b/packages/target-chrome-docker/package.json @@ -26,7 +26,8 @@ "debug": "^4.1.1", "execa": "^5.0.0", "fs-extra": "^9.1.0", - "get-port": "^5.1.1", + "find-free-port-sync": "^1.0.0", + "mime-types": "^2.1.35", "wait-on": "^5.2.1" }, "publishConfig": { diff --git a/packages/target-chrome-docker/src/create-chrome-docker-target.js b/packages/target-chrome-docker/src/create-chrome-docker-target.js index 16f77e5f..a839b628 100644 --- a/packages/target-chrome-docker/src/create-chrome-docker-target.js +++ b/packages/target-chrome-docker/src/create-chrome-docker-target.js @@ -3,7 +3,7 @@ const { execSync } = require('child_process'); const execa = require('execa'); const waitOn = require('wait-on'); const CDP = require('chrome-remote-interface'); -const getRandomPort = require('get-port'); +const getRandomPort = require('find-free-port-sync'); const { ChromeError, ensureDependencyAvailable, @@ -12,6 +12,7 @@ const { const { createChromeTarget } = require('@loki/target-chrome-core'); const { getLocalIPAddress } = require('./get-local-ip-address'); const { getNetworkHost } = require('./get-network-host'); +const { createStaticServer } = require('./create-static-server'); const getExecutor = (dockerWithSudo) => (dockerPath, args) => { if (dockerWithSudo) { @@ -46,16 +47,16 @@ function createChromeDockerTarget({ chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], dockerNet = null, dockerWithSudo = false, - chromeDockerUseCopy = false, chromeDockerWithoutSeccomp = false, }) { - let port; + let debuggerPort; + let staticServer; + let staticServerPath; + let staticServerPort; let dockerId; let host; - let localPath; let dockerUrl = getAbsoluteURL(baseUrl); const isLocalFile = dockerUrl.indexOf('file:') === 0; - const staticMountPath = '/var/loki'; const dockerPath = 'docker'; const runArgs = ['run', '--rm', '-d', '-P']; const execute = getExecutor(dockerWithSudo); @@ -65,21 +66,19 @@ function createChromeDockerTarget({ } runArgs.push('--add-host=host.docker.internal:host-gateway'); - if (dockerUrl.indexOf('http://localhost') === 0) { + if (dockerUrl.indexOf('http://localhost') === 0 || isLocalFile) { const ip = getLocalIPAddress(); if (!ip) { throw new Error( 'Unable to detect local IP address, try passing --host argument' ); } - dockerUrl = dockerUrl.replace('localhost', ip); - } else if (isLocalFile) { - localPath = dockerUrl.substr('file:'.length); - dockerUrl = `file://${staticMountPath}`; - if (!chromeDockerUseCopy) { - // setup volume mount if we're not using copy - runArgs.push('-v'); - runArgs.push(`${localPath}:${staticMountPath}`); + if (isLocalFile) { + staticServerPort = getRandomPort(); + staticServerPath = dockerUrl.substr('file:'.length); + dockerUrl = `http://${ip}:${staticServerPort}`; + } else { + dockerUrl = dockerUrl.replace('localhost', ip); } } @@ -96,19 +95,6 @@ function createChromeDockerTarget({ return stdout.trim().length !== 0; } - async function copyFiles() { - const { exitCode, stdout, stderr } = await execute(dockerPath, [ - 'cp', - localPath, - `${dockerId}:${staticMountPath}`, - ]); - - if (exitCode !== 0) { - throw new Error(`Failed to copy files, ${stderr}`); - } - return stdout.trim().length !== 0; - } - async function ensureImageDownloaded() { ensureDependencyAvailable('docker'); @@ -119,13 +105,19 @@ function createChromeDockerTarget({ } async function start() { - port = await getRandomPort(); - ensureDependencyAvailable('docker'); + + debuggerPort = getRandomPort(); + if (isLocalFile) { + staticServer = createStaticServer(staticServerPath); + staticServer.listen(staticServerPort); + debug(`Starting static file server at ${dockerUrl}`); + } + const dockerArgs = runArgs.concat([ '--shm-size=1g', '-p', - `${port}:${port}`, + `${debuggerPort}:${debuggerPort}`, ]); if (dockerNet) { @@ -139,7 +131,7 @@ function createChromeDockerTarget({ '--no-first-run', '--disable-extensions', '--remote-debugging-address=0.0.0.0', - `--remote-debugging-port=${port}`, + `--remote-debugging-port=${debuggerPort}`, ]) .concat(chromeFlags); @@ -151,9 +143,6 @@ function createChromeDockerTarget({ const { exitCode, stdout, stderr } = await execute(dockerPath, args); if (exitCode === 0) { dockerId = stdout; - if (chromeDockerUseCopy) { - await copyFiles(); - } const logs = execute(dockerPath, ['logs', dockerId, '--follow']); const errorLogs = []; logs.stderr.on('data', (chunk) => { @@ -162,7 +151,7 @@ function createChromeDockerTarget({ host = await getNetworkHost(execute, dockerId); try { - await waitOnCDPAvailable(host, port); + await waitOnCDPAvailable(host, debuggerPort); } catch (error) { if ( error.message.startsWith('Timed out waiting for') && @@ -195,17 +184,20 @@ function createChromeDockerTarget({ } else { debug('No chrome docker instance to kill'); } + if (staticServer) { + staticServer.close(); + } } async function createNewDebuggerInstance() { - debug(`Launching new tab with debugger at port ${host}:${port}`); - const target = await CDP.New({ host, port }); + debug(`Launching new tab with debugger at port ${host}:${debuggerPort}`); + const target = await CDP.New({ host, port: debuggerPort }); debug(`Launched with target id ${target.id}`); - const client = await CDP({ host, port, target }); + const client = await CDP({ host, port: debuggerPort, target }); client.close = () => { debug('Closing tab'); - return CDP.Close({ host, port, id: target.id }); + return CDP.Close({ host, port: debuggerPort, id: target.id }); }; return client; diff --git a/packages/target-chrome-docker/src/create-static-server.js b/packages/target-chrome-docker/src/create-static-server.js new file mode 100644 index 00000000..f70a656a --- /dev/null +++ b/packages/target-chrome-docker/src/create-static-server.js @@ -0,0 +1,55 @@ +/* eslint-disable consistent-return */ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const mime = require('mime-types'); + +async function sendFile(res, filePath) { + const file = await fs.promises.open(filePath, 'r'); + try { + const stat = await file.stat(); + if (!stat.isFile()) { + const err = new Error('Path is directory'); + err.code = 'EISDIR'; + throw err; + } + const contentType = mime.contentType(path.basename(filePath)); + + const headers = { + 'Content-Length': stat.size, + 'Cache-Control': 'no-store, must-revalidate', + }; + if (contentType) { + headers['Content-Type'] = contentType; + } + res.writeHead(200, headers); + + const readStream = file.createReadStream({ autoClose: true }); + readStream.pipe(res, { end: true }); + readStream.on('close', () => { + file.close(); + }); + } catch (err) { + file.close(); + throw err; + } +} + +const createStaticServer = (dir) => + http.createServer(async (req, res) => { + const url = new URL(`http://localhost${req.url}`); + const staticFilePath = path.normalize( + path.join(dir, url.pathname === '/' ? 'index.html' : url.pathname) + ); + if (staticFilePath.startsWith(dir)) { + try { + return await sendFile(res, staticFilePath); + } catch (err) { + if (err.code !== 'ENOENT' && err.code !== 'EISDIR') { + throw err; + } + } + } + }); + +module.exports = { createStaticServer }; diff --git a/yarn.lock b/yarn.lock index 4ac31e25..62337926 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10758,6 +10758,11 @@ find-cache-dir@^3.2.0, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-free-port-sync@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-free-port-sync/-/find-free-port-sync-1.0.0.tgz#10c9007655b6b65a7900e79d391e8da21e31cc19" + integrity sha512-wRkO8crYqjaTvCnqEfQGuV8LOp4JO0Ctjn6qROGPcradK+6jQ7giLMGLnKlNxQm6dEdYD3/TBABQ7Xi/5ZhWcg== + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -11150,11 +11155,6 @@ get-port@^4.2.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== -get-port@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" - integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== - get-proxy@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/get-proxy/-/get-proxy-2.1.0.tgz#349f2b4d91d44c4d4d4e9cba2ad90143fac5ef93" @@ -14785,7 +14785,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, dependencies: mime-db "1.45.0" -mime-types@^2.1.30, mime-types@^2.1.31: +mime-types@^2.1.30, mime-types@^2.1.31, mime-types@^2.1.35: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==