Bug 1497457 - Introduce remote client manager to persist connected remote clients;r=daisuke,ladybenko
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 27 Nov 2018 10:14:50 +0000
changeset 507438 fe4c1c6d7ebf20056824d489e085856b507f4bdf
parent 507437 b078b2df55db814d01c7402587a3dd5c0b6dff36
child 507439 162b26b027a5b63242d37b6c8135ca2171295755
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();