/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* 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;