UNPKG

8.34 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 Downloader = require('./Downloader');
21const {Connection} = require('./Connection');
22const {Browser} = require('./Browser');
23const readline = require('readline');
24const fs = require('fs');
25const {helper} = require('./helper');
26const ChromiumRevision = Downloader.defaultRevision();
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-extensions',
39 '--disable-hang-monitor',
40 '--disable-popup-blocking',
41 '--disable-prompt-on-repost',
42 '--disable-sync',
43 '--disable-translate',
44 '--metrics-recording-only',
45 '--no-first-run',
46 '--remote-debugging-port=0',
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 {!Object=} options
59 * @return {!Promise<!Browser>}
60 */
61 static async launch(options) {
62 options = Object.assign({}, options || {});
63 let temporaryUserDataDir = null;
64 const chromeArguments = [];
65 if (!options.ignoreDefaultArgs)
66 chromeArguments.push(...DEFAULT_ARGS);
67
68 if (options.appMode)
69 options.headless = false;
70 else if (!options.ignoreDefaultArgs)
71 chromeArguments.push(...AUTOMATION_ARGS);
72
73 if (!options.args || !options.args.some(arg => arg.startsWith('--user-data-dir'))) {
74 if (!options.userDataDir)
75 temporaryUserDataDir = await mkdtempAsync(CHROME_PROFILE_PATH);
76
77 chromeArguments.push(`--user-data-dir=${options.userDataDir || temporaryUserDataDir}`);
78 }
79 if (options.devtools === true) {
80 chromeArguments.push('--auto-open-devtools-for-tabs');
81 options.headless = false;
82 }
83 if (typeof options.headless !== 'boolean' || options.headless) {
84 chromeArguments.push(
85 '--headless',
86 '--disable-gpu',
87 '--hide-scrollbars',
88 '--mute-audio'
89 );
90 }
91 let chromeExecutable = options.executablePath;
92 if (typeof chromeExecutable !== 'string') {
93 const downloader = Downloader.createDefault();
94 const revisionInfo = downloader.revisionInfo(downloader.currentPlatform(), ChromiumRevision);
95 console.assert(revisionInfo.downloaded, `Chromium revision is not downloaded. Run "yarn install" or "npm install"`);
96 chromeExecutable = revisionInfo.executablePath;
97 }
98 if (Array.isArray(options.args))
99 chromeArguments.push(...options.args);
100
101 const chromeProcess = childProcess.spawn(
102 chromeExecutable,
103 chromeArguments,
104 {
105 detached: true,
106 env: options.env || process.env
107 }
108 );
109 if (options.dumpio) {
110 chromeProcess.stdout.pipe(process.stdout);
111 chromeProcess.stderr.pipe(process.stderr);
112 }
113
114 let chromeClosed = false;
115 const waitForChromeToClose = new Promise((fulfill, reject) => {
116 chromeProcess.once('close', () => {
117 chromeClosed = true;
118 // Cleanup as processes exit.
119 if (temporaryUserDataDir) {
120 removeFolderAsync(temporaryUserDataDir)
121 .then(() => fulfill())
122 .catch(err => console.error(err));
123 } else {
124 fulfill();
125 }
126 });
127 });
128
129 const listeners = [ helper.addEventListener(process, 'exit', forceKillChrome) ];
130 if (options.handleSIGINT !== false)
131 listeners.push(helper.addEventListener(process, 'SIGINT', forceKillChrome));
132 if (options.handleSIGTERM !== false)
133 listeners.push(helper.addEventListener(process, 'SIGTERM', killChrome));
134 if (options.handleSIGHUP !== false)
135 listeners.push(helper.addEventListener(process, 'SIGHUP', killChrome));
136 /** @type {?Connection} */
137 let connection = null;
138 try {
139 const connectionDelay = options.slowMo || 0;
140 const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, options.timeout || 30 * 1000);
141 connection = await Connection.create(browserWSEndpoint, connectionDelay);
142 return Browser.create(connection, options, chromeProcess, killChrome);
143 } catch (e) {
144 forceKillChrome();
145 throw e;
146 }
147
148 /**
149 * @return {Promise}
150 */
151 function killChrome() {
152 helper.removeEventListeners(listeners);
153 if (temporaryUserDataDir) {
154 forceKillChrome();
155 } else if (connection) {
156 // Attempt to close chrome gracefully
157 connection.send('Browser.close');
158 }
159 return waitForChromeToClose;
160 }
161
162 function forceKillChrome() {
163 helper.removeEventListeners(listeners);
164 if (chromeProcess.pid && !chromeProcess.killed && !chromeClosed) {
165 // Force kill chrome.
166 if (process.platform === 'win32')
167 childProcess.execSync(`taskkill /pid ${chromeProcess.pid} /T /F`);
168 else
169 process.kill(-chromeProcess.pid, 'SIGKILL');
170 }
171 // Attempt to remove temporary profile directory to avoid littering.
172 try {
173 removeFolder.sync(temporaryUserDataDir);
174 } catch (e) { }
175 }
176 }
177
178 /**
179 * @return {!Array<string>}
180 */
181 static defaultArgs() {
182 return DEFAULT_ARGS.concat(AUTOMATION_ARGS);
183 }
184
185 /**
186 * @return {string}
187 */
188 static executablePath() {
189 const downloader = Downloader.createDefault();
190 const revisionInfo = downloader.revisionInfo(downloader.currentPlatform(), ChromiumRevision);
191 return revisionInfo.executablePath;
192 }
193
194 /**
195 * @param {!Object=} options
196 * @return {!Promise<!Browser>}
197 */
198 static async connect(options = {}) {
199 const connection = await Connection.create(options.browserWSEndpoint);
200 return Browser.create(connection, options, null, () => connection.send('Browser.close'));
201 }
202}
203
204/**
205 * @param {!Puppeteer.ChildProcess} chromeProcess
206 * @param {number} timeout
207 * @return {!Promise<string>}
208 */
209function waitForWSEndpoint(chromeProcess, timeout) {
210 return new Promise((resolve, reject) => {
211 const rl = readline.createInterface({ input: chromeProcess.stderr });
212 let stderr = '';
213 const listeners = [
214 helper.addEventListener(rl, 'line', onLine),
215 helper.addEventListener(rl, 'close', () => onClose()),
216 helper.addEventListener(chromeProcess, 'exit', () => onClose()),
217 helper.addEventListener(chromeProcess, 'error', error => onClose(error))
218 ];
219 const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
220
221 /**
222 * @param {!Error=} error
223 */
224 function onClose(error) {
225 cleanup();
226 reject(new Error([
227 'Failed to launch chrome!' + (error ? ' ' + error.message : ''),
228 stderr,
229 '',
230 'TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md',
231 '',
232 ].join('\n')));
233 }
234
235 function onTimeout() {
236 cleanup();
237 reject(new Error(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${ChromiumRevision}`));
238 }
239
240 /**
241 * @param {string} line
242 */
243 function onLine(line) {
244 stderr += line + '\n';
245 const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
246 if (!match)
247 return;
248 cleanup();
249 resolve(match[1]);
250 }
251
252 function cleanup() {
253 if (timeoutId)
254 clearTimeout(timeoutId);
255 helper.removeEventListeners(listeners);
256 }
257 });
258}
259
260module.exports = Launcher;