Skip to content

Commit 29798cb

Browse files
authoredJul 20, 2020
feat: Support custom profile-directory in Chrome extension runner
Change the profile directory selection behavior with `web-ext run --target=chromium`: - Support a custom profile-directory inside the user-data-dir (other than Default). - Use temporary profile directories unless `--keep-profile-changes` is set). Fixes #1909.
1 parent 76e36d5 commit 29798cb

File tree

4 files changed

+379
-21
lines changed

4 files changed

+379
-21
lines changed
 

‎package-lock.json

+58-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"es6-error": "4.1.1",
7272
"event-to-promise": "0.8.0",
7373
"firefox-profile": "2.0.0",
74+
"fs-extra": "9.0.1",
7475
"fx-runner": "1.0.13",
7576
"git-rev-sync": "2.0.0",
7677
"import-fresh": "3.2.1",

‎src/extension-runners/chromium.js

+87-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import path from 'path';
99

10-
import {fs} from 'mz';
10+
import fs from 'fs-extra';
1111
import asyncMkdirp from 'mkdirp';
1212
import {
1313
Launcher as ChromeLauncher,
@@ -21,6 +21,8 @@ import type {
2121
ExtensionRunnerParams,
2222
ExtensionRunnerReloadResult,
2323
} from './base';
24+
import isDirectory from '../util/is-directory';
25+
import fileExists from '../util/file-exists';
2426

2527
type ChromiumSpecificRunnerParams = {|
2628
chromiumBinary?: string,
@@ -81,6 +83,52 @@ export class ChromiumExtensionRunner {
8183
await this._promiseSetupDone;
8284
}
8385

86+
static async isUserDataDir(dirPath: string): Promise<boolean> {
87+
const localStatePath = path.join(dirPath, 'Local State');
88+
const defaultPath = path.join(dirPath, 'Default');
89+
// Local State and Default are typical for the user-data-dir
90+
return await fileExists(localStatePath)
91+
&& await isDirectory(defaultPath);
92+
}
93+
94+
static async isProfileDir(dirPath: string): Promise<boolean> {
95+
const securePreferencesPath = path.join(
96+
dirPath, 'Secure Preferences');
97+
//Secure Preferences is typical for a profile dir inside a user data dir
98+
return await fileExists(securePreferencesPath);
99+
}
100+
101+
static async getProfilePaths(chromiumProfile: ?string): Promise<{
102+
userDataDir: ?string,
103+
profileDirName: ?string
104+
}> {
105+
if (!chromiumProfile) {
106+
return {
107+
userDataDir: null,
108+
profileDirName: null,
109+
};
110+
}
111+
112+
const isProfileDirAndNotUserData =
113+
await ChromiumExtensionRunner.isProfileDir(chromiumProfile)
114+
&& !await ChromiumExtensionRunner.isUserDataDir(chromiumProfile);
115+
116+
if (isProfileDirAndNotUserData) {
117+
const {dir: userDataDir, base: profileDirName} =
118+
path.parse(chromiumProfile);
119+
return {
120+
userDataDir,
121+
profileDirName,
122+
};
123+
}
124+
125+
return {
126+
userDataDir: chromiumProfile,
127+
profileDirName: null,
128+
};
129+
130+
}
131+
84132
/**
85133
* Setup the Chromium Profile and run a Chromium instance.
86134
*/
@@ -126,8 +174,43 @@ export class ChromiumExtensionRunner {
126174
chromeFlags.push(...this.params.args);
127175
}
128176

129-
if (this.params.chromiumProfile) {
130-
chromeFlags.push(`--user-data-dir=${this.params.chromiumProfile}`);
177+
// eslint-disable-next-line prefer-const
178+
let {userDataDir, profileDirName} =
179+
await ChromiumExtensionRunner.getProfilePaths(
180+
this.params.chromiumProfile);
181+
182+
if (userDataDir && this.params.keepProfileChanges) {
183+
if (profileDirName
184+
&& !await ChromiumExtensionRunner.isUserDataDir(userDataDir)) {
185+
throw new Error('The profile you provided is not in a ' +
186+
'user-data-dir. The changes cannot be kept. Please either ' +
187+
'remove --keep-profile-changes or use a profile in a ' +
188+
'user-data-dir directory');
189+
}
190+
} else if (!this.params.keepProfileChanges) {
191+
// the user provided an existing profile directory but doesn't want
192+
// the changes to be kept. we copy this directory to a temporary
193+
// user data dir.
194+
const tmpDir = new TempDir();
195+
await tmpDir.create();
196+
const tmpDirPath = tmpDir.path();
197+
198+
if (userDataDir && profileDirName) {
199+
// copy profile dir to this temp user data dir.
200+
await fs.copy(path.join(
201+
userDataDir,
202+
profileDirName), path.join(
203+
tmpDirPath,
204+
profileDirName),
205+
);
206+
} else if (userDataDir) {
207+
await fs.copy(userDataDir, tmpDirPath);
208+
}
209+
userDataDir = tmpDirPath;
210+
}
211+
212+
if (profileDirName) {
213+
chromeFlags.push(`--profile-directory=${profileDirName}`);
131214
}
132215

133216
let startingUrl;
@@ -143,6 +226,7 @@ export class ChromiumExtensionRunner {
143226
chromePath: chromiumBinary,
144227
chromeFlags,
145228
startingUrl,
229+
userDataDir,
146230
// Ignore default flags to keep the extension enabled.
147231
ignoreDefaultFlags: true,
148232
});

‎tests/unit/test-extension-runners/test.chromium.js

+233-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* @flow */
22

3+
import path from 'path';
34
import EventEmitter from 'events';
45

56
import {assert} from 'chai';
67
import {describe, it, beforeEach, afterEach} from 'mocha';
78
import deepcopy from 'deepcopy';
8-
import fs from 'mz/fs';
9+
import fs from 'fs-extra';
910
import sinon from 'sinon';
1011
import WebSocket from 'ws';
1112

@@ -24,6 +25,9 @@ import type {
2425
import {
2526
consoleStream, // instance is imported to inspect logged messages
2627
} from '../../../src/util/logger';
28+
import { TempDir, withTempDir } from '../../../src/util/temp-dir';
29+
import fileExists from '../../../src/util/file-exists';
30+
import isDirectory from '../../../src/util/is-directory';
2731

2832
function prepareExtensionRunnerParams({params} = {}) {
2933
const fakeChromeInstance = {
@@ -357,33 +361,257 @@ describe('util/extension-runners/chromium', async () => {
357361
await runnerInstance.exit();
358362
});
359363

360-
it('does pass a user-data-dir flag to chrome', async () => {
364+
it('does use a random user-data-dir', async () => {
365+
366+
const {params} = prepareExtensionRunnerParams({
367+
params: {},
368+
});
369+
370+
const spy = sinon.spy(TempDir.prototype, 'path');
371+
372+
const runnerInstance = new ChromiumExtensionRunner(params);
373+
await runnerInstance.run();
374+
375+
const usedTempPath = spy.returnValues[2];
376+
377+
sinon.assert.calledWithMatch(params.chromiumLaunch, {
378+
userDataDir: usedTempPath,
379+
});
380+
381+
await runnerInstance.exit();
382+
spy.restore();
383+
});
384+
385+
it('does pass a user-data-dir flag to chrome', async () => withTempDir(
386+
async (tmpDir) => {
387+
388+
const {params} = prepareExtensionRunnerParams({
389+
params: {
390+
chromiumProfile: tmpDir.path(),
391+
},
392+
});
393+
394+
const spy = sinon.spy(TempDir.prototype, 'path');
395+
396+
const runnerInstance = new ChromiumExtensionRunner(params);
397+
await runnerInstance.run();
398+
399+
const usedTempPath = spy.returnValues[2];
400+
401+
const {reloadManagerExtension} = runnerInstance;
402+
403+
sinon.assert.calledOnce(params.chromiumLaunch);
404+
sinon.assert.calledWithMatch(params.chromiumLaunch, {
405+
ignoreDefaultFlags: true,
406+
enableExtensions: true,
407+
chromePath: undefined,
408+
userDataDir: usedTempPath,
409+
chromeFlags: [
410+
...DEFAULT_CHROME_FLAGS,
411+
`--load-extension=${reloadManagerExtension},/fake/sourceDir`,
412+
],
413+
startingUrl: undefined,
414+
});
415+
416+
await runnerInstance.exit();
417+
spy.restore();
418+
})
419+
);
420+
421+
it('does pass existing user-data-dir and profile-directory flag' +
422+
' to chrome', async () => withTempDir(
423+
async (tmpDir) => {
424+
const tmpPath = tmpDir.path();
425+
await fs.mkdirs(path.join(tmpPath, 'userDataDir/Default'));
426+
await fs.outputFile(path.join(tmpPath, 'userDataDir/Local State'), '');
427+
await fs.mkdirs(path.join(tmpPath, 'userDataDir/profile'));
428+
await fs.outputFile(path.join(
429+
tmpPath, 'userDataDir/profile/Secure Preferences'), '');
430+
431+
const {params} = prepareExtensionRunnerParams({
432+
params: {
433+
chromiumProfile: path.join(tmpPath, 'userDataDir/profile'),
434+
keepProfileChanges: true,
435+
},
436+
});
437+
438+
const runnerInstance = new ChromiumExtensionRunner(params);
439+
await runnerInstance.run();
440+
441+
const {reloadManagerExtension} = runnerInstance;
442+
443+
sinon.assert.calledOnce(params.chromiumLaunch);
444+
sinon.assert.calledWithMatch(params.chromiumLaunch, {
445+
ignoreDefaultFlags: true,
446+
enableExtensions: true,
447+
chromePath: undefined,
448+
userDataDir: path.join(tmpPath, 'userDataDir'),
449+
chromeFlags: [
450+
...DEFAULT_CHROME_FLAGS,
451+
`--load-extension=${reloadManagerExtension},/fake/sourceDir`,
452+
'--profile-directory=profile',
453+
],
454+
startingUrl: undefined,
455+
});
456+
457+
await runnerInstance.exit();
458+
459+
})
460+
);
461+
462+
it('does support some special chars in profile-directory flag',
463+
async () => withTempDir(
464+
async (tmpDir) => {
465+
const tmpPath = tmpDir.path();
466+
// supported to test: [ _-]
467+
// not supported by Chromium: [ßäé]
468+
const profileDirName = ' profile _-\' ';
469+
await fs.mkdirs(path.join(tmpPath, 'userDataDir/Default'));
470+
await fs.outputFile(path.join(tmpPath, 'userDataDir/Local State'), '');
471+
await fs.mkdirs(path.join(tmpPath, 'userDataDir', profileDirName));
472+
await fs.outputFile(path.join(
473+
tmpPath, 'userDataDir', profileDirName, 'Secure Preferences'), '');
474+
475+
const {params} = prepareExtensionRunnerParams({
476+
params: {
477+
chromiumProfile: path.join(tmpPath, 'userDataDir', profileDirName),
478+
keepProfileChanges: true,
479+
},
480+
});
481+
482+
const runnerInstance = new ChromiumExtensionRunner(params);
483+
await runnerInstance.run();
484+
485+
const {reloadManagerExtension} = runnerInstance;
486+
487+
sinon.assert.calledOnce(params.chromiumLaunch);
488+
sinon.assert.calledWithMatch(params.chromiumLaunch, {
489+
ignoreDefaultFlags: true,
490+
enableExtensions: true,
491+
chromePath: undefined,
492+
userDataDir: path.join(tmpPath, 'userDataDir'),
493+
chromeFlags: [
494+
...DEFAULT_CHROME_FLAGS,
495+
`--load-extension=${reloadManagerExtension},/fake/sourceDir`,
496+
`--profile-directory=${profileDirName}`,
497+
],
498+
startingUrl: undefined,
499+
});
500+
501+
await runnerInstance.exit();
502+
503+
})
504+
);
505+
506+
it('does recognize a UserData dir', async () => withTempDir(
507+
async (tmpDir) => {
508+
509+
const tmpPath = tmpDir.path();
510+
await fs.mkdirs(path.join(tmpPath, 'Default'));
511+
await fs.outputFile(path.join(tmpPath, 'Local State'), '');
512+
513+
assert.isTrue(await ChromiumExtensionRunner.isUserDataDir(tmpPath));
514+
515+
}),
516+
);
517+
518+
it('does reject a UserData dir with Local State dir', async () => withTempDir(
519+
async (tmpDir) => {
520+
521+
const tmpPath = tmpDir.path();
522+
await fs.mkdirs(path.join(tmpPath, 'Default'));
523+
// Local State should be a file
524+
await fs.mkdirs(path.join(tmpPath, 'Local State'));
525+
526+
assert.isFalse(await ChromiumExtensionRunner.isUserDataDir(tmpPath));
527+
528+
}),
529+
);
530+
531+
it('does reject a UserData dir with Default file', async () => withTempDir(
532+
async (tmpDir) => {
533+
534+
const tmpPath = tmpDir.path();
535+
await fs.mkdirs(path.join(tmpPath, 'Local State'));
536+
// Default should be a directory
537+
await fs.outputFile(path.join(tmpPath, 'Default'), '');
538+
539+
assert.isFalse(await ChromiumExtensionRunner.isUserDataDir(tmpPath));
540+
541+
}),
542+
);
543+
544+
it('throws an error on profile in invalid user-data-dir',
545+
async () => withTempDir(async (tmpDir) => {
546+
const tmpPath = tmpDir.path();
547+
await fs.mkdirs(
548+
path.join(tmpPath, 'userDataDir/profile'));
549+
// the userDataDir is missing a file Local State to be validated as such
550+
await fs.outputFile(path.join(
551+
tmpPath, 'userDataDir/profile/Secure Preferences'), '');
552+
553+
const {params} = prepareExtensionRunnerParams({
554+
params: {
555+
chromiumProfile: path.join(tmpPath, 'userDataDir/profile'),
556+
keepProfileChanges: true,
557+
},
558+
});
559+
560+
const runnerInstance = new ChromiumExtensionRunner(params);
561+
562+
await assert.isRejected(runnerInstance.run(), /not in a user-data-dir/);
563+
564+
await runnerInstance.exit();
565+
566+
})
567+
);
568+
569+
it('does copy the profile and pass user-data-dir and profile-directory' +
570+
' flags', async () => withTempDir(async (tmpDir) => {
571+
572+
const tmpPath = tmpDir.path();
573+
await fs.mkdirs(
574+
path.join(tmpPath, 'userDataDir/profile'));
575+
await fs.outputFile(path.join(
576+
tmpPath, 'userDataDir/profile/Secure Preferences'), '');
577+
361578
const {params} = prepareExtensionRunnerParams({
362579
params: {
363-
chromiumProfile: '/fake/chrome/profile',
580+
chromiumProfile: path.join(tmpPath, 'userDataDir/profile'),
364581
},
365582
});
366583

584+
const spy = sinon.spy(TempDir.prototype, 'path');
585+
367586
const runnerInstance = new ChromiumExtensionRunner(params);
368587
await runnerInstance.run();
369588

589+
const usedTempPath = spy.returnValues[2];
590+
370591
const {reloadManagerExtension} = runnerInstance;
371592

372593
sinon.assert.calledOnce(params.chromiumLaunch);
373594
sinon.assert.calledWithMatch(params.chromiumLaunch, {
374595
ignoreDefaultFlags: true,
375596
enableExtensions: true,
376597
chromePath: undefined,
598+
userDataDir: usedTempPath,
377599
chromeFlags: [
378600
...DEFAULT_CHROME_FLAGS,
379601
`--load-extension=${reloadManagerExtension},/fake/sourceDir`,
380-
'--user-data-dir=/fake/chrome/profile',
602+
'--profile-directory=profile',
381603
],
382604
startingUrl: undefined,
383605
});
384606

607+
assert.isTrue(await isDirectory(path.join(usedTempPath, 'profile')));
608+
assert.isTrue(await fileExists(path.join(
609+
usedTempPath, 'profile/Secure Preferences')));
610+
385611
await runnerInstance.exit();
386-
});
612+
spy.restore();
613+
})
614+
);
387615

388616
describe('reloadAllExtensions', () => {
389617
let runnerInstance;

0 commit comments

Comments
 (0)
Please sign in to comment.