ChromeLauncher.ts (9876B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import {mkdtemp} from 'node:fs/promises'; 8 import os from 'node:os'; 9 import path from 'node:path'; 10 11 import { 12 computeSystemExecutablePath, 13 Browser as SupportedBrowsers, 14 ChromeReleaseChannel as BrowsersChromeReleaseChannel, 15 } from '@puppeteer/browsers'; 16 17 import type {Browser} from '../api/Browser.js'; 18 import {debugError} from '../common/util.js'; 19 import {assert} from '../util/assert.js'; 20 21 import {BrowserLauncher, type ResolvedLaunchArgs} from './BrowserLauncher.js'; 22 import type {ChromeReleaseChannel, LaunchOptions} from './LaunchOptions.js'; 23 import type {PuppeteerNode} from './PuppeteerNode.js'; 24 import {rm} from './util/fs.js'; 25 26 /** 27 * @internal 28 */ 29 export class ChromeLauncher extends BrowserLauncher { 30 constructor(puppeteer: PuppeteerNode) { 31 super(puppeteer, 'chrome'); 32 } 33 34 override launch(options: LaunchOptions = {}): Promise<Browser> { 35 if ( 36 this.puppeteer.configuration.logLevel === 'warn' && 37 process.platform === 'darwin' && 38 process.arch === 'x64' 39 ) { 40 const cpus = os.cpus(); 41 if (cpus[0]?.model.includes('Apple')) { 42 console.warn( 43 [ 44 '\x1B[1m\x1B[43m\x1B[30m', 45 'Degraded performance warning:\x1B[0m\x1B[33m', 46 'Launching Chrome on Mac Silicon (arm64) from an x64 Node installation results in', 47 'Rosetta translating the Chrome binary, even if Chrome is already arm64. This would', 48 'result in huge performance issues. To resolve this, you must run Puppeteer with', 49 'a version of Node built for arm64.', 50 ].join('\n '), 51 ); 52 } 53 } 54 55 return super.launch(options); 56 } 57 58 /** 59 * @internal 60 */ 61 override async computeLaunchArguments( 62 options: LaunchOptions = {}, 63 ): Promise<ResolvedLaunchArgs> { 64 const { 65 ignoreDefaultArgs = false, 66 args = [], 67 pipe = false, 68 debuggingPort, 69 channel, 70 executablePath, 71 } = options; 72 73 const chromeArguments = []; 74 if (!ignoreDefaultArgs) { 75 chromeArguments.push(...this.defaultArgs(options)); 76 } else if (Array.isArray(ignoreDefaultArgs)) { 77 chromeArguments.push( 78 ...this.defaultArgs(options).filter(arg => { 79 return !ignoreDefaultArgs.includes(arg); 80 }), 81 ); 82 } else { 83 chromeArguments.push(...args); 84 } 85 86 if ( 87 !chromeArguments.some(argument => { 88 return argument.startsWith('--remote-debugging-'); 89 }) 90 ) { 91 if (pipe) { 92 assert( 93 !debuggingPort, 94 'Browser should be launched with either pipe or debugging port - not both.', 95 ); 96 chromeArguments.push('--remote-debugging-pipe'); 97 } else { 98 chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`); 99 } 100 } 101 102 let isTempUserDataDir = false; 103 104 // Check for the user data dir argument, which will always be set even 105 // with a custom directory specified via the userDataDir option. 106 let userDataDirIndex = chromeArguments.findIndex(arg => { 107 return arg.startsWith('--user-data-dir'); 108 }); 109 if (userDataDirIndex < 0) { 110 isTempUserDataDir = true; 111 chromeArguments.push( 112 `--user-data-dir=${await mkdtemp(this.getProfilePath())}`, 113 ); 114 userDataDirIndex = chromeArguments.length - 1; 115 } 116 117 const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1]; 118 assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed'); 119 120 let chromeExecutable = executablePath; 121 if (!chromeExecutable) { 122 assert( 123 channel || !this.puppeteer._isPuppeteerCore, 124 `An \`executablePath\` or \`channel\` must be specified for \`puppeteer-core\``, 125 ); 126 chromeExecutable = channel 127 ? this.executablePath(channel) 128 : this.resolveExecutablePath(options.headless ?? true); 129 } 130 131 return { 132 executablePath: chromeExecutable, 133 args: chromeArguments, 134 isTempUserDataDir, 135 userDataDir, 136 }; 137 } 138 139 /** 140 * @internal 141 */ 142 override async cleanUserDataDir( 143 path: string, 144 opts: {isTemp: boolean}, 145 ): Promise<void> { 146 if (opts.isTemp) { 147 try { 148 await rm(path); 149 } catch (error) { 150 debugError(error); 151 throw error; 152 } 153 } 154 } 155 156 override defaultArgs(options: LaunchOptions = {}): string[] { 157 // See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md 158 159 const userDisabledFeatures = getFeatures( 160 '--disable-features', 161 options.args, 162 ); 163 if (options.args && userDisabledFeatures.length > 0) { 164 removeMatchingFlags(options.args, '--disable-features'); 165 } 166 167 const turnOnExperimentalFeaturesForTesting = 168 process.env['PUPPETEER_TEST_EXPERIMENTAL_CHROME_FEATURES'] === 'true'; 169 170 // Merge default disabled features with user-provided ones, if any. 171 const disabledFeatures = [ 172 'Translate', 173 // AcceptCHFrame disabled because of crbug.com/1348106. 174 'AcceptCHFrame', 175 'MediaRouter', 176 'OptimizationHints', 177 ...(turnOnExperimentalFeaturesForTesting 178 ? [] 179 : [ 180 // https://crbug.com/1492053 181 'ProcessPerSiteUpToMainFrameThreshold', 182 // https://github.com/puppeteer/puppeteer/issues/10715 183 'IsolateSandboxedIframes', 184 ]), 185 ...userDisabledFeatures, 186 ].filter(feature => { 187 return feature !== ''; 188 }); 189 190 const userEnabledFeatures = getFeatures('--enable-features', options.args); 191 if (options.args && userEnabledFeatures.length > 0) { 192 removeMatchingFlags(options.args, '--enable-features'); 193 } 194 195 // Merge default enabled features with user-provided ones, if any. 196 const enabledFeatures = [ 197 'PdfOopif', 198 // Add features to enable by default here. 199 ...userEnabledFeatures, 200 ].filter(feature => { 201 return feature !== ''; 202 }); 203 204 const chromeArguments = [ 205 '--allow-pre-commit-input', 206 '--disable-background-networking', 207 '--disable-background-timer-throttling', 208 '--disable-backgrounding-occluded-windows', 209 '--disable-breakpad', 210 '--disable-client-side-phishing-detection', 211 '--disable-component-extensions-with-background-pages', 212 '--disable-crash-reporter', // No crash reporting in CfT. 213 '--disable-default-apps', 214 '--disable-dev-shm-usage', 215 '--disable-hang-monitor', 216 '--disable-infobars', 217 '--disable-ipc-flooding-protection', 218 '--disable-popup-blocking', 219 '--disable-prompt-on-repost', 220 '--disable-renderer-backgrounding', 221 '--disable-search-engine-choice-screen', 222 '--disable-sync', 223 '--enable-automation', 224 '--export-tagged-pdf', 225 '--force-color-profile=srgb', 226 '--generate-pdf-document-outline', 227 '--metrics-recording-only', 228 '--no-first-run', 229 '--password-store=basic', 230 '--use-mock-keychain', 231 `--disable-features=${disabledFeatures.join(',')}`, 232 `--enable-features=${enabledFeatures.join(',')}`, 233 ].filter(arg => { 234 return arg !== ''; 235 }); 236 const { 237 devtools = false, 238 headless = !devtools, 239 args = [], 240 userDataDir, 241 enableExtensions = false, 242 } = options; 243 if (userDataDir) { 244 chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`); 245 } 246 if (devtools) { 247 chromeArguments.push('--auto-open-devtools-for-tabs'); 248 } 249 if (headless) { 250 chromeArguments.push( 251 headless === 'shell' ? '--headless' : '--headless=new', 252 '--hide-scrollbars', 253 '--mute-audio', 254 ); 255 } 256 chromeArguments.push( 257 enableExtensions 258 ? '--enable-unsafe-extension-debugging' 259 : '--disable-extensions', 260 ); 261 if ( 262 args.every(arg => { 263 return arg.startsWith('-'); 264 }) 265 ) { 266 chromeArguments.push('about:blank'); 267 } 268 chromeArguments.push(...args); 269 return chromeArguments; 270 } 271 272 override executablePath( 273 channel?: ChromeReleaseChannel, 274 validatePath = true, 275 ): string { 276 if (channel) { 277 return computeSystemExecutablePath({ 278 browser: SupportedBrowsers.CHROME, 279 channel: convertPuppeteerChannelToBrowsersChannel(channel), 280 }); 281 } else { 282 return this.resolveExecutablePath(undefined, validatePath); 283 } 284 } 285 } 286 287 function convertPuppeteerChannelToBrowsersChannel( 288 channel: ChromeReleaseChannel, 289 ): BrowsersChromeReleaseChannel { 290 switch (channel) { 291 case 'chrome': 292 return BrowsersChromeReleaseChannel.STABLE; 293 case 'chrome-dev': 294 return BrowsersChromeReleaseChannel.DEV; 295 case 'chrome-beta': 296 return BrowsersChromeReleaseChannel.BETA; 297 case 'chrome-canary': 298 return BrowsersChromeReleaseChannel.CANARY; 299 } 300 } 301 302 /** 303 * Extracts all features from the given command-line flag 304 * (e.g. `--enable-features`, `--enable-features=`). 305 * 306 * Example input: 307 * ["--enable-features=NetworkService,NetworkServiceInProcess", "--enable-features=Foo"] 308 * 309 * Example output: 310 * ["NetworkService", "NetworkServiceInProcess", "Foo"] 311 * 312 * @internal 313 */ 314 export function getFeatures(flag: string, options: string[] = []): string[] { 315 return options 316 .filter(s => { 317 return s.startsWith(flag.endsWith('=') ? flag : `${flag}=`); 318 }) 319 .map(s => { 320 return s.split(new RegExp(`${flag}=\\s*`))[1]?.trim(); 321 }) 322 .filter(s => { 323 return s; 324 }) as string[]; 325 } 326 327 /** 328 * Removes all elements in-place from the given string array 329 * that match the given command-line flag. 330 * 331 * @internal 332 */ 333 export function removeMatchingFlags(array: string[], flag: string): string[] { 334 const regex = new RegExp(`^${flag}=.*`); 335 let i = 0; 336 while (i < array.length) { 337 if (regex.test(array[i]!)) { 338 array.splice(i, 1); 339 } else { 340 i++; 341 } 342 } 343 return array; 344 }