tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

runtimes.js (17161B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
      8 
      9 const {
     10  getAllRuntimes,
     11  getCurrentRuntime,
     12  findRuntimeById,
     13 } = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
     14 
     15 const {
     16  l10n,
     17 } = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
     18 const {
     19  setDefaultPreferencesIfNeeded,
     20  DEFAULT_PREFERENCES,
     21 } = require("resource://devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js");
     22 const {
     23  createClientForRuntime,
     24 } = require("resource://devtools/client/aboutdebugging/src/modules/runtime-client-factory.js");
     25 const {
     26  isSupportedDebugTargetPane,
     27 } = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js");
     28 
     29 const {
     30  remoteClientManager,
     31 } = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
     32 
     33 const {
     34  CONNECT_RUNTIME_CANCEL,
     35  CONNECT_RUNTIME_FAILURE,
     36  CONNECT_RUNTIME_NOT_RESPONDING,
     37  CONNECT_RUNTIME_START,
     38  CONNECT_RUNTIME_SUCCESS,
     39  DEBUG_TARGET_PANE,
     40  DISCONNECT_RUNTIME_FAILURE,
     41  DISCONNECT_RUNTIME_START,
     42  DISCONNECT_RUNTIME_SUCCESS,
     43  PAGE_TYPES,
     44  REMOTE_RUNTIMES_UPDATED,
     45  RUNTIME_PREFERENCE,
     46  RUNTIMES,
     47  THIS_FIREFOX_RUNTIME_CREATED,
     48  UNWATCH_RUNTIME_FAILURE,
     49  UNWATCH_RUNTIME_START,
     50  UNWATCH_RUNTIME_SUCCESS,
     51  UPDATE_CONNECTION_PROMPT_SETTING_FAILURE,
     52  UPDATE_CONNECTION_PROMPT_SETTING_START,
     53  UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
     54  WATCH_RUNTIME_FAILURE,
     55  WATCH_RUNTIME_START,
     56  WATCH_RUNTIME_SUCCESS,
     57 } = require("resource://devtools/client/aboutdebugging/src/constants.js");
     58 
     59 const CONNECTION_TIMING_OUT_DELAY = 3000;
     60 const CONNECTION_CANCEL_DELAY = 13000;
     61 
     62 async function getRuntimeIcon(_runtime, _channel) {
     63  return "chrome://branding/content/about-logo.svg";
     64 }
     65 
     66 function onRemoteDevToolsClientClosed() {
     67  window.AboutDebugging.onNetworkLocationsUpdated();
     68  window.AboutDebugging.onUSBRuntimesUpdated();
     69 }
     70 
     71 function connectRuntime(id) {
     72  // Create a random connection id to track the connection attempt in telemetry.
     73  const connectionId = (Math.random() * 100000) | 0;
     74 
     75  return async ({ dispatch, getState }) => {
     76    dispatch({ type: CONNECT_RUNTIME_START, connectionId, id });
     77 
     78    // The preferences test-connection-timing-out-delay and test-connection-cancel-delay
     79    // don't have a default value but will be overridden during our tests.
     80    const connectionTimingOutDelay = Services.prefs.getIntPref(
     81      "devtools.aboutdebugging.test-connection-timing-out-delay",
     82      CONNECTION_TIMING_OUT_DELAY
     83    );
     84    const connectionCancelDelay = Services.prefs.getIntPref(
     85      "devtools.aboutdebugging.test-connection-cancel-delay",
     86      CONNECTION_CANCEL_DELAY
     87    );
     88 
     89    const connectionNotRespondingTimer = setTimeout(() => {
     90      // If connecting to the runtime takes time over CONNECTION_TIMING_OUT_DELAY,
     91      // we assume the connection prompt is showing on the runtime, show a dialog
     92      // to let user know that.
     93      dispatch({ type: CONNECT_RUNTIME_NOT_RESPONDING, connectionId, id });
     94    }, connectionTimingOutDelay);
     95    const connectionCancelTimer = setTimeout(() => {
     96      // Connect button of the runtime will be disabled during connection, but the status
     97      // continues till the connection was either succeed or failed. This may have a
     98      // possibility that the disabling continues unless page reloading, user will not be
     99      // able to click again. To avoid this, revert the connect button status after
    100      // CONNECTION_CANCEL_DELAY ms.
    101      dispatch({ type: CONNECT_RUNTIME_CANCEL, connectionId, id });
    102    }, connectionCancelDelay);
    103 
    104    try {
    105      const runtime = findRuntimeById(id, getState().runtimes);
    106      const clientWrapper = await createClientForRuntime(runtime);
    107 
    108      await setDefaultPreferencesIfNeeded(clientWrapper, DEFAULT_PREFERENCES);
    109 
    110      const deviceDescription = await clientWrapper.getDeviceDescription();
    111      const compatibilityReport =
    112        await clientWrapper.checkVersionCompatibility();
    113      const icon = await getRuntimeIcon(runtime, deviceDescription.channel);
    114 
    115      const {
    116        CONNECTION_PROMPT,
    117        PERMANENT_PRIVATE_BROWSING,
    118        SERVICE_WORKERS_ENABLED,
    119      } = RUNTIME_PREFERENCE;
    120      const connectionPromptEnabled = await clientWrapper.getPreference(
    121        CONNECTION_PROMPT,
    122        false
    123      );
    124      const privateBrowsing = await clientWrapper.getPreference(
    125        PERMANENT_PRIVATE_BROWSING,
    126        false
    127      );
    128      const serviceWorkersEnabled = await clientWrapper.getPreference(
    129        SERVICE_WORKERS_ENABLED,
    130        true
    131      );
    132      const serviceWorkersAvailable = serviceWorkersEnabled && !privateBrowsing;
    133 
    134      // Fenix specific workarounds are needed until we can get proper server side APIs
    135      // to detect Fenix and get the proper application names and versions.
    136      // See https://github.com/mozilla-mobile/fenix/issues/2016.
    137 
    138      // For Fenix runtimes, the ADB runtime name is more accurate than the one returned
    139      // by the Device actor.
    140      const runtimeName = runtime.isFenix
    141        ? runtime.name
    142        : deviceDescription.name;
    143 
    144      // For Fenix runtimes, the version we should display is the application version
    145      // retrieved from ADB, and not the Gecko version returned by the Device actor.
    146      const version = runtime.isFenix
    147        ? runtime.extra.adbPackageVersion
    148        : deviceDescription.version;
    149 
    150      const runtimeDetails = {
    151        canDebugServiceWorkers: deviceDescription.canDebugServiceWorkers,
    152        clientWrapper,
    153        compatibilityReport,
    154        connectionPromptEnabled,
    155        info: {
    156          deviceName: deviceDescription.deviceName,
    157          icon,
    158          isFenix: runtime.isFenix,
    159          name: runtimeName,
    160          os: deviceDescription.os,
    161          type: runtime.type,
    162          version,
    163        },
    164        serviceWorkersAvailable,
    165      };
    166 
    167      if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
    168        // `closed` event will be emitted when disabling remote debugging
    169        // on the connected remote runtime.
    170        clientWrapper.once("closed", onRemoteDevToolsClientClosed);
    171      }
    172 
    173      dispatch({
    174        type: CONNECT_RUNTIME_SUCCESS,
    175        connectionId,
    176        runtime: {
    177          id,
    178          runtimeDetails,
    179          type: runtime.type,
    180        },
    181      });
    182    } catch (e) {
    183      dispatch({ type: CONNECT_RUNTIME_FAILURE, connectionId, id, error: e });
    184    } finally {
    185      clearTimeout(connectionNotRespondingTimer);
    186      clearTimeout(connectionCancelTimer);
    187    }
    188  };
    189 }
    190 
    191 function createThisFirefoxRuntime() {
    192  return ({ dispatch }) => {
    193    const thisFirefoxRuntime = {
    194      id: RUNTIMES.THIS_FIREFOX,
    195      isConnecting: false,
    196      isConnectionFailed: false,
    197      isConnectionNotResponding: false,
    198      isConnectionTimeout: false,
    199      isUnavailable: false,
    200      isUnplugged: false,
    201      name: l10n.getString("about-debugging-this-firefox-runtime-name"),
    202      type: RUNTIMES.THIS_FIREFOX,
    203    };
    204    dispatch({
    205      type: THIS_FIREFOX_RUNTIME_CREATED,
    206      runtime: thisFirefoxRuntime,
    207    });
    208  };
    209 }
    210 
    211 function disconnectRuntime(id, shouldRedirect = false) {
    212  return async ({ dispatch, getState }) => {
    213    dispatch({ type: DISCONNECT_RUNTIME_START });
    214    try {
    215      const runtime = findRuntimeById(id, getState().runtimes);
    216      const { clientWrapper } = runtime.runtimeDetails;
    217 
    218      if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
    219        clientWrapper.off("closed", onRemoteDevToolsClientClosed);
    220      }
    221      await clientWrapper.close();
    222      if (shouldRedirect) {
    223        await dispatch(
    224          Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
    225        );
    226      }
    227 
    228      dispatch({
    229        type: DISCONNECT_RUNTIME_SUCCESS,
    230        runtime: {
    231          id,
    232          type: runtime.type,
    233        },
    234      });
    235    } catch (e) {
    236      dispatch({ type: DISCONNECT_RUNTIME_FAILURE, error: e });
    237    }
    238  };
    239 }
    240 
    241 function updateConnectionPromptSetting(connectionPromptEnabled) {
    242  return async ({ dispatch, getState }) => {
    243    dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_START });
    244    try {
    245      const runtime = getCurrentRuntime(getState().runtimes);
    246      const { clientWrapper } = runtime.runtimeDetails;
    247      const promptPrefName = RUNTIME_PREFERENCE.CONNECTION_PROMPT;
    248      await clientWrapper.setPreference(
    249        promptPrefName,
    250        connectionPromptEnabled
    251      );
    252      // Re-get actual value from the runtime.
    253      connectionPromptEnabled = await clientWrapper.getPreference(
    254        promptPrefName,
    255        connectionPromptEnabled
    256      );
    257 
    258      dispatch({
    259        type: UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
    260        connectionPromptEnabled,
    261        runtime,
    262      });
    263    } catch (e) {
    264      dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_FAILURE, error: e });
    265    }
    266  };
    267 }
    268 
    269 function watchRuntime(id) {
    270  return async ({ dispatch, getState }) => {
    271    dispatch({ type: WATCH_RUNTIME_START });
    272 
    273    try {
    274      if (id === RUNTIMES.THIS_FIREFOX) {
    275        // THIS_FIREFOX connects and disconnects on the fly when opening the page.
    276        await dispatch(connectRuntime(RUNTIMES.THIS_FIREFOX));
    277      }
    278 
    279      // The selected runtime should already have a connected client assigned.
    280      const runtime = findRuntimeById(id, getState().runtimes);
    281      await dispatch({ type: WATCH_RUNTIME_SUCCESS, runtime });
    282 
    283      dispatch(Actions.requestExtensions());
    284      // we have to wait for tabs, otherwise the requests to getTarget may interfer
    285      // with listProcesses
    286      await dispatch(Actions.requestTabs());
    287      dispatch(Actions.requestWorkers());
    288 
    289      if (
    290        isSupportedDebugTargetPane(
    291          runtime.runtimeDetails.info.type,
    292          DEBUG_TARGET_PANE.PROCESSES
    293        )
    294      ) {
    295        dispatch(Actions.requestProcesses());
    296      }
    297    } catch (e) {
    298      dispatch({ type: WATCH_RUNTIME_FAILURE, error: e });
    299    }
    300  };
    301 }
    302 
    303 function unwatchRuntime(id) {
    304  return async ({ dispatch, getState }) => {
    305    const runtime = findRuntimeById(id, getState().runtimes);
    306 
    307    dispatch({ type: UNWATCH_RUNTIME_START, runtime });
    308 
    309    try {
    310      if (id === RUNTIMES.THIS_FIREFOX) {
    311        // THIS_FIREFOX connects and disconnects on the fly when opening the page.
    312        await dispatch(disconnectRuntime(RUNTIMES.THIS_FIREFOX));
    313      }
    314 
    315      dispatch({ type: UNWATCH_RUNTIME_SUCCESS });
    316    } catch (e) {
    317      dispatch({ type: UNWATCH_RUNTIME_FAILURE, error: e });
    318    }
    319  };
    320 }
    321 
    322 function updateNetworkRuntimes(locations) {
    323  const runtimes = locations.map(location => {
    324    const [host, port] = location.split(":");
    325    return {
    326      id: location,
    327      extra: {
    328        connectionParameters: { host, port: parseInt(port, 10) },
    329      },
    330      isConnecting: false,
    331      isConnectionFailed: false,
    332      isConnectionNotResponding: false,
    333      isConnectionTimeout: false,
    334      isFenix: false,
    335      isUnavailable: false,
    336      isUnplugged: false,
    337      isUnknown: false,
    338      name: location,
    339      type: RUNTIMES.NETWORK,
    340    };
    341  });
    342  return updateRemoteRuntimes(runtimes, RUNTIMES.NETWORK);
    343 }
    344 
    345 function updateUSBRuntimes(adbRuntimes) {
    346  const runtimes = adbRuntimes.map(adbRuntime => {
    347    // Set connectionParameters only for known runtimes.
    348    const socketPath = adbRuntime.socketPath;
    349    const deviceId = adbRuntime.deviceId;
    350    const connectionParameters = socketPath ? { deviceId, socketPath } : null;
    351    return {
    352      id: adbRuntime.id,
    353      extra: {
    354        connectionParameters,
    355        deviceName: adbRuntime.deviceName,
    356        adbPackageVersion: adbRuntime.versionName,
    357      },
    358      isConnecting: false,
    359      isConnectionFailed: false,
    360      isConnectionNotResponding: false,
    361      isConnectionTimeout: false,
    362      isFenix: adbRuntime.isFenix,
    363      isUnavailable: adbRuntime.isUnavailable,
    364      isUnplugged: adbRuntime.isUnplugged,
    365      name: adbRuntime.shortName,
    366      type: RUNTIMES.USB,
    367    };
    368  });
    369  return updateRemoteRuntimes(runtimes, RUNTIMES.USB);
    370 }
    371 
    372 /**
    373 * Check that a given runtime can still be found in the provided array of runtimes, and
    374 * that the connection of the associated DevToolsClient is still valid.
    375 * Note that this check is only valid for runtimes which match the type of the runtimes
    376 * in the array.
    377 */
    378 function _isRuntimeValid(runtime, runtimes) {
    379  const isRuntimeAvailable = runtimes.some(r => r.id === runtime.id);
    380  const isConnectionValid =
    381    runtime.runtimeDetails && !runtime.runtimeDetails.clientWrapper.isClosed();
    382  return isRuntimeAvailable && isConnectionValid;
    383 }
    384 
    385 function updateRemoteRuntimes(runtimes, type) {
    386  return async ({ dispatch, getState }) => {
    387    const currentRuntime = getCurrentRuntime(getState().runtimes);
    388 
    389    // Check if the updated remote runtimes should trigger a navigation out of the current
    390    // runtime page.
    391    if (
    392      currentRuntime &&
    393      currentRuntime.type === type &&
    394      !_isRuntimeValid(currentRuntime, runtimes)
    395    ) {
    396      // Since current remote runtime is invalid, move to this firefox page.
    397      // This case is considered as followings and so on:
    398      // * Remove ADB addon
    399      // * (Physically) Disconnect USB runtime
    400      //
    401      // The reason we call selectPage before REMOTE_RUNTIMES_UPDATED is fired is below.
    402      // Current runtime can not be retrieved after REMOTE_RUNTIMES_UPDATED action, since
    403      // that updates runtime state. So, before that we fire selectPage action to execute
    404      // `unwatchRuntime` correctly.
    405      await dispatch(
    406        Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
    407      );
    408    }
    409 
    410    // For existing runtimes, transfer all properties that are not available in the
    411    // runtime objects passed to this method:
    412    // - runtimeDetails (set by about:debugging after a successful connection)
    413    // - isConnecting (set by about:debugging during the connection)
    414    // - isConnectionFailed (set by about:debugging if connection was failed)
    415    // - isConnectionNotResponding
    416    //     (set by about:debugging if connection is taking too much time)
    417    // - isConnectionTimeout (set by about:debugging if connection was timeout)
    418    runtimes.forEach(runtime => {
    419      const existingRuntime = findRuntimeById(runtime.id, getState().runtimes);
    420      const isConnectionValid =
    421        existingRuntime?.runtimeDetails &&
    422        !existingRuntime.runtimeDetails.clientWrapper.isClosed();
    423      runtime.runtimeDetails = isConnectionValid
    424        ? existingRuntime.runtimeDetails
    425        : null;
    426      runtime.isConnecting = existingRuntime
    427        ? existingRuntime.isConnecting
    428        : false;
    429      runtime.isConnectionFailed = existingRuntime
    430        ? existingRuntime.isConnectionFailed
    431        : false;
    432      runtime.isConnectionNotResponding = existingRuntime
    433        ? existingRuntime.isConnectionNotResponding
    434        : false;
    435      runtime.isConnectionTimeout = existingRuntime
    436        ? existingRuntime.isConnectionTimeout
    437        : false;
    438    });
    439 
    440    const existingRuntimes = getAllRuntimes(getState().runtimes);
    441    for (const runtime of existingRuntimes) {
    442      // Runtime was connected before.
    443      const isConnected = runtime.runtimeDetails;
    444      // Runtime is of the same type as the updated runtimes array, so we should check it.
    445      const isSameType = runtime.type === type;
    446      if (isConnected && isSameType && !_isRuntimeValid(runtime, runtimes)) {
    447        // Disconnect runtimes that were no longer valid.
    448        await dispatch(disconnectRuntime(runtime.id));
    449      }
    450    }
    451 
    452    dispatch({ type: REMOTE_RUNTIMES_UPDATED, runtimes, runtimeType: type });
    453 
    454    for (const runtime of getAllRuntimes(getState().runtimes)) {
    455      if (runtime.type !== type) {
    456        continue;
    457      }
    458 
    459      // Reconnect clients already available in the RemoteClientManager.
    460      const isConnected = !!runtime.runtimeDetails;
    461      const hasConnectedClient = remoteClientManager.hasClient(
    462        runtime.id,
    463        runtime.type
    464      );
    465      if (!isConnected && hasConnectedClient) {
    466        await dispatch(connectRuntime(runtime.id));
    467      }
    468    }
    469  };
    470 }
    471 
    472 /**
    473 * Remove all the listeners added on client objects. Since those objects are persisted
    474 * regardless of the about:debugging lifecycle, all the added events should be removed
    475 * before leaving about:debugging.
    476 */
    477 function removeRuntimeListeners() {
    478  return ({ getState }) => {
    479    const allRuntimes = getAllRuntimes(getState().runtimes);
    480    const remoteRuntimes = allRuntimes.filter(
    481      r => r.type !== RUNTIMES.THIS_FIREFOX
    482    );
    483    for (const runtime of remoteRuntimes) {
    484      if (runtime.runtimeDetails) {
    485        const { clientWrapper } = runtime.runtimeDetails;
    486        clientWrapper.off("closed", onRemoteDevToolsClientClosed);
    487      }
    488    }
    489  };
    490 }
    491 
    492 module.exports = {
    493  connectRuntime,
    494  createThisFirefoxRuntime,
    495  disconnectRuntime,
    496  removeRuntimeListeners,
    497  unwatchRuntime,
    498  updateConnectionPromptSetting,
    499  updateNetworkRuntimes,
    500  updateUSBRuntimes,
    501  watchRuntime,
    502 };