Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 70 additions & 78 deletions src/cmd/run.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/* @flow */
import buildExtension from './build';
import * as defaultFirefox from '../firefox';
import defaultFirefoxConnector from '../firefox/remote';
import {onlyErrorsWithCode} from '../errors';
import {withTempDir} from '../util/temp-dir';
import {createLogger} from '../util/logger';
import getValidatedManifest from '../util/manifest';
import defaultSourceWatcher from '../watcher';
Expand All @@ -12,13 +10,12 @@ const log = createLogger(__filename);


export function defaultWatcherCreator(
{profile, client, sourceDir, artifactsDir, createRunner,
{client, sourceDir, artifactsDir, createRunner,
onSourceChange=defaultSourceWatcher}: Object): Object {
return onSourceChange({
sourceDir, artifactsDir, onChange: () => createRunner(
(runner) => runner.buildExtension()
.then((buildResult) => runner.install(buildResult, {profile}))
.then(() => {
sourceDir, artifactsDir, onChange: () => {
return createRunner()
.then((runner) => {
log.debug('Attempting to reload extension');
const addonId = runner.manifestData.applications.gecko.id;
log.debug(`Reloading add-on ID ${addonId}`);
Expand All @@ -27,40 +24,39 @@ export function defaultWatcherCreator(
.catch((error) => {
log.error(error.stack);
throw error;
})
),
});
},
});
}


export function defaultReloadStrategy(
{firefox, profile, sourceDir, artifactsDir, createRunner}: Object,
{connectToFirefox=defaultFirefoxConnector,
maxRetries=25, retryInterval=120,
createWatcher=defaultWatcherCreator}: Object = {}): Promise {
var watcher;
var client;
var retries = 0;
{firefox, client, profile, sourceDir, artifactsDir, createRunner}: Object,
{createWatcher=defaultWatcherCreator}: Object = {}) {
let watcher;

firefox.on('close', () => {
if (client) {
client.disconnect();
}
if (watcher) {
watcher.close();
}
client.disconnect();
watcher.close();
});

watcher = createWatcher({
client, sourceDir, artifactsDir, createRunner,
});
}


export function defaultFirefoxClient(
{connectToFirefox=defaultFirefoxConnector,
maxRetries=25, retryInterval=120}: Object = {}) {
var retries = 0;

function establishConnection() {
return new Promise((resolve, reject) => {
connectToFirefox()
.then((connectedClient) => {
log.debug('Connected to the Firefox debugger');
client = connectedClient;
watcher = createWatcher({
profile, client, sourceDir, artifactsDir, createRunner,
});
resolve();
resolve(connectedClient);
})
.catch(onlyErrorsWithCode('ECONNREFUSED', (error) => {
if (retries >= maxRetries) {
Expand Down Expand Up @@ -88,72 +84,76 @@ export function defaultReloadStrategy(

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

log.info(`Running web extension from ${sourceDir}`);

function createRunner(callback) {
function createRunner() {
return getValidatedManifest(sourceDir)
.then((manifestData) => withTempDir(
(tmpDir) => {
const runner = new ExtensionRunner({
sourceDir,
firefox,
firefoxBinary,
tmpDirPath: tmpDir.path(),
manifestData,
firefoxProfile,
});
return callback(runner);
}
));
.then((manifestData) => {
return new ExtensionRunner({
sourceDir,
firefox,
firefoxBinary,
manifestData,
firefoxProfile,
});
});
}

return createRunner(
(runner) => runner.buildExtension()
.then((buildResult) => runner.install(buildResult))
.then((profile) => runner.run(profile).then((firefox) => {
return {firefox, profile};
}))
.then(({firefox, profile}) => {
if (noReload) {
log.debug('Extension auto-reloading has been disabled');
} else {
log.debug('Reloading extension when the source changes');
reloadStrategy(
{firefox, profile, sourceDir, artifactsDir, createRunner});
}
return firefox;
})
);
return createRunner()
.then((runner) => {
return runner.getProfile().then((profile) => {
return {runner, profile};
});
})
.then(({runner, profile}) => {
return runner.run(profile).then((firefox) => {
return {runner, profile, firefox};
});
})
.then((config) => {
return firefoxClient().then((client) => {
return {client, ...config};
});
})
.then((config) => {
const {runner, client} = config;
return runner.install(client).then(() => {
return config;
});
})
.then(({firefox, profile, client}) => {
if (noReload) {
log.debug('Extension auto-reloading has been disabled');
} else {
log.debug('Reloading extension when the source changes');
reloadStrategy({firefox, profile, client, sourceDir,
artifactsDir, createRunner});
}
return firefox;
});
}


export class ExtensionRunner {
sourceDir: string;
tmpDirPath: string;
manifestData: Object;
firefoxProfile: Object;
firefox: Object;
firefoxBinary: string;

constructor({firefox, sourceDir, tmpDirPath, manifestData,
constructor({firefox, sourceDir, manifestData,
firefoxProfile, firefoxBinary}: Object) {
this.sourceDir = sourceDir;
this.tmpDirPath = tmpDirPath;
this.manifestData = manifestData;
this.firefoxProfile = firefoxProfile;
this.firefox = firefox;
this.firefoxBinary = firefoxBinary;
}

buildExtension(): Promise {
const {sourceDir, tmpDirPath, manifestData} = this;
return buildExtension({sourceDir, artifactsDir: tmpDirPath},
{manifestData});
}

getProfile(): Promise {
const {firefox, firefoxProfile} = this;
return new Promise((resolve) => {
Expand All @@ -167,16 +167,8 @@ export class ExtensionRunner {
});
}

install(buildResult: Object, {profile}: Object = {}): Promise {
const {firefox, manifestData} = this;
return Promise.resolve(profile ? profile : this.getProfile())
.then((profile) => firefox.installExtension(
{
manifestData,
extensionPath: buildResult.extensionPath,
profile,
})
.then(() => profile));
install(client: Object): Promise {
return client.installTemporaryAddon(this.sourceDir);
}

run(profile: Object): Promise {
Expand Down
45 changes: 38 additions & 7 deletions src/firefox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {promisify} from '../util/es6-modules';
import {onlyErrorsWithCode, WebExtError} from '../errors';
import {getPrefs as defaultPrefGetter} from './preferences';
import {createLogger} from '../util/logger';
import {default as defaultFirefoxConnector, REMOTE_PORT} from './remote';

const log = createLogger(__filename);

Expand All @@ -21,30 +22,56 @@ export const defaultFirefoxEnv = {
NS_TRACE_MALLOC_DISABLE_STACKS: '1',
};


export function defaultRemotePortFinder(
{portToTry=REMOTE_PORT, connectToFirefox=defaultFirefoxConnector}
: Object = {}) {
log.debug(`Checking if remote Firefox port ${portToTry} is available`);

return connectToFirefox(portToTry)
.then((client) => {
log.debug(`Remote Firefox port ${portToTry} is in use`);
client.disconnect();
// TODO: instead of throw an error, pick a new random port until
// one of them is available.
// https://github.com/mozilla/web-ext/issues/283
throw new WebExtError(
`Cannot listen on port ${portToTry} because it's in use`);
})
.catch(onlyErrorsWithCode('ECONNREFUSED', () => {
// The connection was refused so this port is good to use.
return portToTry;
}));
}


/*
* Runs Firefox with the given profile object and resolves a promise on exit.
*/
export function run(
profile: FirefoxProfile,
{fxRunner=defaultFxRunner, firefoxBinary, binaryArgs}
{fxRunner=defaultFxRunner, findRemotePort=defaultRemotePortFinder,
firefoxBinary, binaryArgs}
: Object = {}): Promise {

log.info(`Running Firefox with profile at ${profile.path()}`);
return fxRunner(
{
return findRemotePort()
.then((remotePort) => fxRunner({
// if this is falsey, fxRunner tries to find the default one.
'binary': firefoxBinary,
'binary-args': binaryArgs,
'no-remote': false,
'listen': '6000',
// This ensures a new instance of Firefox is created. It has nothing
// to do with the devtools remote debugger.
'no-remote': true,
'listen': remotePort,
'foreground': true,
'profile': profile.path(),
'env': {
...process.env,
...defaultFirefoxEnv,
},
'verbose': true,
})
}))
.then((results) => {
return new Promise((resolve) => {
let firefox = results.process;
Expand All @@ -59,8 +86,12 @@ export function run(
throw error;
});

log.info(
'Use --verbose or open Tools > Web Developer > Browser Console ' +
'to see logging');

firefox.stderr.on('data', (data) => {
log.error(`Firefox stderr: ${data.toString().trim()}`);
log.debug(`Firefox stderr: ${data.toString().trim()}`);
});

firefox.stdout.on('data', (data) => {
Expand Down
47 changes: 46 additions & 1 deletion src/firefox/remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import {WebExtError} from '../errors';
import defaultFirefoxConnector from 'node-firefox-connect';

const log = createLogger(__filename);
// The default port that Firefox's remote debugger will listen on and the
// client will connect to.
export const REMOTE_PORT = 6005;


export default function connect(
port: number = 6000,
port: number = REMOTE_PORT,
{connectToFirefox=defaultFirefoxConnector}: Object = {}): Promise {
log.debug(`Connecting to Firefox on port ${port}`);
return connectToFirefox(port)
.then((client) => {
log.info('Connected to the Firefox remote debugger');
Expand All @@ -27,6 +31,17 @@ export class RemoteFirefox {
constructor(client: Object) {
this.client = client;
this.checkedForAddonReloading = false;

client.client.on('disconnect', () => {
log.debug('Received "disconnect" from Firefox client');
});
client.client.on('end', () => {
log.debug('Received "end" from Firefox client');
});
client.client.on('message', (info) => {
// These are arbitrary messages that the client library ignores.
log.debug(`Received message from client: ${JSON.stringify(info)}`);
});
}

disconnect() {
Expand All @@ -47,6 +62,36 @@ export class RemoteFirefox {
});
}

installTemporaryAddon(addonPath: string) {
return new Promise((resolve, reject) => {
this.client.request('listTabs', (error, response) => {
if (error) {
return reject(new WebExtError(
`Remote Firefox: listTabs() error: ${error}`));
}
if (!response.addonsActor) {
log.debug(
`listTabs returned a falsey addonsActor: ${response.addonsActor}`);
throw new WebExtError(
'This is an older version of Firefox that does not provide an ' +
'add-ons actor for remote control');
}
this.client.client.makeRequest(
{to: response.addonsActor, type: 'installTemporaryAddon', addonPath},
(response) => {
if (response.error) {
return reject(new WebExtError(
'installTemporaryAddon: Error: ' +
`${response.error}: ${response.message}`));
}
log.debug(`installTemporaryAddon: ${JSON.stringify(response)}`);
log.info(`Installed ${addonPath} as a temporary add-on`);
resolve();
});
});
});
}

getInstalledAddon(addonId: string): Promise {
return new Promise(
(resolve, reject) => {
Expand Down
9 changes: 5 additions & 4 deletions src/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ export default function onSourceChange(
// TODO: For network disks, we would need to add {poll: true}.
const watcher = new Watchpack();

const onFileChange = (filePath) => {
proxyFileChanges({artifactsDir, onChange, filePath, shouldWatchFile});
};
const executeImmediately = true;
watcher.on('change', debounce(onFileChange, 1000, executeImmediately));
onChange = debounce(onChange, 1000, executeImmediately);

watcher.on('change', (filePath) => {
proxyFileChanges({artifactsDir, onChange, filePath, shouldWatchFile});
});

log.debug(`Watching for file changes in ${sourceDir}`);
watcher.watch([], [sourceDir], Date.now());
Expand Down
Loading