Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly prepare Firefox profiles with required preferences for all scenarious #7684

Merged
merged 1 commit into from Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 49 additions & 15 deletions src/node/BrowserRunner.ts
Expand Up @@ -16,21 +16,28 @@

import { debug } from '../common/Debug.js';

import removeFolder from 'rimraf';
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
import removeFolder from 'rimraf';
import { promisify } from 'util';

import { assert } from '../common/assert.js';
import { helper, debugError } from '../common/helper.js';
import { LaunchOptions } from './LaunchOptions.js';
import { Connection } from '../common/Connection.js';
import { NodeWebSocketTransport as WebSocketTransport } from '../node/NodeWebSocketTransport.js';
import { PipeTransport } from './PipeTransport.js';
import { Product } from '../common/Product.js';
import * as readline from 'readline';
import { TimeoutError } from '../common/Errors.js';
import { promisify } from 'util';

const removeFolderAsync = promisify(removeFolder);
const renameAsync = promisify(fs.rename);
const unlinkAsync = promisify(fs.unlink);

const debugLauncher = debug('puppeteer:launcher');

const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary.
This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser.
Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed.
Expand All @@ -40,7 +47,8 @@ export class BrowserRunner {
private _product: Product;
private _executablePath: string;
private _processArguments: string[];
private _tempDirectory?: string;
private _userDataDir: string;
private _isTempUserDataDir?: boolean;

proc = null;
connection = null;
Expand All @@ -53,12 +61,14 @@ export class BrowserRunner {
product: Product,
executablePath: string,
processArguments: string[],
tempDirectory?: string
userDataDir: string,
isTempUserDataDir?: boolean
) {
this._product = product;
this._executablePath = executablePath;
this._processArguments = processArguments;
this._tempDirectory = tempDirectory;
this._userDataDir = userDataDir;
this._isTempUserDataDir = isTempUserDataDir;
}

start(options: LaunchOptions): void {
Expand Down Expand Up @@ -95,17 +105,39 @@ export class BrowserRunner {
}
this._closed = false;
this._processClosing = new Promise((fulfill, reject) => {
this.proc.once('exit', () => {
this.proc.once('exit', async () => {
this._closed = true;
// Cleanup as processes exit.
if (this._tempDirectory) {
removeFolderAsync(this._tempDirectory)
.then(() => fulfill())
.catch((error) => {
if (this._isTempUserDataDir) {
try {
await removeFolderAsync(this._userDataDir);
fulfill();
} catch (error) {
console.error(error);
reject(error);
}
} else {
if (this._product === 'firefox') {
try {
// When an existing user profile has been used remove the user
// preferences file and restore possibly backuped preferences.
await unlinkAsync(path.join(this._userDataDir, 'user.js'));

const prefsBackupPath = path.join(
this._userDataDir,
'prefs.js.puppeteer'
);
if (fs.existsSync(prefsBackupPath)) {
const prefsPath = path.join(this._userDataDir, 'prefs.js');
await unlinkAsync(prefsPath);
await renameAsync(prefsBackupPath, prefsPath);
}
} catch (error) {
console.error(error);
reject(error);
});
} else {
}
}

fulfill();
}
});
Expand All @@ -132,7 +164,7 @@ export class BrowserRunner {

close(): Promise<void> {
if (this._closed) return Promise.resolve();
if (this._tempDirectory && this._product !== 'firefox') {
if (this._isTempUserDataDir && this._product !== 'firefox') {
this.kill();
} else if (this.connection) {
// Attempt to close the browser gracefully
Expand All @@ -150,7 +182,9 @@ export class BrowserRunner {
kill(): void {
// Attempt to remove temporary profile directory to avoid littering.
try {
removeFolder.sync(this._tempDirectory);
if (this._isTempUserDataDir) {
removeFolder.sync(this._userDataDir);
}
} catch (error) {}

// If the process failed to launch (for example if the browser executable path
Expand Down
136 changes: 100 additions & 36 deletions src/node/Launcher.ts
Expand Up @@ -23,6 +23,7 @@ import { Browser } from '../common/Browser.js';
import { BrowserRunner } from './BrowserRunner.js';
import { promisify } from 'util';

const copyFileAsync = promisify(fs.copyFile);
const mkdtempAsync = promisify(fs.mkdtemp);
const writeFileAsync = promisify(fs.writeFile);

Expand Down Expand Up @@ -85,7 +86,6 @@ class ChromeLauncher implements ProductLauncher {
debuggingPort = null,
} = options;

const profilePath = path.join(tmpDir(), 'puppeteer_dev_chrome_profile-');
const chromeArguments = [];
if (!ignoreDefaultArgs) chromeArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
Expand All @@ -96,8 +96,6 @@ class ChromeLauncher implements ProductLauncher {
);
else chromeArguments.push(...args);

let temporaryUserDataDir = null;

if (
!chromeArguments.some((argument) =>
argument.startsWith('--remote-debugging-')
Expand All @@ -113,9 +111,28 @@ class ChromeLauncher implements ProductLauncher {
chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}
}
if (!chromeArguments.some((arg) => arg.startsWith('--user-data-dir'))) {
temporaryUserDataDir = await mkdtempAsync(profilePath);
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);

let userDataDir;
let isTempUserDataDir = true;

// Check for the user data dir argument, which will always be set even
// with a custom directory specified via the userDataDir option.
const userDataDirIndex = chromeArguments.findIndex((arg) => {
return arg.startsWith('--user-data-dir');
});

if (userDataDirIndex !== -1) {
userDataDir = chromeArguments[userDataDirIndex].split('=')[1];
if (!fs.existsSync(userDataDir)) {
throw new Error(`Chrome user data dir not found at '${userDataDir}'`);
}

isTempUserDataDir = false;
} else {
userDataDir = await mkdtempAsync(
path.join(tmpDir(), 'puppeteer_dev_chrome_profile-')
);
chromeArguments.push(`--user-data-dir=${userDataDir}`);
}

let chromeExecutable = executablePath;
Expand Down Expand Up @@ -145,7 +162,8 @@ class ChromeLauncher implements ProductLauncher {
this.product,
chromeExecutable,
chromeArguments,
temporaryUserDataDir
userDataDir,
isTempUserDataDir
);
runner.start({
handleSIGHUP,
Expand Down Expand Up @@ -303,15 +321,30 @@ class FirefoxLauncher implements ProductLauncher {
firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}

let temporaryUserDataDir = null;
let userDataDir = null;
let isTempUserDataDir = true;

if (
!firefoxArguments.includes('-profile') &&
!firefoxArguments.includes('--profile')
) {
temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
// 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 (!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;
const prefs = this.defaultPreferences(extraPrefsFirefox);
this.writePreferences(prefs, userDataDir);
} else {
userDataDir = await this._createProfile(extraPrefsFirefox);
firefoxArguments.push('--profile');
firefoxArguments.push(temporaryUserDataDir);
firefoxArguments.push(userDataDir);
}

await this._updateRevision();
Expand All @@ -326,7 +359,8 @@ class FirefoxLauncher implements ProductLauncher {
this.product,
firefoxExecutable,
firefoxArguments,
temporaryUserDataDir
userDataDir,
isTempUserDataDir
);
runner.start({
handleSIGHUP,
Expand Down Expand Up @@ -381,16 +415,19 @@ class FirefoxLauncher implements ProductLauncher {
}

defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] {
const firefoxArguments = ['--no-remote', '--foreground'];
if (os.platform().startsWith('win')) {
firefoxArguments.push('--wait-for-browser');
}
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null,
} = options;

const firefoxArguments = ['--no-remote'];

if (os.platform() === 'darwin') firefoxArguments.push('--foreground');
else if (os.platform().startsWith('win')) {
firefoxArguments.push('--wait-for-browser');
}
if (userDataDir) {
firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir);
Expand All @@ -403,14 +440,12 @@ class FirefoxLauncher implements ProductLauncher {
return firefoxArguments;
}

async _createProfile(extraPrefs: { [x: string]: unknown }): Promise<string> {
const profilePath = await mkdtempAsync(
path.join(tmpDir(), 'puppeteer_dev_firefox_profile-')
);
const prefsJS = [];
const userJS = [];
defaultPreferences(extraPrefs: { [x: string]: unknown }): {
[x: string]: unknown;
} {
const server = 'dummy.test';
const defaultPreferences = {

const defaultPrefs = {
// Make sure Shield doesn't hit the network.
'app.normandy.api_url': '',
// Disable Firefox old build background check
Expand Down Expand Up @@ -612,17 +647,46 @@ class FirefoxLauncher implements ProductLauncher {
'toolkit.startup.max_resumed_crashes': -1,
};

Object.assign(defaultPreferences, extraPrefs);
for (const [key, value] of Object.entries(defaultPreferences))
userJS.push(
`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`
);
await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n'));
await writeFileAsync(
path.join(profilePath, 'prefs.js'),
prefsJS.join('\n')
return Object.assign(defaultPrefs, extraPrefs);
}

/**
* Populates the user.js file with custom preferences as needed to allow
* Firefox's CDP support to properly function. These preferences will be
* automatically copied over to prefs.js during startup of Firefox. To be
* able to restore the original values of preferences a backup of prefs.js
* will be created.
*
* @param prefs List of preferences to add.
* @param profilePath Firefox profile to write the preferences to.
*/
async writePreferences(
prefs: { [x: string]: unknown },
profilePath: string
): Promise<void> {
const lines = Object.entries(prefs).map(([key, value]) => {
return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`;
});

await writeFileAsync(path.join(profilePath, 'user.js'), lines.join('\n'));

// Create a backup of the preferences file if it already exitsts.
const prefsPath = path.join(profilePath, 'prefs.js');
if (fs.existsSync(prefsPath)) {
const prefsBackupPath = path.join(profilePath, 'prefs.js.puppeteer');
await copyFileAsync(prefsPath, prefsBackupPath);
}
}

async _createProfile(extraPrefs: { [x: string]: unknown }): Promise<string> {
const temporaryProfilePath = await mkdtempAsync(
path.join(tmpDir(), 'puppeteer_dev_firefox_profile-')
);
return profilePath;

const prefs = this.defaultPreferences(extraPrefs);
await this.writePreferences(prefs, temporaryProfilePath);

return temporaryProfilePath;
}
}

Expand Down