summaryrefslogtreecommitdiffstats
path: root/devtools/shared/client/connection-manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/client/connection-manager.js')
-rw-r--r--devtools/shared/client/connection-manager.js382
1 files changed, 382 insertions, 0 deletions
diff --git a/devtools/shared/client/connection-manager.js b/devtools/shared/client/connection-manager.js
new file mode 100644
index 000000000..ef242db85
--- /dev/null
+++ b/devtools/shared/client/connection-manager.js
@@ -0,0 +1,382 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {Cc, Ci, Cu, Cr} = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { DebuggerServer } = require("devtools/server/main");
+const { DebuggerClient } = require("devtools/shared/client/main");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+
+const REMOTE_TIMEOUT = "devtools.debugger.remote-timeout";
+
+/**
+ * Connection Manager.
+ *
+ * To use this module:
+ * const {ConnectionManager} = require("devtools/shared/client/connection-manager");
+ *
+ * # ConnectionManager
+ *
+ * Methods:
+ * . Connection createConnection(host, port)
+ * . void destroyConnection(connection)
+ * . Number getFreeTCPPort()
+ *
+ * Properties:
+ * . Array connections
+ *
+ * # Connection
+ *
+ * A connection is a wrapper around a debugger client. It has a simple
+ * API to instantiate a connection to a debugger server. Once disconnected,
+ * no need to re-create a Connection object. Calling `connect()` again
+ * will re-create a debugger client.
+ *
+ * Methods:
+ * . connect() Connect to host:port. Expect a "connecting" event.
+ * If no host is not specified, a local pipe is used
+ * . connect(transport) Connect via transport. Expect a "connecting" event.
+ * . disconnect() Disconnect if connected. Expect a "disconnecting" event
+ *
+ * Properties:
+ * . host IP address or hostname
+ * . port Port
+ * . logs Current logs. "newlog" event notifies new available logs
+ * . store Reference to a local data store (see below)
+ * . keepConnecting Should the connection keep trying to connect?
+ * . timeoutDelay When should we give up (in ms)?
+ * 0 means wait forever.
+ * . encryption Should the connection be encrypted?
+ * . authentication What authentication scheme should be used?
+ * . authenticator The |Authenticator| instance used. Overriding
+ * properties of this instance may be useful to
+ * customize authentication UX for a specific use case.
+ * . advertisement The server's advertisement if found by discovery
+ * . status Connection status:
+ * Connection.Status.CONNECTED
+ * Connection.Status.DISCONNECTED
+ * Connection.Status.CONNECTING
+ * Connection.Status.DISCONNECTING
+ * Connection.Status.DESTROYED
+ *
+ * Events (as in event-emitter.js):
+ * . Connection.Events.CONNECTING Trying to connect to host:port
+ * . Connection.Events.CONNECTED Connection is successful
+ * . Connection.Events.DISCONNECTING Trying to disconnect from server
+ * . Connection.Events.DISCONNECTED Disconnected (at client request, or because of a timeout or connection error)
+ * . Connection.Events.STATUS_CHANGED The connection status (connection.status) has changed
+ * . Connection.Events.TIMEOUT Connection timeout
+ * . Connection.Events.HOST_CHANGED Host has changed
+ * . Connection.Events.PORT_CHANGED Port has changed
+ * . Connection.Events.NEW_LOG A new log line is available
+ *
+ */
+
+var ConnectionManager = {
+ _connections: new Set(),
+ createConnection: function (host, port) {
+ let c = new Connection(host, port);
+ c.once("destroy", (event) => this.destroyConnection(c));
+ this._connections.add(c);
+ this.emit("new", c);
+ return c;
+ },
+ destroyConnection: function (connection) {
+ if (this._connections.has(connection)) {
+ this._connections.delete(connection);
+ if (connection.status != Connection.Status.DESTROYED) {
+ connection.destroy();
+ }
+ }
+ },
+ get connections() {
+ return [...this._connections];
+ },
+ getFreeTCPPort: function () {
+ let serv = Cc["@mozilla.org/network/server-socket;1"]
+ .createInstance(Ci.nsIServerSocket);
+ serv.init(-1, true, -1);
+ let port = serv.port;
+ serv.close();
+ return port;
+ },
+};
+
+EventEmitter.decorate(ConnectionManager);
+
+var lastID = -1;
+
+function Connection(host, port) {
+ EventEmitter.decorate(this);
+ this.uid = ++lastID;
+ this.host = host;
+ this.port = port;
+ this._setStatus(Connection.Status.DISCONNECTED);
+ this._onDisconnected = this._onDisconnected.bind(this);
+ this._onConnected = this._onConnected.bind(this);
+ this._onTimeout = this._onTimeout.bind(this);
+ this.resetOptions();
+}
+
+Connection.Status = {
+ CONNECTED: "connected",
+ DISCONNECTED: "disconnected",
+ CONNECTING: "connecting",
+ DISCONNECTING: "disconnecting",
+ DESTROYED: "destroyed",
+};
+
+Connection.Events = {
+ CONNECTED: Connection.Status.CONNECTED,
+ DISCONNECTED: Connection.Status.DISCONNECTED,
+ CONNECTING: Connection.Status.CONNECTING,
+ DISCONNECTING: Connection.Status.DISCONNECTING,
+ DESTROYED: Connection.Status.DESTROYED,
+ TIMEOUT: "timeout",
+ STATUS_CHANGED: "status-changed",
+ HOST_CHANGED: "host-changed",
+ PORT_CHANGED: "port-changed",
+ NEW_LOG: "new_log"
+};
+
+Connection.prototype = {
+ logs: "",
+ log: function (str) {
+ let d = new Date();
+ let hours = ("0" + d.getHours()).slice(-2);
+ let minutes = ("0" + d.getMinutes()).slice(-2);
+ let seconds = ("0" + d.getSeconds()).slice(-2);
+ let timestamp = [hours, minutes, seconds].join(":") + ": ";
+ str = timestamp + str;
+ this.logs += "\n" + str;
+ this.emit(Connection.Events.NEW_LOG, str);
+ },
+
+ get client() {
+ return this._client;
+ },
+
+ get host() {
+ return this._host;
+ },
+
+ set host(value) {
+ if (this._host && this._host == value)
+ return;
+ this._host = value;
+ this.emit(Connection.Events.HOST_CHANGED);
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ set port(value) {
+ if (this._port && this._port == value)
+ return;
+ this._port = value;
+ this.emit(Connection.Events.PORT_CHANGED);
+ },
+
+ get authentication() {
+ return this._authentication;
+ },
+
+ set authentication(value) {
+ this._authentication = value;
+ // Create an |Authenticator| of this type
+ if (!value) {
+ this.authenticator = null;
+ return;
+ }
+ let AuthenticatorType = DebuggerClient.Authenticators.get(value);
+ this.authenticator = new AuthenticatorType.Client();
+ },
+
+ get advertisement() {
+ return this._advertisement;
+ },
+
+ set advertisement(advertisement) {
+ // The full advertisement may contain more info than just the standard keys
+ // below, so keep a copy for use during connection later.
+ this._advertisement = advertisement;
+ if (advertisement) {
+ ["host", "port", "encryption", "authentication"].forEach(key => {
+ this[key] = advertisement[key];
+ });
+ }
+ },
+
+ /**
+ * Settings to be passed to |socketConnect| at connection time.
+ */
+ get socketSettings() {
+ let settings = {};
+ if (this.advertisement) {
+ // Use the advertisement as starting point if it exists, as it may contain
+ // extra data, like the server's cert.
+ Object.assign(settings, this.advertisement);
+ }
+ Object.assign(settings, {
+ host: this.host,
+ port: this.port,
+ encryption: this.encryption,
+ authenticator: this.authenticator
+ });
+ return settings;
+ },
+
+ timeoutDelay: Services.prefs.getIntPref(REMOTE_TIMEOUT),
+
+ resetOptions() {
+ this.keepConnecting = false;
+ this.timeoutDelay = Services.prefs.getIntPref(REMOTE_TIMEOUT);
+ this.encryption = false;
+ this.authentication = null;
+ this.advertisement = null;
+ },
+
+ disconnect: function (force) {
+ if (this.status == Connection.Status.DESTROYED) {
+ return;
+ }
+ clearTimeout(this._timeoutID);
+ if (this.status == Connection.Status.CONNECTED ||
+ this.status == Connection.Status.CONNECTING) {
+ this.log("disconnecting");
+ this._setStatus(Connection.Status.DISCONNECTING);
+ if (this._client) {
+ this._client.close();
+ }
+ }
+ },
+
+ connect: function (transport) {
+ if (this.status == Connection.Status.DESTROYED) {
+ return;
+ }
+ if (!this._client) {
+ this._customTransport = transport;
+ if (this._customTransport) {
+ this.log("connecting (custom transport)");
+ } else {
+ this.log("connecting to " + this.host + ":" + this.port);
+ }
+ this._setStatus(Connection.Status.CONNECTING);
+
+ if (this.timeoutDelay > 0) {
+ this._timeoutID = setTimeout(this._onTimeout, this.timeoutDelay);
+ }
+ this._clientConnect();
+ } else {
+ let msg = "Can't connect. Client is not fully disconnected";
+ this.log(msg);
+ throw new Error(msg);
+ }
+ },
+
+ destroy: function () {
+ this.log("killing connection");
+ clearTimeout(this._timeoutID);
+ this.keepConnecting = false;
+ if (this._client) {
+ this._client.close();
+ this._client = null;
+ }
+ this._setStatus(Connection.Status.DESTROYED);
+ },
+
+ _getTransport: Task.async(function* () {
+ if (this._customTransport) {
+ return this._customTransport;
+ }
+ if (!this.host) {
+ return DebuggerServer.connectPipe();
+ }
+ let settings = this.socketSettings;
+ let transport = yield DebuggerClient.socketConnect(settings);
+ return transport;
+ }),
+
+ _clientConnect: function () {
+ this._getTransport().then(transport => {
+ if (!transport) {
+ return;
+ }
+ this._client = new DebuggerClient(transport);
+ this._client.addOneTimeListener("closed", this._onDisconnected);
+ this._client.connect().then(this._onConnected);
+ }, e => {
+ // If we're continuously trying to connect, we expect the connection to be
+ // rejected a couple times, so don't log these.
+ if (!this.keepConnecting || e.result !== Cr.NS_ERROR_CONNECTION_REFUSED) {
+ console.error(e);
+ }
+ // In some cases, especially on Mac, the openOutputStream call in
+ // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
+ // It occurs when we connect agressively to the simulator,
+ // and keep trying to open a socket to the server being started in
+ // the simulator.
+ this._onDisconnected();
+ });
+ },
+
+ get status() {
+ return this._status;
+ },
+
+ _setStatus: function (value) {
+ if (this._status && this._status == value)
+ return;
+ this._status = value;
+ this.emit(value);
+ this.emit(Connection.Events.STATUS_CHANGED, value);
+ },
+
+ _onDisconnected: function () {
+ this._client = null;
+ this._customTransport = null;
+
+ if (this._status == Connection.Status.CONNECTING && this.keepConnecting) {
+ setTimeout(() => this._clientConnect(), 100);
+ return;
+ }
+
+ clearTimeout(this._timeoutID);
+
+ switch (this.status) {
+ case Connection.Status.CONNECTED:
+ this.log("disconnected (unexpected)");
+ break;
+ case Connection.Status.CONNECTING:
+ this.log("connection error. Possible causes: USB port not connected, port not forwarded (adb forward), wrong host or port, remote debugging not enabled on the device.");
+ break;
+ default:
+ this.log("disconnected");
+ }
+ this._setStatus(Connection.Status.DISCONNECTED);
+ },
+
+ _onConnected: function () {
+ this.log("connected");
+ clearTimeout(this._timeoutID);
+ this._setStatus(Connection.Status.CONNECTED);
+ },
+
+ _onTimeout: function () {
+ this.log("connection timeout. Possible causes: didn't click on 'accept' (prompt).");
+ this.emit(Connection.Events.TIMEOUT);
+ this.disconnect();
+ },
+};
+
+exports.ConnectionManager = ConnectionManager;
+exports.Connection = Connection;