402 lines
13 KiB
JavaScript
Executable file
402 lines
13 KiB
JavaScript
Executable file
'use strict'
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
let isWsl = require('is-wsl')
|
|
const which = require('which')
|
|
const { execSync } = require('child_process')
|
|
const { StringDecoder } = require('string_decoder')
|
|
|
|
const PREFS = [
|
|
'user_pref("browser.shell.checkDefaultBrowser", false);',
|
|
'user_pref("browser.bookmarks.restore_default_bookmarks", false);',
|
|
'user_pref("dom.disable_open_during_load", false);',
|
|
'user_pref("dom.max_script_run_time", 0);',
|
|
'user_pref("dom.min_background_timeout_value", 10);',
|
|
'user_pref("extensions.autoDisableScopes", 0);',
|
|
'user_pref("browser.tabs.remote.autostart", false);',
|
|
'user_pref("browser.tabs.remote.autostart.2", false);',
|
|
'user_pref("extensions.enabledScopes", 15);'
|
|
].join('\n')
|
|
|
|
// NOTE: add 'config.browsers' to get which browsers are started
|
|
const $INJECT_LIST = ['baseBrowserDecorator', 'args', 'logger', 'emitter']
|
|
|
|
// Check if Firefox is installed on the WSL side and use that if it's available
|
|
if (isWsl && which.sync('firefox', { nothrow: true })) {
|
|
isWsl = false
|
|
}
|
|
|
|
/**
|
|
* Takes a string from Windows' tasklist.exe with the following arguments:
|
|
* `/FO CSV /NH /SVC` and returns an array of PIDs.
|
|
* @param {string} tasklist Expected to be in the form of:
|
|
* `'"firefox.exe","14972","Console","1","5.084 K"\r\n"firefox.exe","12204","Console","1","221.656 K"'`
|
|
* @returns {string[]} Array of String PIDs. Can be empty.
|
|
*/
|
|
const extractPids = tasklist => tasklist
|
|
.split(',')
|
|
.filter(x => /^"\d{3,10}"$/.test(x))
|
|
.map(pid => pid.replace(/"/g, ''))
|
|
|
|
/**
|
|
* Curried function version of safeExecSync with reference to logger
|
|
* in a closure.
|
|
* @param {function} log An instance of logger.create
|
|
* @returns {{(command:string):string}} A closure with reference to logger
|
|
*/
|
|
const createSafeExecSync = log => command => {
|
|
let output = ''
|
|
try {
|
|
output = String(execSync(command))
|
|
} catch (err) {
|
|
// Something went wrong but we can usually continue.
|
|
// For Windows kill.exe, one common error is trying to kill a PID
|
|
// that no longer exist, which is fine.
|
|
log.debug(String(err))
|
|
}
|
|
return output
|
|
}
|
|
|
|
// Get all possible Program Files folders even on other drives
|
|
// inspect the user's path to find other drives that may contain Program Files folders
|
|
const getAllPrefixes = function () {
|
|
const drives = []
|
|
const paden = process.env.Path.split(';')
|
|
const re = /^[A-Z]:\\/i
|
|
let pad
|
|
for (let p = 0; p < paden.length; p++) {
|
|
pad = paden[p]
|
|
if (re.test(pad) && drives.indexOf(pad[0]) === -1) {
|
|
drives.push(pad[0])
|
|
}
|
|
}
|
|
|
|
const result = []
|
|
const prefixes = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']]
|
|
let prefix
|
|
for (let i = 0; i < prefixes.length; i++) {
|
|
if (typeof prefixes[i] !== 'undefined') {
|
|
for (let d = 0; d < drives.length; d += 1) {
|
|
prefix = drives[d] + prefixes[i].slice(1)
|
|
if (result.indexOf(prefix) === -1) {
|
|
result.push(prefix)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Return location of firefox.exe file for a given Firefox directory
|
|
// (available: "Mozilla Firefox", "Aurora", "Nightly").
|
|
const getFirefoxExe = function (firefoxDirName) {
|
|
if (process.platform !== 'win32' && process.platform !== 'win64') {
|
|
return null
|
|
}
|
|
|
|
const firefoxDirNames = Array.prototype.slice.call(arguments)
|
|
|
|
for (const prefix of getAllPrefixes()) {
|
|
for (const dir of firefoxDirNames) {
|
|
const candidate = path.join(prefix, dir, 'firefox.exe')
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
return path.join('C:\\Program Files', firefoxDirNames[0], 'firefox.exe')
|
|
}
|
|
|
|
const getAllPrefixesWsl = function () {
|
|
const drives = []
|
|
// Some folks configure their wsl.conf to mount Windows drives without the
|
|
// /mnt prefix (e.g. see https://nickjanetakis.com/blog/setting-up-docker-for-windows-and-wsl-to-work-flawlessly)
|
|
//
|
|
// In fact, they could configure this to be any number of things. So we
|
|
// take each path, convert it to a Windows path, check if it looks like
|
|
// it starts with a drive and then record that.
|
|
const re = /^([A-Z]):\\/i
|
|
for (const pathElem of process.env.PATH.split(':')) {
|
|
if (fs.existsSync(pathElem)) {
|
|
const windowsPath = execSync('wslpath -w "' + pathElem + '"').toString()
|
|
const matches = windowsPath.match(re)
|
|
if (matches !== null && drives.indexOf(matches[1]) === -1) {
|
|
drives.push(matches[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = []
|
|
// We don't have the PROGRAMFILES or PROGRAMFILES(X86) environment variables
|
|
// in WSL so we just hard code them.
|
|
const prefixes = ['Program Files', 'Program Files (x86)']
|
|
for (const prefix of prefixes) {
|
|
for (const drive of drives) {
|
|
// We only have the drive, and only wslpath knows exactly what they map to
|
|
// in Linux, so we convert it back here.
|
|
const wslPath =
|
|
execSync('wslpath "' + drive + ':\\' + prefix + '"').toString().trim()
|
|
result.push(wslPath)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
const getFirefoxExeWsl = function (firefoxDirName) {
|
|
if (!isWsl) {
|
|
return null
|
|
}
|
|
|
|
const firefoxDirNames = Array.prototype.slice.call(arguments)
|
|
|
|
for (const prefix of getAllPrefixesWsl()) {
|
|
for (const dir of firefoxDirNames) {
|
|
const candidate = path.join(prefix, dir, 'firefox.exe')
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
return path.join('/mnt/c/Program Files/', firefoxDirNames[0], 'firefox.exe')
|
|
}
|
|
|
|
const getFirefoxWithFallbackOnOSX = function () {
|
|
if (process.platform !== 'darwin') {
|
|
return null
|
|
}
|
|
|
|
const firefoxDirNames = Array.prototype.slice.call(arguments)
|
|
const prefix = '/Applications/'
|
|
const suffix = '.app/Contents/MacOS/firefox'
|
|
|
|
let bin
|
|
let homeBin
|
|
for (let i = 0; i < firefoxDirNames.length; i++) {
|
|
bin = prefix + firefoxDirNames[i] + suffix
|
|
|
|
if ('HOME' in process.env) {
|
|
homeBin = path.join(process.env.HOME, bin)
|
|
|
|
if (fs.existsSync(homeBin)) {
|
|
return homeBin
|
|
}
|
|
}
|
|
|
|
if (fs.existsSync(bin)) {
|
|
return bin
|
|
}
|
|
}
|
|
}
|
|
|
|
const makeHeadlessVersion = function (Browser) {
|
|
const HeadlessBrowser = function () {
|
|
Browser.apply(this, arguments)
|
|
const execCommand = this._execCommand
|
|
this._execCommand = function (command, args) {
|
|
// --start-debugger-server ws:6000 can also be used, since remote debugging protocol also speaks WebSockets
|
|
// https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/
|
|
execCommand.call(this, command, args.concat(['-headless', '--start-debugger-server 6000']))
|
|
}
|
|
}
|
|
|
|
HeadlessBrowser.prototype = Object.create(Browser.prototype, {
|
|
name: { value: Browser.prototype.name + 'Headless' }
|
|
})
|
|
HeadlessBrowser.$inject = Browser.$inject
|
|
return HeadlessBrowser
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Command_Line_Options
|
|
const FirefoxBrowser = function (baseBrowserDecorator, args, logger, emitter) {
|
|
baseBrowserDecorator(this)
|
|
|
|
const log = logger.create(this.name + 'Launcher')
|
|
const safeExecSync = createSafeExecSync(log)
|
|
let browserProcessPid
|
|
let browserProcessPidWsl = []
|
|
|
|
this._getPrefs = function (prefs) {
|
|
if (typeof prefs !== 'object') {
|
|
return PREFS
|
|
}
|
|
let result = PREFS
|
|
for (const key in prefs) {
|
|
result += 'user_pref("' + key + '", ' + JSON.stringify(prefs[key]) + ');\n'
|
|
}
|
|
return result
|
|
}
|
|
|
|
this._start = function (url) {
|
|
const self = this
|
|
const command = args.command || this._getCommand()
|
|
const profilePath = args.profile || self._tempDir
|
|
const flags = args.flags || []
|
|
let extensionsDir
|
|
|
|
if (Array.isArray(args.extensions)) {
|
|
extensionsDir = path.resolve(profilePath, 'extensions')
|
|
fs.mkdirSync(extensionsDir)
|
|
args.extensions.forEach(function (ext) {
|
|
const extBuffer = fs.readFileSync(ext)
|
|
const copyDestination = path.resolve(extensionsDir, path.basename(ext))
|
|
fs.writeFileSync(copyDestination, extBuffer)
|
|
})
|
|
}
|
|
|
|
fs.writeFileSync(path.join(profilePath, 'prefs.js'), this._getPrefs(args.prefs))
|
|
const translatedProfilePath =
|
|
isWsl ? execSync('wslpath -w ' + profilePath).toString().trim() : profilePath
|
|
|
|
if (isWsl) {
|
|
log.warn('WSL environment detected: Please do not open Firefox while running tests as it will be killed after the test!')
|
|
log.warn('WSL environment detected: See https://github.com/karma-runner/karma-firefox-launcher/issues/101#issuecomment-891850143')
|
|
|
|
browserProcessPidWsl = extractPids(safeExecSync('tasklist.exe /FI "IMAGENAME eq firefox.exe" /FO CSV /NH /SVC'))
|
|
log.debug('Recorded PIDs not to kill:', browserProcessPidWsl)
|
|
}
|
|
|
|
// If we are using the launcher process, make it print the child process ID
|
|
// to stderr so we can capture it. Does not work in WSL.
|
|
//
|
|
// https://wiki.mozilla.org/Platform/Integration/InjectEject/Launcher_Process/
|
|
process.env.MOZ_DEBUG_BROWSER_PAUSE = 0
|
|
browserProcessPid = undefined
|
|
self._execCommand(
|
|
command,
|
|
[url, '-profile', translatedProfilePath, '-no-remote', '-wait-for-browser'].concat(flags)
|
|
)
|
|
|
|
self._process.stderr.on('data', errBuff => {
|
|
let errString
|
|
if (typeof errBuff === 'string') {
|
|
errString = errBuff
|
|
} else {
|
|
const decoder = new StringDecoder('utf8')
|
|
errString = decoder.write(errBuff)
|
|
}
|
|
const matches = errString.match(/BROWSERBROWSERBROWSERBROWSER\s+debug me @ (\d+)/)
|
|
if (matches) {
|
|
browserProcessPid = parseInt(matches[1], 10)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (isWsl) {
|
|
// exit: will run for each browser when all tests has finished
|
|
emitter.on('exit', (done) => {
|
|
const tasklist = extractPids(safeExecSync('tasklist.exe /FI "IMAGENAME eq firefox.exe" /FO CSV /NH /SVC'))
|
|
.filter(pid => browserProcessPidWsl.indexOf(pid) === -1)
|
|
|
|
// if this is not the first time 'exit' is called then tasklist is probably empty
|
|
if (tasklist.length > 0) {
|
|
log.debug('Killing the following PIDs:', tasklist)
|
|
const killResult = safeExecSync('taskkill.exe /F ' + tasklist.map(pid => `/PID ${pid}`).join(' ') + ' 2>&1')
|
|
log.debug(killResult)
|
|
}
|
|
|
|
return process.nextTick(done)
|
|
})
|
|
}
|
|
|
|
this.on('kill', function (done) {
|
|
// If we have a separate browser process PID, try killing it.
|
|
if (browserProcessPid) {
|
|
try {
|
|
process.kill(browserProcessPid)
|
|
} catch (e) {
|
|
// Ignore failure -- the browser process might have already been
|
|
// terminated.
|
|
}
|
|
}
|
|
|
|
return process.nextTick(done)
|
|
})
|
|
}
|
|
|
|
FirefoxBrowser.prototype = {
|
|
name: 'Firefox',
|
|
|
|
DEFAULT_CMD: {
|
|
linux: isWsl ? getFirefoxExeWsl('Mozilla Firefox') : 'firefox',
|
|
freebsd: 'firefox',
|
|
darwin: getFirefoxWithFallbackOnOSX('Firefox'),
|
|
win32: getFirefoxExe('Mozilla Firefox')
|
|
},
|
|
ENV_CMD: 'FIREFOX_BIN'
|
|
}
|
|
|
|
FirefoxBrowser.$inject = $INJECT_LIST
|
|
|
|
const FirefoxHeadlessBrowser = makeHeadlessVersion(FirefoxBrowser)
|
|
|
|
const FirefoxDeveloperBrowser = function () {
|
|
FirefoxBrowser.apply(this, arguments)
|
|
}
|
|
|
|
FirefoxDeveloperBrowser.prototype = {
|
|
name: 'FirefoxDeveloper',
|
|
DEFAULT_CMD: {
|
|
linux: isWsl ? getFirefoxExeWsl('Firefox Developer Edition') : 'firefox',
|
|
darwin: getFirefoxWithFallbackOnOSX('Firefox Developer Edition', 'FirefoxDeveloperEdition', 'FirefoxAurora'),
|
|
win32: getFirefoxExe('Firefox Developer Edition')
|
|
},
|
|
ENV_CMD: 'FIREFOX_DEVELOPER_BIN'
|
|
}
|
|
|
|
FirefoxDeveloperBrowser.$inject = $INJECT_LIST
|
|
|
|
const FirefoxDeveloperHeadlessBrowser = makeHeadlessVersion(FirefoxDeveloperBrowser)
|
|
|
|
const FirefoxAuroraBrowser = function () {
|
|
FirefoxBrowser.apply(this, arguments)
|
|
}
|
|
|
|
FirefoxAuroraBrowser.prototype = {
|
|
name: 'FirefoxAurora',
|
|
DEFAULT_CMD: {
|
|
linux: isWsl ? getFirefoxExeWsl('Aurora') : 'firefox',
|
|
darwin: getFirefoxWithFallbackOnOSX('FirefoxAurora'),
|
|
win32: getFirefoxExe('Aurora')
|
|
},
|
|
ENV_CMD: 'FIREFOX_AURORA_BIN'
|
|
}
|
|
|
|
FirefoxAuroraBrowser.$inject = $INJECT_LIST
|
|
|
|
const FirefoxAuroraHeadlessBrowser = makeHeadlessVersion(FirefoxAuroraBrowser)
|
|
|
|
const FirefoxNightlyBrowser = function () {
|
|
FirefoxBrowser.apply(this, arguments)
|
|
}
|
|
|
|
FirefoxNightlyBrowser.prototype = {
|
|
name: 'FirefoxNightly',
|
|
|
|
DEFAULT_CMD: {
|
|
linux: isWsl ? getFirefoxExeWsl('Nightly', 'Firefox Nightly') : 'firefox',
|
|
darwin: getFirefoxWithFallbackOnOSX('FirefoxNightly', 'Firefox Nightly'),
|
|
win32: getFirefoxExe('Nightly', 'Firefox Nightly')
|
|
},
|
|
ENV_CMD: 'FIREFOX_NIGHTLY_BIN'
|
|
}
|
|
|
|
FirefoxNightlyBrowser.$inject = $INJECT_LIST
|
|
|
|
const FirefoxNightlyHeadlessBrowser = makeHeadlessVersion(FirefoxNightlyBrowser)
|
|
|
|
// PUBLISH DI MODULE
|
|
module.exports = {
|
|
'launcher:Firefox': ['type', FirefoxBrowser],
|
|
'launcher:FirefoxHeadless': ['type', FirefoxHeadlessBrowser],
|
|
'launcher:FirefoxDeveloper': ['type', FirefoxDeveloperBrowser],
|
|
'launcher:FirefoxDeveloperHeadless': ['type', FirefoxDeveloperHeadlessBrowser],
|
|
'launcher:FirefoxAurora': ['type', FirefoxAuroraBrowser],
|
|
'launcher:FirefoxAuroraHeadless': ['type', FirefoxAuroraHeadlessBrowser],
|
|
'launcher:FirefoxNightly': ['type', FirefoxNightlyBrowser],
|
|
'launcher:FirefoxNightlyHeadless': ['type', FirefoxNightlyHeadlessBrowser]
|
|
}
|