connection-manager.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const {Cc, Ci, Cu, Cr} = require("chrome");
  7. const EventEmitter = require("devtools/shared/event-emitter");
  8. const DevToolsUtils = require("devtools/shared/DevToolsUtils");
  9. const { DebuggerServer } = require("devtools/server/main");
  10. const { DebuggerClient } = require("devtools/shared/client/main");
  11. const Services = require("Services");
  12. const { Task } = require("devtools/shared/task");
  13. const REMOTE_TIMEOUT = "devtools.debugger.remote-timeout";
  14. /**
  15. * Connection Manager.
  16. *
  17. * To use this module:
  18. * const {ConnectionManager} = require("devtools/shared/client/connection-manager");
  19. *
  20. * # ConnectionManager
  21. *
  22. * Methods:
  23. * . Connection createConnection(host, port)
  24. * . void destroyConnection(connection)
  25. * . Number getFreeTCPPort()
  26. *
  27. * Properties:
  28. * . Array connections
  29. *
  30. * # Connection
  31. *
  32. * A connection is a wrapper around a debugger client. It has a simple
  33. * API to instantiate a connection to a debugger server. Once disconnected,
  34. * no need to re-create a Connection object. Calling `connect()` again
  35. * will re-create a debugger client.
  36. *
  37. * Methods:
  38. * . connect() Connect to host:port. Expect a "connecting" event.
  39. * If no host is not specified, a local pipe is used
  40. * . connect(transport) Connect via transport. Expect a "connecting" event.
  41. * . disconnect() Disconnect if connected. Expect a "disconnecting" event
  42. *
  43. * Properties:
  44. * . host IP address or hostname
  45. * . port Port
  46. * . logs Current logs. "newlog" event notifies new available logs
  47. * . store Reference to a local data store (see below)
  48. * . keepConnecting Should the connection keep trying to connect?
  49. * . timeoutDelay When should we give up (in ms)?
  50. * 0 means wait forever.
  51. * . encryption Should the connection be encrypted?
  52. * . authentication What authentication scheme should be used?
  53. * . authenticator The |Authenticator| instance used. Overriding
  54. * properties of this instance may be useful to
  55. * customize authentication UX for a specific use case.
  56. * . advertisement The server's advertisement if found by discovery
  57. * . status Connection status:
  58. * Connection.Status.CONNECTED
  59. * Connection.Status.DISCONNECTED
  60. * Connection.Status.CONNECTING
  61. * Connection.Status.DISCONNECTING
  62. * Connection.Status.DESTROYED
  63. *
  64. * Events (as in event-emitter.js):
  65. * . Connection.Events.CONNECTING Trying to connect to host:port
  66. * . Connection.Events.CONNECTED Connection is successful
  67. * . Connection.Events.DISCONNECTING Trying to disconnect from server
  68. * . Connection.Events.DISCONNECTED Disconnected (at client request, or because of a timeout or connection error)
  69. * . Connection.Events.STATUS_CHANGED The connection status (connection.status) has changed
  70. * . Connection.Events.TIMEOUT Connection timeout
  71. * . Connection.Events.HOST_CHANGED Host has changed
  72. * . Connection.Events.PORT_CHANGED Port has changed
  73. * . Connection.Events.NEW_LOG A new log line is available
  74. *
  75. */
  76. var ConnectionManager = {
  77. _connections: new Set(),
  78. createConnection: function (host, port) {
  79. let c = new Connection(host, port);
  80. c.once("destroy", (event) => this.destroyConnection(c));
  81. this._connections.add(c);
  82. this.emit("new", c);
  83. return c;
  84. },
  85. destroyConnection: function (connection) {
  86. if (this._connections.has(connection)) {
  87. this._connections.delete(connection);
  88. if (connection.status != Connection.Status.DESTROYED) {
  89. connection.destroy();
  90. }
  91. }
  92. },
  93. get connections() {
  94. return [...this._connections];
  95. },
  96. getFreeTCPPort: function () {
  97. let serv = Cc["@mozilla.org/network/server-socket;1"]
  98. .createInstance(Ci.nsIServerSocket);
  99. serv.init(-1, true, -1);
  100. let port = serv.port;
  101. serv.close();
  102. return port;
  103. },
  104. };
  105. EventEmitter.decorate(ConnectionManager);
  106. var lastID = -1;
  107. function Connection(host, port) {
  108. EventEmitter.decorate(this);
  109. this.uid = ++lastID;
  110. this.host = host;
  111. this.port = port;
  112. this._setStatus(Connection.Status.DISCONNECTED);
  113. this._onDisconnected = this._onDisconnected.bind(this);
  114. this._onConnected = this._onConnected.bind(this);
  115. this._onTimeout = this._onTimeout.bind(this);
  116. this.resetOptions();
  117. }
  118. Connection.Status = {
  119. CONNECTED: "connected",
  120. DISCONNECTED: "disconnected",
  121. CONNECTING: "connecting",
  122. DISCONNECTING: "disconnecting",
  123. DESTROYED: "destroyed",
  124. };
  125. Connection.Events = {
  126. CONNECTED: Connection.Status.CONNECTED,
  127. DISCONNECTED: Connection.Status.DISCONNECTED,
  128. CONNECTING: Connection.Status.CONNECTING,
  129. DISCONNECTING: Connection.Status.DISCONNECTING,
  130. DESTROYED: Connection.Status.DESTROYED,
  131. TIMEOUT: "timeout",
  132. STATUS_CHANGED: "status-changed",
  133. HOST_CHANGED: "host-changed",
  134. PORT_CHANGED: "port-changed",
  135. NEW_LOG: "new_log"
  136. };
  137. Connection.prototype = {
  138. logs: "",
  139. log: function (str) {
  140. let d = new Date();
  141. let hours = ("0" + d.getHours()).slice(-2);
  142. let minutes = ("0" + d.getMinutes()).slice(-2);
  143. let seconds = ("0" + d.getSeconds()).slice(-2);
  144. let timestamp = [hours, minutes, seconds].join(":") + ": ";
  145. str = timestamp + str;
  146. this.logs += "\n" + str;
  147. this.emit(Connection.Events.NEW_LOG, str);
  148. },
  149. get client() {
  150. return this._client;
  151. },
  152. get host() {
  153. return this._host;
  154. },
  155. set host(value) {
  156. if (this._host && this._host == value)
  157. return;
  158. this._host = value;
  159. this.emit(Connection.Events.HOST_CHANGED);
  160. },
  161. get port() {
  162. return this._port;
  163. },
  164. set port(value) {
  165. if (this._port && this._port == value)
  166. return;
  167. this._port = value;
  168. this.emit(Connection.Events.PORT_CHANGED);
  169. },
  170. get authentication() {
  171. return this._authentication;
  172. },
  173. set authentication(value) {
  174. this._authentication = value;
  175. // Create an |Authenticator| of this type
  176. if (!value) {
  177. this.authenticator = null;
  178. return;
  179. }
  180. let AuthenticatorType = DebuggerClient.Authenticators.get(value);
  181. this.authenticator = new AuthenticatorType.Client();
  182. },
  183. get advertisement() {
  184. return this._advertisement;
  185. },
  186. set advertisement(advertisement) {
  187. // The full advertisement may contain more info than just the standard keys
  188. // below, so keep a copy for use during connection later.
  189. this._advertisement = advertisement;
  190. if (advertisement) {
  191. ["host", "port", "encryption", "authentication"].forEach(key => {
  192. this[key] = advertisement[key];
  193. });
  194. }
  195. },
  196. /**
  197. * Settings to be passed to |socketConnect| at connection time.
  198. */
  199. get socketSettings() {
  200. let settings = {};
  201. if (this.advertisement) {
  202. // Use the advertisement as starting point if it exists, as it may contain
  203. // extra data, like the server's cert.
  204. Object.assign(settings, this.advertisement);
  205. }
  206. Object.assign(settings, {
  207. host: this.host,
  208. port: this.port,
  209. encryption: this.encryption,
  210. authenticator: this.authenticator
  211. });
  212. return settings;
  213. },
  214. timeoutDelay: Services.prefs.getIntPref(REMOTE_TIMEOUT),
  215. resetOptions() {
  216. this.keepConnecting = false;
  217. this.timeoutDelay = Services.prefs.getIntPref(REMOTE_TIMEOUT);
  218. this.encryption = false;
  219. this.authentication = null;
  220. this.advertisement = null;
  221. },
  222. disconnect: function (force) {
  223. if (this.status == Connection.Status.DESTROYED) {
  224. return;
  225. }
  226. clearTimeout(this._timeoutID);
  227. if (this.status == Connection.Status.CONNECTED ||
  228. this.status == Connection.Status.CONNECTING) {
  229. this.log("disconnecting");
  230. this._setStatus(Connection.Status.DISCONNECTING);
  231. if (this._client) {
  232. this._client.close();
  233. }
  234. }
  235. },
  236. connect: function (transport) {
  237. if (this.status == Connection.Status.DESTROYED) {
  238. return;
  239. }
  240. if (!this._client) {
  241. this._customTransport = transport;
  242. if (this._customTransport) {
  243. this.log("connecting (custom transport)");
  244. } else {
  245. this.log("connecting to " + this.host + ":" + this.port);
  246. }
  247. this._setStatus(Connection.Status.CONNECTING);
  248. if (this.timeoutDelay > 0) {
  249. this._timeoutID = setTimeout(this._onTimeout, this.timeoutDelay);
  250. }
  251. this._clientConnect();
  252. } else {
  253. let msg = "Can't connect. Client is not fully disconnected";
  254. this.log(msg);
  255. throw new Error(msg);
  256. }
  257. },
  258. destroy: function () {
  259. this.log("killing connection");
  260. clearTimeout(this._timeoutID);
  261. this.keepConnecting = false;
  262. if (this._client) {
  263. this._client.close();
  264. this._client = null;
  265. }
  266. this._setStatus(Connection.Status.DESTROYED);
  267. },
  268. _getTransport: Task.async(function* () {
  269. if (this._customTransport) {
  270. return this._customTransport;
  271. }
  272. if (!this.host) {
  273. return DebuggerServer.connectPipe();
  274. }
  275. let settings = this.socketSettings;
  276. let transport = yield DebuggerClient.socketConnect(settings);
  277. return transport;
  278. }),
  279. _clientConnect: function () {
  280. this._getTransport().then(transport => {
  281. if (!transport) {
  282. return;
  283. }
  284. this._client = new DebuggerClient(transport);
  285. this._client.addOneTimeListener("closed", this._onDisconnected);
  286. this._client.connect().then(this._onConnected);
  287. }, e => {
  288. // If we're continuously trying to connect, we expect the connection to be
  289. // rejected a couple times, so don't log these.
  290. if (!this.keepConnecting || e.result !== Cr.NS_ERROR_CONNECTION_REFUSED) {
  291. console.error(e);
  292. }
  293. // In some cases, especially on Mac, the openOutputStream call in
  294. // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
  295. // It occurs when we connect agressively to the simulator,
  296. // and keep trying to open a socket to the server being started in
  297. // the simulator.
  298. this._onDisconnected();
  299. });
  300. },
  301. get status() {
  302. return this._status;
  303. },
  304. _setStatus: function (value) {
  305. if (this._status && this._status == value)
  306. return;
  307. this._status = value;
  308. this.emit(value);
  309. this.emit(Connection.Events.STATUS_CHANGED, value);
  310. },
  311. _onDisconnected: function () {
  312. this._client = null;
  313. this._customTransport = null;
  314. if (this._status == Connection.Status.CONNECTING && this.keepConnecting) {
  315. setTimeout(() => this._clientConnect(), 100);
  316. return;
  317. }
  318. clearTimeout(this._timeoutID);
  319. switch (this.status) {
  320. case Connection.Status.CONNECTED:
  321. this.log("disconnected (unexpected)");
  322. break;
  323. case Connection.Status.CONNECTING:
  324. 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.");
  325. break;
  326. default:
  327. this.log("disconnected");
  328. }
  329. this._setStatus(Connection.Status.DISCONNECTED);
  330. },
  331. _onConnected: function () {
  332. this.log("connected");
  333. clearTimeout(this._timeoutID);
  334. this._setStatus(Connection.Status.CONNECTED);
  335. },
  336. _onTimeout: function () {
  337. this.log("connection timeout. Possible causes: didn't click on 'accept' (prompt).");
  338. this.emit(Connection.Events.TIMEOUT);
  339. this.disconnect();
  340. },
  341. };
  342. exports.ConnectionManager = ConnectionManager;
  343. exports.Connection = Connection;