Skip to content

Commit

Permalink
refactor: refactor handling of user data directory for clearing of pr…
Browse files Browse the repository at this point in the history
…eferences for Firefox
  • Loading branch information
whimboo committed Nov 8, 2021
1 parent 5ddca21 commit 497a873
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 65 deletions.
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
98 changes: 66 additions & 32 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,25 +321,30 @@ class FirefoxLauncher implements ProductLauncher {
firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}

let temporaryUserDataDir = null;
let userDataDir = null;
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) {
const profilePath = firefoxArguments[profileArgIndex + 1];
if (!profilePath) {
temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
firefoxArguments.push(temporaryUserDataDir);
} else {
const prefs = this.defaultPreferences(extraPrefsFirefox);
await this.writePreferences(prefs, profilePath);
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 {
temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
userDataDir = await this._createProfile(extraPrefsFirefox);
firefoxArguments.push('--profile');
firefoxArguments.push(temporaryUserDataDir);
firefoxArguments.push(userDataDir);
}

await this._updateRevision();
Expand All @@ -336,7 +359,8 @@ class FirefoxLauncher implements ProductLauncher {
this.product,
firefoxExecutable,
firefoxArguments,
temporaryUserDataDir
userDataDir,
isTempUserDataDir
);
runner.start({
handleSIGHUP,
Expand Down Expand Up @@ -626,33 +650,43 @@ class FirefoxLauncher implements ProductLauncher {
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 prefsJS = [];
const userJS = [];
const lines = Object.entries(prefs).map(([key, value]) => {
return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`;
});

for (const [key, value] of Object.entries(prefs))
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')
);
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 profilePath = await mkdtempAsync(
const temporaryProfilePath = await mkdtempAsync(
path.join(tmpDir(), 'puppeteer_dev_firefox_profile-')
);

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

return profilePath;
return temporaryProfilePath;
}
}

Expand Down

0 comments on commit 497a873

Please sign in to comment.