UNPKG

ncc

Version:

node-chrome-canvas | a simple to use and performant HTML5 canvas for Node.js

475 lines (401 loc) 17.6 kB
/** * This is a stripped down and bundeled version of * - https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-cli/chrome-launcher.ts * - https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-cli/chrome-finder.ts * It be replaced when the ChromeLauncher becomes a module: https://github.com/GoogleChrome/lighthouse/issues/2092 * But for now this saves us about 60 MB of modules */ /** * @license * Copyright 2016 Google Inc. All rights reserved. * * 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. */ 'use strict'; var ChromeLauncher = (() => { var childProcess = require('child_process'); var fs = require('fs'); var path = require('path'); const mkdirp = require('mkdirp'); var net = require('net'); const rimraf = require('rimraf'); const spawn = childProcess.spawn; const execSync = childProcess.execSync; const isWindows = process.platform === 'win32'; const execFileSync = require('child_process').execFileSync; const newLineRegex = /\r?\n/; type Priorities = Array<{ regex: RegExp, weight: number }>; function darwin() { const suffixes = ['/Contents/MacOS/Google Chrome Canary', '/Contents/MacOS/Google Chrome']; const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework' + '/Versions/A/Frameworks/LaunchServices.framework' + '/Versions/A/Support/lsregister'; const installations: Array<string> = []; execSync( `${LSREGISTER} -dump` + ' | grep -i \'google chrome\\( canary\\)\\?.app$\'' + ' | awk \'{$1=""; print $0}\'') .toString() .split(newLineRegex) .forEach((inst: string) => { suffixes.forEach(suffix => { const execPath = path.join(inst.trim(), suffix); if (canAccess(execPath)) { installations.push(execPath); } }); }); // Retains one per line to maintain readability. // clang-format off const priorities: Priorities = [ { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome.app`), weight: 50 }, { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome Canary.app`), weight: 51 }, { regex: /^\/Applications\/.*Chrome.app/, weight: 100 }, { regex: /^\/Applications\/.*Chrome Canary.app/, weight: 101 }, { regex: /^\/Volumes\/.*Chrome.app/, weight: -2 }, { regex: /^\/Volumes\/.*Chrome Canary.app/, weight: -1 } ]; // clang-format on return sort(installations, priorities); } /** * Look for linux executables in 3 ways * 1. Look into LIGHTHOUSE_CHROMIUM_PATH env variable * 2. Look into the directories where .desktop are saved on gnome based distro's * 3. Look for google-chrome-stable & google-chrome executables by using the which command */ function linux() { let installations: Array<string> = []; // 1. Look into LIGHTHOUSE_CHROMIUM_PATH env variable if (canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) { installations.push(process.env.LIGHTHOUSE_CHROMIUM_PATH); } // 2. Look into the directories where .desktop are saved on gnome based distro's const desktopInstallationFolders = [ path.join(require('os').homedir(), '.local/share/applications/'), '/usr/share/applications/', ]; desktopInstallationFolders.forEach(folder => { installations = installations.concat(findChromeExecutables(folder)); }); // Look for google-chrome-stable & google-chrome executables by using the which command const executables = [ 'google-chrome-stable', 'google-chrome', ]; executables.forEach((executable: string) => { try { const chromePath = execFileSync('which', [executable]).toString().split(newLineRegex)[0]; if (canAccess(chromePath)) { installations.push(chromePath); } } catch (e) { // Not installed. } }); if (!installations.length) { throw new Error( 'The environment variable LIGHTHOUSE_CHROMIUM_PATH must be set to ' + 'executable of a build of Chromium version 54.0 or later.'); } const priorities: Priorities = [ { regex: /chrome-wrapper$/, weight: 51 }, { regex: /google-chrome-stable$/, weight: 50 }, { regex: /google-chrome$/, weight: 49 }, { regex: new RegExp(process.env.LIGHTHOUSE_CHROMIUM_PATH), weight: 100 } ]; return sort(uniq(installations.filter(Boolean)), priorities); } function win32() { const installations: Array<string> = []; const suffixes = [ '\\Google\\Chrome SxS\\Application\\chrome.exe', '\\Google\\Chrome\\Application\\chrome.exe' ]; const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']]; if (canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) { installations.push(process.env.LIGHTHOUSE_CHROMIUM_PATH); } prefixes.forEach(prefix => suffixes.forEach(suffix => { const chromePath = path.join(prefix, suffix); if (canAccess(chromePath)) { installations.push(chromePath); } })); return installations; } function sort(installations: Array<string>, priorities: Priorities) { const defaultPriority = 10; return installations // assign priorities .map((inst: string) => { for (const pair of priorities) { if (pair.regex.test(inst)) { return [inst, pair.weight]; } } return [inst, defaultPriority]; }) // sort based on priorities .sort((a, b) => (<any>b)[1] - (<any>a)[1]) // remove priority flag .map(pair => pair[0]); } function canAccess(file: string): Boolean { if (!file) { return false; } try { fs.accessSync(file); return true; } catch (e) { return false; } } function uniq(arr: Array<any>) { return Array.from(new Set(arr)); } function findChromeExecutables(folder: string): Array<string> { const argumentsRegex = /(^[^ ]+).*/; // Take everything up to the first space const chromeExecRegex = '^Exec=\/.*\/(google|chrome|chromium)-.*'; let installations: Array<string> = []; if (canAccess(folder)) { // Output of the grep & print looks like: // /opt/google/chrome/google-chrome --profile-directory // /home/user/Downloads/chrome-linux/chrome-wrapper %U let execPaths = execSync(`grep -ER "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`) .toString() .split(newLineRegex) .map((execPath: string) => execPath.replace(argumentsRegex, '$1')); execPaths.forEach((execPath: string) => canAccess(execPath) && installations.push(execPath)); } return installations; } var chromeFinder = { darwin: darwin, linux: linux, win32: win32 } class ChromeLauncher { prepared = false; pollInterval: number = 500; autoSelectChrome: boolean; TMP_PROFILE_DIR: string; outFile?: number; errFile?: number; pidFile: string; startingUrl: string; chromeFlags: Array<string>; chrome?: any; port: number; constructor(opts: { startingUrl?: string, chromeFlags?: Array<string>, autoSelectChrome?: boolean, port?: number } = {}) { // choose the first one (default) this.autoSelectChrome = defaults(opts.autoSelectChrome, true); this.startingUrl = defaults(opts.startingUrl, 'about:blank'); this.chromeFlags = defaults(opts.chromeFlags, []); this.port = defaults(opts.port, 9222); } flags() { const flags = [ `--remote-debugging-port=${this.port}`, // Disable built-in Google Translate service '--disable-translate', // Disable all chrome extensions entirely '--disable-extensions', // Disable various background network services, including extension updating, // safe browsing service, upgrade detector, translate, UMA '--disable-background-networking', // Disable fetching safebrowsing lists, likely redundant due to disable-background-networking '--safebrowsing-disable-auto-update', // Disable syncing to a Google account '--disable-sync', // Disable reporting to UMA, but allows for collection '--metrics-recording-only', // Disable installation of default apps on first run '--disable-default-apps', // Skip first run wizards '--no-first-run', // Place Chrome profile in a custom location we'll rm -rf later `--user-data-dir=${this.TMP_PROFILE_DIR}` ]; if (process.platform === 'linux') { flags.push('--disable-setuid-sandbox'); } flags.push(...this.chromeFlags); flags.push(this.startingUrl); return flags; } prepare() { switch (process.platform) { case 'darwin': case 'linux': this.TMP_PROFILE_DIR = unixTmpDir(); break; case 'win32': this.TMP_PROFILE_DIR = win32TmpDir(); break; default: throw new Error('Platform ' + process.platform + ' is not supported'); } this.outFile = fs.openSync(`${this.TMP_PROFILE_DIR}/chrome-out.log`, 'a'); this.errFile = fs.openSync(`${this.TMP_PROFILE_DIR}/chrome-err.log`, 'a'); // fix for Node4 // you can't pass a fd to fs.writeFileSync this.pidFile = `${this.TMP_PROFILE_DIR}/chrome.pid`; console.log('ChromeLauncher', `created ${this.TMP_PROFILE_DIR}`); this.prepared = true; } run() { if (!this.prepared) { this.prepare(); } return Promise.resolve() .then(() => { const installations = (<any>chromeFinder)[process.platform](); if (installations.length < 1) { return Promise.reject(new Error('No Chrome Installations Found')); } else if (installations.length === 1 || this.autoSelectChrome) { return installations[0]; } //return ask('Choose a Chrome installation to use with Lighthouse', installations); }) .then(execPath => this.spawn(execPath)); } spawn(execPath: string) { const spawnPromise = new Promise(resolve => { if (this.chrome) { console.log('ChromeLauncher', `Chrome already running with pid ${this.chrome.pid}.`); return resolve(this.chrome.pid); } const chrome = spawn( execPath, this.flags(), { detached: true, stdio: ['ignore', this.outFile, this.errFile] }); this.chrome = chrome; fs.writeFileSync(this.pidFile, chrome.pid.toString()); console.log('ChromeLauncher', `Chrome running with pid ${chrome.pid} on port ${this.port}.`); resolve(chrome.pid); }); return spawnPromise.then(pid => Promise.all([pid, this.waitUntilReady()])); } cleanup(client?: any) { if (client) { client.removeAllListeners(); client.end(); client.destroy(); client.unref(); } } // resolves if ready, rejects otherwise isDebuggerReady(): Promise<{}> { return new Promise((resolve, reject) => { const client = net.createConnection(this.port); client.once('error', err => { this.cleanup(client); reject(err); }); client.once('connect', () => { this.cleanup(client); resolve(); }); }); } // resolves when debugger is ready, rejects after 10 polls waitUntilReady() { const launcher = this; return new Promise((resolve, reject) => { let retries = 0; let waitStatus = 'Waiting for browser.'; (function poll() { if (retries === 0) { console.log('ChromeLauncher', waitStatus); } retries++; waitStatus += '..'; console.log('ChromeLauncher', waitStatus); launcher.isDebuggerReady() .then(() => { console.log('ChromeLauncher', waitStatus); resolve(); }) .catch(err => { if (retries > 10) { return reject(err); } delay(launcher.pollInterval).then(poll); }); })(); }); } kill() { return new Promise(resolve => { if (this.chrome) { this.chrome.on('close', () => { this.destroyTmp().then(resolve); }); console.log('ChromeLauncher', 'Killing all Chrome Instances'); try { if (isWindows) { execSync(`taskkill /pid ${this.chrome.pid} /T /F`); } else { process.kill(-this.chrome.pid); } } catch (err) { console.log('ChromeLauncher', `Chrome could not be killed ${err.message}`); } delete this.chrome; } else { // fail silently as we did not start chrome resolve(); } }); } destroyTmp() { return new Promise(resolve => { if (!this.TMP_PROFILE_DIR) { return resolve(); } console.log('ChromeLauncher', `Removing ${this.TMP_PROFILE_DIR}`); if (this.outFile) { fs.closeSync(this.outFile); delete this.outFile; } if (this.errFile) { fs.closeSync(this.errFile); delete this.errFile; } rimraf(this.TMP_PROFILE_DIR, () => resolve()); }); } }; function defaults<T>(val: T | undefined, def: T): T { return typeof val === 'undefined' ? def : val; } function delay(time: number) { return new Promise(resolve => setTimeout(resolve, time)); } function unixTmpDir() { return execSync('mktemp -d -t ncc.XXXXXXX').toString().trim(); } function win32TmpDir() { const winTmpPath = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + '\\temp'; const randomNumber = Math.floor(Math.random() * 9e7 + 1e7); const tmpdir = path.join(winTmpPath, 'ncc.' + randomNumber); mkdirp.sync(tmpdir); return tmpdir; } return ChromeLauncher; })()