123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- /* -*- 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;
|