michael@0: /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: const {setTimeout, clearTimeout} = require('sdk/timers'); michael@0: const EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); michael@0: michael@0: /** michael@0: * Connection Manager. michael@0: * michael@0: * To use this module: michael@0: * const {ConnectionManager} = require("devtools/client/connection-manager"); michael@0: * michael@0: * # ConnectionManager michael@0: * michael@0: * Methods: michael@0: * ⬩ Connection createConnection(host, port) michael@0: * ⬩ void destroyConnection(connection) michael@0: * ⬩ Number getFreeTCPPort() michael@0: * michael@0: * Properties: michael@0: * ⬩ Array connections michael@0: * michael@0: * # Connection michael@0: * michael@0: * A connection is a wrapper around a debugger client. It has a simple michael@0: * API to instantiate a connection to a debugger server. Once disconnected, michael@0: * no need to re-create a Connection object. Calling `connect()` again michael@0: * will re-create a debugger client. michael@0: * michael@0: * Methods: michael@0: * ⬩ connect() Connect to host:port. Expect a "connecting" event. If michael@0: * host is not specified, a local pipe is used michael@0: * ⬩ disconnect() Disconnect if connected. Expect a "disconnecting" event michael@0: * michael@0: * Properties: michael@0: * ⬩ host IP address or hostname michael@0: * ⬩ port Port michael@0: * ⬩ logs Current logs. "newlog" event notifies new available logs michael@0: * ⬩ store Reference to a local data store (see below) michael@0: * ⬩ keepConnecting Should the connection keep trying connecting michael@0: * ⬩ status Connection status: michael@0: * Connection.Status.CONNECTED michael@0: * Connection.Status.DISCONNECTED michael@0: * Connection.Status.CONNECTING michael@0: * Connection.Status.DISCONNECTING michael@0: * Connection.Status.DESTROYED michael@0: * michael@0: * Events (as in event-emitter.js): michael@0: * ⬩ Connection.Events.CONNECTING Trying to connect to host:port michael@0: * ⬩ Connection.Events.CONNECTED Connection is successful michael@0: * ⬩ Connection.Events.DISCONNECTING Trying to disconnect from server michael@0: * ⬩ Connection.Events.DISCONNECTED Disconnected (at client request, or because of a timeout or connection error) michael@0: * ⬩ Connection.Events.STATUS_CHANGED The connection status (connection.status) has changed michael@0: * ⬩ Connection.Events.TIMEOUT Connection timeout michael@0: * ⬩ Connection.Events.HOST_CHANGED Host has changed michael@0: * ⬩ Connection.Events.PORT_CHANGED Port has changed michael@0: * ⬩ Connection.Events.NEW_LOG A new log line is available michael@0: * michael@0: */ michael@0: michael@0: let ConnectionManager = { michael@0: _connections: new Set(), michael@0: createConnection: function(host, port) { michael@0: let c = new Connection(host, port); michael@0: c.once("destroy", (event) => this.destroyConnection(c)); michael@0: this._connections.add(c); michael@0: this.emit("new", c); michael@0: return c; michael@0: }, michael@0: destroyConnection: function(connection) { michael@0: if (this._connections.has(connection)) { michael@0: this._connections.delete(connection); michael@0: if (connection.status != Connection.Status.DESTROYED) { michael@0: connection.destroy(); michael@0: } michael@0: } michael@0: }, michael@0: get connections() { michael@0: return [c for (c of this._connections)]; michael@0: }, michael@0: getFreeTCPPort: function () { michael@0: let serv = Cc['@mozilla.org/network/server-socket;1'] michael@0: .createInstance(Ci.nsIServerSocket); michael@0: serv.init(-1, true, -1); michael@0: let port = serv.port; michael@0: serv.close(); michael@0: return port; michael@0: }, michael@0: } michael@0: michael@0: EventEmitter.decorate(ConnectionManager); michael@0: michael@0: let lastID = -1; michael@0: michael@0: function Connection(host, port) { michael@0: EventEmitter.decorate(this); michael@0: this.uid = ++lastID; michael@0: this.host = host; michael@0: this.port = port; michael@0: this._setStatus(Connection.Status.DISCONNECTED); michael@0: this._onDisconnected = this._onDisconnected.bind(this); michael@0: this._onConnected = this._onConnected.bind(this); michael@0: this._onTimeout = this._onTimeout.bind(this); michael@0: this.keepConnecting = false; michael@0: } michael@0: michael@0: Connection.Status = { michael@0: CONNECTED: "connected", michael@0: DISCONNECTED: "disconnected", michael@0: CONNECTING: "connecting", michael@0: DISCONNECTING: "disconnecting", michael@0: DESTROYED: "destroyed", michael@0: } michael@0: michael@0: Connection.Events = { michael@0: CONNECTED: Connection.Status.CONNECTED, michael@0: DISCONNECTED: Connection.Status.DISCONNECTED, michael@0: CONNECTING: Connection.Status.CONNECTING, michael@0: DISCONNECTING: Connection.Status.DISCONNECTING, michael@0: DESTROYED: Connection.Status.DESTROYED, michael@0: TIMEOUT: "timeout", michael@0: STATUS_CHANGED: "status-changed", michael@0: HOST_CHANGED: "host-changed", michael@0: PORT_CHANGED: "port-changed", michael@0: NEW_LOG: "new_log" michael@0: } michael@0: michael@0: Connection.prototype = { michael@0: logs: "", michael@0: log: function(str) { michael@0: let d = new Date(); michael@0: let hours = ("0" + d.getHours()).slice(-2); michael@0: let minutes = ("0" + d.getMinutes()).slice(-2); michael@0: let seconds = ("0" + d.getSeconds()).slice(-2); michael@0: let timestamp = [hours, minutes, seconds].join(":") + ": "; michael@0: str = timestamp + str; michael@0: this.logs += "\n" + str; michael@0: this.emit(Connection.Events.NEW_LOG, str); michael@0: }, michael@0: michael@0: get client() { michael@0: return this._client michael@0: }, michael@0: michael@0: get host() { michael@0: return this._host michael@0: }, michael@0: michael@0: set host(value) { michael@0: if (this._host && this._host == value) michael@0: return; michael@0: this._host = value; michael@0: this.emit(Connection.Events.HOST_CHANGED); michael@0: }, michael@0: michael@0: get port() { michael@0: return this._port michael@0: }, michael@0: michael@0: set port(value) { michael@0: if (this._port && this._port == value) michael@0: return; michael@0: this._port = value; michael@0: this.emit(Connection.Events.PORT_CHANGED); michael@0: }, michael@0: michael@0: disconnect: function(force) { michael@0: if (this.status == Connection.Status.DESTROYED) { michael@0: return; michael@0: } michael@0: clearTimeout(this._timeoutID); michael@0: if (this.status == Connection.Status.CONNECTED || michael@0: this.status == Connection.Status.CONNECTING) { michael@0: this.log("disconnecting"); michael@0: this._setStatus(Connection.Status.DISCONNECTING); michael@0: this._client.close(); michael@0: } michael@0: }, michael@0: michael@0: connect: function() { michael@0: if (this.status == Connection.Status.DESTROYED) { michael@0: return; michael@0: } michael@0: if (!this._client) { michael@0: this.log("connecting to " + this.host + ":" + this.port); michael@0: this._setStatus(Connection.Status.CONNECTING); michael@0: let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout"); michael@0: this._timeoutID = setTimeout(this._onTimeout, delay); michael@0: michael@0: this._clientConnect(); michael@0: } else { michael@0: let msg = "Can't connect. Client is not fully disconnected"; michael@0: this.log(msg); michael@0: throw new Error(msg); michael@0: } michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.log("killing connection"); michael@0: clearTimeout(this._timeoutID); michael@0: this.keepConnecting = false; michael@0: if (this._client) { michael@0: this._client.close(); michael@0: this._client = null; michael@0: } michael@0: this._setStatus(Connection.Status.DESTROYED); michael@0: }, michael@0: michael@0: _clientConnect: function () { michael@0: let transport; michael@0: if (!this.host) { michael@0: transport = DebuggerServer.connectPipe(); michael@0: } else { michael@0: try { michael@0: transport = debuggerSocketConnect(this.host, this.port); michael@0: } catch (e) { michael@0: // In some cases, especially on Mac, the openOutputStream call in michael@0: // debuggerSocketConnect may throw NS_ERROR_NOT_INITIALIZED. michael@0: // It occurs when we connect agressively to the simulator, michael@0: // and keep trying to open a socket to the server being started in michael@0: // the simulator. michael@0: this._onDisconnected(); michael@0: return; michael@0: } michael@0: } michael@0: this._client = new DebuggerClient(transport); michael@0: this._client.addOneTimeListener("closed", this._onDisconnected); michael@0: this._client.connect(this._onConnected); michael@0: }, michael@0: michael@0: get status() { michael@0: return this._status michael@0: }, michael@0: michael@0: _setStatus: function(value) { michael@0: if (this._status && this._status == value) michael@0: return; michael@0: this._status = value; michael@0: this.emit(value); michael@0: this.emit(Connection.Events.STATUS_CHANGED, value); michael@0: }, michael@0: michael@0: _onDisconnected: function() { michael@0: this._client = null; michael@0: michael@0: if (this._status == Connection.Status.CONNECTING && this.keepConnecting) { michael@0: setTimeout(() => this._clientConnect(), 100); michael@0: return; michael@0: } michael@0: michael@0: clearTimeout(this._timeoutID); michael@0: michael@0: switch (this.status) { michael@0: case Connection.Status.CONNECTED: michael@0: this.log("disconnected (unexpected)"); michael@0: break; michael@0: case Connection.Status.CONNECTING: michael@0: 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."); michael@0: break; michael@0: default: michael@0: this.log("disconnected"); michael@0: } michael@0: this._setStatus(Connection.Status.DISCONNECTED); michael@0: }, michael@0: michael@0: _onConnected: function() { michael@0: this.log("connected"); michael@0: clearTimeout(this._timeoutID); michael@0: this._setStatus(Connection.Status.CONNECTED); michael@0: }, michael@0: michael@0: _onTimeout: function() { michael@0: this.log("connection timeout. Possible causes: didn't click on 'accept' (prompt)."); michael@0: this.emit(Connection.Events.TIMEOUT); michael@0: this.disconnect(); michael@0: }, michael@0: } michael@0: michael@0: exports.ConnectionManager = ConnectionManager; michael@0: exports.Connection = Connection; michael@0: