Skip to content

Commit

Permalink
fix: Re-enabled support for running extensions in Firefox 48
Browse files Browse the repository at this point in the history
  • Loading branch information
kumar303 committed Jun 22, 2016
1 parent 03b1b7a commit 2622d73
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 33 deletions.
2 changes: 1 addition & 1 deletion src/cmd/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function defaultPackageCreator(


export default function build(
{sourceDir, artifactsDir, asNeeded}: Object,
{sourceDir, artifactsDir, asNeeded=false}: Object,
{manifestData, fileFilter=new FileFilter(),
onSourceChange=defaultSourceWatcher,
packageCreator=defaultPackageCreator}
Expand Down
66 changes: 59 additions & 7 deletions src/cmd/run.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/* @flow */
import * as defaultFirefox from '../firefox';
import defaultFirefoxConnector from '../firefox/remote';
import {onlyErrorsWithCode} from '../errors';
import {
onlyInstancesOf, onlyErrorsWithCode, RemoteTempInstallNotSupported,
WebExtError,
} from '../errors';
import {createLogger} from '../util/logger';
import getValidatedManifest from '../util/manifest';
import defaultSourceWatcher from '../watcher';
Expand Down Expand Up @@ -83,12 +86,18 @@ export function defaultFirefoxClient(


export default function run(
{sourceDir, artifactsDir, firefoxBinary, firefoxProfile, noReload}: Object,
{sourceDir, artifactsDir, firefoxBinary, firefoxProfile,
preInstall=false, noReload=false}: Object,
{firefoxClient=defaultFirefoxClient, firefox=defaultFirefox,
reloadStrategy=defaultReloadStrategy}
: Object = {}): Promise {

log.info(`Running web extension from ${sourceDir}`);
if (preInstall) {
log.info('Disabled auto-reloading because it\'s not possible with ' +
'--pre-install');
noReload = true;
}

function createRunner() {
return getValidatedManifest(sourceDir)
Expand All @@ -103,12 +112,30 @@ export default function run(
});
}

let installed = false;
return createRunner()
.then((runner) => {
return runner.getProfile().then((profile) => {
return {runner, profile};
});
})
.then((config) => {
const {runner, profile} = config;
return new Promise(
(resolve) => {
if (!preInstall) {
log.debug('Deferring extension installation until after ' +
'connecting to the remote debugger');
resolve();
} else {
log.debug('Pre-installing extension as a proxy file');
resolve(runner.installAsProxy(profile).then(() => {
installed = true;
}));
}
})
.then(() => config);
})
.then(({runner, profile}) => {
return runner.run(profile).then((firefox) => {
return {runner, profile, firefox};
Expand All @@ -120,11 +147,26 @@ export default function run(
});
})
.then((config) => {
const {runner, client} = config;
return runner.install(client).then(() => {
return config;
});
return new Promise(
(resolve) => {
const {runner, client} = config;
if (installed) {
log.debug('Not installing as temporary add-on because the ' +
'add-on was already installed');
resolve();
} else {
resolve(runner.installAsTemporaryAddon(client));
}
})
.then(() => config);
})
.catch(onlyInstancesOf(RemoteTempInstallNotSupported, (error) => {
log.debug(`Caught: ${error}`);
throw new WebExtError(
'Temporary add-on installation is not supported in this version ' +
'of Firefox (you need Firefox 49 or higher). For older Firefox ' +
'versions, use --pre-install');
}))
.then(({firefox, profile, client}) => {
if (noReload) {
log.debug('Extension auto-reloading has been disabled');
Expand Down Expand Up @@ -167,10 +209,20 @@ export class ExtensionRunner {
});
}

install(client: Object): Promise {
installAsTemporaryAddon(client: Object): Promise {
return client.installTemporaryAddon(this.sourceDir);
}

installAsProxy(profile: Object): Promise {
const {firefox, sourceDir, manifestData} = this;
return firefox.installExtension({
manifestData,
asProxy: true,
extensionPath: sourceDir,
profile,
});
}

run(profile: Object): Promise {
const {firefox, firefoxBinary} = this;
return firefox.run(profile, {firefoxBinary});
Expand Down
10 changes: 10 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export class InvalidManifest extends WebExtError {
}


/*
* The remote Firefox does not support temporary add-on installation.
*/
export class RemoteTempInstallNotSupported extends WebExtError {
constructor(message: string) {
super(message);
}
}


/*
* Sugar-y way to catch only instances of a certain error.
*
Expand Down
56 changes: 39 additions & 17 deletions src/firefox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,12 @@ export function copyProfile(
* The extension is copied into a special location and you need to turn
* on some preferences to allow this. See extensions.autoDisableScopes in
* ./preferences.js.
*
* When asProxy is true, a special proxy file will be installed. This is a
* text file that contains the path to the extension source.
*/
export function installExtension(
{manifestData, profile, extensionPath}: Object): Promise {
{asProxy=false, manifestData, profile, extensionPath}: Object): Promise {

// This more or less follows
// https://github.com/saadtazi/firefox-profile-js/blob/master/lib/firefox_profile.js#L531
Expand All @@ -216,21 +219,40 @@ export function installExtension(
return fs.mkdir(profile.extensionsDir);
}))
.then(() => {
let readStream = nodeFs.createReadStream(extensionPath);
let id = manifestData.applications.gecko.id;

// TODO: also support copying a directory of code to this
// destination. That is, to name the directory ${id}.
// https://github.com/mozilla/web-ext/issues/70
let destPath = path.join(profile.extensionsDir, `${id}.xpi`);
let writeStream = nodeFs.createWriteStream(destPath);

log.debug(`Copying ${extensionPath} to ${destPath}`);
readStream.pipe(writeStream);

return Promise.all([
streamToPromise(readStream),
streamToPromise(writeStream),
]);
const id = manifestData.applications.gecko.id;

if (asProxy) {
log.debug(`Installing as an extension proxy; source: ${extensionPath}`);
return isDirectory(extensionPath)
.then((isDir) => {
if (!isDir) {
throw new WebExtError(
'proxy install: extensionPath must be the extension source ' +
`directory; got: ${extensionPath}`);
}
})
.then(() => {
// Write a special extension proxy file containing the source
// directory. See:
// https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment#Firefox_extension_proxy_file
const destPath = path.join(profile.extensionsDir, `${id}`);
const writeStream = nodeFs.createWriteStream(destPath);
writeStream.write(extensionPath);
writeStream.end();
return streamToPromise(writeStream);
});
} else {
const readStream = nodeFs.createReadStream(extensionPath);
const destPath = path.join(profile.extensionsDir, `${id}.xpi`);
const writeStream = nodeFs.createWriteStream(destPath);

log.debug(`Installing extension from ${extensionPath} to ${destPath}`);
readStream.pipe(writeStream);

return Promise.all([
streamToPromise(readStream),
streamToPromise(writeStream),
]);
}
});
}
10 changes: 6 additions & 4 deletions src/firefox/remote.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* @flow */
import {createLogger} from '../util/logger';
import {WebExtError} from '../errors';
import {RemoteTempInstallNotSupported, WebExtError} from '../errors';
import defaultFirefoxConnector from 'node-firefox-connect';

const log = createLogger(__filename);
Expand Down Expand Up @@ -73,9 +73,10 @@ export class RemoteFirefox {
if (!response.addonsActor) {
log.debug(
`listTabs returned a falsey addonsActor: ${response.addonsActor}`);
throw new WebExtError(
return reject(new RemoteTempInstallNotSupported(
'This is an older version of Firefox that does not provide an ' +
'add-ons actor for remote installation. Try Firefox 49 or higher.');
'add-ons actor for remote installation. Try Firefox 49 or ' +
'higher.'));
}
this.client.client.makeRequest(
{to: response.addonsActor, type: 'installTemporaryAddon', addonPath},
Expand Down Expand Up @@ -129,7 +130,8 @@ export class RemoteFirefox {
log.debug(
`Remote Firefox only supports: ${response.requestTypes}`);
throw new WebExtError(
'This Firefox version does not support addon.reload() yet');
'This Firefox version does not support add-on reloading. ' +
'Re-run with --no-reload');
} else {
this.checkedForAddonReloading = true;
return addon;
Expand Down
10 changes: 9 additions & 1 deletion src/program.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,15 @@ Example: $0 --help run.
type: 'string',
},
'no-reload': {
describe: 'Do not reload the extension as the source changes',
describe: 'Do not reload the extension when source files change',
demand: false,
type: 'boolean',
},
'pre-install': {
describe: 'Pre-install the extension into the profile before ' +
'startup. This is only needed to support older versions ' +
'of Firefox.',
demand: false,
type: 'boolean',
},
});
Expand Down
57 changes: 57 additions & 0 deletions tests/test-cmd/test.run.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {describe, it} from 'mocha';
import {assert} from 'chai';
import sinon from 'sinon';

import {onlyInstancesOf, WebExtError, RemoteTempInstallNotSupported}
from '../../src/errors';
import run, {
defaultFirefoxClient, defaultWatcherCreator, defaultReloadStrategy,
ExtensionRunner,
Expand Down Expand Up @@ -86,6 +88,23 @@ describe('run', () => {
});
});

it('suggests --pre-install when remote install not supported', () => {
const cmd = prepareRun();
const firefoxClient = fake(RemoteFirefox.prototype, {
// Simulate an older Firefox that will throw this error.
installTemporaryAddon:
() => Promise.reject(new RemoteTempInstallNotSupported('')),
});

return cmd.run(
{}, {firefoxClient: () => Promise.resolve(firefoxClient)})
.then(makeSureItFails())
.catch(onlyInstancesOf(WebExtError, (error) => {
assert.equal(firefoxClient.installTemporaryAddon.called, true);
assert.match(error.message, /use --pre-install/);
}));
});

it('passes a custom Firefox binary when specified', () => {
const firefoxBinary = '/pretend/path/to/Firefox/firefox-bin';
const cmd = prepareRun();
Expand All @@ -111,6 +130,34 @@ describe('run', () => {
});
});

it('can pre-install into the profile before startup', () => {
const cmd = prepareRun();
const firefoxClient = fake(RemoteFirefox.prototype, {
installTemporaryAddon: () => Promise.resolve(),
});
const fakeProfile = {};
const firefox = getFakeFirefox({
copyProfile: () => fakeProfile,
});
const {sourceDir} = cmd.argv;

return cmd.run({preInstall: true}, {
firefox,
firefoxClient: sinon.spy(() => Promise.resolve(firefoxClient)),
}).then(() => {
assert.equal(firefox.installExtension.called, true);
assert.equal(firefoxClient.installTemporaryAddon.called, false);

const install = firefox.installExtension.firstCall.args[0];
assert.equal(install.asProxy, true);
assert.equal(install.manifestData.applications.gecko.id,
'minimal-example@web-ext-test-suite');
assert.deepEqual(install.profile, fakeProfile);
// This needs to be the source of the extension.
assert.equal(install.extensionPath, sourceDir);
});
});

it('can watch and reload the extension', () => {
const cmd = prepareRun();
const {sourceDir, artifactsDir} = cmd.argv;
Expand All @@ -125,6 +172,16 @@ describe('run', () => {
});
});

it('will not reload when using --pre-install', () => {
const cmd = prepareRun();
const {reloadStrategy} = cmd.options;

// --pre-install should imply --no-reload
return cmd.run({noReload: false, preInstall: true}).then(() => {
assert.equal(reloadStrategy.called, false);
});
});

it('allows you to opt out of extension reloading', () => {
const cmd = prepareRun();
const {reloadStrategy} = cmd.options;
Expand Down
41 changes: 41 additions & 0 deletions tests/test-firefox/test.firefox.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,47 @@ describe('firefox', () => {
}
));

it('can install the extension as a proxy', () => setUp(
(data) => {
const sourceDir = fixturePath('minimal-web-ext');
return firefox.installExtension(
{
manifestData: basicManifest,
profile: data.profile,
extensionPath: sourceDir,
asProxy: true,
})
.then(() => {
const proxyFile = path.join(data.profile.extensionsDir,
'basic-manifest@web-ext-test-suite');
return fs.readFile(proxyFile);
})
.then((proxyData) => {
// The proxy file should contain the path to the extension.
assert.equal(proxyData.toString(), sourceDir);
});
}
));

it('requires a directory path for proxy installs', () => setUp(
(data) => {
const xpiPath = fixturePath('minimal_extension-1.0.xpi');
return firefox.installExtension(
{
manifestData: basicManifest,
profile: data.profile,
extensionPath: xpiPath,
asProxy: true,
})
.then(makeSureItFails())
.catch(onlyInstancesOf(WebExtError, (error) => {
assert.match(error.message,
/must be the extension source directory/);
assert.include(error.message, xpiPath);
}));
}
));

it('re-uses an existing extension directory', () => setUp(
(data) => {
return fs.mkdir(path.join(data.profile.extensionsDir))
Expand Down

0 comments on commit 2622d73

Please sign in to comment.