FirefoxLauncher.ts (5885B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import fs from 'node:fs'; 8 import {rename, unlink, mkdtemp} from 'node:fs/promises'; 9 import os from 'node:os'; 10 import path from 'node:path'; 11 12 import {Browser as SupportedBrowsers, createProfile} from '@puppeteer/browsers'; 13 14 import {debugError} from '../common/util.js'; 15 import {assert} from '../util/assert.js'; 16 17 import {BrowserLauncher, type ResolvedLaunchArgs} from './BrowserLauncher.js'; 18 import type {LaunchOptions} from './LaunchOptions.js'; 19 import type {PuppeteerNode} from './PuppeteerNode.js'; 20 import {rm} from './util/fs.js'; 21 22 /** 23 * @internal 24 */ 25 export class FirefoxLauncher extends BrowserLauncher { 26 constructor(puppeteer: PuppeteerNode) { 27 super(puppeteer, 'firefox'); 28 } 29 30 static getPreferences( 31 extraPrefsFirefox?: Record<string, unknown>, 32 ): Record<string, unknown> { 33 return { 34 ...extraPrefsFirefox, 35 // Force all web content to use a single content process. TODO: remove 36 // this once Firefox supports mouse event dispatch from the main frame 37 // context. See https://bugzilla.mozilla.org/show_bug.cgi?id=1773393. 38 'fission.webContentIsolationStrategy': 0, 39 }; 40 } 41 42 /** 43 * @internal 44 */ 45 override async computeLaunchArguments( 46 options: LaunchOptions = {}, 47 ): Promise<ResolvedLaunchArgs> { 48 const { 49 ignoreDefaultArgs = false, 50 args = [], 51 executablePath, 52 pipe = false, 53 extraPrefsFirefox = {}, 54 debuggingPort = null, 55 } = options; 56 57 const firefoxArguments = []; 58 if (!ignoreDefaultArgs) { 59 firefoxArguments.push(...this.defaultArgs(options)); 60 } else if (Array.isArray(ignoreDefaultArgs)) { 61 firefoxArguments.push( 62 ...this.defaultArgs(options).filter(arg => { 63 return !ignoreDefaultArgs.includes(arg); 64 }), 65 ); 66 } else { 67 firefoxArguments.push(...args); 68 } 69 70 if ( 71 !firefoxArguments.some(argument => { 72 return argument.startsWith('--remote-debugging-'); 73 }) 74 ) { 75 if (pipe) { 76 assert( 77 debuggingPort === null, 78 'Browser should be launched with either pipe or debugging port - not both.', 79 ); 80 } 81 firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`); 82 } 83 84 let userDataDir: string | undefined; 85 let isTempUserDataDir = true; 86 87 // Check for the profile argument, which will always be set even 88 // with a custom directory specified via the userDataDir option. 89 const profileArgIndex = firefoxArguments.findIndex(arg => { 90 return ['-profile', '--profile'].includes(arg); 91 }); 92 93 if (profileArgIndex !== -1) { 94 userDataDir = firefoxArguments[profileArgIndex + 1]; 95 if (!userDataDir) { 96 throw new Error(`Missing value for profile command line argument`); 97 } 98 99 // When using a custom Firefox profile it needs to be populated 100 // with required preferences. 101 isTempUserDataDir = false; 102 } else { 103 userDataDir = await mkdtemp(this.getProfilePath()); 104 firefoxArguments.push('--profile'); 105 firefoxArguments.push(userDataDir); 106 } 107 108 await createProfile(SupportedBrowsers.FIREFOX, { 109 path: userDataDir, 110 preferences: FirefoxLauncher.getPreferences(extraPrefsFirefox), 111 }); 112 113 let firefoxExecutable: string; 114 if (this.puppeteer._isPuppeteerCore || executablePath) { 115 assert( 116 executablePath, 117 `An \`executablePath\` must be specified for \`puppeteer-core\``, 118 ); 119 firefoxExecutable = executablePath; 120 } else { 121 firefoxExecutable = this.executablePath(undefined); 122 } 123 124 return { 125 isTempUserDataDir, 126 userDataDir, 127 args: firefoxArguments, 128 executablePath: firefoxExecutable, 129 }; 130 } 131 132 /** 133 * @internal 134 */ 135 override async cleanUserDataDir( 136 userDataDir: string, 137 opts: {isTemp: boolean}, 138 ): Promise<void> { 139 if (opts.isTemp) { 140 try { 141 await rm(userDataDir); 142 } catch (error) { 143 debugError(error); 144 throw error; 145 } 146 } else { 147 try { 148 const backupSuffix = '.puppeteer'; 149 const backupFiles = ['prefs.js', 'user.js']; 150 151 const results = await Promise.allSettled( 152 backupFiles.map(async file => { 153 const prefsBackupPath = path.join(userDataDir, file + backupSuffix); 154 if (fs.existsSync(prefsBackupPath)) { 155 const prefsPath = path.join(userDataDir, file); 156 await unlink(prefsPath); 157 await rename(prefsBackupPath, prefsPath); 158 } 159 }), 160 ); 161 for (const result of results) { 162 if (result.status === 'rejected') { 163 throw result.reason; 164 } 165 } 166 } catch (error) { 167 debugError(error); 168 } 169 } 170 } 171 172 override executablePath(_: unknown, validatePath = true): string { 173 return this.resolveExecutablePath( 174 undefined, 175 /* validatePath=*/ validatePath, 176 ); 177 } 178 179 override defaultArgs(options: LaunchOptions = {}): string[] { 180 const { 181 devtools = false, 182 headless = !devtools, 183 args = [], 184 userDataDir = null, 185 } = options; 186 187 const firefoxArguments = []; 188 189 switch (os.platform()) { 190 case 'darwin': 191 firefoxArguments.push('--foreground'); 192 break; 193 case 'win32': 194 firefoxArguments.push('--wait-for-browser'); 195 break; 196 } 197 if (userDataDir) { 198 firefoxArguments.push('--profile'); 199 firefoxArguments.push(userDataDir); 200 } 201 if (headless) { 202 firefoxArguments.push('--headless'); 203 } 204 if (devtools) { 205 firefoxArguments.push('--devtools'); 206 } 207 if ( 208 args.every(arg => { 209 return arg.startsWith('-'); 210 }) 211 ) { 212 firefoxArguments.push('about:blank'); 213 } 214 firefoxArguments.push(...args); 215 return firefoxArguments; 216 } 217 }