UNPKG

10.6 kBJavaScriptView Raw
1/**
2 * Copyright 2017 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16const os = require('os');
17const path = require('path');
18const removeFolder = require('rimraf');
19const childProcess = require('child_process');
20const BrowserFetcher = require('./BrowserFetcher');
21const {Connection} = require('./Connection');
22const Browser = require('./Browser');
23const readline = require('readline');
24const fs = require('fs');
25const {helper, debugError} = require('./helper');
26const ChromiumRevision = require(path.join(helper.projectRoot(), 'package.json')).puppeteer.chromium_revision;
27
28const mkdtempAsync = helper.promisify(fs.mkdtemp);
29const removeFolderAsync = helper.promisify(removeFolder);
30
31const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_dev_profile-');
32
33const DEFAULT_ARGS = [
34 '--disable-background-networking',
35 '--disable-background-timer-throttling',
36 '--disable-client-side-phishing-detection',
37 '--disable-default-apps',
38 '--disable-dev-shm-usage',
39 '--disable-extensions',
40 '--disable-hang-monitor',
41 '--disable-popup-blocking',
42 '--disable-prompt-on-repost',
43 '--disable-sync',
44 '--disable-translate',
45 '--metrics-recording-only',
46 '--no-first-run',
47 '--safebrowsing-disable-auto-update',
48];
49
50const AUTOMATION_ARGS = [
51 '--enable-automation',
52 '--password-store=basic',
53 '--use-mock-keychain',
54];
55
56class Launcher {
57 /**
58 * @param {!LaunchOptions=} options
59 * @return {!Promise<!Browser>}
60 */
61 static async launch(options) {
62 options = Object.assign({}, options || {});
63 console.assert(!options.ignoreDefaultArgs || !options.appMode, '`appMode` flag cannot be used together with `ignoreDefaultArgs`');
64 let temporaryUserDataDir = null;
65 const chromeArguments = [];
66 if (!options.ignoreDefaultArgs)
67 chromeArguments.push(...DEFAULT_ARGS);
68 if (options.appMode) {
69 options.headless = false;
70 options.pipe = true;
71 } else if (!options.ignoreDefaultArgs) {
72 chromeArguments.push(...AUTOMATION_ARGS);
73 }
74
75 if (!options.ignoreDefaultArgs || !chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
76 chromeArguments.push(options.pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
77
78 if (!options.args || !options.args.some(arg => arg.startsWith('--user-data-dir'))) {
79 if (!options.userDataDir)
80 temporaryUserDataDir = await mkdtempAsync(CHROME_PROFILE_PATH);
81
82 chromeArguments.push(`--user-data-dir=${options.userDataDir || temporaryUserDataDir}`);
83 }
84 if (options.devtools === true) {
85 chromeArguments.push('--auto-open-devtools-for-tabs');
86 options.headless = false;
87 }
88 if (typeof options.headless !== 'boolean' || options.headless) {
89 chromeArguments.push(
90 '--headless',
91 '--disable-gpu',
92 '--hide-scrollbars',
93 '--mute-audio'
94 );
95 }
96 let chromeExecutable = options.executablePath;
97 if (typeof chromeExecutable !== 'string') {
98 const browserFetcher = new BrowserFetcher();
99 const revisionInfo = browserFetcher.revisionInfo(ChromiumRevision);
100 console.assert(revisionInfo.local, `Chromium revision is not downloaded. Run "npm install" or "yarn install"`);
101 chromeExecutable = revisionInfo.executablePath;
102 }
103 if (Array.isArray(options.args))
104 chromeArguments.push(...options.args);
105
106
107 const usePipe = chromeArguments.includes('--remote-debugging-pipe');
108 const stdio = ['pipe', 'pipe', 'pipe'];
109 if (usePipe)
110 stdio.push('pipe', 'pipe');
111 const chromeProcess = childProcess.spawn(
112 chromeExecutable,
113 chromeArguments,
114 {
115 // On non-windows platforms, `detached: false` makes child process a leader of a new
116 // process group, making it possible to kill child process tree with `.kill(-pid)` command.
117 // @see https://nodejs.org/api/child_process.html#child_process_options_detached
118 detached: process.platform !== 'win32',
119 env: options.env || process.env,
120 stdio
121 }
122 );
123
124 if (options.dumpio) {
125 chromeProcess.stderr.pipe(process.stderr);
126 chromeProcess.stdout.pipe(process.stdout);
127 }
128
129 let chromeClosed = false;
130 const waitForChromeToClose = new Promise((fulfill, reject) => {
131 chromeProcess.once('close', () => {
132 chromeClosed = true;
133 // Cleanup as processes exit.
134 if (temporaryUserDataDir) {
135 removeFolderAsync(temporaryUserDataDir)
136 .then(() => fulfill())
137 .catch(err => console.error(err));
138 } else {
139 fulfill();
140 }
141 });
142 });
143
144 const listeners = [ helper.addEventListener(process, 'exit', killChrome) ];
145 if (options.handleSIGINT !== false)
146 listeners.push(helper.addEventListener(process, 'SIGINT', killChrome));
147 if (options.handleSIGTERM !== false)
148 listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyCloseChrome));
149 if (options.handleSIGHUP !== false)
150 listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyCloseChrome));
151 /** @type {?Connection} */
152 let connection = null;
153 try {
154 const connectionDelay = options.slowMo || 0;
155 if (!usePipe) {
156 const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
157 const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout);
158 connection = await Connection.createForWebSocket(browserWSEndpoint, connectionDelay);
159 } else {
160 connection = Connection.createForPipe(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4]), connectionDelay);
161 }
162 return Browser.create(connection, options, chromeProcess, gracefullyCloseChrome);
163 } catch (e) {
164 killChrome();
165 throw e;
166 }
167
168 /**
169 * @return {Promise}
170 */
171 function gracefullyCloseChrome() {
172 helper.removeEventListeners(listeners);
173 if (temporaryUserDataDir) {
174 killChrome();
175 } else if (connection) {
176 // Attempt to close chrome gracefully
177 connection.send('Browser.close').catch(error => {
178 debugError(error);
179 killChrome();
180 });
181 }
182 return waitForChromeToClose;
183 }
184
185 // This method has to be sync to be used as 'exit' event handler.
186 function killChrome() {
187 helper.removeEventListeners(listeners);
188 if (chromeProcess.pid && !chromeProcess.killed && !chromeClosed) {
189 // Force kill chrome.
190 try {
191 if (process.platform === 'win32')
192 childProcess.execSync(`taskkill /pid ${chromeProcess.pid} /T /F`);
193 else
194 process.kill(-chromeProcess.pid, 'SIGKILL');
195 } catch (e) {
196 // the process might have already stopped
197 }
198 }
199 // Attempt to remove temporary profile directory to avoid littering.
200 try {
201 removeFolder.sync(temporaryUserDataDir);
202 } catch (e) { }
203 }
204 }
205
206 /**
207 * @return {!Array<string>}
208 */
209 static defaultArgs() {
210 return DEFAULT_ARGS.concat(AUTOMATION_ARGS);
211 }
212
213 /**
214 * @return {string}
215 */
216 static executablePath() {
217 const browserFetcher = new BrowserFetcher();
218 const revisionInfo = browserFetcher.revisionInfo(ChromiumRevision);
219 return revisionInfo.executablePath;
220 }
221
222 /**
223 * @param {!Object=} options
224 * @return {!Promise<!Browser>}
225 */
226 static async connect(options = {}) {
227 const connectionDelay = options.slowMo || 0;
228 const connection = await Connection.createForWebSocket(options.browserWSEndpoint, connectionDelay);
229 return Browser.create(connection, options, null, () => connection.send('Browser.close').catch(debugError));
230 }
231}
232
233/**
234 * @param {!Puppeteer.ChildProcess} chromeProcess
235 * @param {number} timeout
236 * @return {!Promise<string>}
237 */
238function waitForWSEndpoint(chromeProcess, timeout) {
239 return new Promise((resolve, reject) => {
240 const rl = readline.createInterface({ input: chromeProcess.stderr });
241 let stderr = '';
242 const listeners = [
243 helper.addEventListener(rl, 'line', onLine),
244 helper.addEventListener(rl, 'close', () => onClose()),
245 helper.addEventListener(chromeProcess, 'exit', () => onClose()),
246 helper.addEventListener(chromeProcess, 'error', error => onClose(error))
247 ];
248 const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
249
250 /**
251 * @param {!Error=} error
252 */
253 function onClose(error) {
254 cleanup();
255 reject(new Error([
256 'Failed to launch chrome!' + (error ? ' ' + error.message : ''),
257 stderr,
258 '',
259 'TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md',
260 '',
261 ].join('\n')));
262 }
263
264 function onTimeout() {
265 cleanup();
266 reject(new Error(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${ChromiumRevision}`));
267 }
268
269 /**
270 * @param {string} line
271 */
272 function onLine(line) {
273 stderr += line + '\n';
274 const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
275 if (!match)
276 return;
277 cleanup();
278 resolve(match[1]);
279 }
280
281 function cleanup() {
282 if (timeoutId)
283 clearTimeout(timeoutId);
284 helper.removeEventListeners(listeners);
285 }
286 });
287}
288
289/**
290 * @typedef {Object} LaunchOptions
291 * @property {boolean=} ignoreHTTPSErrors
292 * @property {boolean=} headless
293 * @property {string=} executablePath
294 * @property {number=} slowMo
295 * @property {!Array<string>=} args
296 * @property {boolean=} ignoreDefaultArgs
297 * @property {boolean=} handleSIGINT
298 * @property {boolean=} handleSIGTERM
299 * @property {boolean=} handleSIGHUP
300 * @property {number=} timeout
301 * @property {boolean=} dumpio
302 * @property {string=} userDataDir
303 * @property {!Object<string, string | undefined>=} env
304 * @property {boolean=} devtools
305 * @property {boolean=} pipe
306 * @property {boolean=} appMode
307 */
308
309
310module.exports = Launcher;