diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/aboutdebugging/src/middleware | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/aboutdebugging/src/middleware')
8 files changed, 699 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js new file mode 100644 index 0000000000..655e667a6d --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js @@ -0,0 +1,111 @@ +/* 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 { + EXTENSION_BGSCRIPT_STATUSES, + EXTENSION_BGSCRIPT_STATUS_UPDATED, + UNWATCH_RUNTIME_START, + WATCH_RUNTIME_SUCCESS, +} = require("resource://devtools/client/aboutdebugging/src/constants.js"); + +const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js"); + +const RootResourceCommand = require("resource://devtools/shared/commands/root-resource/root-resource-command.js"); + +function debugTargetListenerMiddleware(store) { + const onExtensionsUpdated = () => { + store.dispatch(Actions.requestExtensions()); + }; + + const onTabsUpdated = () => { + store.dispatch(Actions.requestTabs()); + }; + + const onWorkersUpdated = () => { + store.dispatch(Actions.requestWorkers()); + }; + + let rootResourceCommand; + + function onExtensionsBackgroundScriptStatusAvailable(resources) { + for (const resource of resources) { + const backgroundScriptStatus = resource.payload.isRunning + ? EXTENSION_BGSCRIPT_STATUSES.RUNNING + : EXTENSION_BGSCRIPT_STATUSES.STOPPED; + + store.dispatch({ + type: EXTENSION_BGSCRIPT_STATUS_UPDATED, + id: resource.payload.addonId, + backgroundScriptStatus, + }); + } + } + + return next => async action => { + switch (action.type) { + case WATCH_RUNTIME_SUCCESS: { + const { runtime } = action; + const { clientWrapper } = runtime.runtimeDetails; + + rootResourceCommand = clientWrapper.createRootResourceCommand(); + + // Watch extensions background script status updates. + await rootResourceCommand + .watchResources( + [RootResourceCommand.TYPES.EXTENSIONS_BGSCRIPT_STATUS], + { onAvailable: onExtensionsBackgroundScriptStatusAvailable } + ) + .catch(e => { + // Log an error if watching this resource rejects (e.g. if + // the promise was not resolved yet when about:debugging tab + // or the RDP connection to a remote target has been closed). + console.error(e); + }); + + // Tabs + clientWrapper.on("tabListChanged", onTabsUpdated); + + // Addons + clientWrapper.on("addonListChanged", onExtensionsUpdated); + + // Workers + clientWrapper.on("workersUpdated", onWorkersUpdated); + break; + } + case UNWATCH_RUNTIME_START: { + const { runtime } = action; + const { clientWrapper } = runtime.runtimeDetails; + + // Stop watching extensions background script status updates. + try { + rootResourceCommand?.unwatchResources( + [RootResourceCommand.TYPES.EXTENSIONS_BGSCRIPT_STATUS], + { onAvailable: onExtensionsBackgroundScriptStatusAvailable } + ); + } catch (e) { + // Log an error if watching this resource rejects (e.g. if + // the promise was not resolved yet when about:debugging tab + // or the RDP connection to a remote target has been closed). + console.error(e); + } + + // Tabs + clientWrapper.off("tabListChanged", onTabsUpdated); + + // Addons + clientWrapper.off("addonListChanged", onExtensionsUpdated); + + // Workers + clientWrapper.off("workersUpdated", onWorkersUpdated); + break; + } + } + + return next(action); + }; +} + +module.exports = debugTargetListenerMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/error-logging.js b/devtools/client/aboutdebugging/src/middleware/error-logging.js new file mode 100644 index 0000000000..fd04f34b76 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/error-logging.js @@ -0,0 +1,35 @@ +/* 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"; + +/** + * Error logging middleware that will forward all actions that contain an error property + * to the console. + */ +function errorLoggingMiddleware() { + return next => action => { + if (action.error) { + const { error } = action; + if (error.message) { + console.error(`[ACTION FAILED] ${action.type}: ${error.message}`); + } else if (typeof error === "string") { + // All failure actions should dispatch an error object instead of a message. + // We allow some flexibility to still provide some error logging. + console.error(`[ACTION FAILED] ${action.type}: ${error}`); + console.error( + `[ACTION FAILED] ${action.type} should dispatch the error object!` + ); + } + + if (error.stack) { + console.error(error.stack); + } + } + + return next(action); + }; +} + +module.exports = errorLoggingMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/event-recording.js b/devtools/client/aboutdebugging/src/middleware/event-recording.js new file mode 100644 index 0000000000..f926100b8b --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/event-recording.js @@ -0,0 +1,268 @@ +/* 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 = require("resource://devtools/client/shared/telemetry.js"); +loader.lazyGetter( + this, + "telemetry", + () => new Telemetry({ useSessionId: true }) +); + +const { + CONNECT_RUNTIME_CANCEL, + CONNECT_RUNTIME_FAILURE, + CONNECT_RUNTIME_NOT_RESPONDING, + CONNECT_RUNTIME_START, + CONNECT_RUNTIME_SUCCESS, + DISCONNECT_RUNTIME_SUCCESS, + REMOTE_RUNTIMES_UPDATED, + RUNTIMES, + SELECT_PAGE_SUCCESS, + SHOW_PROFILER_DIALOG, + TELEMETRY_RECORD, + UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS, +} = require("resource://devtools/client/aboutdebugging/src/constants.js"); + +const { + findRuntimeById, + getAllRuntimes, + getCurrentRuntime, +} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js"); + +function recordEvent(method, details) { + telemetry.recordEvent(method, "aboutdebugging", null, details); + + // For close and open events, also ping the regular telemetry helpers used + // for all DevTools UIs. + if (method === "open_adbg") { + telemetry.toolOpened("aboutdebugging", window.AboutDebugging); + } else if (method === "close_adbg") { + // XXX: Note that aboutdebugging has no histogram created for + // TIME_ACTIVE_SECOND, so calling toolClosed will not actually + // record anything. + telemetry.toolClosed("aboutdebugging", window.AboutDebugging); + } +} + +const telemetryRuntimeIds = new Map(); +// Create an anonymous id that will allow to track all events related to a runtime without +// leaking personal data related to this runtime. +function getTelemetryRuntimeId(id) { + if (!telemetryRuntimeIds.has(id)) { + const randomId = (Math.random() * 100000) | 0; + telemetryRuntimeIds.set(id, "runtime-" + randomId); + } + return telemetryRuntimeIds.get(id); +} + +function getCurrentRuntimeIdForTelemetry(store) { + const id = getCurrentRuntime(store.getState().runtimes).id; + return getTelemetryRuntimeId(id); +} + +function getRuntimeEventExtras(runtime) { + const { extra, runtimeDetails } = runtime; + + // deviceName can be undefined for non-usb devices, but we should not log "undefined". + const deviceName = extra?.deviceName || ""; + const runtimeShortName = runtime.type === RUNTIMES.USB ? runtime.name : ""; + const runtimeName = runtimeDetails?.info.name || ""; + return { + connection_type: runtime.type, + device_name: deviceName, + runtime_id: getTelemetryRuntimeId(runtime.id), + runtime_name: runtimeName || runtimeShortName, + }; +} + +function onConnectRuntimeSuccess(action, store) { + if (action.runtime.type === RUNTIMES.THIS_FIREFOX) { + // Only record connection and disconnection events for remote runtimes. + return; + } + // When we just connected to a runtime, the runtimeDetails are not in the store yet, + // so we merge it here to retrieve the expected telemetry data. + const storeRuntime = findRuntimeById( + action.runtime.id, + store.getState().runtimes + ); + const runtime = Object.assign({}, storeRuntime, { + runtimeDetails: action.runtime.runtimeDetails, + }); + const extras = Object.assign({}, getRuntimeEventExtras(runtime), { + runtime_os: action.runtime.runtimeDetails.info.os, + runtime_version: action.runtime.runtimeDetails.info.version, + }); + recordEvent("runtime_connected", extras); +} + +function onDisconnectRuntimeSuccess(action, store) { + const runtime = findRuntimeById(action.runtime.id, store.getState().runtimes); + if (runtime.type === RUNTIMES.THIS_FIREFOX) { + // Only record connection and disconnection events for remote runtimes. + return; + } + + recordEvent("runtime_disconnected", getRuntimeEventExtras(runtime)); +} + +function onRemoteRuntimesUpdated(action, store) { + // Compare new runtimes with the existing runtimes to detect if runtimes, devices + // have been added or removed. + const newRuntimes = action.runtimes; + const allRuntimes = getAllRuntimes(store.getState().runtimes); + const oldRuntimes = allRuntimes.filter(r => r.type === action.runtimeType); + + // Check if all the old runtimes and devices are still available in the updated + // array. + for (const oldRuntime of oldRuntimes) { + const runtimeRemoved = newRuntimes.every(r => r.id !== oldRuntime.id); + if (runtimeRemoved && !oldRuntime.isUnplugged) { + recordEvent("runtime_removed", getRuntimeEventExtras(oldRuntime)); + } + } + + // Using device names as unique IDs is inaccurate. See Bug 1544582. + const oldDeviceNames = new Set(oldRuntimes.map(r => r.extra.deviceName)); + for (const oldDeviceName of oldDeviceNames) { + const newRuntime = newRuntimes.find( + r => r.extra.deviceName === oldDeviceName + ); + const oldRuntime = oldRuntimes.find( + r => r.extra.deviceName === oldDeviceName + ); + const isUnplugged = newRuntime?.isUnplugged && !oldRuntime.isUnplugged; + if (oldDeviceName && (!newRuntime || isUnplugged)) { + recordEvent("device_removed", { + connection_type: action.runtimeType, + device_name: oldDeviceName, + }); + } + } + + // Check if the new runtimes and devices were already available in the existing + // array. + for (const newRuntime of newRuntimes) { + const runtimeAdded = oldRuntimes.every(r => r.id !== newRuntime.id); + if (runtimeAdded && !newRuntime.isUnplugged) { + recordEvent("runtime_added", getRuntimeEventExtras(newRuntime)); + } + } + + // Using device names as unique IDs is inaccurate. See Bug 1544582. + const newDeviceNames = new Set(newRuntimes.map(r => r.extra.deviceName)); + for (const newDeviceName of newDeviceNames) { + const newRuntime = newRuntimes.find( + r => r.extra.deviceName === newDeviceName + ); + const oldRuntime = oldRuntimes.find( + r => r.extra.deviceName === newDeviceName + ); + const isPlugged = oldRuntime?.isUnplugged && !newRuntime.isUnplugged; + + if (newDeviceName && (!oldRuntime || isPlugged)) { + recordEvent("device_added", { + connection_type: action.runtimeType, + device_name: newDeviceName, + }); + } + } +} + +function recordConnectionAttempt(connectionId, runtimeId, status, store) { + const runtime = findRuntimeById(runtimeId, store.getState().runtimes); + if (runtime.type === RUNTIMES.THIS_FIREFOX) { + // Only record connection_attempt events for remote runtimes. + return; + } + + recordEvent("connection_attempt", { + connection_id: connectionId, + connection_type: runtime.type, + runtime_id: getTelemetryRuntimeId(runtimeId), + status, + }); +} + +/** + * This middleware will record events to telemetry for some specific actions. + */ +function eventRecordingMiddleware(store) { + return next => action => { + switch (action.type) { + case CONNECT_RUNTIME_CANCEL: + recordConnectionAttempt( + action.connectionId, + action.id, + "cancelled", + store + ); + break; + case CONNECT_RUNTIME_FAILURE: + recordConnectionAttempt( + action.connectionId, + action.id, + "failed", + store + ); + break; + case CONNECT_RUNTIME_NOT_RESPONDING: + recordConnectionAttempt( + action.connectionId, + action.id, + "not responding", + store + ); + break; + case CONNECT_RUNTIME_START: + recordConnectionAttempt(action.connectionId, action.id, "start", store); + break; + case CONNECT_RUNTIME_SUCCESS: + recordConnectionAttempt( + action.connectionId, + action.runtime.id, + "success", + store + ); + onConnectRuntimeSuccess(action, store); + break; + case DISCONNECT_RUNTIME_SUCCESS: + onDisconnectRuntimeSuccess(action, store); + break; + case REMOTE_RUNTIMES_UPDATED: + onRemoteRuntimesUpdated(action, store); + break; + case SELECT_PAGE_SUCCESS: + recordEvent("select_page", { page_type: action.page }); + break; + case SHOW_PROFILER_DIALOG: + recordEvent("show_profiler", { + runtime_id: getCurrentRuntimeIdForTelemetry(store), + }); + break; + case TELEMETRY_RECORD: + const { method, details } = action; + if (method) { + recordEvent(method, details); + } else { + console.error( + `[RECORD EVENT FAILED] ${action.type}: no "method" property` + ); + } + break; + case UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS: + recordEvent("update_conn_prompt", { + prompt_enabled: `${action.connectionPromptEnabled}`, + runtime_id: getCurrentRuntimeIdForTelemetry(store), + }); + break; + } + + return next(action); + }; +} + +module.exports = eventRecordingMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/extension-component-data.js b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js new file mode 100644 index 0000000000..5987f36398 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js @@ -0,0 +1,84 @@ +/* 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_TARGETS, + REQUEST_EXTENSIONS_SUCCESS, +} = require("resource://devtools/client/aboutdebugging/src/constants.js"); + +const { + getExtensionUuid, + parseFileUri, +} = require("resource://devtools/client/aboutdebugging/src/modules/extensions-helper.js"); + +/** + * This middleware converts extensions object that get from DevToolsClient.listAddons() + * to data which is used in DebugTargetItem. + */ +const extensionComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_EXTENSIONS_SUCCESS: { + action.installedExtensions = toComponentData(action.installedExtensions); + action.temporaryExtensions = toComponentData(action.temporaryExtensions); + break; + } + } + + return next(action); +}; + +function getFilePath(extension) { + // Only show file system paths, and only for temporarily installed add-ons. + if ( + !extension.temporarilyInstalled || + !extension.url || + !extension.url.startsWith("file://") + ) { + return null; + } + + return parseFileUri(extension.url); +} + +function toComponentData(extensions) { + return extensions.map(extension => { + const type = DEBUG_TARGETS.EXTENSION; + const { + actor, + backgroundScriptStatus, + iconDataURL, + iconURL, + id, + manifestURL, + name, + persistentBackgroundScript, + warnings, + } = extension; + const icon = + iconDataURL || + iconURL || + "chrome://mozapps/skin/extensions/extensionGeneric.svg"; + const location = getFilePath(extension); + const uuid = getExtensionUuid(extension); + return { + name, + icon, + id, + type, + details: { + actor, + backgroundScriptStatus, + location, + manifestURL, + persistentBackgroundScript, + uuid, + warnings: warnings || [], + }, + }; + }); +} + +module.exports = extensionComponentDataMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/moz.build b/devtools/client/aboutdebugging/src/middleware/moz.build new file mode 100644 index 0000000000..f50150f569 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/moz.build @@ -0,0 +1,13 @@ +# 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-target-listener.js", + "error-logging.js", + "event-recording.js", + "extension-component-data.js", + "process-component-data.js", + "tab-component-data.js", + "worker-component-data.js", +) diff --git a/devtools/client/aboutdebugging/src/middleware/process-component-data.js b/devtools/client/aboutdebugging/src/middleware/process-component-data.js new file mode 100644 index 0000000000..d5cdc6365b --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/process-component-data.js @@ -0,0 +1,55 @@ +/* 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 { + l10n, +} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js"); + +const { + DEBUG_TARGETS, + REQUEST_PROCESSES_SUCCESS, +} = require("resource://devtools/client/aboutdebugging/src/constants.js"); + +/** + * This middleware converts tabs object that get from DevToolsClient.listProcesses() to + * data which is used in DebugTargetItem. + */ +const processComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_PROCESSES_SUCCESS: { + const mainProcessComponentData = toMainProcessComponentData( + action.mainProcess + ); + action.processes = [mainProcessComponentData]; + break; + } + } + + return next(action); +}; + +function toMainProcessComponentData(process) { + const type = DEBUG_TARGETS.PROCESS; + const icon = "chrome://devtools/skin/images/aboutdebugging-process-icon.svg"; + + // For now, we assume there is only one process and this is the main process + // So the name and title are for a remote (multiprocess) browser toolbox. + const name = l10n.getString("about-debugging-multiprocess-toolbox-name"); + const description = l10n.getString( + "about-debugging-multiprocess-toolbox-description" + ); + + return { + name, + icon, + type, + details: { + description, + }, + }; +} + +module.exports = processComponentDataMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/tab-component-data.js b/devtools/client/aboutdebugging/src/middleware/tab-component-data.js new file mode 100644 index 0000000000..f468926f81 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/tab-component-data.js @@ -0,0 +1,51 @@ +/* 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_TARGETS, + REQUEST_TABS_SUCCESS, +} = require("resource://devtools/client/aboutdebugging/src/constants.js"); + +/** + * This middleware converts tabs object that get from DevToolsClient.listTabs() to data + * which is used in DebugTargetItem. + */ +const tabComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_TABS_SUCCESS: { + action.tabs = toComponentData(action.tabs); + break; + } + } + + return next(action); +}; + +function toComponentData(tabs) { + return tabs.map(tab => { + const type = DEBUG_TARGETS.TAB; + const id = tab.browserId; + const icon = tab.favicon + ? `data:image/png;base64,${btoa( + String.fromCharCode.apply(String, tab.favicon) + )}` + : "chrome://devtools/skin/images/globe.svg"; + const name = tab.title || tab.url; + const { url, isZombieTab } = tab; + return { + name, + icon, + id, + type, + details: { + isZombieTab, + url, + }, + }; + }); +} + +module.exports = tabComponentDataMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/worker-component-data.js b/devtools/client/aboutdebugging/src/middleware/worker-component-data.js new file mode 100644 index 0000000000..178c99e322 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/worker-component-data.js @@ -0,0 +1,82 @@ +/* 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_TARGETS, + REQUEST_WORKERS_SUCCESS, + SERVICE_WORKER_FETCH_STATES, + SERVICE_WORKER_STATUSES, +} = require("resource://devtools/client/aboutdebugging/src/constants.js"); + +/** + * This middleware converts workers object that get from DevToolsClient.listAllWorkers() + * to data which is used in DebugTargetItem. + */ +const workerComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_WORKERS_SUCCESS: { + action.otherWorkers = toComponentData(action.otherWorkers); + action.serviceWorkers = toComponentData(action.serviceWorkers, true); + action.sharedWorkers = toComponentData(action.sharedWorkers); + break; + } + } + + return next(action); +}; + +function getServiceWorkerStatus(worker) { + const isActive = worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED; + const isRunning = !!worker.workerDescriptorFront; + + if (isActive && isRunning) { + return SERVICE_WORKER_STATUSES.RUNNING; + } else if (isActive) { + return SERVICE_WORKER_STATUSES.STOPPED; + } + + // We cannot get service worker registrations unless the registration is in + // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we + // display a custom state "registering" for now. See Bug 1153292. + return SERVICE_WORKER_STATUSES.REGISTERING; +} + +function toComponentData(workers, isServiceWorker) { + return workers.map(worker => { + // Here `worker` is the worker object created by RootFront.listAllWorkers + const type = DEBUG_TARGETS.WORKER; + const icon = "chrome://devtools/skin/images/debugging-workers.svg"; + let { fetch } = worker; + const { id, name, registrationFront, scope, subscription } = worker; + + let pushServiceEndpoint = null; + let status = null; + + if (isServiceWorker) { + fetch = fetch + ? SERVICE_WORKER_FETCH_STATES.LISTENING + : SERVICE_WORKER_FETCH_STATES.NOT_LISTENING; + status = getServiceWorkerStatus(worker); + pushServiceEndpoint = subscription ? subscription.endpoint : null; + } + + return { + details: { + fetch, + pushServiceEndpoint, + registrationFront, + scope, + status, + }, + icon, + id, + name, + type, + }; + }); +} + +module.exports = workerComponentDataMiddleware; |