summaryrefslogtreecommitdiffstats
path: root/devtools/client/aboutdebugging/src/actions
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/aboutdebugging/src/actions
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/aboutdebugging/src/actions')
-rw-r--r--devtools/client/aboutdebugging/src/actions/debug-targets.js316
-rw-r--r--devtools/client/aboutdebugging/src/actions/index.js12
-rw-r--r--devtools/client/aboutdebugging/src/actions/moz.build11
-rw-r--r--devtools/client/aboutdebugging/src/actions/runtimes.js513
-rw-r--r--devtools/client/aboutdebugging/src/actions/telemetry.js23
-rw-r--r--devtools/client/aboutdebugging/src/actions/ui.js202
6 files changed, 1077 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/src/actions/debug-targets.js b/devtools/client/aboutdebugging/src/actions/debug-targets.js
new file mode 100644
index 0000000000..1d0fcb7da9
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/debug-targets.js
@@ -0,0 +1,316 @@
+/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const {
+ remoteClientManager,
+} = require("devtools/client/shared/remote-debugging/remote-client-manager");
+const Services = require("Services");
+
+const { l10n } = require("devtools/client/aboutdebugging/src/modules/l10n");
+
+const {
+ isSupportedDebugTargetPane,
+} = require("devtools/client/aboutdebugging/src/modules/debug-target-support");
+
+const {
+ openTemporaryExtension,
+ uninstallAddon,
+} = require("devtools/client/aboutdebugging/src/modules/extensions-helper");
+
+const {
+ getCurrentClient,
+ getCurrentRuntime,
+} = require("devtools/client/aboutdebugging/src/modules/runtimes-state-helper");
+
+const {
+ DEBUG_TARGET_PANE,
+ REQUEST_EXTENSIONS_FAILURE,
+ REQUEST_EXTENSIONS_START,
+ REQUEST_EXTENSIONS_SUCCESS,
+ REQUEST_PROCESSES_FAILURE,
+ REQUEST_PROCESSES_START,
+ REQUEST_PROCESSES_SUCCESS,
+ REQUEST_TABS_FAILURE,
+ REQUEST_TABS_START,
+ REQUEST_TABS_SUCCESS,
+ REQUEST_WORKERS_FAILURE,
+ REQUEST_WORKERS_START,
+ REQUEST_WORKERS_SUCCESS,
+ TEMPORARY_EXTENSION_INSTALL_FAILURE,
+ TEMPORARY_EXTENSION_INSTALL_START,
+ TEMPORARY_EXTENSION_INSTALL_SUCCESS,
+ TEMPORARY_EXTENSION_RELOAD_FAILURE,
+ TEMPORARY_EXTENSION_RELOAD_START,
+ TEMPORARY_EXTENSION_RELOAD_SUCCESS,
+ RUNTIMES,
+} = require("devtools/client/aboutdebugging/src/constants");
+
+const Actions = require("devtools/client/aboutdebugging/src/actions/index");
+
+function getTabForUrl(url) {
+ for (const navigator of Services.wm.getEnumerator("navigator:browser")) {
+ for (const browser of navigator.gBrowser.browsers) {
+ if (
+ browser.contentWindow &&
+ browser.contentWindow.location.href === url
+ ) {
+ return navigator.gBrowser.getTabForBrowser(browser);
+ }
+ }
+ }
+
+ return null;
+}
+
+function inspectDebugTarget(type, id) {
+ return async ({ dispatch, getState }) => {
+ const runtime = getCurrentRuntime(getState().runtimes);
+
+ const urlParams = {
+ id,
+ type,
+ };
+
+ if (runtime.id !== RUNTIMES.THIS_FIREFOX) {
+ urlParams.remoteId = remoteClientManager.getRemoteId(
+ runtime.id,
+ runtime.type
+ );
+ }
+
+ const url = `about:devtools-toolbox?${new window.URLSearchParams(
+ urlParams
+ )}`;
+
+ const existingTab = getTabForUrl(url);
+ if (existingTab) {
+ const navigator = existingTab.ownerGlobal;
+ navigator.gBrowser.selectedTab = existingTab;
+ navigator.focus();
+ } else {
+ window.open(url);
+ }
+
+ dispatch(
+ Actions.recordTelemetryEvent("inspect", {
+ target_type: type.toUpperCase(),
+ runtime_type: runtime.type,
+ })
+ );
+ };
+}
+
+function installTemporaryExtension() {
+ const message = l10n.getString(
+ "about-debugging-tmp-extension-install-message"
+ );
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_START });
+ const file = await openTemporaryExtension(window, message);
+ try {
+ await AddonManager.installTemporaryAddon(file);
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function pushServiceWorker(id, registrationFront) {
+ return async ({ dispatch, getState }) => {
+ try {
+ // The push button is only available if canDebugServiceWorkers is true,
+ // which is only true if dom.serviceWorkers.parent_intercept is true.
+ // With this configuration, `push` should always be called on the
+ // registration front, and not on the (service) WorkerTargetActor.
+ await registrationFront.push();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function reloadTemporaryExtension(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_START, id });
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const addonTargetFront = await clientWrapper.getAddon({ id });
+ await addonTargetFront.reload();
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_SUCCESS, id });
+ } catch (e) {
+ const error = typeof e === "string" ? new Error(e) : e;
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_FAILURE, id, error });
+ }
+ };
+}
+
+function removeTemporaryExtension(id) {
+ return async () => {
+ try {
+ await uninstallAddon(id);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function requestTabs() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_TABS_START });
+
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const isSupported = isSupportedDebugTargetPane(
+ runtime.runtimeDetails.info.type,
+ DEBUG_TARGET_PANE.TAB
+ );
+ const tabs = isSupported ? await clientWrapper.listTabs() : [];
+
+ // Fetch the favicon for all tabs.
+ await Promise.all(
+ tabs.map(descriptorFront => descriptorFront.retrieveFavicon())
+ );
+
+ dispatch({ type: REQUEST_TABS_SUCCESS, tabs });
+ } catch (e) {
+ dispatch({ type: REQUEST_TABS_FAILURE, error: e });
+ }
+ };
+}
+
+function requestExtensions() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_EXTENSIONS_START });
+
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const isIconDataURLRequired = runtime.type !== RUNTIMES.THIS_FIREFOX;
+ const addons = await clientWrapper.listAddons({
+ iconDataURL: isIconDataURLRequired,
+ });
+
+ const showHiddenAddons = getState().ui.showHiddenAddons;
+
+ // Filter out non-debuggable addons as well as hidden ones, unless the dedicated
+ // preference is set to true.
+ const extensions = addons.filter(
+ a => a.debuggable && (!a.hidden || showHiddenAddons)
+ );
+
+ const installedExtensions = extensions.filter(
+ e => !e.temporarilyInstalled
+ );
+ const temporaryExtensions = extensions.filter(
+ e => e.temporarilyInstalled
+ );
+
+ dispatch({
+ type: REQUEST_EXTENSIONS_SUCCESS,
+ installedExtensions,
+ temporaryExtensions,
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_EXTENSIONS_FAILURE, error: e });
+ }
+ };
+}
+
+function requestProcesses() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_PROCESSES_START });
+
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const mainProcessDescriptorFront = await clientWrapper.getMainProcess();
+ dispatch({
+ type: REQUEST_PROCESSES_SUCCESS,
+ mainProcess: {
+ id: 0,
+ processFront: mainProcessDescriptorFront,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_PROCESSES_FAILURE, error: e });
+ }
+ };
+}
+
+function requestWorkers() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_WORKERS_START });
+
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const {
+ otherWorkers,
+ serviceWorkers,
+ sharedWorkers,
+ } = await clientWrapper.listWorkers();
+
+ for (const serviceWorker of serviceWorkers) {
+ const { registrationFront } = serviceWorker;
+ if (!registrationFront) {
+ continue;
+ }
+
+ const subscription = await registrationFront.getPushSubscription();
+ serviceWorker.subscription = subscription;
+ }
+
+ dispatch({
+ type: REQUEST_WORKERS_SUCCESS,
+ otherWorkers,
+ serviceWorkers,
+ sharedWorkers,
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_WORKERS_FAILURE, error: e });
+ }
+ };
+}
+
+function startServiceWorker(registrationFront) {
+ return async () => {
+ try {
+ await registrationFront.start();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function unregisterServiceWorker(registrationFront) {
+ return async () => {
+ try {
+ await registrationFront.unregister();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+module.exports = {
+ inspectDebugTarget,
+ installTemporaryExtension,
+ pushServiceWorker,
+ reloadTemporaryExtension,
+ removeTemporaryExtension,
+ requestTabs,
+ requestExtensions,
+ requestProcesses,
+ requestWorkers,
+ startServiceWorker,
+ unregisterServiceWorker,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/index.js b/devtools/client/aboutdebugging/src/actions/index.js
new file mode 100644
index 0000000000..e9dac32eeb
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/index.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 debugTargets = require("devtools/client/aboutdebugging/src/actions/debug-targets");
+const runtimes = require("devtools/client/aboutdebugging/src/actions/runtimes");
+const telemetry = require("devtools/client/aboutdebugging/src/actions/telemetry");
+const ui = require("devtools/client/aboutdebugging/src/actions/ui");
+
+Object.assign(exports, ui, runtimes, telemetry, debugTargets);
diff --git a/devtools/client/aboutdebugging/src/actions/moz.build b/devtools/client/aboutdebugging/src/actions/moz.build
new file mode 100644
index 0000000000..a750640d06
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/moz.build
@@ -0,0 +1,11 @@
+# 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(
+ "debug-targets.js",
+ "index.js",
+ "runtimes.js",
+ "telemetry.js",
+ "ui.js",
+)
diff --git a/devtools/client/aboutdebugging/src/actions/runtimes.js b/devtools/client/aboutdebugging/src/actions/runtimes.js
new file mode 100644
index 0000000000..3d9ce0490b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/runtimes.js
@@ -0,0 +1,513 @@
+/* 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 Services = require("Services");
+
+const Actions = require("devtools/client/aboutdebugging/src/actions/index");
+
+const {
+ getAllRuntimes,
+ getCurrentRuntime,
+ findRuntimeById,
+} = require("devtools/client/aboutdebugging/src/modules/runtimes-state-helper");
+
+const { l10n } = require("devtools/client/aboutdebugging/src/modules/l10n");
+const {
+ setDefaultPreferencesIfNeeded,
+ DEFAULT_PREFERENCES,
+} = require("devtools/client/aboutdebugging/src/modules/runtime-default-preferences");
+const {
+ createClientForRuntime,
+} = require("devtools/client/aboutdebugging/src/modules/runtime-client-factory");
+const {
+ isSupportedDebugTargetPane,
+} = require("devtools/client/aboutdebugging/src/modules/debug-target-support");
+
+const {
+ remoteClientManager,
+} = require("devtools/client/shared/remote-debugging/remote-client-manager");
+
+const {
+ CONNECT_RUNTIME_CANCEL,
+ CONNECT_RUNTIME_FAILURE,
+ CONNECT_RUNTIME_NOT_RESPONDING,
+ CONNECT_RUNTIME_START,
+ CONNECT_RUNTIME_SUCCESS,
+ DEBUG_TARGET_PANE,
+ DISCONNECT_RUNTIME_FAILURE,
+ DISCONNECT_RUNTIME_START,
+ DISCONNECT_RUNTIME_SUCCESS,
+ PAGE_TYPES,
+ REMOTE_RUNTIMES_UPDATED,
+ RUNTIME_PREFERENCE,
+ RUNTIMES,
+ THIS_FIREFOX_RUNTIME_CREATED,
+ UNWATCH_RUNTIME_FAILURE,
+ UNWATCH_RUNTIME_START,
+ UNWATCH_RUNTIME_SUCCESS,
+ UPDATE_CONNECTION_PROMPT_SETTING_FAILURE,
+ UPDATE_CONNECTION_PROMPT_SETTING_START,
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ WATCH_RUNTIME_FAILURE,
+ WATCH_RUNTIME_START,
+ WATCH_RUNTIME_SUCCESS,
+} = require("devtools/client/aboutdebugging/src/constants");
+
+const CONNECTION_TIMING_OUT_DELAY = 3000;
+const CONNECTION_CANCEL_DELAY = 13000;
+
+async function getRuntimeIcon(runtime, channel) {
+ if (runtime.isFenix) {
+ switch (channel) {
+ case "release":
+ case "beta":
+ return "chrome://devtools/skin/images/aboutdebugging-fenix.svg";
+ case "aurora":
+ default:
+ return "chrome://devtools/skin/images/aboutdebugging-fenix-nightly.svg";
+ }
+ }
+
+ return channel === "release" || channel === "beta" || channel === "aurora"
+ ? `chrome://devtools/skin/images/aboutdebugging-firefox-${channel}.svg`
+ : "chrome://devtools/skin/images/aboutdebugging-firefox-nightly.svg";
+}
+
+function onRemoteDevToolsClientClosed() {
+ window.AboutDebugging.onNetworkLocationsUpdated();
+ window.AboutDebugging.onUSBRuntimesUpdated();
+}
+
+function connectRuntime(id) {
+ // Create a random connection id to track the connection attempt in telemetry.
+ const connectionId = (Math.random() * 100000) | 0;
+
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: CONNECT_RUNTIME_START, connectionId, id });
+
+ // The preferences test-connection-timing-out-delay and test-connection-cancel-delay
+ // don't have a default value but will be overridden during our tests.
+ const connectionTimingOutDelay = Services.prefs.getIntPref(
+ "devtools.aboutdebugging.test-connection-timing-out-delay",
+ CONNECTION_TIMING_OUT_DELAY
+ );
+ const connectionCancelDelay = Services.prefs.getIntPref(
+ "devtools.aboutdebugging.test-connection-cancel-delay",
+ CONNECTION_CANCEL_DELAY
+ );
+
+ const connectionNotRespondingTimer = setTimeout(() => {
+ // If connecting to the runtime takes time over CONNECTION_TIMING_OUT_DELAY,
+ // we assume the connection prompt is showing on the runtime, show a dialog
+ // to let user know that.
+ dispatch({ type: CONNECT_RUNTIME_NOT_RESPONDING, connectionId, id });
+ }, connectionTimingOutDelay);
+ const connectionCancelTimer = setTimeout(() => {
+ // Connect button of the runtime will be disabled during connection, but the status
+ // continues till the connection was either succeed or failed. This may have a
+ // possibility that the disabling continues unless page reloading, user will not be
+ // able to click again. To avoid this, revert the connect button status after
+ // CONNECTION_CANCEL_DELAY ms.
+ dispatch({ type: CONNECT_RUNTIME_CANCEL, connectionId, id });
+ }, connectionCancelDelay);
+
+ try {
+ const runtime = findRuntimeById(id, getState().runtimes);
+ const clientWrapper = await createClientForRuntime(runtime);
+
+ await setDefaultPreferencesIfNeeded(clientWrapper, DEFAULT_PREFERENCES);
+
+ const deviceDescription = await clientWrapper.getDeviceDescription();
+ const compatibilityReport = await clientWrapper.checkVersionCompatibility();
+ const icon = await getRuntimeIcon(runtime, deviceDescription.channel);
+
+ const {
+ CONNECTION_PROMPT,
+ PERMANENT_PRIVATE_BROWSING,
+ SERVICE_WORKERS_ENABLED,
+ } = RUNTIME_PREFERENCE;
+ const connectionPromptEnabled = await clientWrapper.getPreference(
+ CONNECTION_PROMPT,
+ false
+ );
+ const privateBrowsing = await clientWrapper.getPreference(
+ PERMANENT_PRIVATE_BROWSING,
+ false
+ );
+ const serviceWorkersEnabled = await clientWrapper.getPreference(
+ SERVICE_WORKERS_ENABLED,
+ true
+ );
+ const serviceWorkersAvailable = serviceWorkersEnabled && !privateBrowsing;
+
+ // Fenix specific workarounds are needed until we can get proper server side APIs
+ // to detect Fenix and get the proper application names and versions.
+ // See https://github.com/mozilla-mobile/fenix/issues/2016.
+
+ // For Fenix runtimes, the ADB runtime name is more accurate than the one returned
+ // by the Device actor.
+ const runtimeName = runtime.isFenix
+ ? runtime.name
+ : deviceDescription.name;
+
+ // For Fenix runtimes, the version we should display is the application version
+ // retrieved from ADB, and not the Gecko version returned by the Device actor.
+ const version = runtime.isFenix
+ ? runtime.extra.adbPackageVersion
+ : deviceDescription.version;
+
+ const runtimeDetails = {
+ canDebugServiceWorkers: deviceDescription.canDebugServiceWorkers,
+ clientWrapper,
+ compatibilityReport,
+ connectionPromptEnabled,
+ info: {
+ deviceName: deviceDescription.deviceName,
+ icon,
+ name: runtimeName,
+ os: deviceDescription.os,
+ type: runtime.type,
+ version,
+ },
+ serviceWorkersAvailable,
+ };
+
+ if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
+ // `closed` event will be emitted when disabling remote debugging
+ // on the connected remote runtime.
+ clientWrapper.once("closed", onRemoteDevToolsClientClosed);
+ }
+
+ dispatch({
+ type: CONNECT_RUNTIME_SUCCESS,
+ connectionId,
+ runtime: {
+ id,
+ runtimeDetails,
+ type: runtime.type,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: CONNECT_RUNTIME_FAILURE, connectionId, id, error: e });
+ } finally {
+ clearTimeout(connectionNotRespondingTimer);
+ clearTimeout(connectionCancelTimer);
+ }
+ };
+}
+
+function createThisFirefoxRuntime() {
+ return ({ dispatch, getState }) => {
+ const thisFirefoxRuntime = {
+ id: RUNTIMES.THIS_FIREFOX,
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isUnavailable: false,
+ isUnplugged: false,
+ name: l10n.getString("about-debugging-this-firefox-runtime-name"),
+ type: RUNTIMES.THIS_FIREFOX,
+ };
+ dispatch({
+ type: THIS_FIREFOX_RUNTIME_CREATED,
+ runtime: thisFirefoxRuntime,
+ });
+ };
+}
+
+function disconnectRuntime(id, shouldRedirect = false) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: DISCONNECT_RUNTIME_START });
+ try {
+ const runtime = findRuntimeById(id, getState().runtimes);
+ const { clientWrapper } = runtime.runtimeDetails;
+
+ if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
+ clientWrapper.off("closed", onRemoteDevToolsClientClosed);
+ }
+ await clientWrapper.close();
+ if (shouldRedirect) {
+ await dispatch(
+ Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
+ );
+ }
+
+ dispatch({
+ type: DISCONNECT_RUNTIME_SUCCESS,
+ runtime: {
+ id,
+ type: runtime.type,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: DISCONNECT_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function updateConnectionPromptSetting(connectionPromptEnabled) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_START });
+ try {
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const { clientWrapper } = runtime.runtimeDetails;
+ const promptPrefName = RUNTIME_PREFERENCE.CONNECTION_PROMPT;
+ await clientWrapper.setPreference(
+ promptPrefName,
+ connectionPromptEnabled
+ );
+ // Re-get actual value from the runtime.
+ connectionPromptEnabled = await clientWrapper.getPreference(
+ promptPrefName,
+ connectionPromptEnabled
+ );
+
+ dispatch({
+ type: UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ connectionPromptEnabled,
+ runtime,
+ });
+ } catch (e) {
+ dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_FAILURE, error: e });
+ }
+ };
+}
+
+function watchRuntime(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: WATCH_RUNTIME_START });
+
+ try {
+ if (id === RUNTIMES.THIS_FIREFOX) {
+ // THIS_FIREFOX connects and disconnects on the fly when opening the page.
+ await dispatch(connectRuntime(RUNTIMES.THIS_FIREFOX));
+ }
+
+ // The selected runtime should already have a connected client assigned.
+ const runtime = findRuntimeById(id, getState().runtimes);
+ await dispatch({ type: WATCH_RUNTIME_SUCCESS, runtime });
+
+ dispatch(Actions.requestExtensions());
+ // we have to wait for tabs, otherwise the requests to getTarget may interfer
+ // with listProcesses
+ await dispatch(Actions.requestTabs());
+ dispatch(Actions.requestWorkers());
+
+ if (
+ isSupportedDebugTargetPane(
+ runtime.runtimeDetails.info.type,
+ DEBUG_TARGET_PANE.PROCESSES
+ )
+ ) {
+ dispatch(Actions.requestProcesses());
+ }
+ } catch (e) {
+ dispatch({ type: WATCH_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function unwatchRuntime(id) {
+ return async ({ dispatch, getState }) => {
+ const runtime = findRuntimeById(id, getState().runtimes);
+
+ dispatch({ type: UNWATCH_RUNTIME_START, runtime });
+
+ try {
+ if (id === RUNTIMES.THIS_FIREFOX) {
+ // THIS_FIREFOX connects and disconnects on the fly when opening the page.
+ await dispatch(disconnectRuntime(RUNTIMES.THIS_FIREFOX));
+ }
+
+ dispatch({ type: UNWATCH_RUNTIME_SUCCESS });
+ } catch (e) {
+ dispatch({ type: UNWATCH_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function updateNetworkRuntimes(locations) {
+ const runtimes = locations.map(location => {
+ const [host, port] = location.split(":");
+ return {
+ id: location,
+ extra: {
+ connectionParameters: { host, port: parseInt(port, 10) },
+ },
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isFenix: false,
+ isUnavailable: false,
+ isUnplugged: false,
+ isUnknown: false,
+ name: location,
+ type: RUNTIMES.NETWORK,
+ };
+ });
+ return updateRemoteRuntimes(runtimes, RUNTIMES.NETWORK);
+}
+
+function updateUSBRuntimes(adbRuntimes) {
+ const runtimes = adbRuntimes.map(adbRuntime => {
+ // Set connectionParameters only for known runtimes.
+ const socketPath = adbRuntime.socketPath;
+ const deviceId = adbRuntime.deviceId;
+ const connectionParameters = socketPath ? { deviceId, socketPath } : null;
+ return {
+ id: adbRuntime.id,
+ extra: {
+ connectionParameters,
+ deviceName: adbRuntime.deviceName,
+ adbPackageVersion: adbRuntime.versionName,
+ },
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isFenix: adbRuntime.isFenix,
+ isUnavailable: adbRuntime.isUnavailable,
+ isUnplugged: adbRuntime.isUnplugged,
+ name: adbRuntime.shortName,
+ type: RUNTIMES.USB,
+ };
+ });
+ return updateRemoteRuntimes(runtimes, RUNTIMES.USB);
+}
+
+/**
+ * Check that a given runtime can still be found in the provided array of runtimes, and
+ * that the connection of the associated DevToolsClient is still valid.
+ * Note that this check is only valid for runtimes which match the type of the runtimes
+ * in the array.
+ */
+function _isRuntimeValid(runtime, runtimes) {
+ const isRuntimeAvailable = runtimes.some(r => r.id === runtime.id);
+ const isConnectionValid =
+ runtime.runtimeDetails && !runtime.runtimeDetails.clientWrapper.isClosed();
+ return isRuntimeAvailable && isConnectionValid;
+}
+
+function updateRemoteRuntimes(runtimes, type) {
+ return async ({ dispatch, getState }) => {
+ const currentRuntime = getCurrentRuntime(getState().runtimes);
+
+ // Check if the updated remote runtimes should trigger a navigation out of the current
+ // runtime page.
+ if (
+ currentRuntime &&
+ currentRuntime.type === type &&
+ !_isRuntimeValid(currentRuntime, runtimes)
+ ) {
+ // Since current remote runtime is invalid, move to this firefox page.
+ // This case is considered as followings and so on:
+ // * Remove ADB addon
+ // * (Physically) Disconnect USB runtime
+ //
+ // The reason we call selectPage before REMOTE_RUNTIMES_UPDATED is fired is below.
+ // Current runtime can not be retrieved after REMOTE_RUNTIMES_UPDATED action, since
+ // that updates runtime state. So, before that we fire selectPage action to execute
+ // `unwatchRuntime` correctly.
+ await dispatch(
+ Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
+ );
+ }
+
+ // For existing runtimes, transfer all properties that are not available in the
+ // runtime objects passed to this method:
+ // - runtimeDetails (set by about:debugging after a successful connection)
+ // - isConnecting (set by about:debugging during the connection)
+ // - isConnectionFailed (set by about:debugging if connection was failed)
+ // - isConnectionNotResponding
+ // (set by about:debugging if connection is taking too much time)
+ // - isConnectionTimeout (set by about:debugging if connection was timeout)
+ runtimes.forEach(runtime => {
+ const existingRuntime = findRuntimeById(runtime.id, getState().runtimes);
+ const isConnectionValid =
+ existingRuntime?.runtimeDetails &&
+ !existingRuntime.runtimeDetails.clientWrapper.isClosed();
+ runtime.runtimeDetails = isConnectionValid
+ ? existingRuntime.runtimeDetails
+ : null;
+ runtime.isConnecting = existingRuntime
+ ? existingRuntime.isConnecting
+ : false;
+ runtime.isConnectionFailed = existingRuntime
+ ? existingRuntime.isConnectionFailed
+ : false;
+ runtime.isConnectionNotResponding = existingRuntime
+ ? existingRuntime.isConnectionNotResponding
+ : false;
+ runtime.isConnectionTimeout = existingRuntime
+ ? existingRuntime.isConnectionTimeout
+ : false;
+ });
+
+ const existingRuntimes = getAllRuntimes(getState().runtimes);
+ for (const runtime of existingRuntimes) {
+ // Runtime was connected before.
+ const isConnected = runtime.runtimeDetails;
+ // Runtime is of the same type as the updated runtimes array, so we should check it.
+ const isSameType = runtime.type === type;
+ if (isConnected && isSameType && !_isRuntimeValid(runtime, runtimes)) {
+ // Disconnect runtimes that were no longer valid.
+ await dispatch(disconnectRuntime(runtime.id));
+ }
+ }
+
+ dispatch({ type: REMOTE_RUNTIMES_UPDATED, runtimes, runtimeType: type });
+
+ for (const runtime of getAllRuntimes(getState().runtimes)) {
+ if (runtime.type !== type) {
+ continue;
+ }
+
+ // Reconnect clients already available in the RemoteClientManager.
+ const isConnected = !!runtime.runtimeDetails;
+ const hasConnectedClient = remoteClientManager.hasClient(
+ runtime.id,
+ runtime.type
+ );
+ if (!isConnected && hasConnectedClient) {
+ await dispatch(connectRuntime(runtime.id));
+ }
+ }
+ };
+}
+
+/**
+ * Remove all the listeners added on client objects. Since those objects are persisted
+ * regardless of the about:debugging lifecycle, all the added events should be removed
+ * before leaving about:debugging.
+ */
+function removeRuntimeListeners() {
+ return ({ dispatch, getState }) => {
+ const allRuntimes = getAllRuntimes(getState().runtimes);
+ const remoteRuntimes = allRuntimes.filter(
+ r => r.type !== RUNTIMES.THIS_FIREFOX
+ );
+ for (const runtime of remoteRuntimes) {
+ if (runtime.runtimeDetails) {
+ const { clientWrapper } = runtime.runtimeDetails;
+ clientWrapper.off("closed", onRemoteDevToolsClientClosed);
+ }
+ }
+ };
+}
+
+module.exports = {
+ connectRuntime,
+ createThisFirefoxRuntime,
+ disconnectRuntime,
+ removeRuntimeListeners,
+ unwatchRuntime,
+ updateConnectionPromptSetting,
+ updateNetworkRuntimes,
+ updateUSBRuntimes,
+ watchRuntime,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/telemetry.js b/devtools/client/aboutdebugging/src/actions/telemetry.js
new file mode 100644
index 0000000000..ef69bdcfd0
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/telemetry.js
@@ -0,0 +1,23 @@
+/* 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 {
+ TELEMETRY_RECORD,
+} = require("devtools/client/aboutdebugging/src/constants");
+
+/**
+ * If a given event cannot be mapped to an existing action, use this action that will only
+ * be processed by the event recording middleware.
+ */
+function recordTelemetryEvent(method, details) {
+ return ({ dispatch, getState }) => {
+ dispatch({ type: TELEMETRY_RECORD, method, details });
+ };
+}
+
+module.exports = {
+ recordTelemetryEvent,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/ui.js b/devtools/client/aboutdebugging/src/actions/ui.js
new file mode 100644
index 0000000000..e99786781c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/ui.js
@@ -0,0 +1,202 @@
+/* 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 {
+ ADB_ADDON_INSTALL_START,
+ ADB_ADDON_INSTALL_SUCCESS,
+ ADB_ADDON_INSTALL_FAILURE,
+ ADB_ADDON_UNINSTALL_START,
+ ADB_ADDON_UNINSTALL_SUCCESS,
+ ADB_ADDON_UNINSTALL_FAILURE,
+ ADB_ADDON_STATUS_UPDATED,
+ ADB_READY_UPDATED,
+ DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
+ HIDE_PROFILER_DIALOG,
+ NETWORK_LOCATIONS_UPDATE_FAILURE,
+ NETWORK_LOCATIONS_UPDATE_START,
+ NETWORK_LOCATIONS_UPDATE_SUCCESS,
+ PAGE_TYPES,
+ SELECT_PAGE_FAILURE,
+ SELECT_PAGE_START,
+ SELECT_PAGE_SUCCESS,
+ SELECTED_RUNTIME_ID_UPDATED,
+ SHOW_PROFILER_DIALOG,
+ SWITCH_PROFILER_CONTEXT,
+ USB_RUNTIMES_SCAN_START,
+ USB_RUNTIMES_SCAN_SUCCESS,
+} = require("devtools/client/aboutdebugging/src/constants");
+
+const NetworkLocationsModule = require("devtools/client/aboutdebugging/src/modules/network-locations");
+const {
+ adbAddon,
+} = require("devtools/client/shared/remote-debugging/adb/adb-addon");
+const {
+ refreshUSBRuntimes,
+} = require("devtools/client/aboutdebugging/src/modules/usb-runtimes");
+
+const Actions = require("devtools/client/aboutdebugging/src/actions/index");
+
+function selectPage(page, runtimeId) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: SELECT_PAGE_START });
+
+ try {
+ const isSamePage = (oldPage, newPage) => {
+ if (newPage === PAGE_TYPES.RUNTIME && oldPage === PAGE_TYPES.RUNTIME) {
+ return runtimeId === getState().runtimes.selectedRuntimeId;
+ }
+ return newPage === oldPage;
+ };
+
+ if (!page) {
+ throw new Error("No page provided.");
+ }
+
+ const currentPage = getState().ui.selectedPage;
+ // Nothing to dispatch if the page is the same as the current page
+ if (isSamePage(currentPage, page)) {
+ return;
+ }
+
+ // Stop showing the profiler dialog if we are navigating to another page.
+ if (getState().ui.showProfilerDialog) {
+ await dispatch({ type: HIDE_PROFILER_DIALOG });
+ }
+
+ // Stop watching current runtime, if currently on a RUNTIME page.
+ if (currentPage === PAGE_TYPES.RUNTIME) {
+ const currentRuntimeId = getState().runtimes.selectedRuntimeId;
+ await dispatch(Actions.unwatchRuntime(currentRuntimeId));
+ }
+
+ // Always update the selected runtime id.
+ // If we are navigating to a non-runtime page, the Runtime page components are no
+ // longer rendered so it is safe to nullify the runtimeId.
+ // If we are navigating to a runtime page, the runtime corresponding to runtimeId
+ // is already connected, so components can safely get runtimeDetails on this new
+ // runtime.
+ dispatch({ type: SELECTED_RUNTIME_ID_UPDATED, runtimeId });
+
+ // Start watching current runtime, if moving to a RUNTIME page.
+ if (page === PAGE_TYPES.RUNTIME) {
+ await dispatch(Actions.watchRuntime(runtimeId));
+ }
+
+ dispatch({ type: SELECT_PAGE_SUCCESS, page });
+ } catch (e) {
+ dispatch({ type: SELECT_PAGE_FAILURE, error: e });
+ }
+ };
+}
+
+function updateDebugTargetCollapsibility(key, isCollapsed) {
+ return { type: DEBUG_TARGET_COLLAPSIBILITY_UPDATED, key, isCollapsed };
+}
+
+function addNetworkLocation(location) {
+ return ({ dispatch, getState }) => {
+ NetworkLocationsModule.addNetworkLocation(location);
+ };
+}
+
+function removeNetworkLocation(location) {
+ return ({ dispatch, getState }) => {
+ NetworkLocationsModule.removeNetworkLocation(location);
+ };
+}
+
+function showProfilerDialog() {
+ return { type: SHOW_PROFILER_DIALOG };
+}
+
+/**
+ * The profiler can switch between "devtools-remote" and "aboutprofiling-remote"
+ * page contexts.
+ */
+function switchProfilerContext(profilerContext) {
+ return { type: SWITCH_PROFILER_CONTEXT, profilerContext };
+}
+
+function hideProfilerDialog() {
+ return { type: HIDE_PROFILER_DIALOG };
+}
+
+function updateAdbAddonStatus(adbAddonStatus) {
+ return { type: ADB_ADDON_STATUS_UPDATED, adbAddonStatus };
+}
+
+function updateAdbReady(isAdbReady) {
+ return { type: ADB_READY_UPDATED, isAdbReady };
+}
+
+function updateNetworkLocations(locations) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_START });
+ try {
+ await dispatch(Actions.updateNetworkRuntimes(locations));
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_SUCCESS, locations });
+ } catch (e) {
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_FAILURE, error: e });
+ }
+ };
+}
+
+function installAdbAddon() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: ADB_ADDON_INSTALL_START });
+
+ try {
+ // "aboutdebugging" will be forwarded to telemetry as the installation source
+ // for the addon.
+ await adbAddon.install("about:debugging");
+ dispatch({ type: ADB_ADDON_INSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: ADB_ADDON_INSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function uninstallAdbAddon() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: ADB_ADDON_UNINSTALL_START });
+
+ try {
+ await adbAddon.uninstall();
+ dispatch({ type: ADB_ADDON_UNINSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: ADB_ADDON_UNINSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function scanUSBRuntimes() {
+ return async ({ dispatch, getState }) => {
+ // do not re-scan if we are already doing it
+ if (getState().ui.isScanningUsb) {
+ return;
+ }
+
+ dispatch({ type: USB_RUNTIMES_SCAN_START });
+ await refreshUSBRuntimes();
+ dispatch({ type: USB_RUNTIMES_SCAN_SUCCESS });
+ };
+}
+
+module.exports = {
+ addNetworkLocation,
+ hideProfilerDialog,
+ installAdbAddon,
+ removeNetworkLocation,
+ scanUSBRuntimes,
+ selectPage,
+ showProfilerDialog,
+ switchProfilerContext,
+ uninstallAdbAddon,
+ updateAdbAddonStatus,
+ updateAdbReady,
+ updateDebugTargetCollapsibility,
+ updateNetworkLocations,
+};