This repository was archived by the owner on Sep 6, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
Expand file tree
/
Copy pathInspector.js
More file actions
402 lines (354 loc) · 14.4 KB
/
Inspector.js
File metadata and controls
402 lines (354 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
/*
* Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */
/*global define, $, WebSocket, FileError, window, XMLHttpRequest */
/**
* Inspector manages the connection to Chrome/Chromium's remote debugger.
* See inspector.html for the documentation of the remote debugger.
*
* # SETUP
*
* To enable remote debugging in Chrome or Chromium open either application
* with the following parameters:
*
* --enable-remote-debugger --remote-debugging-port=9222
*
* This will open an HTTP server on the specified port, which can be used to
* browse the available remote debugger sessions. In general, every open
* browser tab can host an individual remote debugger session. The
* available interfaces can be exported by requesting:
*
* http://127.0.0.1:9222/json
*
* The response is a JSON-formatted array that specifies all available remote
* debugger sessions including the remote debugging web sockets.
*
* Inspector can connect directly to a web socket via `connect(socketURL)`, or
* it can find the web socket that corresponds to the tab at the given URL and
* connect to it via `connectToURL(url)`. The later returns a promise. To
* disconnect use `disconnect()`.
*
* # EVENTS
*
* Inspector dispatches several connectivity-related events + all remote debugger
* events (see below). Event handlers are attached via `on(event, function)` and
* detached via `off(event, function)`.
*
* `connect` Inspector did successfully connect to the remote debugger
* `disconnect` Inspector did disconnect from the remote debugger
* `error` Inspector encountered an error
* `message` Inspector received a message from the remote debugger - this
* provides a low-level entry point to remote debugger events
*
* # REMOTE DEBUGGER COMMANDS
*
* Commands are executed by calling `{Domain}.{Command}()` with the parameters
* specified in the order of the remote debugger documentation. These command
* functions are generated automatically at runtime from Inspector.json. The
* actual implementation of these functions is found in
* `_send(method, signature, varargs)`, which verifies, serializes, and
* transmits the command to the remote debugger. If the last parameter of any
* command function call is a function, it will be used as the callback.
*
* # REMOTE DEBUGGER EVENTS
*
* Debugger events are dispatched as regular events using {Domain}.{Event} as
* the event name. The handler function will be called with a single parameter
* that stores all returned values as an object.
*/
define(function Inspector(require, exports, module) {
"use strict";
var Async = require("utils/Async");
// jQuery exports object for events
var $exports = $(exports);
/**
* Map message IDs to the callback function and original JSON message
* @type {Object.<number, {callback: function, message: Object}}
*/
var _messageCallbacks = {};
var _messageId = 1; // id used for remote method calls, auto-incrementing
var _socket; // remote debugger WebSocket
var _connectDeferred; // The deferred connect
/** Check a parameter value against the given signature
* This only checks for optional parameters, not types
* Type checking is complex because of $ref and done on the remote end anyways
* @param {signature}
* @param {value}
*/
function _verifySignature(signature, value) {
if (value === undefined) {
console.assert(signature.optional === true, "Missing argument: " + signature.name);
}
return true;
}
/** Send a message to the remote debugger
* All passed arguments after the signature are passed on as parameters.
* If the last argument is a function, it is used as the callback function.
* @param {string} remote method
* @param {object} the method signature
*/
function _send(method, signature, varargs) {
if (!_socket) {
console.log("You must connect to the WebSocket before sending messages.");
// FUTURE: Our current implementation closes and re-opens an inspector connection whenever
// a new HTML file is selected. If done quickly enough, pending requests from the previous
// connection could come in before the new socket connection is established. For now we
// simply ignore this condition.
// This race condition will go away once we support multiple inspector connections and turn
// off auto re-opening when a new HTML file is selected.
return (new $.Deferred()).reject().promise();
}
var id, callback, args, i, params = {}, promise, msg;
// extract the parameters, the callback function, and the message id
args = Array.prototype.slice.call(arguments, 2);
if (typeof args[args.length - 1] === "function") {
callback = args.pop();
} else {
var deferred = new $.Deferred();
promise = deferred.promise();
callback = function (result, error) {
if (error) {
deferred.reject(error);
} else {
deferred.resolve(result);
}
};
}
id = _messageId++;
// verify the parameters against the method signature
// this also constructs the params object of type {name -> value}
if (signature) {
for (i in signature) {
if (_verifySignature(args[i], signature[i])) {
params[signature[i].name] = args[i];
}
}
}
// Store message callback and send message
msg = { method: method, id: id, params: params };
_messageCallbacks[id] = { callback: callback, message: msg };
_socket.send(JSON.stringify(msg));
return promise;
}
/**
* Manually send a message to the remote debugger
* All passed arguments after the command are passed on as parameters.
* If the last argument is a function, it is used as the callback function.
* @param {string} domain
* @param {string} command
*/
function send(domain, command, varargs) {
return _send(domain + "." + command, null, varargs);
}
/** WebSocket did close */
function _onDisconnect() {
_socket = undefined;
$exports.triggerHandler("disconnect");
}
/** WebSocket reported an error */
function _onError(error) {
if (_connectDeferred) {
_connectDeferred.reject();
_connectDeferred = null;
}
$exports.triggerHandler("error", [error]);
}
/** WebSocket did open */
function _onConnect() {
if (_connectDeferred) {
_connectDeferred.resolve();
_connectDeferred = null;
}
$exports.triggerHandler("connect");
}
/** Received message from the WebSocket
* A message can be one of three things:
* 1. an error -> report it
* 2. the response to a previous command -> run the stored callback
* 3. an event -> trigger an event handler method
* @param {object} message
*/
function _onMessage(message) {
var response = JSON.parse(message.data),
msgRecord = _messageCallbacks[response.id],
callback = msgRecord && msgRecord.callback,
message = (msgRecord && msgRecord.message) || "No message";
if (msgRecord) {
// Messages with an ID are a response to a command, fire callback
callback(response.result, response.error);
delete _messageCallbacks[response.id];
} else if (response.method) {
// Messages with a method are an event, trigger event handlers
var domainAndMethod = response.method.split("."),
domain = domainAndMethod[0],
method = domainAndMethod[1];
$(exports[domain]).triggerHandler(method, response.params);
}
// Always fire event handlers for all messages/errors
$exports.triggerHandler("message", [response]);
if (response.error) {
$exports.triggerHandler("error", [response.error, message]);
}
}
/** Public Functions *****************************************************/
/** Get a list of the available windows/tabs/extensions that are remote-debuggable
* @param {string} host IP or name
* @param {integer} debugger port
*/
function getDebuggableWindows(host, port) {
if (!host) {
host = "127.0.0.1";
}
if (!port) {
port = 9222;
}
var def = new $.Deferred();
var request = new XMLHttpRequest();
request.open("GET", "http://" + host + ":" + port + "/json");
request.onload = function onLoad() {
var sockets = JSON.parse(request.response);
def.resolve(sockets);
};
request.onerror = function onError() {
def.reject(request.response);
};
request.send(null);
return def.promise();
}
/** Register a handler to be called when the given event is triggered
* @param {string} event name
* @param {function} handler function
*/
function on(name, handler) {
$exports.on(name, handler);
}
/** Remove the given or all event handler(s) for the given event or remove all event handlers
* @param {string} optional event name
* @param {function} optional handler function
*/
function off(name, handler) {
$exports.off(name, handler);
}
/**
* Disconnect from the remote debugger WebSocket
* @return {jQuery.Promise} Promise that is resolved immediately if not
* currently connected or asynchronously when the socket is closed.
*/
function disconnect() {
var deferred = new $.Deferred(),
promise = deferred.promise();
if (_socket && (_socket.readyState === WebSocket.OPEN)) {
_socket.onclose = function () {
// trigger disconnect event
_onDisconnect();
deferred.resolve();
};
promise = Async.withTimeout(promise, 5000);
_socket.close();
} else {
if (_socket) {
delete _socket.onmessage;
delete _socket.onopen;
delete _socket.onclose;
delete _socket.onerror;
_socket = undefined;
}
deferred.resolve();
}
return promise;
}
/**
* Connect to the remote debugger WebSocket at the given URL.
* Clients must listen for the `connect` event.
* @param {string} WebSocket URL
*/
function connect(socketURL) {
disconnect().done(function () {
_socket = new WebSocket(socketURL);
_socket.onmessage = _onMessage;
_socket.onopen = _onConnect;
_socket.onclose = _onDisconnect;
_socket.onerror = _onError;
});
}
/** Connect to the remote debugger of the page that is at the given URL
* @param {string} url
*/
function connectToURL(url) {
if (_connectDeferred) {
// reject an existing connection attempt
_connectDeferred.reject("CANCEL");
}
var deferred = new $.Deferred();
_connectDeferred = deferred;
var promise = getDebuggableWindows();
promise.done(function onGetAvailableSockets(response) {
var i, page;
for (i in response) {
page = response[i];
if (page.webSocketDebuggerUrl && page.url.indexOf(url) === 0) {
connect(page.webSocketDebuggerUrl);
// _connectDeferred may be resolved by onConnect or rejected by onError
return;
}
}
deferred.reject(FileError.ERR_NOT_FOUND); // Reject with a "not found" error
});
promise.fail(function onFail(err) {
deferred.reject(err);
});
return deferred.promise();
}
/** Check if the inspector is connected */
function connected() {
return _socket !== undefined && _socket.readyState === WebSocket.OPEN;
}
/** Initialize the Inspector
* Read the Inspector.json configuration and define the command objects
* -> Inspector.domain.command()
*/
function init(theConfig) {
exports.config = theConfig;
var InspectorText = require("text!LiveDevelopment/Inspector/Inspector.json"),
InspectorJSON = JSON.parse(InspectorText);
var i, j, domain, domainDef, command;
for (i in InspectorJSON.domains) {
domain = InspectorJSON.domains[i];
exports[domain.domain] = {};
for (j in domain.commands) {
command = domain.commands[j];
exports[domain.domain][command.name] = _send.bind(undefined, domain.domain + "." + command.name, command.parameters);
}
}
}
// Export public functions
exports.getDebuggableWindows = getDebuggableWindows;
exports.on = on;
exports.off = off;
exports.disconnect = disconnect;
exports.connect = connect;
exports.connectToURL = connectToURL;
exports.connected = connected;
exports.send = send;
exports.init = init;
});