text: /**
   * Copyright 2017 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.
   */
  const os = require('os');
  const path = require('path');
  const removeFolder = require('rimraf');
  const childProcess = require('child_process');
  const BrowserFetcher = require('./BrowserFetcher');
  const {Connection} = require('./Connection');
  const {Browser} = require('./Browser');
  const readline = require('readline');
  const fs = require('fs');
  const {helper, debugError} = require('./helper');
  const {TimeoutError} = require('./Errors');
  
  const mkdtempAsync = helper.promisify(fs.mkdtemp);
  const removeFolderAsync = helper.promisify(removeFolder);
  
  const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_dev_profile-');
  
  const DEFAULT_ARGS = [
    '--disable-background-networking',
    '--disable-background-timer-throttling',
    '--disable-breakpad',
    '--disable-client-side-phishing-detection',
    '--disable-default-apps',
    '--disable-dev-shm-usage',
    '--disable-extensions',
    // TODO: Support OOOPIF. @see https://github.com/GoogleChrome/puppeteer/issues/2548
    '--disable-features=site-per-process',
    '--disable-hang-monitor',
    '--disable-popup-blocking',
    '--disable-prompt-on-repost',
    '--disable-sync',
    '--disable-translate',
    '--metrics-recording-only',
    '--no-first-run',
    '--safebrowsing-disable-auto-update',
    '--enable-automation',
    '--password-store=basic',
    '--use-mock-keychain',
  ];
  
  class Launcher {
    /**
     * @param {string} projectRoot
     * @param {string} preferredRevision
     * @param {boolean} isPuppeteerCore
     */
    constructor(projectRoot, preferredRevision, isPuppeteerCore) {
      this._projectRoot = projectRoot;
      this._preferredRevision = preferredRevision;
      this._isPuppeteerCore = isPuppeteerCore;
    }
  
    /**
     * @param {!(LaunchOptions & ChromeArgOptions & BrowserOptions)=} options
     * @return {!Promise<!Browser>}
     */
    async launch(options = {}) {
      const {
        ignoreDefaultArgs = false,
        args = [],
        dumpio = false,
        executablePath = null,
        pipe = false,
        env = process.env,
        handleSIGINT = true,
        handleSIGTERM = true,
        handleSIGHUP = true,
        ignoreHTTPSErrors = false,
        defaultViewport = {width: 800, height: 600},
        slowMo = 0,
        timeout = 30000
      } = options;
  
      const chromeArguments = [];
      if (!ignoreDefaultArgs)
        chromeArguments.push(...this.defaultArgs(options));
      else if (Array.isArray(ignoreDefaultArgs))
        chromeArguments.push(...this.defaultArgs(options).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
      else
        chromeArguments.push(...args);
  
      let temporaryUserDataDir = null;
  
      if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
        chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
      if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) {
        temporaryUserDataDir = await mkdtempAsync(CHROME_PROFILE_PATH);
        chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
      }
  
      let chromeExecutable = executablePath;
      if (!executablePath) {
        const {missingText, executablePath} = this._resolveExecutablePath();
        if (missingText)
          throw new Error(missingText);
        chromeExecutable = executablePath;
      }
  
      const usePipe = chromeArguments.includes('--remote-debugging-pipe');
      const stdio = usePipe ? ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
      const chromeProcess = childProcess.spawn(
          chromeExecutable,
          chromeArguments,
          {
            // On non-windows platforms, `detached: false` makes child process a leader of a new
            // process group, making it possible to kill child process tree with `.kill(-pid)` command.
            // @see https://nodejs.org/api/child_process.html#child_process_options_detached
            detached: process.platform !== 'win32',
            env,
            stdio
          }
      );
  
      if (dumpio) {
        chromeProcess.stderr.pipe(process.stderr);
        chromeProcess.stdout.pipe(process.stdout);
      }
  
      let chromeClosed = false;
      const waitForChromeToClose = new Promise((fulfill, reject) => {
        chromeProcess.once('exit', () => {
          chromeClosed = true;
          // Cleanup as processes exit.
          if (temporaryUserDataDir) {
            removeFolderAsync(temporaryUserDataDir)
                .then(() => fulfill())
                .catch(err => console.error(err));
          } else {
            fulfill();
          }
        });
      });
  
      const listeners = [ helper.addEventListener(process, 'exit', killChrome) ];
      if (handleSIGINT)
        listeners.push(helper.addEventListener(process, 'SIGINT', () => { killChrome(); process.exit(130); }));
      if (handleSIGTERM)
        listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyCloseChrome));
      if (handleSIGHUP)
        listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyCloseChrome));
      /** @type {?Connection} */
      let connection = null;
      try {
        if (!usePipe) {
          const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout, this._preferredRevision);
          connection = await Connection.createForWebSocket(browserWSEndpoint, slowMo);
        } else {
          connection = Connection.createForPipe(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4]), slowMo);
        }
        const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, chromeProcess, gracefullyCloseChrome);
        await ensureInitialPage(browser);
        return browser;
      } catch (e) {
        killChrome();
        throw e;
      }
  
      /**
       * @param {!Browser} browser
       */
      async function ensureInitialPage(browser) {
        // Wait for initial page target to be created.
        if (browser.targets().find(target => target.type() === 'page'))
          return;
  
        let initialPageCallback;
        const initialPagePromise = new Promise(resolve => initialPageCallback = resolve);
        const listeners = [helper.addEventListener(browser, 'targetcreated', target => {
          if (target.type() === 'page')
            initialPageCallback();
        })];
  
        await initialPagePromise;
        helper.removeEventListeners(listeners);
      }
  
      /**
       * @return {Promise}
       */
      function gracefullyCloseChrome() {
        helper.removeEventListeners(listeners);
        if (temporaryUserDataDir) {
          killChrome();
        } else if (connection) {
          // Attempt to close chrome gracefully
          connection.send('Browser.close').catch(error => {
            debugError(error);
            killChrome();
          });
        }
        return waitForChromeToClose;
      }
  
      // This method has to be sync to be used as 'exit' event handler.
      function killChrome() {
        helper.removeEventListeners(listeners);
        if (chromeProcess.pid && !chromeProcess.killed && !chromeClosed) {
          // Force kill chrome.
          try {
            if (process.platform === 'win32')
              childProcess.execSync(`taskkill /pid ${chromeProcess.pid} /T /F`);
            else
              process.kill(-chromeProcess.pid, 'SIGKILL');
          } catch (e) {
            // the process might have already stopped
          }
        }
        // Attempt to remove temporary profile directory to avoid littering.
        try {
          removeFolder.sync(temporaryUserDataDir);
        } catch (e) { }
      }
    }
  
    /**
     * @param {!ChromeArgOptions=} options
     * @return {!Array<string>}
     */
    defaultArgs(options = {}) {
      const {
        devtools = false,
        headless = !devtools,
        args = [],
        userDataDir = null
      } = options;
      const chromeArguments = [...DEFAULT_ARGS];
      if (userDataDir)
        chromeArguments.push(`--user-data-dir=${userDataDir}`);
      if (devtools)
        chromeArguments.push('--auto-open-devtools-for-tabs');
      if (headless) {
        chromeArguments.push(
            '--headless',
            '--hide-scrollbars',
            '--mute-audio'
        );
        if (os.platform() === 'win32')
          chromeArguments.push('--disable-gpu');
      }
      if (args.every(arg => arg.startsWith('-')))
        chromeArguments.push('about:blank');
      chromeArguments.push(...args);
      return chromeArguments;
    }
  
    /**
     * @return {string}
     */
    executablePath() {
      return this._resolveExecutablePath().executablePath;
    }
  
    /**
     * @param {!(BrowserOptions & {browserWSEndpoint: string})} options
     * @return {!Promise<!Browser>}
     */
    async connect(options) {
      const {
        browserWSEndpoint,
        ignoreHTTPSErrors = false,
        defaultViewport = {width: 800, height: 600},
        slowMo = 0,
      } = options;
      const connection = await Connection.createForWebSocket(browserWSEndpoint, slowMo);
      const {browserContextIds} = await connection.send('Target.getBrowserContexts');
      return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
    }
  
    /**
     * @return {{executablePath: string, missingText: ?string}}
     */
    _resolveExecutablePath() {
      const browserFetcher = new BrowserFetcher(this._projectRoot);
      // puppeteer-core doesn't take into account PUPPETEER_* env variables.
      if (!this._isPuppeteerCore) {
        const executablePath = process.env['PUPPETEER_EXECUTABLE_PATH'];
        if (executablePath) {
          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;
          return { executablePath, missingText };
        }
        const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
        if (revision) {
          const revisionInfo = browserFetcher.revisionInfo(revision);
          const missingText = !revisionInfo.local ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + revisionInfo.executablePath : null;
          return {executablePath: revisionInfo.executablePath, missingText};
        }
      }
      const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
      const missingText = !revisionInfo.local ? `Chromium revision is not downloaded. Run "npm install" or "yarn install"` : null;
      return {executablePath: revisionInfo.executablePath, missingText};
    }
  
  }
  
  /**
   * @param {!Puppeteer.ChildProcess} chromeProcess
   * @param {number} timeout
   * @param {string} preferredRevision
   * @return {!Promise<string>}
   */
  function waitForWSEndpoint(chromeProcess, timeout, preferredRevision) {
    return new Promise((resolve, reject) => {
      const rl = readline.createInterface({ input: chromeProcess.stderr });
      let stderr = '';
      const listeners = [
        helper.addEventListener(rl, 'line', onLine),
        helper.addEventListener(rl, 'close', () => onClose()),
        helper.addEventListener(chromeProcess, 'exit', () => onClose()),
        helper.addEventListener(chromeProcess, 'error', error => onClose(error))
      ];
      const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
  
      /**
       * @param {!Error=} error
       */
      function onClose(error) {
        cleanup();
        reject(new Error([
          'Failed to launch chrome!' + (error ? ' ' + error.message : ''),
          stderr,
          '',
          'TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md',
          '',
        ].join('\n')));
      }
  
      function onTimeout() {
        cleanup();
        reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${preferredRevision}`));
      }
  
      /**
       * @param {string} line
       */
      function onLine(line) {
        stderr += line + '\n';
        const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
        if (!match)
          return;
        cleanup();
        resolve(match[1]);
      }
  
      function cleanup() {
        if (timeoutId)
          clearTimeout(timeoutId);
        helper.removeEventListeners(listeners);
      }
    });
  }
  
  /**
   * @typedef {Object} ChromeArgOptions
   * @property {boolean=} headless
   * @property {Array<string>=} args
   * @property {string=} userDataDir
   * @property {boolean=} devtools
   */
  
  /**
   * @typedef {Object} LaunchOptions
   * @property {string=} executablePath
   * @property {boolean=} ignoreDefaultArgs
   * @property {boolean=} handleSIGINT
   * @property {boolean=} handleSIGTERM
   * @property {boolean=} handleSIGHUP
   * @property {number=} timeout
   * @property {boolean=} dumpio
   * @property {!Object<string, string | undefined>=} env
   * @property {boolean=} pipe
   */
  
  /**
   * @typedef {Object} BrowserOptions
   * @property {boolean=} ignoreHTTPSErrors
   * @property {(?Puppeteer.Viewport)=} defaultViewport
   * @property {number=} slowMo
   */
  
  
  module.exports = Launcher;
  