/** * @license * Copyright 2023 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import fs from 'fs'; import {rename, unlink, mkdtemp} from 'fs/promises'; import os from 'os'; import path from 'path'; import { Browser as SupportedBrowsers, createProfile, Cache, detectBrowserPlatform, Browser, } from '@puppeteer/browsers'; import {debugError} from '../common/util.js'; import {assert} from '../util/assert.js'; import type { BrowserLaunchArgumentOptions, PuppeteerNodeLaunchOptions, } from './LaunchOptions.js'; import {ProductLauncher, type ResolvedLaunchArgs} from './ProductLauncher.js'; import type {PuppeteerNode} from './PuppeteerNode.js'; import {rm} from './util/fs.js'; /** * @internal */ export class FirefoxLauncher extends ProductLauncher { constructor(puppeteer: PuppeteerNode) { super(puppeteer, 'firefox'); } static getPreferences( extraPrefsFirefox?: Record, protocol?: 'cdp' | 'webDriverBiDi' ): Record { return { ...extraPrefsFirefox, ...(protocol === 'webDriverBiDi' ? {} : { // Do not close the window when the last tab gets closed 'browser.tabs.closeWindowWithLastTab': false, // Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263) 'fission.bfcacheInParent': false, }), // Force all web content to use a single content process. TODO: remove // this once Firefox supports mouse event dispatch from the main frame // context. Once this happens, webContentIsolationStrategy should only // be set for CDP. See // https://bugzilla.mozilla.org/show_bug.cgi?id=1773393 'fission.webContentIsolationStrategy': 0, }; } /** * @internal */ override async computeLaunchArguments( options: PuppeteerNodeLaunchOptions = {} ): Promise { const { ignoreDefaultArgs = false, args = [], executablePath, pipe = false, extraPrefsFirefox = {}, debuggingPort = null, } = options; const firefoxArguments = []; if (!ignoreDefaultArgs) { firefoxArguments.push(...this.defaultArgs(options)); } else if (Array.isArray(ignoreDefaultArgs)) { firefoxArguments.push( ...this.defaultArgs(options).filter(arg => { return !ignoreDefaultArgs.includes(arg); }) ); } else { firefoxArguments.push(...args); } if ( !firefoxArguments.some(argument => { return argument.startsWith('--remote-debugging-'); }) ) { if (pipe) { assert( debuggingPort === null, 'Browser should be launched with either pipe or debugging port - not both.' ); } firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`); } let userDataDir: string | undefined; let isTempUserDataDir = true; // Check for the profile argument, which will always be set even // with a custom directory specified via the userDataDir option. const profileArgIndex = firefoxArguments.findIndex(arg => { return ['-profile', '--profile'].includes(arg); }); if (profileArgIndex !== -1) { userDataDir = firefoxArguments[profileArgIndex + 1]; if (!userDataDir || !fs.existsSync(userDataDir)) { throw new Error(`Firefox profile not found at '${userDataDir}'`); } // When using a custom Firefox profile it needs to be populated // with required preferences. isTempUserDataDir = false; } else { userDataDir = await mkdtemp(this.getProfilePath()); firefoxArguments.push('--profile'); firefoxArguments.push(userDataDir); } await createProfile(SupportedBrowsers.FIREFOX, { path: userDataDir, preferences: FirefoxLauncher.getPreferences( extraPrefsFirefox, options.protocol ), }); let firefoxExecutable: string; if (this.puppeteer._isPuppeteerCore || executablePath) { assert( executablePath, `An \`executablePath\` must be specified for \`puppeteer-core\`` ); firefoxExecutable = executablePath; } else { firefoxExecutable = this.executablePath(); } return { isTempUserDataDir, userDataDir, args: firefoxArguments, executablePath: firefoxExecutable, }; } /** * @internal */ override async cleanUserDataDir( userDataDir: string, opts: {isTemp: boolean} ): Promise { if (opts.isTemp) { try { await rm(userDataDir); } catch (error) { debugError(error); throw error; } } else { try { // When an existing user profile has been used remove the user // preferences file and restore possibly backuped preferences. await unlink(path.join(userDataDir, 'user.js')); const prefsBackupPath = path.join(userDataDir, 'prefs.js.puppeteer'); if (fs.existsSync(prefsBackupPath)) { const prefsPath = path.join(userDataDir, 'prefs.js'); await unlink(prefsPath); await rename(prefsBackupPath, prefsPath); } } catch (error) { debugError(error); } } } override executablePath(): string { // replace 'latest' placeholder with actual downloaded revision if (this.puppeteer.browserRevision === 'latest') { const cache = new Cache(this.puppeteer.defaultDownloadPath!); const installedFirefox = cache.getInstalledBrowsers().find(browser => { return ( browser.platform === detectBrowserPlatform() && browser.browser === Browser.FIREFOX ); }); if (installedFirefox) { this.actualBrowserRevision = installedFirefox.buildId; } } return this.resolveExecutablePath(); } override defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] { const { devtools = false, headless = !devtools, args = [], userDataDir = null, } = options; const firefoxArguments = ['--no-remote']; switch (os.platform()) { case 'darwin': firefoxArguments.push('--foreground'); break; case 'win32': firefoxArguments.push('--wait-for-browser'); break; } if (userDataDir) { firefoxArguments.push('--profile'); firefoxArguments.push(userDataDir); } if (headless) { firefoxArguments.push('--headless'); } if (devtools) { firefoxArguments.push('--devtools'); } if ( args.every(arg => { return arg.startsWith('-'); }) ) { firefoxArguments.push('about:blank'); } firefoxArguments.push(...args); return firefoxArguments; } }