diff --git a/.gitignore b/.gitignore index 2aaee853..f98092c3 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ event.json headless-chromium-amazonlinux-2017-03.zip .eslintcache packages/lambda/integration-test/headless-chromium +.rpt2_cache diff --git a/package.json b/package.json index aaa5958c..b88e39fe 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "homepage": "https://github.com/adieuadieu/serverless-chrome", "dependencies": {}, "devDependencies": { + "@types/node": "^8.10.48", "ava": "0.25.0", "babel-core": "6.26.3", "babel-eslint": "8.2.3", @@ -66,11 +67,19 @@ "prettier": "1.12.1", "prettier-eslint": "8.8.1", "prettier-eslint-cli": "4.7.1", - "tap-xunit": "2.3.0" + "tap-xunit": "2.3.0", + "ts-node": "^8.1.0" }, "ava": { - "require": "babel-register", - "babel": "inherit" + "require": [ + "ts-node/register", + "babel-register" + ], + "babel": "inherit", + "extensions": [ + "ts", + "js" + ] }, "babel": { "sourceMaps": "inline", diff --git a/packages/lambda/package.json b/packages/lambda/package.json index 4da46b0d..d9cc3ebf 100644 --- a/packages/lambda/package.json +++ b/packages/lambda/package.json @@ -25,7 +25,7 @@ "homepage": "https://github.com/adieuadieu/serverless-chrome/tree/master/packages/lambda", "license": "MIT", "engines": { - "node": ">= 6.10" + "node": ">= 8.10" }, "config": { "jsSrc": "src/" @@ -34,6 +34,7 @@ "clean": "rm -Rf dist/ ./**.zip", "test": "npm run test:integration", "test:integration": "scripts/test-integration.sh", + "lint": "tslint src/**/*.ts", "build": "rollup -c", "dev": "rollup -c -w", "prepublishOnly": "npm run clean && npm run build", @@ -43,30 +44,18 @@ "upgrade-dependencies": "yarn upgrade-interactive --latest --exact" }, "dependencies": { + "debug": "^4.1.1", "extract-zip": "1.6.6" }, "devDependencies": { + "@types/debug": "^4.1.4", + "@types/node": "^8.10.48", "ava": "0.25.0", - "babel-core": "6.26.3", - "babel-preset-env": "1.7.0", - "babel-register": "6.26.0", "chrome-launcher": "0.10.2", - "rollup": "0.59.1", - "rollup-plugin-babel": "3.0.4", - "rollup-plugin-node-resolve": "3.3.0" - }, - "babel": { - "sourceMaps": true, - "presets": [ - [ - "env", - { - "modules": "commonjs", - "targets": { - "node": "6.10" - } - } - ] - ] + "rollup": "^1.11.3", + "rollup-plugin-node-resolve": "^4.2.3", + "rollup-plugin-typescript2": "^0.21.0", + "tslint": "^5.16.0", + "typescript": "^3.4.5" } } diff --git a/packages/lambda/rollup.config.js b/packages/lambda/rollup.config.js index 6fa976be..953107e9 100644 --- a/packages/lambda/rollup.config.js +++ b/packages/lambda/rollup.config.js @@ -1,8 +1,8 @@ -import babel from 'rollup-plugin-babel' import resolve from 'rollup-plugin-node-resolve' +import typescript from 'rollup-plugin-typescript2' export default { - input: 'src/index.js', + input: 'src/index.ts', output: [ { file: 'dist/bundle.cjs.js', format: 'cjs' }, { file: 'dist/bundle.es.js', format: 'es' }, @@ -20,20 +20,7 @@ export default { // ES2015 modules // modulesOnly: true, // Default: false }), - babel({ - babelrc: false, - presets: [ - [ - 'env', - { - modules: false, - targets: { - node: '6.10', - }, - }, - ], - ], - }), + typescript(), ], - external: ['fs', 'child_process', 'net', 'http', 'path', 'chrome-launcher'], + external: ['fs', 'child_process', 'net', 'path', 'chrome-launcher', 'debug'], } diff --git a/packages/lambda/src/flags.js b/packages/lambda/src/flags.js deleted file mode 100644 index 33c8e2e7..00000000 --- a/packages/lambda/src/flags.js +++ /dev/null @@ -1,17 +0,0 @@ -const LOGGING_FLAGS = process.env.DEBUG - ? ['--enable-logging', '--log-level=0', '--v=99'] - : [] - -export default [ - ...LOGGING_FLAGS, - '--disable-dev-shm-usage', // disable /dev/shm tmpfs usage on Lambda - - // @TODO: review if these are still relevant: - '--disable-gpu', - '--single-process', // Currently wont work without this :-( - - // https://groups.google.com/a/chromium.org/d/msg/headless-dev/qqbZVZ2IwEw/Y95wJUh2AAAJ - '--no-zygote', // helps avoid zombies - - '--no-sandbox', -] diff --git a/packages/lambda/src/index.js b/packages/lambda/src/index.js deleted file mode 100644 index abf4e728..00000000 --- a/packages/lambda/src/index.js +++ /dev/null @@ -1,117 +0,0 @@ -import fs from 'fs' -// import path from 'path' -import LambdaChromeLauncher from './launcher' -import { debug, processExists } from './utils' -import DEFAULT_CHROME_FLAGS from './flags' - -const DEVTOOLS_PORT = 9222 -const DEVTOOLS_HOST = 'http://127.0.0.1' - -// Prepend NSS related libraries and binaries to the library path and path respectively on lambda. -/* if (process.env.AWS_EXECUTION_ENV) { - const nssSubPath = fs.readFileSync(path.join(__dirname, 'nss', 'latest'), 'utf8').trim(); - const nssPath = path.join(__dirname, 'nss', subnssSubPathPath); - - process.env.LD_LIBRARY_PATH = path.join(nssPath, 'lib') + ':' + process.env.LD_LIBRARY_PATH; - process.env.PATH = path.join(nssPath, 'bin') + ':' + process.env.PATH; -} */ - -// persist the instance across invocations -// when the *lambda* container is reused. -let chromeInstance - -export default async function launch ({ - flags = [], - chromePath, - port = DEVTOOLS_PORT, - forceLambdaLauncher = false, -} = {}) { - const chromeFlags = [...DEFAULT_CHROME_FLAGS, ...flags] - - if (!chromeInstance || !processExists(chromeInstance.pid)) { - if (process.env.AWS_EXECUTION_ENV || forceLambdaLauncher) { - chromeInstance = new LambdaChromeLauncher({ - chromePath, - chromeFlags, - port, - }) - } else { - // This let's us use chrome-launcher in local development, - // but omit it from the lambda function's zip artefact - try { - // eslint-disable-next-line - const { Launcher: LocalChromeLauncher } = require('chrome-launcher') - chromeInstance = new LocalChromeLauncher({ - chromePath, - chromeFlags: flags, - port, - }) - } catch (error) { - throw new Error('@serverless-chrome/lambda: Unable to find "chrome-launcher". ' + - "Make sure it's installed if you wish to develop locally.") - } - } - } - - debug('Spawning headless shell') - - const launchStartTime = Date.now() - - try { - await chromeInstance.launch() - } catch (error) { - debug('Error trying to spawn chrome:', error) - - if (process.env.DEBUG) { - debug( - 'stdout log:', - fs.readFileSync(`${chromeInstance.userDataDir}/chrome-out.log`, 'utf8') - ) - debug( - 'stderr log:', - fs.readFileSync(`${chromeInstance.userDataDir}/chrome-err.log`, 'utf8') - ) - } - - throw new Error('Unable to start Chrome. If you have the DEBUG env variable set,' + - 'there will be more in the logs.') - } - - const launchTime = Date.now() - launchStartTime - - debug(`It took ${launchTime}ms to spawn chrome.`) - - // unref the chrome instance, otherwise the lambda process won't end correctly - /* @TODO: make this an option? - There's an option to change callbackWaitsForEmptyEventLoop in the Lambda context - http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html - Which means you could log chrome output to cloudwatch directly - without unreffing chrome. - */ - if (chromeInstance.chrome) { - chromeInstance.chrome.removeAllListeners() - chromeInstance.chrome.unref() - } - - return { - pid: chromeInstance.pid, - port: chromeInstance.port, - url: `${DEVTOOLS_HOST}:${chromeInstance.port}`, - log: `${chromeInstance.userDataDir}/chrome-out.log`, - errorLog: `${chromeInstance.userDataDir}/chrome-err.log`, - pidFile: `${chromeInstance.userDataDir}/chrome.pid`, - metaData: { - launchTime, - didLaunch: !!chromeInstance.pid, - }, - async kill () { - // Defer killing chrome process to the end of the execution stack - // so that the node process doesn't end before chrome exists, - // avoiding chrome becoming orphaned. - setTimeout(async () => { - chromeInstance.kill() - chromeInstance = undefined - }, 0) - }, - } -} diff --git a/packages/lambda/src/index.test.js b/packages/lambda/src/index.test.js deleted file mode 100644 index 7a415ce6..00000000 --- a/packages/lambda/src/index.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import test from 'ava' -import * as chromeFinder from 'chrome-launcher/dist/chrome-finder' -import launch from './index' - -const DEFAULT_TEST_FLAGS = ['--headless'] - -async function getLocalChromePath () { - const installations = await chromeFinder[process.platform]() - - if (installations.length === 0) { - throw new Error('No Chrome Installations Found') - } - - return installations[0] -} - -test.serial('Chrome should launch using LocalChromeLauncher', async (t) => { - const chromePath = await getLocalChromePath() - const chrome = launch({ - flags: DEFAULT_TEST_FLAGS, - chromePath, - port: 9220, - }) - - t.notThrows(chrome) - - const instance = await chrome - - t.truthy(instance.pid, 'pid should be set') - t.truthy(instance.port, 'port should be set') - t.is(instance.port, 9220, 'port should be 9220') - - instance.kill() -}) - -// Covered by the integration-test. -test('Chrome should launch using LambdaChromeLauncher', (t) => { - // @TODO: proper test.. - t.pass() -}) diff --git a/packages/lambda/src/index.test.ts b/packages/lambda/src/index.test.ts new file mode 100644 index 00000000..b1306baa --- /dev/null +++ b/packages/lambda/src/index.test.ts @@ -0,0 +1,40 @@ +import test from "ava"; +import * as chromeFinder from "chrome-launcher/dist/chrome-finder"; +import launch from "./index"; + +const DEFAULT_TEST_FLAGS = ["--headless"]; + +async function getLocalChromePath() { + const installations = await chromeFinder[process.platform](); + + if (installations.length === 0) { + throw new Error("No Chrome Installations Found"); + } + + return installations[0]; +} + +test.serial("Chrome should launch using LocalChromeLauncher", async (t) => { + const chromePath = await getLocalChromePath(); + const chrome = await launch({ + flags: DEFAULT_TEST_FLAGS, + chromePath, + port: 9220, + }); + + t.notThrows(chrome); + + const instance = await chrome; + + t.truthy(instance.pid, "pid should be set"); + t.truthy(instance.port, "port should be set"); + t.is(instance.port, 9220, "port should be 9220"); + + await instance.kill(); +}); + +// Covered by the integration-test. +test("Chrome should launch using LambdaChromeLauncher", (t) => { + // @TODO: proper test.. + t.pass(); +}); diff --git a/packages/lambda/src/index.ts b/packages/lambda/src/index.ts new file mode 100644 index 00000000..f8f8da2e --- /dev/null +++ b/packages/lambda/src/index.ts @@ -0,0 +1,110 @@ +import debug from "debug"; +import LambdaChromeLauncher from "./launcher"; + +const log = debug("@serverless-chrome/lambda"); + +const DEVTOOLS_HOST = "http://127.0.0.1"; + +// Prepend NSS related libraries and binaries to the library path and path respectively on lambda. +/* if (process.env.AWS_EXECUTION_ENV) { + const nssSubPath = fs.readFileSync(path.join(__dirname, 'nss', 'latest'), 'utf8').trim(); + const nssPath = path.join(__dirname, 'nss', subnssSubPathPath); + + process.env.LD_LIBRARY_PATH = path.join(nssPath, 'lib') + ':' + process.env.LD_LIBRARY_PATH; + process.env.PATH = path.join(nssPath, 'bin') + ':' + process.env.PATH; +} */ + +// persist the instance across invocations +// when the *lambda* container is reused. +let chromeInstance: LambdaChromeLauncher | undefined; + +export interface LaunchOption { + flags?: string[]; + chromePath?: string; + port?: number; + forceLambdaLauncher?: boolean; +} + +export default async function launch({ + flags: chromeFlags = [], + chromePath, + port, + forceLambdaLauncher = false, +}: LaunchOption = {}) { + if (!chromeInstance || !processExists(chromeInstance.pid!)) { + if (process.env.AWS_EXECUTION_ENV || forceLambdaLauncher) { + chromeInstance = new LambdaChromeLauncher({ + chromePath, + chromeFlags, + port, + }); + } else { + // This let's us use chrome-launcher in local development, + // but omit it from the lambda function's zip artifact + try { + const { Launcher: LocalChromeLauncher } = require("chrome-launcher"); + chromeInstance = (new LocalChromeLauncher({ + chromePath, + chromeFlags, + port, + })) as LambdaChromeLauncher; + } catch (error) { + throw new Error('@serverless-chrome/lambda: Unable to find "chrome-launcher". ' + + "Make sure it's installed if you wish to develop locally."); + } + } + } + + log("Spawning headless shell"); + + const launchStartTime = Date.now(); + + try { + await chromeInstance.launch(); + } catch (error) { + log("Error trying to spawn chrome:", error); + + throw new Error("Unable to start Chrome. If you have the DEBUG env variable set," + + "there will be more in the logs."); + } + + const launchTime = Date.now() - launchStartTime; + + log("It took %dms to spawn chrome.", launchTime); + + // unref the chrome instance, otherwise the lambda process won't end correctly + /* @TODO: make this an option? + There's an option to change callbackWaitsForEmptyEventLoop in the Lambda context + http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html + Which means you could log chrome output to cloudwatch directly + without unreffing chrome. + */ + if (chromeInstance.chrome) { + chromeInstance.chrome.unref(); + } + + return { + pid: chromeInstance.pid, + port: chromeInstance.port, + url: `${DEVTOOLS_HOST}:${chromeInstance.port}`, + metaData: { + launchTime, + didLaunch: !!chromeInstance.pid, + }, + async kill() { + if (chromeInstance) { + await chromeInstance.kill(); + chromeInstance = undefined; + } + }, + }; +} + +function processExists(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return false; + } +} diff --git a/packages/lambda/src/launcher.js b/packages/lambda/src/launcher.js deleted file mode 100644 index 8db546e5..00000000 --- a/packages/lambda/src/launcher.js +++ /dev/null @@ -1,258 +0,0 @@ -/* - Large portion of this is inspired by/taken from Lighthouse/chrome-launcher. - It is Copyright Google Inc, licensed under Apache License, Version 2.0. - https://github.com/GoogleChrome/lighthouse/blob/master/chrome-launcher/chrome-launcher.ts - - We ship a modified version because the original verion comes with too - many dependencies which complicates packaging of serverless services. -*/ - -import path from 'path' -import fs from 'fs' -import { execSync, spawn } from 'child_process' -import net from 'net' -import http from 'http' -import { delay, debug, makeTempDir, clearConnection } from './utils' -import DEFAULT_CHROME_FLAGS from './flags' - -const CHROME_PATH = path.resolve(__dirname, './headless-chromium') - -export default class Launcher { - constructor (options = {}) { - const { - chromePath = CHROME_PATH, - chromeFlags = [], - startingUrl = 'about:blank', - port = 0, - } = options - - this.tmpDirandPidFileReady = false - this.pollInterval = 500 - this.pidFile = '' - this.startingUrl = 'about:blank' - this.outFile = null - this.errFile = null - this.chromePath = CHROME_PATH - this.chromeFlags = [] - this.requestedPort = 0 - this.userDataDir = '' - this.port = 9222 - this.pid = null - this.chrome = undefined - - this.options = options - this.startingUrl = startingUrl - this.chromeFlags = chromeFlags - this.chromePath = chromePath - this.requestedPort = port - } - - get flags () { - return [ - ...DEFAULT_CHROME_FLAGS, - `--remote-debugging-port=${this.port}`, - `--user-data-dir=${this.userDataDir}`, - '--disable-setuid-sandbox', - ...this.chromeFlags, - this.startingUrl, - ] - } - - prepare () { - this.userDataDir = this.options.userDataDir || makeTempDir() - this.outFile = fs.openSync(`${this.userDataDir}/chrome-out.log`, 'a') - this.errFile = fs.openSync(`${this.userDataDir}/chrome-err.log`, 'a') - this.pidFile = '/tmp/chrome.pid' - this.tmpDirandPidFileReady = true - } - - // resolves if ready, rejects otherwise - isReady () { - return new Promise((resolve, reject) => { - const client = net.createConnection(this.port) - - client.once('error', (error) => { - clearConnection(client) - reject(error) - }) - - client.once('connect', () => { - clearConnection(client) - resolve() - }) - }) - } - - // resolves when debugger is ready, rejects after 10 polls - waitUntilReady () { - const launcher = this - - return new Promise((resolve, reject) => { - let retries = 0; - (function poll () { - debug('Waiting for Chrome', retries) - - launcher - .isReady() - .then(() => { - debug('Started Chrome') - resolve() - }) - .catch((error) => { - retries += 1 - - if (retries > 10) { - return reject(error) - } - - return delay(launcher.pollInterval).then(poll) - }) - }()) - }) - } - - // resolves when chrome is killed, rejects after 10 polls - waitUntilKilled () { - return Promise.all([ - new Promise((resolve, reject) => { - let retries = 0 - const server = http.createServer() - - server.once('listening', () => { - debug('Confirmed Chrome killed') - server.close(resolve) - }) - - server.on('error', () => { - retries += 1 - - debug('Waiting for Chrome to terminate..', retries) - - if (retries > 10) { - reject(new Error('Chrome is still running after 10 retries')) - } - - setTimeout(() => { - server.listen(this.port) - }, this.pollInterval) - }) - - server.listen(this.port) - }), - new Promise((resolve) => { - this.chrome.on('close', resolve) - }), - ]) - } - - async spawn () { - const spawnPromise = new Promise(async (resolve) => { - if (this.chrome) { - debug(`Chrome already running with pid ${this.chrome.pid}.`) - return resolve(this.chrome.pid) - } - - const chrome = spawn(this.chromePath, this.flags, { - detached: true, - stdio: ['ignore', this.outFile, this.errFile], - }) - - this.chrome = chrome - - // unref the chrome instance, otherwise the lambda process won't end correctly - if (chrome.chrome) { - chrome.chrome.removeAllListeners() - chrome.chrome.unref() - } - - fs.writeFileSync(this.pidFile, chrome.pid.toString()) - - debug( - 'Launcher', - `Chrome running with pid ${chrome.pid} on port ${this.port}.` - ) - - return resolve(chrome.pid) - }) - - const pid = await spawnPromise - await this.waitUntilReady() - return pid - } - - async launch () { - if (this.requestedPort !== 0) { - this.port = this.requestedPort - - // If an explict port is passed first look for an open connection... - try { - return await this.isReady() - } catch (err) { - debug( - 'ChromeLauncher', - `No debugging port found on port ${ - this.port - }, launching a new Chrome.` - ) - } - } - - if (!this.tmpDirandPidFileReady) { - this.prepare() - } - - this.pid = await this.spawn() - return Promise.resolve() - } - - kill () { - return new Promise(async (resolve, reject) => { - if (this.chrome) { - debug('Trying to terminate Chrome instance') - - try { - process.kill(-this.chrome.pid) - - debug('Waiting for Chrome to terminate..') - await this.waitUntilKilled() - debug('Chrome successfully terminated.') - - this.destroyTemp() - - delete this.chrome - return resolve() - } catch (error) { - debug('Chrome could not be killed', error) - return reject(error) - } - } else { - // fail silently as we did not start chrome - return resolve() - } - }) - } - - destroyTemp () { - return new Promise((resolve) => { - // Only clean up the tmp dir if we created it. - if ( - this.userDataDir === undefined || - this.options.userDataDir !== undefined - ) { - return resolve() - } - - if (this.outFile) { - fs.closeSync(this.outFile) - delete this.outFile - } - - if (this.errFile) { - fs.closeSync(this.errFile) - delete this.errFile - } - - return execSync(`rm -Rf ${this.userDataDir}`, resolve) - }) - } -} diff --git a/packages/lambda/src/launcher.ts b/packages/lambda/src/launcher.ts new file mode 100644 index 00000000..d916f4ed --- /dev/null +++ b/packages/lambda/src/launcher.ts @@ -0,0 +1,234 @@ +/* + Large portion of this is inspired by/taken from Lighthouse/chrome-launcher. + It is Copyright Google Inc, licensed under Apache License, Version 2.0. + https://github.com/GoogleChrome/lighthouse/blob/master/chrome-launcher/chrome-launcher.ts + + We ship a modified version because the original verion comes with too + many dependencies which complicates packaging of serverless services. +*/ + +import { ChildProcess, spawn } from "child_process"; +import debug from "debug"; +import * as net from "net"; +import * as path from "path"; + +const DEFAULT_CHROME_FLAGS = new Set([ + "--disable-dev-shm-usage", // disable /dev/shm tmpfs usage on Lambda + + // @TODO: review if these are still relevant: + "--disable-gpu", + "--single-process", // Currently wont work without this :-( + + // https://groups.google.com/a/chromium.org/d/msg/headless-dev/qqbZVZ2IwEw/Y95wJUh2AAAJ + "--no-zygote", // helps avoid zombies + + "--no-sandbox", +]); + +const CHROME_PATH = path.resolve(__dirname, "./headless-chromium"); + +export interface LauncherOptions { + pollInterval?: number; + chromePath?: string; + chromeFlags?: string[]; + startingUrl?: string; + port?: number; + debug?: boolean; +} + +export default class LambdaChromeLauncher { + public chrome?: ChildProcess; + + private readonly requestedPort?: number; + private readonly chromePath: string; + private readonly chromeFlags: Set; + private readonly pollInterval: number; + private readonly startingUrl: string; + private readonly debug: boolean; + + private readonly log = debug("@serverless-chrome/lambda"); + + constructor(options: LauncherOptions = {}) { + const { + pollInterval = 500, + chromePath = CHROME_PATH, + chromeFlags = [], + startingUrl = "about:blank", + port, + } = options; + + this.debug = !!options.debug; + this.pollInterval = pollInterval; + this.requestedPort = port; + this.startingUrl = startingUrl; + this.chromePath = chromePath; + this.chromeFlags = new Set([ + ...this.debug ? ["--enable-logging", "--log-level=0"] : [], + ...DEFAULT_CHROME_FLAGS, + `--remote-debugging-port=${this.port}`, + "--disable-setuid-sandbox", + ...chromeFlags, + ]); + } + + public get port() { + return this.requestedPort || 9222; + } + + public get flags() { + return [ + ...this.chromeFlags, + this.startingUrl, + ]; + } + + public get pid() { + return this.chrome ? this.chrome.pid : null; + } + + public async launch(): Promise { + if (this.requestedPort) { + // If an explicit port is passed first look for an open connection... + try { + return await this.ensureReady(); + } catch (e) { + this.log("No debugging port found on port %d, launching a new Chrome", this.port); + } + } + + if (this.chrome) { + this.log(`Chrome already running with pid %d.`, this.chrome.pid); + } else { + this.chrome = await this.spawn(); + await this.waitUntilReady(); + } + } + + public async kill() { + if (this.chrome) { + this.log("Trying to terminate Chrome instance"); + try { + process.kill(-this.chrome.pid); + + this.log("Waiting for Chrome to terminate.."); + await this.waitUntilKilled(); + this.chrome = undefined; + this.log("Chrome successfully terminated."); + } catch (e) { + this.log("Chrome could not be killed", e); + throw e; + } + } + } + + private spawn(): ChildProcess { + const proc = spawn(this.chromePath, this.flags, { + detached: true, + stdio: this.debug ? ["ignore", process.stdout, process.stderr] : "ignore", + }); + + // unref the chrome instance, otherwise the lambda process won't end correctly + proc.unref(); + + this.log("Chrome running with pid %d on port %d.", proc.pid, this.port); + + return proc; + } + + // resolves if ready, rejects otherwise + private ensureReady(): Promise { + return new Promise((resolve, reject) => { + const client = net.createConnection(this.port) + .setTimeout(1000) // @todo make it customizable? + .once("error", onError) + .once("connect", onConnect) + .once("timeout", onTimeout); + + function onError(e: Error) { + client + .removeListener("connect", onConnect) + .removeListener("timeout", onTimeout); + + reject(e); + } + + function onConnect() { + client + .removeListener("error", onError) + .removeListener("timeout", onTimeout); + + client.end(); + resolve(); + } + + function onTimeout() { + client + .removeListener("error", onError) + .removeListener("connect", onConnect); + + reject(new Error("Connection timed out")); + } + }); + } + + // resolves when debugger is ready, rejects after 10 polls + private async waitUntilReady(): Promise { + const MAX_RETRIES = 10; + let retries = 0; + + while (retries++ < MAX_RETRIES) { + log("Waiting for Chrome", retries); + + try { + await this.ensureReady(); + this.log("Started Chrome"); + return; + } catch (e) { + await this.sleep(this.pollInterval); + } + } + } + + // resolves when chrome is killed, rejects after 10 polls + private async waitUntilKilled(): Promise { + await Promise.all([ + new Promise((resolve, reject) => { + const self = this; + const MAX_RETRIES = 10; + let retries = 0; + + const server = net.createServer() + .on("listening", onListen) + .on("error", onError); + + function onListen() { + self.log("Confirmed Chrome killed"); + server.close(resolve); + } + + function onError(e: Error) { + if (retries++ < MAX_RETRIES) { + setTimeout(() => server.listen(self.port), self.pollInterval); + } else { + reject(new Error("Chrome is still running after 10 retries")); + } + } + + this.log("Waiting for Chrome to terminate..", retries); + + server.listen(this.port); + }), + new Promise((resolve) => { + if (!this.chrome || this.chrome.killed) { + return resolve(); + } else { + this.chrome.once("close", resolve); + } + }), + ]); + } + + private sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/packages/lambda/src/utils.js b/packages/lambda/src/utils.js deleted file mode 100644 index 8631c955..00000000 --- a/packages/lambda/src/utils.js +++ /dev/null @@ -1,41 +0,0 @@ -import { execSync } from 'child_process' - -export function clearConnection (client) { - if (client) { - client.removeAllListeners() - client.end() - client.destroy() - client.unref() - } -} - -export function debug (...args) { - return process.env.DEBUG - ? console.log('@serverless-chrome/lambda:', ...args) - : undefined -} - -export async function delay (time) { - return new Promise(resolve => setTimeout(resolve, time)) -} - -export function makeTempDir () { - return execSync('mktemp -d -t chrome.XXXXXXX') - .toString() - .trim() -} - -/** - * Checks if a process currently exists by process id. - * @param pid number process id to check if exists - * @returns boolean true if process exists, false if otherwise - */ -export function processExists (pid) { - let exists = true - try { - process.kill(pid, 0) - } catch (error) { - exists = false - } - return exists -} diff --git a/packages/lambda/tsconfig.json b/packages/lambda/tsconfig.json new file mode 100644 index 00000000..49b81747 --- /dev/null +++ b/packages/lambda/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "es2015", + "target": "es2017", + "noImplicitAny": true, + "declaration": true, + "sourceMap": true, + "strictNullChecks": true + } +} diff --git a/packages/lambda/tslint.json b/packages/lambda/tslint.json new file mode 100644 index 00000000..5f27e42f --- /dev/null +++ b/packages/lambda/tslint.json @@ -0,0 +1,12 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "object-literal-sort-keys": false, + "interface-name": [true, "never-prefix"] + }, + "rulesDirectory": [] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..51121b69 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "es2015", + "target": "es2017", + "noImplicitAny": true, + "declaration": true, + "sourceMap": true, + "strictNullChecks": true + }, + "include": [ + "src/*.ts" + ] +}