text: /*
   * Copyright 2014 Mozilla Foundation
   *
   * 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.
   */
  /* jslint node: true */
  
  'use strict';
  
  const os = require('os');
  const fs = require('fs');
  const path = require('path');
  const spawn = require('child_process').spawn;
  const testUtils = require('./testutils.js');
  const shelljs = require('shelljs');
  const crypto = require('crypto');
  
  const tempDirPrefix = 'pdfjs_';
  
  function WebBrowser(name, path) {
    this.name = name;
    this.path = path;
    this.tmpDir = null;
    this.profileDir = null;
    this.process = null;
    this.requestedExit = false;
    this.finished = false;
    this.callback = null;
    // Used to identify processes whose pid is lost. This string is directly used
    // as a command-line argument, so it only consists of letters.
    this.uniqStringId = `webbrowser${crypto.randomBytes(32).toString('hex')}`;
  }
  WebBrowser.prototype = {
    start(url) {
      this.tmpDir = path.join(os.tmpdir(), tempDirPrefix + this.name);
      if (!fs.existsSync(this.tmpDir)) {
        fs.mkdirSync(this.tmpDir);
      }
      this.startProcess(url);
    },
    getProfileDir() {
      if (!this.profileDir) {
        const profileDir = path.join(this.tmpDir, 'profile');
        if (fs.existsSync(profileDir)) {
          testUtils.removeDirSync(profileDir);
        }
        fs.mkdirSync(profileDir);
        this.profileDir = profileDir;
        this.setupProfileDir(profileDir);
      }
      return this.profileDir;
    },
    buildArguments(url) {
      return [url];
    },
    setupProfileDir(dir) {
    },
    startProcess(url) {
      console.assert(!this.process, 'startProcess may be called only once');
  
      let args = this.buildArguments(url);
      args = args.concat(`--${this.uniqStringId}`);
      this.process = spawn(this.path, args);
      this.process.on('exit', (code, signal) => {
        this.process = null;
        const exitInfo = code !== null ? ` with status ${code}`
          : ` in response to signal ${signal}`;
        if (this.requestedExit) {
          this.log(`Browser process exited${exitInfo}`);
        } else {
          // This was observed on Windows bots with Firefox. Apparently the
          // Firefox Maintenance Service restarts Firefox shortly after starting
          // up. When this happens, we no longer know the pid of the process.
          this.log(`Browser process unexpectedly exited${exitInfo}`);
        }
      });
    },
    cleanup() {
      console.assert(this.requestedExit,
          'cleanup should only be called after an explicit stop() request');
  
      try {
        testUtils.removeDirSync(this.tmpDir);
      } catch (e) {
        if (e.code !== 'ENOENT') {
          this.log(`Failed to remove profile directory: ${e}`);
          if (!this.cleanupFailStart) {
            this.cleanupFailStart = Date.now();
          } else if (Date.now() - this.cleanupFailStart > 10000) {
            throw new Error('Failed to remove profile dir within 10 seconds');
          }
          this.log('Retrying in a second...');
          setTimeout(this.cleanup.bind(this), 1000);
          return;
        }
        // This should not happen, but we just warn instead of failing loudly
        // because the post-condition of cleanup is that the profile directory is
        // gone. If the directory does not exists, then this post-condition is
        // satisfied.
        this.log(`Cannot remove non-existent directory: ${e}`);
      }
      this.finished = true;
      this.log('Clean-up finished. Going to call callback...');
      this.callback();
    },
    stop(callback) {
      console.assert(this.tmpDir, '.start() must be called before stop()');
      // Require the callback to ensure that callers do not make any assumptions
      // on the state of this browser instance until the callback is called.
      console.assert(typeof callback === 'function', 'callback is required');
      console.assert(!this.requestedExit, '.stop() may be called only once');
  
      this.requestedExit = true;
      if (this.finished) {
        this.log('Browser already stopped, invoking callback...');
        callback();
      } else if (this.process) {
        this.log('Going to wait until the browser process has exited.');
        this.callback = callback;
        this.process.once('exit', this.cleanup.bind(this));
        this.process.kill('SIGTERM');
      } else {
        this.log('Process already exited, checking if the process restarted...');
        this.callback = callback;
        this.killProcessUnknownPid(this.cleanup.bind(this));
      }
    },
    killProcessUnknownPid(callback) {
      this.log(`pid unknown, killing processes matching ${this.uniqStringId}`);
  
      let cmdKillAll, cmdCheckAllKilled, isAllKilled;
  
      if (process.platform === 'win32') {
        const wmicPrefix = `${'wmic process where "not Name = \'cmd.exe\' ' +
          'and not Name like \'%wmic%\' ' +
          'and CommandLine like \'%'}${this.uniqStringId}%'" `;
        cmdKillAll = `${wmicPrefix}call terminate`;
        cmdCheckAllKilled = `${wmicPrefix}get CommandLine`;
        isAllKilled = function (exitCode, stdout) {
          return stdout.indexOf(this.uniqStringId) === -1;
        }.bind(this);
      } else {
        cmdKillAll = `pkill -f ${this.uniqStringId}`;
        cmdCheckAllKilled = `pgrep -f ${this.uniqStringId}`;
        isAllKilled = function (pgrepStatus) {
          return pgrepStatus === 1; // "No process matched.", per man pgrep.
        };
      }
      function execAsyncNoStdin(cmd, onExit) {
        const proc = shelljs.exec(cmd, {
          async: true,
          silent: true,
        }, onExit);
        // Close stdin, otherwise wmic won't run.
        proc.stdin.end();
      }
      const killDateStart = Date.now();
      // Note: First process' output it shown, the later outputs are suppressed.
      execAsyncNoStdin(cmdKillAll, function checkAlive(exitCode, firstStdout) {
        execAsyncNoStdin(cmdCheckAllKilled, (exitCode, stdout) => {
          if (isAllKilled(exitCode, stdout)) {
            callback();
          } else if (Date.now() - killDateStart > 10000) {
            // Should finish termination within 10 (generous) seconds.
            if (firstStdout) {
              this.log(`Output of first command:\n${firstStdout}`);
            }
            if (stdout) {
              this.log(`Output of last command:\n${stdout}`);
            }
            throw new Error(`Failed to kill process of ${this.name}`);
          } else {
            setTimeout(checkAlive.bind(this), 500);
          }
        });
      }.bind(this));
    },
    log(msg) {
      console.log(`[${this.name}] ${msg}`);
    },
  };
  
  const firefoxResourceDir = path.join(__dirname, 'resources', 'firefox');
  
  function FirefoxBrowser(name, path) {
    if (os.platform() === 'darwin') {
      const m = /([^.\/]+)\.app(\/?)$/.exec(path);
      if (m) {
        path += `${m[2] ? '' : '/'}Contents/MacOS/firefox`;
      }
    }
    WebBrowser.call(this, name, path);
  }
  FirefoxBrowser.prototype = Object.create(WebBrowser.prototype);
  FirefoxBrowser.prototype.buildArguments = function (url) {
    const profileDir = this.getProfileDir();
    const args = [];
    if (os.platform() === 'darwin') {
      args.push('-foreground');
    }
    args.push('-no-remote', '-profile', profileDir, url);
    return args;
  };
  FirefoxBrowser.prototype.setupProfileDir = function (dir) {
    testUtils.copySubtreeSync(firefoxResourceDir, dir);
  };
  
  function ChromiumBrowser(name, path) {
    if (os.platform() === 'darwin') {
      const m = /([^.\/]+)\.app(\/?)$/.exec(path);
      if (m) {
        path += `${m[2] ? '' : '/'}Contents/MacOS/${m[1]}`;
        console.log(path);
      }
    }
    WebBrowser.call(this, name, path);
  }
  ChromiumBrowser.prototype = Object.create(WebBrowser.prototype);
  ChromiumBrowser.prototype.buildArguments = function (url) {
    const profileDir = this.getProfileDir();
    return [`--user-data-dir=${profileDir}`,
      '--no-first-run',
      '--disable-sync',
      url];
  };
  
  WebBrowser.create = function (desc) {
    const name = desc.name;
    const path = shelljs.which(desc.path);
    if (!path) {
      throw new Error(`Browser executable not found: ${desc.path}`);
    }
  
    if (/firefox/i.test(name)) {
      return new FirefoxBrowser(name, path);
    }
    if (/(chrome|chromium|opera)/i.test(name)) {
      return new ChromiumBrowser(name, path);
    }
    return new WebBrowser(name, path);
  };
  
  
  exports.WebBrowser = WebBrowser;
  