UNPKG

web-ext-run

Version:

A tool to open and run web extensions

193 lines (187 loc) 6.87 kB
import net from 'net'; import { connectToFirefox as defaultFirefoxConnector } from './rdp-client.js'; import { createLogger } from '../util/logger.js'; import { isErrorWithCode, RemoteTempInstallNotSupported, UsageError, WebExtError } from '../errors.js'; const log = createLogger(import.meta.url); // NOTE: this type aliases Object to catch any other possible response. // Convert a request rejection to a message string. function requestErrorToMessage(err) { if (err instanceof Error) { return String(err); } return `${err.error}: ${err.message}`; } export class RemoteFirefox { client; checkedForAddonReloading; constructor(client) { this.client = client; this.checkedForAddonReloading = false; client.on('disconnect', () => { log.debug('Received "disconnect" from Firefox client'); }); client.on('end', () => { log.debug('Received "end" from Firefox client'); }); client.on('unsolicited-event', info => { log.debug(`Received message from client: ${JSON.stringify(info)}`); }); client.on('rdp-error', rdpError => { log.debug(`Received error from client: ${JSON.stringify(rdpError)}`); }); client.on('error', error => { log.debug(`Received error from client: ${String(error)}`); }); } disconnect() { this.client.disconnect(); } async addonRequest(addon, request) { try { const response = await this.client.request({ to: addon.actor, type: request }); return response; } catch (err) { log.debug(`Client responded to '${request}' request with error:`, err); const message = requestErrorToMessage(err); throw new WebExtError(`Remote Firefox: addonRequest() error: ${message}`); } } async getAddonsActor() { try { // getRoot should work since Firefox 55 (bug 1352157). const response = await this.client.request('getRoot'); if (response.addonsActor == null) { return Promise.reject(new RemoteTempInstallNotSupported('This version of Firefox does not provide an add-ons actor for ' + 'remote installation.')); } return response.addonsActor; } catch (err) { // Fallback to listTabs otherwise, Firefox 49 - 77 (bug 1618691). log.debug('Falling back to listTabs because getRoot failed', err); } try { const response = await this.client.request('listTabs'); // addonsActor was added to listTabs in Firefox 49 (bug 1273183). if (response.addonsActor == null) { log.debug('listTabs returned a falsey addonsActor: ' + `${JSON.stringify(response)}`); return Promise.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.')); } return response.addonsActor; } catch (err) { log.debug('listTabs error', err); const message = requestErrorToMessage(err); throw new WebExtError(`Remote Firefox: listTabs() error: ${message}`); } } async installTemporaryAddon(addonPath, openDevTools) { const addonsActor = await this.getAddonsActor(); try { const response = await this.client.request({ to: addonsActor, type: 'installTemporaryAddon', addonPath, openDevTools }); log.debug(`installTemporaryAddon: ${JSON.stringify(response)}`); log.info(`Installed ${addonPath} as a temporary add-on`); return response; } catch (err) { const message = requestErrorToMessage(err); throw new WebExtError(`installTemporaryAddon: Error: ${message}`); } } async getInstalledAddon(addonId) { try { const response = await this.client.request('listAddons'); for (const addon of response.addons) { if (addon.id === addonId) { return addon; } } log.debug(`Remote Firefox has these addons: ${response.addons.map(a => a.id)}`); return Promise.reject(new WebExtError('The remote Firefox does not have your extension installed')); } catch (err) { const message = requestErrorToMessage(err); throw new WebExtError(`Remote Firefox: listAddons() error: ${message}`); } } async checkForAddonReloading(addon) { if (this.checkedForAddonReloading) { // We only need to check once if reload() is supported. return addon; } else { const response = await this.addonRequest(addon, 'requestTypes'); if (response.requestTypes.indexOf('reload') === -1) { const supportedRequestTypes = JSON.stringify(response.requestTypes); log.debug(`Remote Firefox only supports: ${supportedRequestTypes}`); throw new UsageError('This Firefox version does not support add-on reloading. ' + 'Re-run with --no-reload'); } else { this.checkedForAddonReloading = true; return addon; } } } async reloadAddon(addonId) { const addon = await this.getInstalledAddon(addonId); await this.checkForAddonReloading(addon); await this.addonRequest(addon, 'reload'); process.stdout.write(`\rLast extension reload: ${new Date().toTimeString()}`); log.debug('\n'); } } // Connect types and implementation export async function connect(port, { connectToFirefox = defaultFirefoxConnector } = {}) { log.debug(`Connecting to Firefox on port ${port}`); const client = await connectToFirefox(port); log.debug(`Connected to the remote Firefox debugger on port ${port}`); return new RemoteFirefox(client); } // ConnectWithMaxRetries types and implementation export async function connectWithMaxRetries( // A max of 250 will try connecting for 30 seconds. { maxRetries = 250, retryInterval = 120, port }, { connectToFirefox = connect } = {}) { async function establishConnection() { var lastError; for (let retries = 0; retries <= maxRetries; retries++) { try { return await connectToFirefox(port); } catch (error) { if (isErrorWithCode('ECONNREFUSED', error)) { // Wait for `retryInterval` ms. await new Promise(resolve => { setTimeout(resolve, retryInterval); }); lastError = error; log.debug(`Retrying Firefox (${retries}); connection error: ${error}`); } else { log.error(error.stack); throw error; } } } log.debug('Connect to Firefox debugger: too many retries'); throw lastError; } log.debug('Connecting to the remote Firefox debugger'); return establishConnection(); } export function findFreeTcpPort() { return new Promise(resolve => { const srv = net.createServer(); srv.listen(0, '127.0.0.1', () => { const freeTcpPort = srv.address().port; srv.close(() => resolve(freeTcpPort)); }); }); } //# sourceMappingURL=remote.js.map