Skip to content

Commit 79c6bf5

Browse files
committed
feat: Run now installs as temporary add-on
1 parent 77c49fc commit 79c6bf5

8 files changed

Lines changed: 438 additions & 225 deletions

File tree

src/cmd/run.js

Lines changed: 70 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
/* @flow */
2-
import buildExtension from './build';
32
import * as defaultFirefox from '../firefox';
43
import defaultFirefoxConnector from '../firefox/remote';
54
import {onlyErrorsWithCode} from '../errors';
6-
import {withTempDir} from '../util/temp-dir';
75
import {createLogger} from '../util/logger';
86
import getValidatedManifest from '../util/manifest';
97
import defaultSourceWatcher from '../watcher';
@@ -12,13 +10,12 @@ const log = createLogger(__filename);
1210

1311

1412
export function defaultWatcherCreator(
15-
{profile, client, sourceDir, artifactsDir, createRunner,
13+
{client, sourceDir, artifactsDir, createRunner,
1614
onSourceChange=defaultSourceWatcher}: Object): Object {
1715
return onSourceChange({
18-
sourceDir, artifactsDir, onChange: () => createRunner(
19-
(runner) => runner.buildExtension()
20-
.then((buildResult) => runner.install(buildResult, {profile}))
21-
.then(() => {
16+
sourceDir, artifactsDir, onChange: () => {
17+
return createRunner()
18+
.then((runner) => {
2219
log.debug('Attempting to reload extension');
2320
const addonId = runner.manifestData.applications.gecko.id;
2421
log.debug(`Reloading add-on ID ${addonId}`);
@@ -27,40 +24,39 @@ export function defaultWatcherCreator(
2724
.catch((error) => {
2825
log.error(error.stack);
2926
throw error;
30-
})
31-
),
27+
});
28+
},
3229
});
3330
}
3431

3532

3633
export function defaultReloadStrategy(
37-
{firefox, profile, sourceDir, artifactsDir, createRunner}: Object,
38-
{connectToFirefox=defaultFirefoxConnector,
39-
maxRetries=25, retryInterval=120,
40-
createWatcher=defaultWatcherCreator}: Object = {}): Promise {
41-
var watcher;
42-
var client;
43-
var retries = 0;
34+
{firefox, client, profile, sourceDir, artifactsDir, createRunner}: Object,
35+
{createWatcher=defaultWatcherCreator}: Object = {}) {
36+
let watcher;
4437

4538
firefox.on('close', () => {
46-
if (client) {
47-
client.disconnect();
48-
}
49-
if (watcher) {
50-
watcher.close();
51-
}
39+
client.disconnect();
40+
watcher.close();
41+
});
42+
43+
watcher = createWatcher({
44+
client, sourceDir, artifactsDir, createRunner,
5245
});
46+
}
47+
48+
49+
export function defaultFirefoxClient(
50+
{connectToFirefox=defaultFirefoxConnector,
51+
maxRetries=25, retryInterval=120}: Object = {}) {
52+
var retries = 0;
5353

5454
function establishConnection() {
5555
return new Promise((resolve, reject) => {
5656
connectToFirefox()
5757
.then((connectedClient) => {
5858
log.debug('Connected to the Firefox debugger');
59-
client = connectedClient;
60-
watcher = createWatcher({
61-
profile, client, sourceDir, artifactsDir, createRunner,
62-
});
63-
resolve();
59+
resolve(connectedClient);
6460
})
6561
.catch(onlyErrorsWithCode('ECONNREFUSED', (error) => {
6662
if (retries >= maxRetries) {
@@ -88,72 +84,76 @@ export function defaultReloadStrategy(
8884

8985
export default function run(
9086
{sourceDir, artifactsDir, firefoxBinary, firefoxProfile, noReload}: Object,
91-
{firefox=defaultFirefox, reloadStrategy=defaultReloadStrategy}
87+
{firefoxClient=defaultFirefoxClient, firefox=defaultFirefox,
88+
reloadStrategy=defaultReloadStrategy}
9289
: Object = {}): Promise {
9390

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

96-
function createRunner(callback) {
93+
function createRunner() {
9794
return getValidatedManifest(sourceDir)
98-
.then((manifestData) => withTempDir(
99-
(tmpDir) => {
100-
const runner = new ExtensionRunner({
101-
sourceDir,
102-
firefox,
103-
firefoxBinary,
104-
tmpDirPath: tmpDir.path(),
105-
manifestData,
106-
firefoxProfile,
107-
});
108-
return callback(runner);
109-
}
110-
));
95+
.then((manifestData) => {
96+
return new ExtensionRunner({
97+
sourceDir,
98+
firefox,
99+
firefoxBinary,
100+
manifestData,
101+
firefoxProfile,
102+
});
103+
});
111104
}
112105

113-
return createRunner(
114-
(runner) => runner.buildExtension()
115-
.then((buildResult) => runner.install(buildResult))
116-
.then((profile) => runner.run(profile).then((firefox) => {
117-
return {firefox, profile};
118-
}))
119-
.then(({firefox, profile}) => {
120-
if (noReload) {
121-
log.debug('Extension auto-reloading has been disabled');
122-
} else {
123-
log.debug('Reloading extension when the source changes');
124-
reloadStrategy(
125-
{firefox, profile, sourceDir, artifactsDir, createRunner});
126-
}
127-
return firefox;
128-
})
129-
);
106+
return createRunner()
107+
.then((runner) => {
108+
return runner.getProfile().then((profile) => {
109+
return {runner, profile};
110+
});
111+
})
112+
.then(({runner, profile}) => {
113+
return runner.run(profile).then((firefox) => {
114+
return {runner, profile, firefox};
115+
});
116+
})
117+
.then((config) => {
118+
return firefoxClient().then((client) => {
119+
return {client, ...config};
120+
});
121+
})
122+
.then((config) => {
123+
const {runner, client} = config;
124+
return runner.install(client).then(() => {
125+
return config;
126+
});
127+
})
128+
.then(({firefox, profile, client}) => {
129+
if (noReload) {
130+
log.debug('Extension auto-reloading has been disabled');
131+
} else {
132+
log.debug('Reloading extension when the source changes');
133+
reloadStrategy({firefox, profile, client, sourceDir,
134+
artifactsDir, createRunner});
135+
}
136+
return firefox;
137+
});
130138
}
131139

132140

133141
export class ExtensionRunner {
134142
sourceDir: string;
135-
tmpDirPath: string;
136143
manifestData: Object;
137144
firefoxProfile: Object;
138145
firefox: Object;
139146
firefoxBinary: string;
140147

141-
constructor({firefox, sourceDir, tmpDirPath, manifestData,
148+
constructor({firefox, sourceDir, manifestData,
142149
firefoxProfile, firefoxBinary}: Object) {
143150
this.sourceDir = sourceDir;
144-
this.tmpDirPath = tmpDirPath;
145151
this.manifestData = manifestData;
146152
this.firefoxProfile = firefoxProfile;
147153
this.firefox = firefox;
148154
this.firefoxBinary = firefoxBinary;
149155
}
150156

151-
buildExtension(): Promise {
152-
const {sourceDir, tmpDirPath, manifestData} = this;
153-
return buildExtension({sourceDir, artifactsDir: tmpDirPath},
154-
{manifestData});
155-
}
156-
157157
getProfile(): Promise {
158158
const {firefox, firefoxProfile} = this;
159159
return new Promise((resolve) => {
@@ -167,16 +167,8 @@ export class ExtensionRunner {
167167
});
168168
}
169169

170-
install(buildResult: Object, {profile}: Object = {}): Promise {
171-
const {firefox, manifestData} = this;
172-
return Promise.resolve(profile ? profile : this.getProfile())
173-
.then((profile) => firefox.installExtension(
174-
{
175-
manifestData,
176-
extensionPath: buildResult.extensionPath,
177-
profile,
178-
})
179-
.then(() => profile));
170+
install(client: Object): Promise {
171+
return client.installTemporaryAddon(this.sourceDir);
180172
}
181173

182174
run(profile: Object): Promise {

src/firefox/index.js

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {promisify} from '../util/es6-modules';
1212
import {onlyErrorsWithCode, WebExtError} from '../errors';
1313
import {getPrefs as defaultPrefGetter} from './preferences';
1414
import {createLogger} from '../util/logger';
15+
import {default as defaultFirefoxConnector, REMOTE_PORT} from './remote';
1516

1617
const log = createLogger(__filename);
1718

@@ -21,30 +22,56 @@ export const defaultFirefoxEnv = {
2122
NS_TRACE_MALLOC_DISABLE_STACKS: '1',
2223
};
2324

25+
26+
export function defaultRemotePortFinder(
27+
{portToTry=REMOTE_PORT, connectToFirefox=defaultFirefoxConnector}
28+
: Object = {}) {
29+
log.debug(`Checking if remote Firefox port ${portToTry} is available`);
30+
31+
return connectToFirefox(portToTry)
32+
.then((client) => {
33+
log.debug(`Remote Firefox port ${portToTry} is in use`);
34+
client.disconnect();
35+
// TODO: instead of throw an error, pick a new random port until
36+
// one of them is available.
37+
// https://github.com/mozilla/web-ext/issues/283
38+
throw new WebExtError(
39+
`Cannot listen on port ${portToTry} because it's in use`);
40+
})
41+
.catch(onlyErrorsWithCode('ECONNREFUSED', () => {
42+
// The connection was refused so this port is good to use.
43+
return portToTry;
44+
}));
45+
}
46+
47+
2448
/*
2549
* Runs Firefox with the given profile object and resolves a promise on exit.
2650
*/
2751
export function run(
2852
profile: FirefoxProfile,
29-
{fxRunner=defaultFxRunner, firefoxBinary, binaryArgs}
53+
{fxRunner=defaultFxRunner, findRemotePort=defaultRemotePortFinder,
54+
firefoxBinary, binaryArgs}
3055
: Object = {}): Promise {
3156

3257
log.info(`Running Firefox with profile at ${profile.path()}`);
33-
return fxRunner(
34-
{
58+
return findRemotePort()
59+
.then((remotePort) => fxRunner({
3560
// if this is falsey, fxRunner tries to find the default one.
3661
'binary': firefoxBinary,
3762
'binary-args': binaryArgs,
38-
'no-remote': false,
39-
'listen': '6000',
63+
// This ensures a new instance of Firefox is created. It has nothing
64+
// to do with the devtools remote debugger.
65+
'no-remote': true,
66+
'listen': remotePort,
4067
'foreground': true,
4168
'profile': profile.path(),
4269
'env': {
4370
...process.env,
4471
...defaultFirefoxEnv,
4572
},
4673
'verbose': true,
47-
})
74+
}))
4875
.then((results) => {
4976
return new Promise((resolve) => {
5077
let firefox = results.process;
@@ -59,8 +86,12 @@ export function run(
5986
throw error;
6087
});
6188

89+
log.info(
90+
'Use --verbose or open Tools > Web Developer > Browser Console ' +
91+
'to see logging');
92+
6293
firefox.stderr.on('data', (data) => {
63-
log.error(`Firefox stderr: ${data.toString().trim()}`);
94+
log.debug(`Firefox stderr: ${data.toString().trim()}`);
6495
});
6596

6697
firefox.stdout.on('data', (data) => {

src/firefox/remote.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import {WebExtError} from '../errors';
44
import defaultFirefoxConnector from 'node-firefox-connect';
55

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

811

912
export default function connect(
10-
port: number = 6000,
13+
port: number = REMOTE_PORT,
1114
{connectToFirefox=defaultFirefoxConnector}: Object = {}): Promise {
15+
log.debug(`Connecting to Firefox on port ${port}`);
1216
return connectToFirefox(port)
1317
.then((client) => {
1418
log.info('Connected to the Firefox remote debugger');
@@ -27,6 +31,17 @@ export class RemoteFirefox {
2731
constructor(client: Object) {
2832
this.client = client;
2933
this.checkedForAddonReloading = false;
34+
35+
client.client.on('disconnect', () => {
36+
log.debug('Received "disconnect" from Firefox client');
37+
});
38+
client.client.on('end', () => {
39+
log.debug('Received "end" from Firefox client');
40+
});
41+
client.client.on('message', (info) => {
42+
// These are arbitrary messages that the client library ignores.
43+
log.debug(`Received message from client: ${JSON.stringify(info)}`);
44+
});
3045
}
3146

3247
disconnect() {
@@ -47,6 +62,36 @@ export class RemoteFirefox {
4762
});
4863
}
4964

65+
installTemporaryAddon(addonPath: string) {
66+
return new Promise((resolve, reject) => {
67+
this.client.request('listTabs', (error, response) => {
68+
if (error) {
69+
return reject(new WebExtError(
70+
`Remote Firefox: listTabs() error: ${error}`));
71+
}
72+
if (!response.addonsActor) {
73+
log.debug(
74+
`listTabs returned a falsey addonsActor: ${response.addonsActor}`);
75+
throw new WebExtError(
76+
'This is an older version of Firefox that does not provide an ' +
77+
'add-ons actor for remote control');
78+
}
79+
this.client.client.makeRequest(
80+
{to: response.addonsActor, type: 'installTemporaryAddon', addonPath},
81+
(response) => {
82+
if (response.error) {
83+
return reject(new WebExtError(
84+
'installTemporaryAddon: Error: ' +
85+
`${response.error}: ${response.message}`));
86+
}
87+
log.debug(`installTemporaryAddon: ${JSON.stringify(response)}`);
88+
log.info(`Installed ${addonPath} as a temporary add-on`);
89+
resolve();
90+
});
91+
});
92+
});
93+
}
94+
5095
getInstalledAddon(addonId: string): Promise {
5196
return new Promise(
5297
(resolve, reject) => {

src/watcher.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ export default function onSourceChange(
1313
// TODO: For network disks, we would need to add {poll: true}.
1414
const watcher = new Watchpack();
1515

16-
const onFileChange = (filePath) => {
17-
proxyFileChanges({artifactsDir, onChange, filePath, shouldWatchFile});
18-
};
1916
const executeImmediately = true;
20-
watcher.on('change', debounce(onFileChange, 1000, executeImmediately));
17+
onChange = debounce(onChange, 1000, executeImmediately);
18+
19+
watcher.on('change', (filePath) => {
20+
proxyFileChanges({artifactsDir, onChange, filePath, shouldWatchFile});
21+
});
2122

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

0 commit comments

Comments
 (0)