summaryrefslogtreecommitdiffstats
path: root/devtools/client/aboutdebugging/src/modules
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/aboutdebugging/src/modules/client-wrapper.js217
-rw-r--r--devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js46
-rw-r--r--devtools/client/aboutdebugging/src/modules/debug-target-support.js98
-rw-r--r--devtools/client/aboutdebugging/src/modules/extensions-helper.js92
-rw-r--r--devtools/client/aboutdebugging/src/modules/l10n.js12
-rw-r--r--devtools/client/aboutdebugging/src/modules/moz.build16
-rw-r--r--devtools/client/aboutdebugging/src/modules/network-locations.js69
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtime-client-factory.js68
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js101
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js37
-rw-r--r--devtools/client/aboutdebugging/src/modules/usb-runtimes.js122
11 files changed, 878 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/src/modules/client-wrapper.js b/devtools/client/aboutdebugging/src/modules/client-wrapper.js
new file mode 100644
index 0000000000..6d537b60a4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/client-wrapper.js
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ checkVersionCompatibility,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const {
+ RUNTIME_PREFERENCE,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const {
+ WorkersListener,
+} = require("resource://devtools/client/shared/workers-listener.js");
+const RootResourceCommand = require("resource://devtools/shared/commands/root-resource/root-resource-command.js");
+
+const PREF_TYPES = {
+ BOOL: "BOOL",
+};
+
+// Map of preference to preference type.
+const PREF_TO_TYPE = {
+ [RUNTIME_PREFERENCE.CHROME_DEBUG_ENABLED]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.CONNECTION_PROMPT]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.PERMANENT_PRIVATE_BROWSING]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.REMOTE_DEBUG_ENABLED]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.SERVICE_WORKERS_ENABLED]: PREF_TYPES.BOOL,
+};
+
+// Some events are fired by mainRoot rather than client.
+const MAIN_ROOT_EVENTS = ["addonListChanged", "tabListChanged"];
+
+/**
+ * The ClientWrapper class is used to isolate aboutdebugging from the DevTools client API
+ * The modules of about:debugging should never call DevTools client APIs directly.
+ */
+class ClientWrapper {
+ constructor(client) {
+ this.client = client;
+ this.workersListener = new WorkersListener(client.mainRoot);
+ }
+
+ once(evt, listener) {
+ if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.once(evt, listener);
+ } else {
+ this.client.once(evt, listener);
+ }
+ }
+
+ on(evt, listener) {
+ if (evt === "workersUpdated") {
+ this.workersListener.addListener(listener);
+ } else if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.on(evt, listener);
+ } else {
+ this.client.on(evt, listener);
+ }
+ }
+
+ off(evt, listener) {
+ if (evt === "workersUpdated") {
+ this.workersListener.removeListener(listener);
+ } else if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.off(evt, listener);
+ } else {
+ this.client.off(evt, listener);
+ }
+ }
+
+ async getFront(typeName) {
+ return this.client.mainRoot.getFront(typeName);
+ }
+
+ async getDeviceDescription() {
+ const deviceFront = await this.getFront("device");
+ const description = await deviceFront.getDescription();
+
+ // Only expose a specific set of properties.
+ return {
+ canDebugServiceWorkers: description.canDebugServiceWorkers,
+ channel: description.channel,
+ deviceName: description.deviceName,
+ name: description.brandName,
+ os: description.os,
+ version: description.version,
+ };
+ }
+
+ createRootResourceCommand() {
+ return new RootResourceCommand({ rootFront: this.client.mainRoot });
+ }
+
+ async checkVersionCompatibility() {
+ return checkVersionCompatibility(this.client);
+ }
+
+ async setPreference(prefName, value) {
+ const prefType = PREF_TO_TYPE[prefName];
+ const preferenceFront = await this.client.mainRoot.getFront("preference");
+ switch (prefType) {
+ case PREF_TYPES.BOOL:
+ return preferenceFront.setBoolPref(prefName, value);
+ default:
+ throw new Error("Unsupported preference" + prefName);
+ }
+ }
+
+ async getPreference(prefName, defaultValue) {
+ if (typeof defaultValue === "undefined") {
+ throw new Error(
+ "Default value is mandatory for getPreference, the actor will " +
+ "throw if the preference is not set on the target runtime"
+ );
+ }
+
+ const prefType = PREF_TO_TYPE[prefName];
+ const preferenceFront = await this.client.mainRoot.getFront("preference");
+ switch (prefType) {
+ case PREF_TYPES.BOOL:
+ // TODO: Add server-side trait and methods to pass a default value to getBoolPref.
+ // See Bug 1522588.
+ let prefValue;
+ try {
+ prefValue = await preferenceFront.getBoolPref(prefName);
+ } catch (e) {
+ prefValue = defaultValue;
+ }
+ return prefValue;
+ default:
+ throw new Error("Unsupported preference:" + prefName);
+ }
+ }
+
+ async listTabs() {
+ return this.client.mainRoot.listTabs();
+ }
+
+ async listAddons(options) {
+ return this.client.mainRoot.listAddons(options);
+ }
+
+ async getAddon({ id }) {
+ return this.client.mainRoot.getAddon({ id });
+ }
+
+ async uninstallAddon({ id }) {
+ const addonsFront = await this.getFront("addons");
+ return addonsFront.uninstallAddon(id);
+ }
+
+ async getMainProcess() {
+ return this.client.mainRoot.getMainProcess();
+ }
+
+ async getServiceWorkerFront({ id }) {
+ return this.client.mainRoot.getWorker(id);
+ }
+
+ async listWorkers() {
+ const { other, service, shared } =
+ await this.client.mainRoot.listAllWorkers();
+
+ return {
+ otherWorkers: other,
+ serviceWorkers: service,
+ sharedWorkers: shared,
+ };
+ }
+
+ async close() {
+ return this.client.close();
+ }
+
+ isClosed() {
+ return this.client._transportClosed;
+ }
+
+ // This method will be mocked to return a dummy URL during mochitests
+ getPerformancePanelUrl() {
+ return "chrome://devtools/content/performance-new/panel/index.xhtml";
+ }
+
+ /**
+ * @param {Window} win - The window of the dialog window.
+ * @param {Function} openAboutProfiling
+ */
+ async loadPerformanceProfiler(win, openAboutProfiling) {
+ const perfFront = await this.getFront("perf");
+ const { traits } = this.client;
+ await win.gInit(perfFront, traits, "devtools-remote", openAboutProfiling);
+ }
+
+ /**
+ * @param {Window} win - The window of the dialog window.
+ * @param {Function} openRemoteDevTools
+ */
+ async loadAboutProfiling(win, openRemoteDevTools) {
+ const perfFront = await this.getFront("perf");
+ const isSupportedPlatform = await perfFront.isSupportedPlatform();
+ const supportedFeatures = await perfFront.getSupportedFeatures();
+ await win.gInit(
+ "aboutprofiling-remote",
+ isSupportedPlatform,
+ supportedFeatures,
+ openRemoteDevTools
+ );
+ }
+
+ get traits() {
+ return { ...this.client.mainRoot.traits };
+ }
+}
+
+exports.ClientWrapper = ClientWrapper;
diff --git a/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js b/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js
new file mode 100644
index 0000000000..efba47e03a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PREF_PREFIX = "devtools.aboutdebugging.collapsibilities.";
+const {
+ DEBUG_TARGET_PANE,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This module provides a collection of helper methods to read and update the debug
+ * target pane's collapsibilities.
+ */
+
+/**
+ * @return {Object}
+ * {
+ * key: constants.DEBUG_TARGET_PANE
+ * value: true - collapsed
+ * false - expanded
+ * }
+ */
+function getDebugTargetCollapsibilities() {
+ const map = new Map();
+
+ for (const key of Object.values(DEBUG_TARGET_PANE)) {
+ const pref = Services.prefs.getBoolPref(PREF_PREFIX + key, false);
+ map.set(key, pref);
+ }
+
+ return map;
+}
+exports.getDebugTargetCollapsibilities = getDebugTargetCollapsibilities;
+
+/**
+ * @param collapsibilities - Same format to getDebugTargetCollapsibilities.
+ */
+function setDebugTargetCollapsibilities(collapsibilities) {
+ for (const key of Object.values(DEBUG_TARGET_PANE)) {
+ const isCollapsed = collapsibilities.get(key);
+ Services.prefs.setBoolPref(PREF_PREFIX + key, isCollapsed);
+ }
+}
+exports.setDebugTargetCollapsibilities = setDebugTargetCollapsibilities;
diff --git a/devtools/client/aboutdebugging/src/modules/debug-target-support.js b/devtools/client/aboutdebugging/src/modules/debug-target-support.js
new file mode 100644
index 0000000000..2b8d4afa5b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/debug-target-support.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ DEBUG_TARGET_PANE,
+ PREFERENCES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+// Process target debugging is disabled by default.
+function isProcessDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ PREFERENCES.PROCESS_DEBUGGING_ENABLED,
+ false
+ );
+}
+
+// Local tab debugging is disabled by default.
+function isLocalTabDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ PREFERENCES.LOCAL_TAB_DEBUGGING_ENABLED,
+ false
+ );
+}
+
+// Local process debugging is disabled by default.
+// This preference has no default value in
+// devtools/client/preferences/devtools-client.js
+// because it is only intended for tests.
+function isLocalProcessDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ "devtools.aboutdebugging.test-local-process-debugging",
+ false
+ );
+}
+
+// Installing extensions can be disabled in enterprise policy.
+// Note: Temporary Extensions are only supported when debugging This Firefox, so checking
+// the local preference is acceptable here. If we enable Temporary extensions for remote
+// runtimes, we should retrieve the preference from the target runtime instead.
+function isTemporaryExtensionSupported() {
+ return Services.prefs.getBoolPref(PREFERENCES.XPINSTALL_ENABLED, true);
+}
+
+const ALL_DEBUG_TARGET_PANES = [
+ DEBUG_TARGET_PANE.INSTALLED_EXTENSION,
+ ...(isProcessDebuggingSupported() ? [DEBUG_TARGET_PANE.PROCESSES] : []),
+ DEBUG_TARGET_PANE.OTHER_WORKER,
+ DEBUG_TARGET_PANE.SERVICE_WORKER,
+ DEBUG_TARGET_PANE.SHARED_WORKER,
+ DEBUG_TARGET_PANE.TAB,
+ ...(isTemporaryExtensionSupported()
+ ? [DEBUG_TARGET_PANE.TEMPORARY_EXTENSION]
+ : []),
+];
+
+// All debug target panes (to filter out if any of the panels should be excluded).
+const REMOTE_DEBUG_TARGET_PANES = [...ALL_DEBUG_TARGET_PANES];
+
+const THIS_FIREFOX_DEBUG_TARGET_PANES = ALL_DEBUG_TARGET_PANES
+ // Main process debugging is not available for This Firefox.
+ // At the moment only the main process is listed under processes, so remove the category
+ // for this runtime.
+ .filter(
+ p => p !== DEBUG_TARGET_PANE.PROCESSES || isLocalProcessDebuggingSupported()
+ )
+ // Showing tab targets for This Firefox is behind a preference.
+ .filter(p => p !== DEBUG_TARGET_PANE.TAB || isLocalTabDebuggingSupported());
+
+const SUPPORTED_TARGET_PANE_BY_RUNTIME = {
+ [RUNTIMES.THIS_FIREFOX]: THIS_FIREFOX_DEBUG_TARGET_PANES,
+ [RUNTIMES.USB]: REMOTE_DEBUG_TARGET_PANES,
+ [RUNTIMES.NETWORK]: REMOTE_DEBUG_TARGET_PANES,
+};
+
+/**
+ * A debug target pane is more specialized than a debug target. For instance EXTENSION is
+ * a DEBUG_TARGET but INSTALLED_EXTENSION and TEMPORARY_EXTENSION are DEBUG_TARGET_PANES.
+ */
+function isSupportedDebugTargetPane(runtimeType, debugTargetPaneKey) {
+ return SUPPORTED_TARGET_PANE_BY_RUNTIME[runtimeType].includes(
+ debugTargetPaneKey
+ );
+}
+exports.isSupportedDebugTargetPane = isSupportedDebugTargetPane;
+
+/**
+ * Check if the given runtimeType supports temporary extension installation
+ * from about:debugging (currently disallowed on non-local runtimes).
+ */
+function supportsTemporaryExtensionInstaller(runtimeType) {
+ return runtimeType === RUNTIMES.THIS_FIREFOX;
+}
+exports.supportsTemporaryExtensionInstaller =
+ supportsTemporaryExtensionInstaller;
diff --git a/devtools/client/aboutdebugging/src/modules/extensions-helper.js b/devtools/client/aboutdebugging/src/modules/extensions-helper.js
new file mode 100644
index 0000000000..ec0d7c2661
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/extensions-helper.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+const {
+ PREFERENCES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+exports.parseFileUri = function (url) {
+ // Strip a leading slash from Windows drive letter URIs.
+ // file:///home/foo ~> /home/foo
+ // file:///C:/foo ~> C:/foo
+ const windowsRegex = /^file:\/\/\/([a-zA-Z]:\/.*)/;
+ if (windowsRegex.test(url)) {
+ return windowsRegex.exec(url)[1];
+ }
+ return url.slice("file://".length);
+};
+
+exports.getExtensionUuid = function (extension) {
+ const { manifestURL } = extension;
+ // Strip off the protocol and rest, leaving us with just the UUID.
+ return manifestURL ? /moz-extension:\/\/([^/]*)/.exec(manifestURL)[1] : null;
+};
+
+/**
+ * Open a file picker to allow the user to locate a temporary extension. A temporary
+ * extension can either be:
+ * - a folder
+ * - a .xpi file
+ * - a .zip file
+ *
+ * @param {Window} win
+ * The window object where the filepicker should be opened.
+ * Note: We cannot use the global window object here because it is undefined if
+ * this module is loaded from a file outside of devtools/client/aboutdebugging/.
+ * See browser-loader.js `uri.startsWith(baseURI)` for more details.
+ * @param {String} message
+ * The help message that should be displayed to the user in the filepicker.
+ * @return {Promise} returns a promise that resolves a File object corresponding to the
+ * file selected by the user.
+ */
+exports.openTemporaryExtension = function (win, message) {
+ return new Promise(resolve => {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(win, message, Ci.nsIFilePicker.modeOpen);
+
+ // Try to set the last directory used as "displayDirectory".
+ try {
+ const lastDirPath = Services.prefs.getCharPref(
+ PREFERENCES.TEMPORARY_EXTENSION_PATH,
+ ""
+ );
+ const lastDir = new lazy.FileUtils.File(lastDirPath);
+ fp.displayDirectory = lastDir;
+ } catch (e) {
+ // Empty or invalid value, nothing to handle.
+ }
+
+ fp.open(res => {
+ if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
+ return;
+ }
+ let file = fp.file;
+ // AddonManager.installTemporaryAddon accepts either
+ // addon directory or final xpi file.
+ if (
+ !file.isDirectory() &&
+ !file.leafName.endsWith(".xpi") &&
+ !file.leafName.endsWith(".zip")
+ ) {
+ file = file.parent;
+ }
+
+ // We are about to resolve, store the path to the file for the next call.
+ Services.prefs.setCharPref(
+ PREFERENCES.TEMPORARY_EXTENSION_PATH,
+ file.path
+ );
+
+ resolve(file);
+ });
+ });
+};
diff --git a/devtools/client/aboutdebugging/src/modules/l10n.js b/devtools/client/aboutdebugging/src/modules/l10n.js
new file mode 100644
index 0000000000..88c7ae8bb1
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/l10n.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ FluentL10n,
+} = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");
+
+// exports a singleton, which will be used across all aboutdebugging modules
+exports.l10n = new FluentL10n();
diff --git a/devtools/client/aboutdebugging/src/modules/moz.build b/devtools/client/aboutdebugging/src/modules/moz.build
new file mode 100644
index 0000000000..909e181714
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/moz.build
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "client-wrapper.js",
+ "debug-target-collapsibilities.js",
+ "debug-target-support.js",
+ "extensions-helper.js",
+ "l10n.js",
+ "network-locations.js",
+ "runtime-client-factory.js",
+ "runtime-default-preferences.js",
+ "runtimes-state-helper.js",
+ "usb-runtimes.js",
+)
diff --git a/devtools/client/aboutdebugging/src/modules/network-locations.js b/devtools/client/aboutdebugging/src/modules/network-locations.js
new file mode 100644
index 0000000000..cbae436df7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/network-locations.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const NETWORK_LOCATIONS_PREF = "devtools.aboutdebugging.network-locations";
+
+/**
+ * This module provides a collection of helper methods to read and update network
+ * locations monitored by about-debugging.
+ */
+
+function addNetworkLocationsObserver(listener) {
+ Services.prefs.addObserver(NETWORK_LOCATIONS_PREF, listener);
+}
+exports.addNetworkLocationsObserver = addNetworkLocationsObserver;
+
+function removeNetworkLocationsObserver(listener) {
+ Services.prefs.removeObserver(NETWORK_LOCATIONS_PREF, listener);
+}
+exports.removeNetworkLocationsObserver = removeNetworkLocationsObserver;
+
+/**
+ * Read the current preference value for aboutdebugging network locations.
+ * Will throw if the value cannot be parsed or is not an array.
+ */
+function _parsePreferenceAsArray() {
+ const pref = Services.prefs.getStringPref(NETWORK_LOCATIONS_PREF, "[]");
+ const parsedValue = JSON.parse(pref);
+ if (!Array.isArray(parsedValue)) {
+ throw new Error("Expected array value in " + NETWORK_LOCATIONS_PREF);
+ }
+ return parsedValue;
+}
+
+function getNetworkLocations() {
+ try {
+ return _parsePreferenceAsArray();
+ } catch (e) {
+ Services.prefs.clearUserPref(NETWORK_LOCATIONS_PREF);
+ return [];
+ }
+}
+exports.getNetworkLocations = getNetworkLocations;
+
+function addNetworkLocation(location) {
+ const locations = getNetworkLocations();
+ const locationsSet = new Set(locations);
+ locationsSet.add(location);
+
+ Services.prefs.setStringPref(
+ NETWORK_LOCATIONS_PREF,
+ JSON.stringify([...locationsSet])
+ );
+}
+exports.addNetworkLocation = addNetworkLocation;
+
+function removeNetworkLocation(location) {
+ const locations = getNetworkLocations();
+ const locationsSet = new Set(locations);
+ locationsSet.delete(location);
+
+ Services.prefs.setStringPref(
+ NETWORK_LOCATIONS_PREF,
+ JSON.stringify([...locationsSet])
+ );
+}
+exports.removeNetworkLocation = removeNetworkLocation;
diff --git a/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js b/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js
new file mode 100644
index 0000000000..e553416c18
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ prepareTCPConnection,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/index.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ ClientWrapper,
+} = require("resource://devtools/client/aboutdebugging/src/modules/client-wrapper.js");
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+async function createLocalClient() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+ return new ClientWrapper(client);
+}
+
+async function createNetworkClient(host, port) {
+ const transport = await DevToolsClient.socketConnect({ host, port });
+ const client = new DevToolsClient(transport);
+ await client.connect();
+ return new ClientWrapper(client);
+}
+
+async function createUSBClient(deviceId, socketPath) {
+ const port = await prepareTCPConnection(deviceId, socketPath);
+ return createNetworkClient("localhost", port);
+}
+
+async function createClientForRuntime(runtime) {
+ const { extra, id, type } = runtime;
+
+ if (type === RUNTIMES.THIS_FIREFOX) {
+ return createLocalClient();
+ } else if (remoteClientManager.hasClient(id, type)) {
+ const client = remoteClientManager.getClient(id, type);
+ return new ClientWrapper(client);
+ } else if (type === RUNTIMES.NETWORK) {
+ const { host, port } = extra.connectionParameters;
+ return createNetworkClient(host, port);
+ } else if (type === RUNTIMES.USB) {
+ const { deviceId, socketPath } = extra.connectionParameters;
+ return createUSBClient(deviceId, socketPath);
+ }
+
+ return null;
+}
+
+exports.createClientForRuntime = createClientForRuntime;
diff --git a/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js b/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js
new file mode 100644
index 0000000000..02c06334f7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module provides a workaround for remote debugging when a preference is
+ * defined in the firefox preference file (browser/app/profile/firefox.js) but
+ * still read from the server, without any default value.
+ *
+ * This causes the server to crash and can't easily be recovered.
+ *
+ * While we work on better linting to prevent such issues (Bug 1660182), this
+ * module will be able to set default values for all missing preferences.
+ */
+
+const PREFERENCE_TYPES = {
+ BOOL: "BOOL",
+ CHAR: "CHAR",
+ INT: "INT",
+};
+exports.PREFERENCE_TYPES = PREFERENCE_TYPES;
+
+/**
+ * Expected properties for the preference descriptors:
+ * - prefName {String}: the name of the preference.
+ * - defaultValue {String|Bool|Number}: the value to set if the preference is
+ * missing.
+ * - trait {String}: the name of the trait corresponding to this pref on the
+ * PreferenceFront.
+ * - type {String}: the preference type (either BOOL, CHAR or INT).
+ */
+const DEFAULT_PREFERENCES = [];
+exports.DEFAULT_PREFERENCES = DEFAULT_PREFERENCES;
+
+const METHODS = {
+ [PREFERENCE_TYPES.BOOL]: {
+ setPref: "setBoolPref",
+ getPref: "getBoolPref",
+ },
+ [PREFERENCE_TYPES.CHAR]: {
+ setPref: "setCharPref",
+ getPref: "getCharPref",
+ },
+ [PREFERENCE_TYPES.INT]: {
+ setPref: "setIntPref",
+ getPref: "getIntPref",
+ },
+};
+
+/**
+ * Set default values for all the provided preferences on the runtime
+ * corresponding to the provided clientWrapper, if needed.
+ *
+ * Note: prefDescriptors will most likely be DEFAULT_PREFERENCES when
+ * used in production code, but can be parameterized for tests.
+ *
+ * @param {ClientWrapper} clientWrapper
+ * @param {Array} prefDescriptors
+ * Array of preference descriptors, see DEFAULT_PREFERENCES.
+ */
+async function setDefaultPreferencesIfNeeded(clientWrapper, prefDescriptors) {
+ if (!prefDescriptors || prefDescriptors.length === 0) {
+ return;
+ }
+
+ const preferenceFront = await clientWrapper.getFront("preference");
+ const preferenceTraits = await preferenceFront.getTraits();
+
+ // Note: using Promise.all here fails because the request/responses get mixed.
+ for (const prefDescriptor of prefDescriptors) {
+ // If the fix for this preference is already on this server, skip it.
+ if (preferenceTraits[prefDescriptor.trait]) {
+ continue;
+ }
+
+ await setDefaultPreference(preferenceFront, prefDescriptor);
+ }
+}
+exports.setDefaultPreferencesIfNeeded = setDefaultPreferencesIfNeeded;
+
+async function setDefaultPreference(preferenceFront, prefDescriptor) {
+ const { prefName, type, defaultValue } = prefDescriptor;
+
+ if (!Object.values(PREFERENCE_TYPES).includes(type)) {
+ throw new Error(`Unsupported type for setDefaultPreference "${type}"`);
+ }
+
+ const prefMethods = METHODS[type];
+ try {
+ // Try to read the preference only to check if the call is successful.
+ // If not, this means the preference is missing and should be initialized.
+ await preferenceFront[prefMethods.getPref](prefName);
+ } catch (e) {
+ console.warn(
+ `Preference "${prefName}"" is not set on the remote runtime. Setting default value.`
+ );
+ await preferenceFront[prefMethods.setPref](prefName, defaultValue);
+ }
+}
diff --git a/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js b/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js
new file mode 100644
index 0000000000..1864ef8341
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function getCurrentRuntime(runtimesState) {
+ const selectedRuntimeId = runtimesState.selectedRuntimeId;
+ return findRuntimeById(selectedRuntimeId, runtimesState);
+}
+exports.getCurrentRuntime = getCurrentRuntime;
+
+function getCurrentClient(runtimesState) {
+ const runtimeDetails = getCurrentRuntimeDetails(runtimesState);
+ return runtimeDetails ? runtimeDetails.clientWrapper : null;
+}
+exports.getCurrentClient = getCurrentClient;
+
+function findRuntimeById(id, runtimesState) {
+ return getAllRuntimes(runtimesState).find(r => r.id === id);
+}
+exports.findRuntimeById = findRuntimeById;
+
+function getAllRuntimes(runtimesState) {
+ return [
+ ...runtimesState.networkRuntimes,
+ ...runtimesState.thisFirefoxRuntimes,
+ ...runtimesState.usbRuntimes,
+ ];
+}
+exports.getAllRuntimes = getAllRuntimes;
+
+function getCurrentRuntimeDetails(runtimesState) {
+ const runtime = getCurrentRuntime(runtimesState);
+ return runtime ? runtime.runtimeDetails : null;
+}
+exports.getCurrentRuntimeDetails = getCurrentRuntimeDetails;
diff --git a/devtools/client/aboutdebugging/src/modules/usb-runtimes.js b/devtools/client/aboutdebugging/src/modules/usb-runtimes.js
new file mode 100644
index 0000000000..887dc6788e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/usb-runtimes.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+loader.lazyRequireGetter(
+ this,
+ "adb",
+ "resource://devtools/client/shared/remote-debugging/adb/adb.js",
+ true
+);
+
+/**
+ * Used to represent a regular runtime returned by ADB.
+ */
+class UsbRuntime {
+ constructor(adbRuntime) {
+ this.id = adbRuntime.id;
+ this.deviceId = adbRuntime.deviceId;
+ this.deviceName = adbRuntime.deviceName;
+ this.shortName = adbRuntime.shortName;
+ this.socketPath = adbRuntime.socketPath;
+ this.isFenix = adbRuntime.isFenix;
+ this.isUnavailable = false;
+ this.isUnplugged = false;
+ this.versionName = adbRuntime.versionName;
+ }
+}
+
+/**
+ * Used when a device was detected, meaning USB debugging is enabled on the device, but no
+ * runtime/browser is available for connection.
+ */
+class UnavailableUsbRuntime {
+ constructor(adbDevice) {
+ this.id = adbDevice.id + "|unavailable";
+ this.deviceId = adbDevice.id;
+ this.deviceName = adbDevice.name;
+ this.shortName = "Unavailable runtime";
+ this.socketPath = null;
+ this.isFenix = false;
+ this.isUnavailable = true;
+ this.isUnplugged = false;
+ this.versionName = null;
+ }
+}
+
+/**
+ * Used to represent USB devices that were previously connected but are now missing
+ * (presumably after being unplugged/disconnected from the computer).
+ */
+class UnpluggedUsbRuntime {
+ constructor(deviceId, deviceName) {
+ this.id = deviceId + "|unplugged";
+ this.deviceId = deviceId;
+ this.deviceName = deviceName;
+ this.shortName = "Unplugged runtime";
+ this.socketPath = null;
+ this.isFenix = false;
+ this.isUnavailable = true;
+ this.isUnplugged = true;
+ this.versionName = null;
+ }
+}
+
+/**
+ * Map used to keep track of discovered usb devices. Will be used to create the unplugged
+ * usb runtimes.
+ */
+const devices = new Map();
+
+/**
+ * This module provides a collection of helper methods to detect USB runtimes whom Firefox
+ * is running on.
+ */
+function addUSBRuntimesObserver(listener) {
+ adb.registerListener(listener);
+}
+exports.addUSBRuntimesObserver = addUSBRuntimesObserver;
+
+async function getUSBRuntimes() {
+ // Get the available runtimes
+ const runtimes = adb.getRuntimes().map(r => new UsbRuntime(r));
+
+ // Get devices found by ADB, but without any available runtime.
+ const runtimeDevices = runtimes.map(r => r.deviceId);
+ const unavailableRuntimes = adb
+ .getDevices()
+ .filter(d => !runtimeDevices.includes(d.id))
+ .map(d => new UnavailableUsbRuntime(d));
+
+ // Add all devices to the map detected devices.
+ const allRuntimes = runtimes.concat(unavailableRuntimes);
+ for (const runtime of allRuntimes) {
+ devices.set(runtime.deviceId, runtime.deviceName);
+ }
+
+ // Get devices previously found by ADB but no longer available.
+ const currentDevices = allRuntimes.map(r => r.deviceId);
+ const detectedDevices = [...devices.keys()];
+ const unpluggedDevices = detectedDevices.filter(
+ id => !currentDevices.includes(id)
+ );
+ const unpluggedRuntimes = unpluggedDevices.map(deviceId => {
+ const deviceName = devices.get(deviceId);
+ return new UnpluggedUsbRuntime(deviceId, deviceName);
+ });
+
+ return allRuntimes.concat(unpluggedRuntimes);
+}
+exports.getUSBRuntimes = getUSBRuntimes;
+
+function removeUSBRuntimesObserver(listener) {
+ adb.unregisterListener(listener);
+}
+exports.removeUSBRuntimesObserver = removeUSBRuntimesObserver;
+
+function refreshUSBRuntimes() {
+ return adb.updateRuntimes();
+}
+exports.refreshUSBRuntimes = refreshUSBRuntimes;