382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
|
/* -*- 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;
|