Bug 1497457 - Introduce remote client manager to persist connected remote clients;r=daisuke,ladybenko
☠☠ backed out by a6eccac5baa2 ☠ ☠
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 26 Nov 2018 19:53:47 +0000
changeset 507322 95365a2d747034833ee6380f837f0fd600552ae9
parent 507321 0e93acc09952e559958991399bba7c11571343b4
child 507323 f2a39a18f2f602b230062c3e6735f80cf9d84272
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdaisuke, ladybenko
bugs1497457
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1497457 - Introduce remote client manager to persist connected remote clients;r=daisuke,ladybenko Depends on D11992 Differential Revision: https://phabricator.services.mozilla.com/D11993
devtools/client/aboutdebugging-new/aboutdebugging.js
devtools/client/aboutdebugging-new/src/actions/runtimes.js
devtools/client/aboutdebugging-new/src/modules/runtime-client-factory.js
devtools/client/aboutdebugging-new/src/reducers/runtimes-state.js
devtools/client/aboutdebugging-new/test/browser/mocks/head-client-wrapper-mock.js
devtools/client/shared/moz.build
devtools/client/shared/remote-debugging/moz.build
devtools/client/shared/remote-debugging/remote-client-manager.js
--- a/devtools/client/aboutdebugging-new/aboutdebugging.js
+++ b/devtools/client/aboutdebugging-new/aboutdebugging.js
@@ -94,16 +94,19 @@ const AboutDebugging = {
 
     l10n.destroy();
 
     const currentRuntimeId = state.runtimes.selectedRuntimeId;
     if (currentRuntimeId) {
       await this.actions.unwatchRuntime(currentRuntimeId);
     }
 
+    // Remove all client listeners.
+    this.actions.removeRuntimeListeners();
+
     removeNetworkLocationsObserver(this.onNetworkLocationsUpdated);
     removeUSBRuntimesObserver(this.onUSBRuntimesUpdated);
     disableUSBRuntimes();
     adbAddon.off("update", this.onAdbAddonUpdated);
     setDebugTargetCollapsibilities(state.ui.debugTargetCollapsibilities);
     unmountComponentAtNode(this.mount);
   },
 
--- a/devtools/client/aboutdebugging-new/src/actions/runtimes.js
+++ b/devtools/client/aboutdebugging-new/src/actions/runtimes.js
@@ -11,16 +11,19 @@ const Actions = require("./index");
 const {
   getCurrentRuntime,
   findRuntimeById,
 } = require("../modules/runtimes-state-helper");
 const { isSupportedDebugTarget } = require("../modules/debug-target-support");
 
 const { createClientForRuntime } = require("../modules/runtime-client-factory");
 
+const { remoteClientManager } =
+  require("devtools/client/shared/remote-debugging/remote-client-manager");
+
 const {
   CONNECT_RUNTIME_FAILURE,
   CONNECT_RUNTIME_START,
   CONNECT_RUNTIME_SUCCESS,
   DEBUG_TARGETS,
   DISCONNECT_RUNTIME_FAILURE,
   DISCONNECT_RUNTIME_START,
   DISCONNECT_RUNTIME_SUCCESS,
@@ -223,19 +226,45 @@ function updateUSBRuntimes(runtimes) {
     const existingRuntimes = getState().runtimes.usbRuntimes;
     const invalidRuntimes = existingRuntimes.filter(r => !validIds.includes(r.id));
 
     for (const invalidRuntime of invalidRuntimes) {
       await dispatch(disconnectRuntime(invalidRuntime.id));
     }
 
     dispatch({ type: USB_RUNTIMES_UPDATED, runtimes });
+
+    for (const runtime of getState().runtimes.usbRuntimes) {
+      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 { usbRuntimes } = getState().runtimes;
+    for (const runtime of usbRuntimes) {
+      if (runtime.runtimeDetails) {
+        const { clientWrapper } = runtime.runtimeDetails;
+        clientWrapper.removeListener("closed", onUSBDebuggerClientClosed);
+      }
+    }
   };
 }
 
 module.exports = {
   connectRuntime,
   disconnectRuntime,
+  removeRuntimeListeners,
   unwatchRuntime,
   updateConnectionPromptSetting,
   updateUSBRuntimes,
   watchRuntime,
 };
--- a/devtools/client/aboutdebugging-new/src/modules/runtime-client-factory.js
+++ b/devtools/client/aboutdebugging-new/src/modules/runtime-client-factory.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { ADB } = require("devtools/shared/adb/adb");
 const { DebuggerClient } = require("devtools/shared/client/debugger-client");
 const { DebuggerServer } = require("devtools/server/main");
 const { ClientWrapper } = require("./client-wrapper");
+const { remoteClientManager } =
+  require("devtools/client/shared/remote-debugging/remote-client-manager");
 
 const { RUNTIMES } = require("../constants");
 
 async function createLocalClient() {
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   await client.connect();
@@ -28,20 +30,23 @@ async function createNetworkClient(host,
 }
 
 async function createUSBClient(socketPath) {
   const port = await ADB.prepareTCPConnection(socketPath);
   return createNetworkClient("localhost", port);
 }
 
 async function createClientForRuntime(runtime) {
-  const { extra, type } = runtime;
+  const { extra, id, type } = runtime;
 
   if (type === RUNTIMES.THIS_FIREFOX) {
     return createLocalClient();
+  } else if (remoteClientManager.hasClient(id, type)) {
+    const { client, transportDetails } = remoteClientManager.getClient(id, type);
+    return { clientWrapper: new ClientWrapper(client), transportDetails };
   } else if (type === RUNTIMES.NETWORK) {
     const { host, port } = extra.connectionParameters;
     return createNetworkClient(host, port);
   } else if (type === RUNTIMES.USB) {
     const { socketPath } = extra.connectionParameters;
     return createUSBClient(socketPath);
   }
 
--- a/devtools/client/aboutdebugging-new/src/reducers/runtimes-state.js
+++ b/devtools/client/aboutdebugging-new/src/reducers/runtimes-state.js
@@ -14,16 +14,19 @@ const {
   USB_RUNTIMES_UPDATED,
   WATCH_RUNTIME_SUCCESS,
 } = require("../constants");
 
 const {
   findRuntimeById,
 } = require("../modules/runtimes-state-helper");
 
+const { remoteClientManager } =
+  require("devtools/client/shared/remote-debugging/remote-client-manager");
+
 // Map between known runtime types and nodes in the runtimes state.
 const TYPE_TO_RUNTIMES_KEY = {
   [RUNTIMES.THIS_FIREFOX]: "thisFirefoxRuntimes",
   [RUNTIMES.NETWORK]: "networkRuntimes",
   [RUNTIMES.USB]: "usbRuntimes",
 };
 
 function RuntimesState() {
@@ -65,22 +68,27 @@ function _updateRuntimeById(runtimeId, u
     return r;
   });
   return Object.assign({}, state, { [key]: updatedRuntimes });
 }
 
 function runtimesReducer(state = RuntimesState(), action) {
   switch (action.type) {
     case CONNECT_RUNTIME_SUCCESS: {
-      const { id, runtimeDetails } = action.runtime;
+      const { id, runtimeDetails, type } = action.runtime;
+      remoteClientManager.setClient(id, type, {
+        client: runtimeDetails.clientWrapper.client,
+        transportDetails: runtimeDetails.transportDetails,
+      });
       return _updateRuntimeById(id, { runtimeDetails }, state);
     }
 
     case DISCONNECT_RUNTIME_SUCCESS: {
-      const { id } = action.runtime;
+      const { id, type } = action.runtime;
+      remoteClientManager.removeClient(id, type);
       return _updateRuntimeById(id, { runtimeDetails: null }, state);
     }
 
     case NETWORK_LOCATIONS_UPDATED: {
       const { locations } = action;
       const networkRuntimes = locations.map(location => {
         const [ host, port ] = location.split(":");
         return {
@@ -107,18 +115,18 @@ function runtimesReducer(state = Runtime
         Object.assign({}, runtime.runtimeDetails, { connectionPromptEnabled });
       return _updateRuntimeById(runtimeId, { runtimeDetails }, state);
     }
 
     case USB_RUNTIMES_UPDATED: {
       const { runtimes } = action;
       const usbRuntimes = runtimes.map(runtime => {
         const existingRuntime = findRuntimeById(runtime.id, state);
-        const existingRuntimeDetails =
-          existingRuntime ? existingRuntime.runtimeDetails : null;
+        const existingRuntimeDetails = existingRuntime ?
+          existingRuntime.runtimeDetails : null;
 
         return {
           id: runtime.id,
           extra: {
             connectionParameters: { socketPath: runtime._socketPath },
             deviceName: runtime.deviceName,
           },
           name: runtime.shortName,
--- a/devtools/client/aboutdebugging-new/test/browser/mocks/head-client-wrapper-mock.js
+++ b/devtools/client/aboutdebugging-new/test/browser/mocks/head-client-wrapper-mock.js
@@ -30,16 +30,29 @@ function createClientMock() {
       eventEmitter.once(evt, listener);
     },
     addListener: (evt, listener) => {
       eventEmitter.on(evt, listener);
     },
     removeListener: (evt, listener) => {
       eventEmitter.off(evt, listener);
     },
+
+    client: {
+      addOneTimeListener: (evt, listener) => {
+        eventEmitter.once(evt, listener);
+      },
+      addListener: (evt, listener) => {
+        eventEmitter.on(evt, listener);
+      },
+      removeListener: (evt, listener) => {
+        eventEmitter.off(evt, listener);
+      },
+    },
+
     // no-op
     close: () => {},
     // no-op
     connect: () => {},
     // no-op
     getDeviceDescription: () => {},
     // Return default preference value or null if no match.
     getPreference: (prefName) => {
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -8,16 +8,17 @@ BROWSER_CHROME_MANIFESTS += ['test/brows
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 TEST_HARNESS_FILES.xpcshell.devtools.client.shared.test += [
     'test/shared-redux-head.js',
 ]
 
 DIRS += [
     'components',
     'redux',
+    'remote-debugging',
     'source-map',
     'vendor',
     'webpack',
     'widgets',
 ]
 
 DevToolsModules(
     'autocomplete-popup.js',
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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(
+    'remote-client-manager.js',
+)
+
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'about:debugging')
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/remote-client-manager.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This class is designed to be a singleton shared by all DevTools to get access to
+ * existing clients created for remote debugging.
+ */
+class RemoteClientManager {
+  constructor() {
+    this._clients = new Map();
+    this._onClientClosed = this._onClientClosed.bind(this);
+  }
+
+  /**
+   * Store a remote client that is already connected.
+   *
+   * @param {String} id
+   *        Remote runtime id (see devtools/client/aboutdebugging-new/src/types).
+   * @param {String} type
+   *        Remote runtime type (see devtools/client/aboutdebugging-new/src/types).
+   * @param {Object}
+   *        - client: {DebuggerClient}
+   *        - transportDetails: {Object} typically a host object ({hostname, port}) that
+   *          allows consumers to easily find the connection information for this client.
+   */
+  setClient(id, type, { client, transportDetails }) {
+    const key = this._getKey(id, type);
+    this._clients.set(key, { client, transportDetails });
+
+    client.addOneTimeListener("closed", this._onClientClosed);
+  }
+
+  // See JSDoc for id, type from setClient.
+  hasClient(id, type) {
+    return this._clients.has(this._getKey(id, type));
+  }
+
+  // See JSDoc for id, type from setClient.
+  getClient(id, type) {
+    return this._clients.get(this._getKey(id, type));
+  }
+
+  // See JSDoc for id, type from setClient.
+  removeClient(id, type) {
+    const key = this._getKey(id, type);
+    this._removeClientByKey(key);
+  }
+
+  _getKey(id, type) {
+    return id + "-" + type;
+  }
+
+  _removeClientByKey(key) {
+    if (this.hasClient(key)) {
+      this.getClient(key).client.removeListener("closed", this._onClientClosed);
+      this._clients.delete(key);
+    }
+  }
+
+  /**
+   * Cleanup all closed clients when a "closed" notification is received from a client.
+   */
+  _onClientClosed() {
+    const closedClientKeys = [...this._clients.keys()].filter(key => {
+      const clientInfo = this._clients.get(key);
+      return clientInfo.client._closed;
+    });
+
+    for (const key of closedClientKeys) {
+      this._removeClientByKey(key);
+    }
+  }
+}
+
+// Expose a singleton of RemoteClientManager.
+exports.remoteClientManager = new RemoteClientManager();