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.
Map message IDs to the callback function and original JSON message
var _messageCallbacks = {};
var _messageId = 1, // id used for remote method calls, auto-incrementing
_socket, // remote debugger WebSocket
_connectDeferred, // The deferred connect
_userAgent = ""; // user agent string
WebSocket did open
function _onConnect() {
if (_connectDeferred) {
_connectDeferred.resolve();
_connectDeferred = null;
}
exports.trigger("connect");
}
WebSocket did close
function _onDisconnect() {
_socket = undefined;
exports.trigger("disconnect");
}
WebSocket reported an error
function _onError(error) {
if (_connectDeferred) {
_connectDeferred.reject();
_connectDeferred = null;
}
exports.trigger("error", error);
}
Received message from the WebSocket A message can be one of three things:
function _onMessage(message) {
var response = JSON.parse(message.data),
msgRecord = _messageCallbacks[response.id],
callback = msgRecord && msgRecord.callback,
msgText = (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];
EventDispatcher.triggerWithArray(exports[domain], method, response.params);
}
// Always fire event handlers for all messages/errors
exports.trigger("message", response);
if (response.error) {
exports.trigger("error", response.error, msgText);
}
}
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.
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;
}
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
function _verifySignature(signature, value) {
if (value === undefined) {
console.assert(signature.optional === true, "Missing argument: " + signature.name);
}
return true;
}
Connect to the remote debugger WebSocket at the given URL.
Clients must listen for the connect
event.
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
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;
}
Disconnect from the remote debugger WebSocket
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;
}
Get a list of the available windows/tabs/extensions that are remote-debuggable
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();
}
Get user agent string
function getUserAgent() {
return _userAgent;
}
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, command;
for (i in InspectorJSON.domains) {
domain = InspectorJSON.domains[i];
var exportedDomain = {};
exports[domain.domain] = exportedDomain;
EventDispatcher.makeEventDispatcher(exportedDomain);
for (j in domain.commands) {
command = domain.commands[j];
exportedDomain[command.name] = _send.bind(undefined, domain.domain + "." + command.name, command.parameters);
}
}
}
EventDispatcher.makeEventDispatcher(exports);
// Export public functions
exports.connect = connect;
exports.connected = connected;
exports.connectToURL = connectToURL;
exports.disconnect = disconnect;
exports.getDebuggableWindows = getDebuggableWindows;
exports.getUserAgent = getUserAgent;
exports.init = init;
exports.send = send;
exports.setUserAgent = setUserAgent;
});
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.
function send(domain, command, varargs) {
return _send(domain + "." + command, null, varargs);
}