Skip to content

Commit

Permalink
chore: refactor handling of user data directory for clearing of prefe…
Browse files Browse the repository at this point in the history
…rences for Firefox
  • Loading branch information
whimboo committed Nov 8, 2021
1 parent 5ddca21 commit 8b22617
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 59 deletions.
43 changes: 33 additions & 10 deletions src/node/BrowserRunner.ts
Expand Up @@ -16,18 +16,21 @@

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 debugLauncher = debug('puppeteer:launcher');
Expand All @@ -40,7 +43,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 +57,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 @@ -98,14 +104,29 @@ export class BrowserRunner {
this.proc.once('exit', () => {
this._closed = true;
// Cleanup as processes exit.
if (this._tempDirectory) {
removeFolderAsync(this._tempDirectory)
if (this._isTempUserDataDir) {
removeFolderAsync(this._userDataDir)
.then(() => fulfill())
.catch((error) => {
console.error(error);
reject(error);
});
} else {
if (this._product === 'firefox') {
// When an existing user profile has been used remove the user
// preferences file and restore possibly backuped preferences.
fs.unlinkSync(path.join(this._userDataDir, 'user.js'));
const prefsBackupPath = path.join(
this._userDataDir,
'prefs.js.puppeteer'
);
if (fs.existsSync(prefsBackupPath)) {
fs.renameSync(
prefsBackupPath,
path.join(this._userDataDir, 'prefs.js')
);
}
}
fulfill();
}
});
Expand All @@ -132,7 +153,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 +171,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
97 changes: 65 additions & 32 deletions src/node/Launcher.ts
Expand Up @@ -85,7 +85,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 +95,6 @@ class ChromeLauncher implements ProductLauncher {
);
else chromeArguments.push(...args);

let temporaryUserDataDir = null;

if (
!chromeArguments.some((argument) =>
argument.startsWith('--remote-debugging-')
Expand All @@ -113,9 +110,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 +161,8 @@ class ChromeLauncher implements ProductLauncher {
this.product,
chromeExecutable,
chromeArguments,
temporaryUserDataDir
userDataDir,
isTempUserDataDir
);
runner.start({
handleSIGHUP,
Expand Down Expand Up @@ -303,25 +320,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 +358,8 @@ class FirefoxLauncher implements ProductLauncher {
this.product,
firefoxExecutable,
firefoxArguments,
temporaryUserDataDir
userDataDir,
isTempUserDataDir
);
runner.start({
handleSIGHUP,
Expand Down Expand Up @@ -626,33 +649,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'));

const prefsJSPath = path.join(profilePath, 'prefs.js');
if (fs.existsSync(prefsJSPath)) {
fs.cpSync(prefsJSPath, path.join(profilePath, 'prefs.js.puppeteer'), {
force: true,
});
}
}

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
31 changes: 14 additions & 17 deletions test/launcher.spec.ts
Expand Up @@ -240,7 +240,7 @@ describe('Launcher specs', function () {
} else {
options.args = [
...(defaultBrowserOptions.args || []),
`-profile`,
'-profile',
userDataDir,
];
}
Expand Down Expand Up @@ -344,7 +344,7 @@ describe('Launcher specs', function () {
if (isChrome) expect(puppeteer.product).toBe('chrome');
else if (isFirefox) expect(puppeteer.product).toBe('firefox');
});
itFailsFirefox('should work with no default arguments', async () => {
it('should work with no default arguments', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const options = Object.assign({}, defaultBrowserOptions);
options.ignoreDefaultArgs = true;
Expand Down Expand Up @@ -380,22 +380,19 @@ describe('Launcher specs', function () {
expect(pages).toEqual(['about:blank']);
await browser.close();
});
itFailsFirefox(
'should have custom URL when launching browser',
async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
it('should have custom URL when launching browser', async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();

const options = Object.assign({}, defaultBrowserOptions);
options.args = [server.EMPTY_PAGE].concat(options.args || []);
const browser = await puppeteer.launch(options);
const pages = await browser.pages();
expect(pages.length).toBe(1);
const page = pages[0];
if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation();
expect(page.url()).toBe(server.EMPTY_PAGE);
await browser.close();
}
);
const options = Object.assign({}, defaultBrowserOptions);
options.args = [server.EMPTY_PAGE].concat(options.args || []);
const browser = await puppeteer.launch(options);
const pages = await browser.pages();
expect(pages.length).toBe(1);
const page = pages[0];
if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation();
expect(page.url()).toBe(server.EMPTY_PAGE);
await browser.close();
});
it('should pass the timeout parameter to browser.waitForTarget', async () => {
const { puppeteer, defaultBrowserOptions } = getTestState();
const options = Object.assign({}, defaultBrowserOptions, {
Expand Down

0 comments on commit 8b22617

Please sign in to comment.