diff options
Diffstat (limited to '')
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; |