Launcher.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895
  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. */
  16. const os = require('os');
  17. const path = require('path');
  18. const http = require('http');
  19. const https = require('https');
  20. const URL = require('url');
  21. const removeFolder = require('rimraf');
  22. const childProcess = require('child_process');
  23. const BrowserFetcher = require('./BrowserFetcher');
  24. const {Connection} = require('./Connection');
  25. const {Browser} = require('./Browser');
  26. const readline = require('readline');
  27. const fs = require('fs');
  28. const {helper, assert, debugError} = require('./helper');
  29. const debugLauncher = require('debug')(`puppeteer:launcher`);
  30. const {TimeoutError} = require('./Errors');
  31. const WebSocketTransport = require('./WebSocketTransport');
  32. const PipeTransport = require('./PipeTransport');
  33. const mkdtempAsync = helper.promisify(fs.mkdtemp);
  34. const removeFolderAsync = helper.promisify(removeFolder);
  35. const writeFileAsync = helper.promisify(fs.writeFile);
  36. class BrowserRunner {
  37. /**
  38. * @param {string} executablePath
  39. * @param {!Array<string>} processArguments
  40. * @param {string=} tempDirectory
  41. */
  42. constructor(executablePath, processArguments, tempDirectory) {
  43. this._executablePath = executablePath;
  44. this._processArguments = processArguments;
  45. this._tempDirectory = tempDirectory;
  46. this.proc = null;
  47. this.connection = null;
  48. this._closed = true;
  49. this._listeners = [];
  50. }
  51. /**
  52. * @param {!(Launcher.LaunchOptions)=} options
  53. */
  54. start(options = {}) {
  55. const {
  56. handleSIGINT,
  57. handleSIGTERM,
  58. handleSIGHUP,
  59. dumpio,
  60. env,
  61. pipe
  62. } = options;
  63. /** @type {!Array<"ignore"|"pipe">} */
  64. let stdio = ['pipe', 'pipe', 'pipe'];
  65. if (pipe) {
  66. if (dumpio)
  67. stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
  68. else
  69. stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
  70. }
  71. assert(!this.proc, 'This process has previously been started.');
  72. debugLauncher(`Calling ${this._executablePath} ${this._processArguments.join(' ')}`);
  73. this.proc = childProcess.spawn(
  74. this._executablePath,
  75. this._processArguments,
  76. {
  77. // On non-windows platforms, `detached: true` makes child process a leader of a new
  78. // process group, making it possible to kill child process tree with `.kill(-pid)` command.
  79. // @see https://nodejs.org/api/child_process.html#child_process_options_detached
  80. detached: process.platform !== 'win32',
  81. env,
  82. stdio
  83. }
  84. );
  85. if (dumpio) {
  86. this.proc.stderr.pipe(process.stderr);
  87. this.proc.stdout.pipe(process.stdout);
  88. }
  89. this._closed = false;
  90. this._processClosing = new Promise((fulfill, reject) => {
  91. this.proc.once('exit', () => {
  92. this._closed = true;
  93. // Cleanup as processes exit.
  94. if (this._tempDirectory) {
  95. removeFolderAsync(this._tempDirectory)
  96. .then(() => fulfill())
  97. .catch(err => console.error(err));
  98. } else {
  99. fulfill();
  100. }
  101. });
  102. });
  103. this._listeners = [ helper.addEventListener(process, 'exit', this.kill.bind(this)) ];
  104. if (handleSIGINT)
  105. this._listeners.push(helper.addEventListener(process, 'SIGINT', () => { this.kill(); process.exit(130); }));
  106. if (handleSIGTERM)
  107. this._listeners.push(helper.addEventListener(process, 'SIGTERM', this.close.bind(this)));
  108. if (handleSIGHUP)
  109. this._listeners.push(helper.addEventListener(process, 'SIGHUP', this.close.bind(this)));
  110. }
  111. /**
  112. * @return {Promise}
  113. */
  114. close() {
  115. if (this._closed)
  116. return Promise.resolve();
  117. helper.removeEventListeners(this._listeners);
  118. if (this._tempDirectory) {
  119. this.kill();
  120. } else if (this.connection) {
  121. // Attempt to close the browser gracefully
  122. this.connection.send('Browser.close').catch(error => {
  123. debugError(error);
  124. this.kill();
  125. });
  126. }
  127. return this._processClosing;
  128. }
  129. // This function has to be sync to be used as 'exit' event handler.
  130. kill() {
  131. helper.removeEventListeners(this._listeners);
  132. if (this.proc && this.proc.pid && !this.proc.killed && !this._closed) {
  133. try {
  134. if (process.platform === 'win32')
  135. childProcess.execSync(`taskkill /pid ${this.proc.pid} /T /F`);
  136. else
  137. process.kill(-this.proc.pid, 'SIGKILL');
  138. } catch (error) {
  139. // the process might have already stopped
  140. }
  141. }
  142. // Attempt to remove temporary profile directory to avoid littering.
  143. try {
  144. removeFolder.sync(this._tempDirectory);
  145. } catch (error) { }
  146. }
  147. /**
  148. * @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string})} options
  149. *
  150. * @return {!Promise<!Connection>}
  151. */
  152. async setupConnection(options) {
  153. const {
  154. usePipe,
  155. timeout,
  156. slowMo,
  157. preferredRevision
  158. } = options;
  159. if (!usePipe) {
  160. const browserWSEndpoint = await waitForWSEndpoint(this.proc, timeout, preferredRevision);
  161. const transport = await WebSocketTransport.create(browserWSEndpoint);
  162. this.connection = new Connection(browserWSEndpoint, transport, slowMo);
  163. } else {
  164. const transport = new PipeTransport(/** @type {!NodeJS.WritableStream} */(this.proc.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (this.proc.stdio[4]));
  165. this.connection = new Connection('', transport, slowMo);
  166. }
  167. return this.connection;
  168. }
  169. }
  170. /**
  171. * @implements {!Puppeteer.ProductLauncher}
  172. */
  173. class ChromeLauncher {
  174. /**
  175. * @param {string} projectRoot
  176. * @param {string} preferredRevision
  177. * @param {boolean} isPuppeteerCore
  178. */
  179. constructor(projectRoot, preferredRevision, isPuppeteerCore) {
  180. this._projectRoot = projectRoot;
  181. this._preferredRevision = preferredRevision;
  182. this._isPuppeteerCore = isPuppeteerCore;
  183. }
  184. /**
  185. * @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions)=} options
  186. * @return {!Promise<!Browser>}
  187. */
  188. async launch(options = {}) {
  189. const {
  190. ignoreDefaultArgs = false,
  191. args = [],
  192. dumpio = false,
  193. executablePath = null,
  194. pipe = false,
  195. env = process.env,
  196. handleSIGINT = true,
  197. handleSIGTERM = true,
  198. handleSIGHUP = true,
  199. ignoreHTTPSErrors = false,
  200. defaultViewport = {width: 800, height: 600},
  201. slowMo = 0,
  202. timeout = 30000
  203. } = options;
  204. const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-');
  205. const chromeArguments = [];
  206. if (!ignoreDefaultArgs)
  207. chromeArguments.push(...this.defaultArgs(options));
  208. else if (Array.isArray(ignoreDefaultArgs))
  209. chromeArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
  210. else
  211. chromeArguments.push(...args);
  212. let temporaryUserDataDir = null;
  213. if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
  214. chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
  215. if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) {
  216. temporaryUserDataDir = await mkdtempAsync(profilePath);
  217. chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
  218. }
  219. let chromeExecutable = executablePath;
  220. if (!executablePath) {
  221. const {missingText, executablePath} = resolveExecutablePath(this);
  222. if (missingText)
  223. throw new Error(missingText);
  224. chromeExecutable = executablePath;
  225. }
  226. const usePipe = chromeArguments.includes('--remote-debugging-pipe');
  227. const runner = new BrowserRunner(chromeExecutable, chromeArguments, temporaryUserDataDir);
  228. runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe: usePipe});
  229. try {
  230. const connection = await runner.setupConnection({usePipe, timeout, slowMo, preferredRevision: this._preferredRevision});
  231. const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
  232. await browser.waitForTarget(t => t.type() === 'page');
  233. return browser;
  234. } catch (error) {
  235. runner.kill();
  236. throw error;
  237. }
  238. }
  239. /**
  240. * @param {!Launcher.ChromeArgOptions=} options
  241. * @return {!Array<string>}
  242. */
  243. defaultArgs(options = {}) {
  244. const chromeArguments = [
  245. '--disable-background-networking',
  246. '--enable-features=NetworkService,NetworkServiceInProcess',
  247. '--disable-background-timer-throttling',
  248. '--disable-backgrounding-occluded-windows',
  249. '--disable-breakpad',
  250. '--disable-client-side-phishing-detection',
  251. '--disable-component-extensions-with-background-pages',
  252. '--disable-default-apps',
  253. '--disable-dev-shm-usage',
  254. '--disable-extensions',
  255. '--disable-features=TranslateUI',
  256. '--disable-hang-monitor',
  257. '--disable-ipc-flooding-protection',
  258. '--disable-popup-blocking',
  259. '--disable-prompt-on-repost',
  260. '--disable-renderer-backgrounding',
  261. '--disable-sync',
  262. '--force-color-profile=srgb',
  263. '--metrics-recording-only',
  264. '--no-first-run',
  265. '--enable-automation',
  266. '--password-store=basic',
  267. '--use-mock-keychain',
  268. ];
  269. const {
  270. devtools = false,
  271. headless = !devtools,
  272. args = [],
  273. userDataDir = null
  274. } = options;
  275. if (userDataDir)
  276. chromeArguments.push(`--user-data-dir=${userDataDir}`);
  277. if (devtools)
  278. chromeArguments.push('--auto-open-devtools-for-tabs');
  279. if (headless) {
  280. chromeArguments.push(
  281. '--headless',
  282. '--hide-scrollbars',
  283. '--mute-audio'
  284. );
  285. }
  286. if (args.every(arg => arg.startsWith('-')))
  287. chromeArguments.push('about:blank');
  288. chromeArguments.push(...args);
  289. return chromeArguments;
  290. }
  291. /**
  292. * @return {string}
  293. */
  294. executablePath() {
  295. return resolveExecutablePath(this).executablePath;
  296. }
  297. /**
  298. * @return {string}
  299. */
  300. get product() {
  301. return 'chrome';
  302. }
  303. /**
  304. * @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
  305. * @return {!Promise<!Browser>}
  306. */
  307. async connect(options) {
  308. const {
  309. browserWSEndpoint,
  310. browserURL,
  311. ignoreHTTPSErrors = false,
  312. defaultViewport = {width: 800, height: 600},
  313. transport,
  314. slowMo = 0,
  315. } = options;
  316. assert(Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect');
  317. let connection = null;
  318. if (transport) {
  319. connection = new Connection('', transport, slowMo);
  320. } else if (browserWSEndpoint) {
  321. const connectionTransport = await WebSocketTransport.create(browserWSEndpoint);
  322. connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
  323. } else if (browserURL) {
  324. const connectionURL = await getWSEndpoint(browserURL);
  325. const connectionTransport = await WebSocketTransport.create(connectionURL);
  326. connection = new Connection(connectionURL, connectionTransport, slowMo);
  327. }
  328. const {browserContextIds} = await connection.send('Target.getBrowserContexts');
  329. return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
  330. }
  331. }
  332. /**
  333. * @implements {!Puppeteer.ProductLauncher}
  334. */
  335. class FirefoxLauncher {
  336. /**
  337. * @param {string} projectRoot
  338. * @param {string} preferredRevision
  339. * @param {boolean} isPuppeteerCore
  340. */
  341. constructor(projectRoot, preferredRevision, isPuppeteerCore) {
  342. this._projectRoot = projectRoot;
  343. this._preferredRevision = preferredRevision;
  344. this._isPuppeteerCore = isPuppeteerCore;
  345. }
  346. /**
  347. * @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions & {extraPrefsFirefox?: !object})=} options
  348. * @return {!Promise<!Browser>}
  349. */
  350. async launch(options = {}) {
  351. const {
  352. ignoreDefaultArgs = false,
  353. args = [],
  354. dumpio = false,
  355. executablePath = null,
  356. pipe = false,
  357. env = process.env,
  358. handleSIGINT = true,
  359. handleSIGTERM = true,
  360. handleSIGHUP = true,
  361. ignoreHTTPSErrors = false,
  362. defaultViewport = {width: 800, height: 600},
  363. slowMo = 0,
  364. timeout = 30000,
  365. extraPrefsFirefox = {}
  366. } = options;
  367. const firefoxArguments = [];
  368. if (!ignoreDefaultArgs)
  369. firefoxArguments.push(...this.defaultArgs(options));
  370. else if (Array.isArray(ignoreDefaultArgs))
  371. firefoxArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
  372. else
  373. firefoxArguments.push(...args);
  374. let temporaryUserDataDir = null;
  375. if (!firefoxArguments.includes('-profile') && !firefoxArguments.includes('--profile')) {
  376. temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
  377. firefoxArguments.push('--profile');
  378. firefoxArguments.push(temporaryUserDataDir);
  379. }
  380. let executable = executablePath;
  381. if (!executablePath) {
  382. const {missingText, executablePath} = resolveExecutablePath(this);
  383. if (missingText)
  384. throw new Error(missingText);
  385. executable = executablePath;
  386. }
  387. const runner = new BrowserRunner(executable, firefoxArguments, temporaryUserDataDir);
  388. runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe});
  389. try {
  390. const connection = await runner.setupConnection({usePipe: pipe, timeout, slowMo, preferredRevision: this._preferredRevision});
  391. const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
  392. await browser.waitForTarget(t => t.type() === 'page');
  393. return browser;
  394. } catch (error) {
  395. runner.kill();
  396. throw error;
  397. }
  398. }
  399. /**
  400. * @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
  401. * @return {!Promise<!Browser>}
  402. */
  403. async connect(options) {
  404. const {
  405. browserWSEndpoint,
  406. browserURL,
  407. ignoreHTTPSErrors = false,
  408. defaultViewport = {width: 800, height: 600},
  409. transport,
  410. slowMo = 0,
  411. } = options;
  412. assert(Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect');
  413. let connection = null;
  414. if (transport) {
  415. connection = new Connection('', transport, slowMo);
  416. } else if (browserWSEndpoint) {
  417. const connectionTransport = await WebSocketTransport.create(browserWSEndpoint);
  418. connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
  419. } else if (browserURL) {
  420. const connectionURL = await getWSEndpoint(browserURL);
  421. const connectionTransport = await WebSocketTransport.create(connectionURL);
  422. connection = new Connection(connectionURL, connectionTransport, slowMo);
  423. }
  424. const {browserContextIds} = await connection.send('Target.getBrowserContexts');
  425. return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
  426. }
  427. /**
  428. * @return {string}
  429. */
  430. executablePath() {
  431. const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
  432. // TODO get resolveExecutablePath working for Firefox
  433. if (!executablePath)
  434. throw new Error('Please set PUPPETEER_EXECUTABLE_PATH to a Firefox binary.');
  435. return executablePath;
  436. }
  437. /**
  438. * @return {string}
  439. */
  440. get product() {
  441. return 'firefox';
  442. }
  443. /**
  444. * @param {!Launcher.ChromeArgOptions=} options
  445. * @return {!Array<string>}
  446. */
  447. defaultArgs(options = {}) {
  448. const firefoxArguments = [
  449. '--remote-debugging-port=0',
  450. '--no-remote',
  451. '--foreground',
  452. ];
  453. const {
  454. devtools = false,
  455. headless = !devtools,
  456. args = [],
  457. userDataDir = null
  458. } = options;
  459. if (userDataDir) {
  460. firefoxArguments.push('--profile');
  461. firefoxArguments.push(userDataDir);
  462. }
  463. if (headless)
  464. firefoxArguments.push('--headless');
  465. if (devtools)
  466. firefoxArguments.push('--devtools');
  467. if (args.every(arg => arg.startsWith('-')))
  468. firefoxArguments.push('about:blank');
  469. firefoxArguments.push(...args);
  470. return firefoxArguments;
  471. }
  472. /**
  473. * @param {!Object=} extraPrefs
  474. * @return {!Promise<string>}
  475. */
  476. async _createProfile(extraPrefs) {
  477. const profilePath = await mkdtempAsync(path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-'));
  478. const prefsJS = [];
  479. const userJS = [];
  480. const server = 'dummy.test';
  481. const defaultPreferences = {
  482. // Make sure Shield doesn't hit the network.
  483. 'app.normandy.api_url': '',
  484. // Disable Firefox old build background check
  485. 'app.update.checkInstallTime': false,
  486. // Disable automatically upgrading Firefox
  487. 'app.update.disabledForTesting': true,
  488. // Increase the APZ content response timeout to 1 minute
  489. 'apz.content_response_timeout': 60000,
  490. // Prevent various error message on the console
  491. // jest-puppeteer asserts that no error message is emitted by the console
  492. 'browser.contentblocking.features.standard': '-tp,tpPrivate,cookieBehavior0,-cm,-fp',
  493. // Enable the dump function: which sends messages to the system
  494. // console
  495. // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
  496. 'browser.dom.window.dump.enabled': true,
  497. // Disable topstories
  498. 'browser.newtabpage.activity-stream.feeds.section.topstories': false,
  499. // Always display a blank page
  500. 'browser.newtabpage.enabled': false,
  501. // Background thumbnails in particular cause grief: and disabling
  502. // thumbnails in general cannot hurt
  503. 'browser.pagethumbnails.capturing_disabled': true,
  504. // Disable safebrowsing components.
  505. 'browser.safebrowsing.blockedURIs.enabled': false,
  506. 'browser.safebrowsing.downloads.enabled': false,
  507. 'browser.safebrowsing.malware.enabled': false,
  508. 'browser.safebrowsing.passwords.enabled': false,
  509. 'browser.safebrowsing.phishing.enabled': false,
  510. // Disable updates to search engines.
  511. 'browser.search.update': false,
  512. // Do not restore the last open set of tabs if the browser has crashed
  513. 'browser.sessionstore.resume_from_crash': false,
  514. // Skip check for default browser on startup
  515. 'browser.shell.checkDefaultBrowser': false,
  516. // Disable newtabpage
  517. 'browser.startup.homepage': 'about:blank',
  518. // Do not redirect user when a milstone upgrade of Firefox is detected
  519. 'browser.startup.homepage_override.mstone': 'ignore',
  520. // Start with a blank page about:blank
  521. 'browser.startup.page': 0,
  522. // Do not allow background tabs to be zombified on Android: otherwise for
  523. // tests that open additional tabs: the test harness tab itself might get
  524. // unloaded
  525. 'browser.tabs.disableBackgroundZombification': false,
  526. // Do not warn when closing all other open tabs
  527. 'browser.tabs.warnOnCloseOtherTabs': false,
  528. // Do not warn when multiple tabs will be opened
  529. 'browser.tabs.warnOnOpen': false,
  530. // Disable the UI tour.
  531. 'browser.uitour.enabled': false,
  532. // Turn off search suggestions in the location bar so as not to trigger
  533. // network connections.
  534. 'browser.urlbar.suggest.searches': false,
  535. // Disable first run splash page on Windows 10
  536. 'browser.usedOnWindows10.introURL': '',
  537. // Do not warn on quitting Firefox
  538. 'browser.warnOnQuit': false,
  539. // Do not show datareporting policy notifications which can
  540. // interfere with tests
  541. 'datareporting.healthreport.about.reportUrl': `http://${server}/dummy/abouthealthreport/`,
  542. 'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
  543. 'datareporting.healthreport.logging.consoleEnabled': false,
  544. 'datareporting.healthreport.service.enabled': false,
  545. 'datareporting.healthreport.service.firstRun': false,
  546. 'datareporting.healthreport.uploadEnabled': false,
  547. 'datareporting.policy.dataSubmissionEnabled': false,
  548. 'datareporting.policy.dataSubmissionPolicyAccepted': false,
  549. 'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
  550. // DevTools JSONViewer sometimes fails to load dependencies with its require.js.
  551. // This doesn't affect Puppeteer but spams console (Bug 1424372)
  552. 'devtools.jsonview.enabled': false,
  553. // Disable popup-blocker
  554. 'dom.disable_open_during_load': false,
  555. // Enable the support for File object creation in the content process
  556. // Required for |Page.setFileInputFiles| protocol method.
  557. 'dom.file.createInChild': true,
  558. // Disable the ProcessHangMonitor
  559. 'dom.ipc.reportProcessHangs': false,
  560. // Disable slow script dialogues
  561. 'dom.max_chrome_script_run_time': 0,
  562. 'dom.max_script_run_time': 0,
  563. // Only load extensions from the application and user profile
  564. // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
  565. 'extensions.autoDisableScopes': 0,
  566. 'extensions.enabledScopes': 5,
  567. // Disable metadata caching for installed add-ons by default
  568. 'extensions.getAddons.cache.enabled': false,
  569. // Disable installing any distribution extensions or add-ons.
  570. 'extensions.installDistroAddons': false,
  571. // Disabled screenshots extension
  572. 'extensions.screenshots.disabled': true,
  573. // Turn off extension updates so they do not bother tests
  574. 'extensions.update.enabled': false,
  575. // Turn off extension updates so they do not bother tests
  576. 'extensions.update.notifyUser': false,
  577. // Make sure opening about:addons will not hit the network
  578. 'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`,
  579. // Allow the application to have focus even it runs in the background
  580. 'focusmanager.testmode': true,
  581. // Disable useragent updates
  582. 'general.useragent.updates.enabled': false,
  583. // Always use network provider for geolocation tests so we bypass the
  584. // macOS dialog raised by the corelocation provider
  585. 'geo.provider.testing': true,
  586. // Do not scan Wifi
  587. 'geo.wifi.scan': false,
  588. // No hang monitor
  589. 'hangmonitor.timeout': 0,
  590. // Show chrome errors and warnings in the error console
  591. 'javascript.options.showInConsole': true,
  592. // Disable download and usage of OpenH264: and Widevine plugins
  593. 'media.gmp-manager.updateEnabled': false,
  594. // Prevent various error message on the console
  595. // jest-puppeteer asserts that no error message is emitted by the console
  596. 'network.cookie.cookieBehavior': 0,
  597. // Do not prompt for temporary redirects
  598. 'network.http.prompt-temp-redirect': false,
  599. // Disable speculative connections so they are not reported as leaking
  600. // when they are hanging around
  601. 'network.http.speculative-parallel-limit': 0,
  602. // Do not automatically switch between offline and online
  603. 'network.manage-offline-status': false,
  604. // Make sure SNTP requests do not hit the network
  605. 'network.sntp.pools': server,
  606. // Disable Flash.
  607. 'plugin.state.flash': 0,
  608. 'privacy.trackingprotection.enabled': false,
  609. // Enable Remote Agent
  610. // https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
  611. 'remote.enabled': true,
  612. // Don't do network connections for mitm priming
  613. 'security.certerrors.mitm.priming.enabled': false,
  614. // Local documents have access to all other local documents,
  615. // including directory listings
  616. 'security.fileuri.strict_origin_policy': false,
  617. // Do not wait for the notification button security delay
  618. 'security.notification_enable_delay': 0,
  619. // Ensure blocklist updates do not hit the network
  620. 'services.settings.server': `http://${server}/dummy/blocklist/`,
  621. // Do not automatically fill sign-in forms with known usernames and
  622. // passwords
  623. 'signon.autofillForms': false,
  624. // Disable password capture, so that tests that include forms are not
  625. // influenced by the presence of the persistent doorhanger notification
  626. 'signon.rememberSignons': false,
  627. // Disable first-run welcome page
  628. 'startup.homepage_welcome_url': 'about:blank',
  629. // Disable first-run welcome page
  630. 'startup.homepage_welcome_url.additional': '',
  631. // Disable browser animations (tabs, fullscreen, sliding alerts)
  632. 'toolkit.cosmeticAnimations.enabled': false,
  633. // We want to collect telemetry, but we don't want to send in the results
  634. 'toolkit.telemetry.server': `https://${server}/dummy/telemetry/`,
  635. // Prevent starting into safe mode after application crashes
  636. 'toolkit.startup.max_resumed_crashes': -1,
  637. };
  638. Object.assign(defaultPreferences, extraPrefs);
  639. for (const [key, value] of Object.entries(defaultPreferences))
  640. userJS.push(`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`);
  641. await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n'));
  642. await writeFileAsync(path.join(profilePath, 'prefs.js'), prefsJS.join('\n'));
  643. return profilePath;
  644. }
  645. }
  646. /**
  647. * @param {!Puppeteer.ChildProcess} browserProcess
  648. * @param {number} timeout
  649. * @param {string} preferredRevision
  650. * @return {!Promise<string>}
  651. */
  652. function waitForWSEndpoint(browserProcess, timeout, preferredRevision) {
  653. return new Promise((resolve, reject) => {
  654. const rl = readline.createInterface({ input: browserProcess.stderr });
  655. let stderr = '';
  656. const listeners = [
  657. helper.addEventListener(rl, 'line', onLine),
  658. helper.addEventListener(rl, 'close', () => onClose()),
  659. helper.addEventListener(browserProcess, 'exit', () => onClose()),
  660. helper.addEventListener(browserProcess, 'error', error => onClose(error))
  661. ];
  662. const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
  663. /**
  664. * @param {!Error=} error
  665. */
  666. function onClose(error) {
  667. cleanup();
  668. reject(new Error([
  669. 'Failed to launch the browser process!' + (error ? ' ' + error.message : ''),
  670. stderr,
  671. '',
  672. 'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md',
  673. '',
  674. ].join('\n')));
  675. }
  676. function onTimeout() {
  677. cleanup();
  678. reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`));
  679. }
  680. /**
  681. * @param {string} line
  682. */
  683. function onLine(line) {
  684. stderr += line + '\n';
  685. const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
  686. if (!match)
  687. return;
  688. cleanup();
  689. resolve(match[1]);
  690. }
  691. function cleanup() {
  692. if (timeoutId)
  693. clearTimeout(timeoutId);
  694. helper.removeEventListeners(listeners);
  695. }
  696. });
  697. }
  698. /**
  699. * @param {string} browserURL
  700. * @return {!Promise<string>}
  701. */
  702. function getWSEndpoint(browserURL) {
  703. let resolve, reject;
  704. const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
  705. const endpointURL = URL.resolve(browserURL, '/json/version');
  706. const protocol = endpointURL.startsWith('https') ? https : http;
  707. const requestOptions = Object.assign(URL.parse(endpointURL), { method: 'GET' });
  708. const request = protocol.request(requestOptions, res => {
  709. let data = '';
  710. if (res.statusCode !== 200) {
  711. // Consume response data to free up memory.
  712. res.resume();
  713. reject(new Error('HTTP ' + res.statusCode));
  714. return;
  715. }
  716. res.setEncoding('utf8');
  717. res.on('data', chunk => data += chunk);
  718. res.on('end', () => resolve(JSON.parse(data).webSocketDebuggerUrl));
  719. });
  720. request.on('error', reject);
  721. request.end();
  722. return promise.catch(e => {
  723. e.message = `Failed to fetch browser webSocket url from ${endpointURL}: ` + e.message;
  724. throw e;
  725. });
  726. }
  727. /**
  728. * @param {ChromeLauncher|FirefoxLauncher} launcher
  729. *
  730. * @return {{executablePath: string, missingText: ?string}}
  731. */
  732. function resolveExecutablePath(launcher) {
  733. // puppeteer-core doesn't take into account PUPPETEER_* env variables.
  734. if (!launcher._isPuppeteerCore) {
  735. const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
  736. if (executablePath) {
  737. const missingText = !fs.existsSync(executablePath) ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + executablePath : null;
  738. return { executablePath, missingText };
  739. }
  740. }
  741. const browserFetcher = new BrowserFetcher(launcher._projectRoot);
  742. if (!launcher._isPuppeteerCore) {
  743. const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
  744. if (revision) {
  745. const revisionInfo = browserFetcher.revisionInfo(revision);
  746. const missingText = !revisionInfo.local ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + revisionInfo.executablePath : null;
  747. return {executablePath: revisionInfo.executablePath, missingText};
  748. }
  749. }
  750. const revisionInfo = browserFetcher.revisionInfo(launcher._preferredRevision);
  751. const missingText = !revisionInfo.local ? `Browser is not downloaded. Run "npm install" or "yarn install"` : null;
  752. return {executablePath: revisionInfo.executablePath, missingText};
  753. }
  754. /**
  755. * @param {string} projectRoot
  756. * @param {string} preferredRevision
  757. * @param {boolean} isPuppeteerCore
  758. * @param {string=} product
  759. * @return {!Puppeteer.ProductLauncher}
  760. */
  761. function Launcher(projectRoot, preferredRevision, isPuppeteerCore, product) {
  762. // puppeteer-core doesn't take into account PUPPETEER_* env variables.
  763. if (!product && !isPuppeteerCore)
  764. product = process.env.PUPPETEER_PRODUCT || process.env.npm_config_puppeteer_product || process.env.npm_package_config_puppeteer_product;
  765. switch (product) {
  766. case 'firefox':
  767. return new FirefoxLauncher(projectRoot, preferredRevision, isPuppeteerCore);
  768. case 'chrome':
  769. default:
  770. return new ChromeLauncher(projectRoot, preferredRevision, isPuppeteerCore);
  771. }
  772. }
  773. /**
  774. * @typedef {Object} Launcher.ChromeArgOptions
  775. * @property {boolean=} headless
  776. * @property {Array<string>=} args
  777. * @property {string=} userDataDir
  778. * @property {boolean=} devtools
  779. */
  780. /**
  781. * @typedef {Object} Launcher.LaunchOptions
  782. * @property {string=} executablePath
  783. * @property {boolean|Array<string>=} ignoreDefaultArgs
  784. * @property {boolean=} handleSIGINT
  785. * @property {boolean=} handleSIGTERM
  786. * @property {boolean=} handleSIGHUP
  787. * @property {number=} timeout
  788. * @property {boolean=} dumpio
  789. * @property {!Object<string, string | undefined>=} env
  790. * @property {boolean=} pipe
  791. */
  792. /**
  793. * @typedef {Object} Launcher.BrowserOptions
  794. * @property {boolean=} ignoreHTTPSErrors
  795. * @property {(?Puppeteer.Viewport)=} defaultViewport
  796. * @property {number=} slowMo
  797. */
  798. module.exports = Launcher;