Created
December 5, 2017 12:34
-
-
Save rkistner/c695c64ec573581b47e349e8bbe98d86 to your computer and use it in GitHub Desktop.
Using Chrome on Lambda
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { spawn, execSync, ChildProcess } from 'child_process' | |
import * as net from 'net' | |
const { chromePath } = require('aws-lambda-chrome'); | |
const Cdp = require('chrome-remote-interface'); | |
export type ChromeHandler<T> = (client: ChromeClient) => Promise<T>; | |
// These security features are not supported at all, and need to be disabled. | |
const SECURITY_NOT_SUPPORTED = [ | |
'--single-process', | |
'--no-sandbox', | |
'--no-zygote' | |
// These additional features are disabled in the upstream project, but Chromium seems | |
// to run fine if we leave them enabled: | |
// '--nacl-dangerous-no-sandbox-nonsfi', | |
// '--disable-setuid-sandbox', | |
// '--disable-seccomp-filter-sandbox', | |
// '--disable-kill-after-bad-ipc', | |
// '--disable-namespace-sandbox' | |
]; | |
// These features are not relevant for generating PDFs | |
const IRRELEVANT_FEATURES = [ | |
'--disable-background-networking', | |
'--disable-breakpad', | |
'--disable-canvas-aa', | |
'--disable-client-side-phishing-detection', | |
'--disable-cloud-import', | |
'--disable-gpu', | |
'--disable-gpu-sandbox', | |
'--disable-plugins', | |
'--disable-print-preview', | |
'--disable-renderer-backgrounding', | |
'--disable-smooth-scrolling', | |
'--disable-sync', | |
'--disable-translate', | |
'--disable-translate-new-ux', | |
'--disable-webgl', | |
'--disable-composited-antialiasing', | |
'--disable-default-apps', | |
'--disable-extensions-http-throttling', | |
'--no-default-browser-check', | |
'--no-experiments', | |
'--no-first-run', | |
'--no-pings', | |
'--prerender-from-omnibox=disabled' | |
]; | |
// Just some internal configuration | |
const INTERNAL_CONFIGURATION = [ | |
'--disk-cache-dir=/tmp/cache-dir', | |
'--disk-cache-size=10000000', | |
'--ipc-connection-timeout=10000', | |
'--media-cache-size=10000000' | |
]; | |
// Relevant configuration | |
const CONFIGURATION = [ | |
'--remote-debugging-port=9222', | |
'--user-data-dir=tmp/user-data', | |
'--window-size=1280,720' | |
]; | |
const START_URL = 'about:blank'; | |
const CHROME_ARGS = [ | |
...SECURITY_NOT_SUPPORTED, | |
...IRRELEVANT_FEATURES, | |
...INTERNAL_CONFIGURATION, | |
...CONFIGURATION, | |
START_URL | |
]; | |
export interface ChromeClient { | |
Network: any; | |
Page: any; | |
Runtime: any; | |
Storage: any; | |
Console: any; | |
Log: any; | |
close: () => Promise<void>; | |
} | |
let chromeProcess: ChildProcess = null; | |
export async function launchChrome<T>(handler: ChromeHandler<T>): Promise<T> { | |
console.log('Spawning Chrome'); | |
if (chromeProcess == null) { | |
chromeProcess = spawn(chromePath, CHROME_ARGS, { | |
env: process.env, | |
// No need to detach | |
detached: false | |
}); | |
// Unref process, so that it doesn't prevent the Lambda request from finishing | |
chromeProcess.unref(); | |
chromeProcess.on('exit', (code: number) => { | |
// Fatal error, can't recover from this | |
console.error(`Chrome has exited with code ${code}`); | |
process.exit(1); | |
}); | |
chromeProcess.stdout.on('data', (data) => { | |
console.log(data); | |
}); | |
chromeProcess.stderr.on('data', (data) => { | |
console.error(data); | |
}); | |
(chromeProcess.stdout as any).unref(); | |
(chromeProcess.stderr as any).unref(); | |
(chromeProcess.stdin as any).unref(); | |
} | |
const { tab, client } = await tryConnect(100); | |
try { | |
return await handler(client); | |
} finally { | |
await Cdp.Close({ id: tab.id }); | |
await client.close(); | |
console.log('Closed connection'); | |
if (process.env.LAMBDA_TASK_ROOT) { | |
// Running on Lambda: Output process stats | |
console.log(execSync('ps auxf').toString()) | |
} | |
} | |
} | |
async function tryConnect(tries: number) { | |
for (let i = 0; i < tries; i++) { | |
try { | |
const tab = await Cdp.New({ host: '127.0.0.1', port: 9222 }); | |
const client: ChromeClient = await Cdp({target: tab}); | |
return { tab, client }; | |
} catch (error) { | |
if ((error.code == 'ECONNREFUSED' || error.code == 'ECONNRESET') && i < tries - 1) { | |
await delay(10); | |
continue; | |
} else { | |
throw error; | |
} | |
} | |
} | |
throw new Error('Unreachable code reached'); | |
} | |
async function delay(ms: number) { | |
return new Promise((resolve) => setTimeout(resolve, ms)); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { launchChrome } from './chrome'; | |
export async function test(url: string) { | |
const startedAt = Date.now(); | |
return await launchChrome(async (client) => { | |
const { Network, Page, Runtime, Storage, Console } = client; | |
await Promise.all([ | |
Network.enable(), | |
Page.enable(), | |
Console.enable(), | |
Runtime.enable() | |
]); | |
const loadEvent = Page.loadEventFired(); | |
// Navigate to provided URL. | |
await Page.navigate({ url: url }); | |
// Wait for loading | |
await loadEvent; | |
// const pdf = await Page.printToPDF({}); | |
}); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment