diff options
Diffstat (limited to 'devtools/client/aboutdebugging/src/actions/runtimes.js')
-rw-r--r-- | devtools/client/aboutdebugging/src/actions/runtimes.js | 513 |
1 files changed, 513 insertions, 0 deletions
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, +}; |