|
1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 const {Cc, Ci, Cu} = require("chrome"); |
|
10 const {setTimeout, clearTimeout} = require('sdk/timers'); |
|
11 const EventEmitter = require("devtools/toolkit/event-emitter"); |
|
12 |
|
13 Cu.import("resource://gre/modules/Services.jsm"); |
|
14 Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); |
|
15 Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); |
|
16 |
|
17 /** |
|
18 * Connection Manager. |
|
19 * |
|
20 * To use this module: |
|
21 * const {ConnectionManager} = require("devtools/client/connection-manager"); |
|
22 * |
|
23 * # ConnectionManager |
|
24 * |
|
25 * Methods: |
|
26 * ⬩ Connection createConnection(host, port) |
|
27 * ⬩ void destroyConnection(connection) |
|
28 * ⬩ Number getFreeTCPPort() |
|
29 * |
|
30 * Properties: |
|
31 * ⬩ Array connections |
|
32 * |
|
33 * # Connection |
|
34 * |
|
35 * A connection is a wrapper around a debugger client. It has a simple |
|
36 * API to instantiate a connection to a debugger server. Once disconnected, |
|
37 * no need to re-create a Connection object. Calling `connect()` again |
|
38 * will re-create a debugger client. |
|
39 * |
|
40 * Methods: |
|
41 * ⬩ connect() Connect to host:port. Expect a "connecting" event. If |
|
42 * host is not specified, a local pipe is used |
|
43 * ⬩ disconnect() Disconnect if connected. Expect a "disconnecting" event |
|
44 * |
|
45 * Properties: |
|
46 * ⬩ host IP address or hostname |
|
47 * ⬩ port Port |
|
48 * ⬩ logs Current logs. "newlog" event notifies new available logs |
|
49 * ⬩ store Reference to a local data store (see below) |
|
50 * ⬩ keepConnecting Should the connection keep trying connecting |
|
51 * ⬩ status Connection status: |
|
52 * Connection.Status.CONNECTED |
|
53 * Connection.Status.DISCONNECTED |
|
54 * Connection.Status.CONNECTING |
|
55 * Connection.Status.DISCONNECTING |
|
56 * Connection.Status.DESTROYED |
|
57 * |
|
58 * Events (as in event-emitter.js): |
|
59 * ⬩ Connection.Events.CONNECTING Trying to connect to host:port |
|
60 * ⬩ Connection.Events.CONNECTED Connection is successful |
|
61 * ⬩ Connection.Events.DISCONNECTING Trying to disconnect from server |
|
62 * ⬩ Connection.Events.DISCONNECTED Disconnected (at client request, or because of a timeout or connection error) |
|
63 * ⬩ Connection.Events.STATUS_CHANGED The connection status (connection.status) has changed |
|
64 * ⬩ Connection.Events.TIMEOUT Connection timeout |
|
65 * ⬩ Connection.Events.HOST_CHANGED Host has changed |
|
66 * ⬩ Connection.Events.PORT_CHANGED Port has changed |
|
67 * ⬩ Connection.Events.NEW_LOG A new log line is available |
|
68 * |
|
69 */ |
|
70 |
|
71 let ConnectionManager = { |
|
72 _connections: new Set(), |
|
73 createConnection: function(host, port) { |
|
74 let c = new Connection(host, port); |
|
75 c.once("destroy", (event) => this.destroyConnection(c)); |
|
76 this._connections.add(c); |
|
77 this.emit("new", c); |
|
78 return c; |
|
79 }, |
|
80 destroyConnection: function(connection) { |
|
81 if (this._connections.has(connection)) { |
|
82 this._connections.delete(connection); |
|
83 if (connection.status != Connection.Status.DESTROYED) { |
|
84 connection.destroy(); |
|
85 } |
|
86 } |
|
87 }, |
|
88 get connections() { |
|
89 return [c for (c of this._connections)]; |
|
90 }, |
|
91 getFreeTCPPort: function () { |
|
92 let serv = Cc['@mozilla.org/network/server-socket;1'] |
|
93 .createInstance(Ci.nsIServerSocket); |
|
94 serv.init(-1, true, -1); |
|
95 let port = serv.port; |
|
96 serv.close(); |
|
97 return port; |
|
98 }, |
|
99 } |
|
100 |
|
101 EventEmitter.decorate(ConnectionManager); |
|
102 |
|
103 let lastID = -1; |
|
104 |
|
105 function Connection(host, port) { |
|
106 EventEmitter.decorate(this); |
|
107 this.uid = ++lastID; |
|
108 this.host = host; |
|
109 this.port = port; |
|
110 this._setStatus(Connection.Status.DISCONNECTED); |
|
111 this._onDisconnected = this._onDisconnected.bind(this); |
|
112 this._onConnected = this._onConnected.bind(this); |
|
113 this._onTimeout = this._onTimeout.bind(this); |
|
114 this.keepConnecting = false; |
|
115 } |
|
116 |
|
117 Connection.Status = { |
|
118 CONNECTED: "connected", |
|
119 DISCONNECTED: "disconnected", |
|
120 CONNECTING: "connecting", |
|
121 DISCONNECTING: "disconnecting", |
|
122 DESTROYED: "destroyed", |
|
123 } |
|
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 |
|
138 Connection.prototype = { |
|
139 logs: "", |
|
140 log: function(str) { |
|
141 let d = new Date(); |
|
142 let hours = ("0" + d.getHours()).slice(-2); |
|
143 let minutes = ("0" + d.getMinutes()).slice(-2); |
|
144 let seconds = ("0" + d.getSeconds()).slice(-2); |
|
145 let timestamp = [hours, minutes, seconds].join(":") + ": "; |
|
146 str = timestamp + str; |
|
147 this.logs += "\n" + str; |
|
148 this.emit(Connection.Events.NEW_LOG, str); |
|
149 }, |
|
150 |
|
151 get client() { |
|
152 return this._client |
|
153 }, |
|
154 |
|
155 get host() { |
|
156 return this._host |
|
157 }, |
|
158 |
|
159 set host(value) { |
|
160 if (this._host && this._host == value) |
|
161 return; |
|
162 this._host = value; |
|
163 this.emit(Connection.Events.HOST_CHANGED); |
|
164 }, |
|
165 |
|
166 get port() { |
|
167 return this._port |
|
168 }, |
|
169 |
|
170 set port(value) { |
|
171 if (this._port && this._port == value) |
|
172 return; |
|
173 this._port = value; |
|
174 this.emit(Connection.Events.PORT_CHANGED); |
|
175 }, |
|
176 |
|
177 disconnect: function(force) { |
|
178 if (this.status == Connection.Status.DESTROYED) { |
|
179 return; |
|
180 } |
|
181 clearTimeout(this._timeoutID); |
|
182 if (this.status == Connection.Status.CONNECTED || |
|
183 this.status == Connection.Status.CONNECTING) { |
|
184 this.log("disconnecting"); |
|
185 this._setStatus(Connection.Status.DISCONNECTING); |
|
186 this._client.close(); |
|
187 } |
|
188 }, |
|
189 |
|
190 connect: function() { |
|
191 if (this.status == Connection.Status.DESTROYED) { |
|
192 return; |
|
193 } |
|
194 if (!this._client) { |
|
195 this.log("connecting to " + this.host + ":" + this.port); |
|
196 this._setStatus(Connection.Status.CONNECTING); |
|
197 let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout"); |
|
198 this._timeoutID = setTimeout(this._onTimeout, delay); |
|
199 |
|
200 this._clientConnect(); |
|
201 } else { |
|
202 let msg = "Can't connect. Client is not fully disconnected"; |
|
203 this.log(msg); |
|
204 throw new Error(msg); |
|
205 } |
|
206 }, |
|
207 |
|
208 destroy: function() { |
|
209 this.log("killing connection"); |
|
210 clearTimeout(this._timeoutID); |
|
211 this.keepConnecting = false; |
|
212 if (this._client) { |
|
213 this._client.close(); |
|
214 this._client = null; |
|
215 } |
|
216 this._setStatus(Connection.Status.DESTROYED); |
|
217 }, |
|
218 |
|
219 _clientConnect: function () { |
|
220 let transport; |
|
221 if (!this.host) { |
|
222 transport = DebuggerServer.connectPipe(); |
|
223 } else { |
|
224 try { |
|
225 transport = debuggerSocketConnect(this.host, this.port); |
|
226 } catch (e) { |
|
227 // In some cases, especially on Mac, the openOutputStream call in |
|
228 // debuggerSocketConnect may throw NS_ERROR_NOT_INITIALIZED. |
|
229 // It occurs when we connect agressively to the simulator, |
|
230 // and keep trying to open a socket to the server being started in |
|
231 // the simulator. |
|
232 this._onDisconnected(); |
|
233 return; |
|
234 } |
|
235 } |
|
236 this._client = new DebuggerClient(transport); |
|
237 this._client.addOneTimeListener("closed", this._onDisconnected); |
|
238 this._client.connect(this._onConnected); |
|
239 }, |
|
240 |
|
241 get status() { |
|
242 return this._status |
|
243 }, |
|
244 |
|
245 _setStatus: function(value) { |
|
246 if (this._status && this._status == value) |
|
247 return; |
|
248 this._status = value; |
|
249 this.emit(value); |
|
250 this.emit(Connection.Events.STATUS_CHANGED, value); |
|
251 }, |
|
252 |
|
253 _onDisconnected: function() { |
|
254 this._client = null; |
|
255 |
|
256 if (this._status == Connection.Status.CONNECTING && this.keepConnecting) { |
|
257 setTimeout(() => this._clientConnect(), 100); |
|
258 return; |
|
259 } |
|
260 |
|
261 clearTimeout(this._timeoutID); |
|
262 |
|
263 switch (this.status) { |
|
264 case Connection.Status.CONNECTED: |
|
265 this.log("disconnected (unexpected)"); |
|
266 break; |
|
267 case Connection.Status.CONNECTING: |
|
268 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."); |
|
269 break; |
|
270 default: |
|
271 this.log("disconnected"); |
|
272 } |
|
273 this._setStatus(Connection.Status.DISCONNECTED); |
|
274 }, |
|
275 |
|
276 _onConnected: function() { |
|
277 this.log("connected"); |
|
278 clearTimeout(this._timeoutID); |
|
279 this._setStatus(Connection.Status.CONNECTED); |
|
280 }, |
|
281 |
|
282 _onTimeout: function() { |
|
283 this.log("connection timeout. Possible causes: didn't click on 'accept' (prompt)."); |
|
284 this.emit(Connection.Events.TIMEOUT); |
|
285 this.disconnect(); |
|
286 }, |
|
287 } |
|
288 |
|
289 exports.ConnectionManager = ConnectionManager; |
|
290 exports.Connection = Connection; |
|
291 |