// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.collide.client.code.debugging;
import com.google.collide.client.code.debugging.DebuggerApiTypes.BreakpointInfo;
import com.google.collide.client.code.debugging.DebuggerApiTypes.CallFrame;
import com.google.collide.client.code.debugging.DebuggerApiTypes.ConsoleMessage;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnAllCssStyleSheetsResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnBreakpointResolvedResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnEvaluateExpressionResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnPausedResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnRemoteObjectPropertiesResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnRemoteObjectPropertyChanged;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnScriptParsedResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.PauseOnExceptionsState;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObject;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectId;
import com.google.collide.client.util.JsIntegerMap;
import com.google.collide.client.util.logging.Log;
import com.google.collide.json.client.Jso;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonStringMap;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.StringUtils;
import elemental.client.Browser;
import elemental.events.CustomEvent;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventRemover;
/**
* Implements communication API with the Chrome browser debugger.
*
* <p>On the side of the Chrome browser the "Collide Debugger" extension should
* be installed to accept the requests from the Collide client.
*/
class DebuggerChromeApi implements DebuggerApi {
// TODO: Replace the URL when extension is public.
private static final String EXTENSION_URL =
"http://www.example.com/collide.crx";
private static final String DEBUGGER_EXTENSION_REQUEST_EVENT = "DebuggerExtensionRequest";
private static final String DEBUGGER_EXTENSION_RESPONSE_EVENT = "DebuggerExtensionResponse";
private static final String METHOD_WINDOW_OPEN = "window.open";
private static final String METHOD_WINDOW_CLOSE = "window.close";
private static final String METHOD_ON_ATTACH = "onAttach";
private static final String METHOD_ON_DETACH = "onDetach";
private static final String METHOD_ON_GLOBAL_OBJECT_CHANGED = "onGlobalObjectChanged";
private static final String METHOD_ON_EXTENSION_INSTALLED_CHANGED = "onExtensionInstalledChanged";
private static final String METHOD_CSS_GET_ALL_STYLE_SHEETS = "CSS.getAllStyleSheets";
private static final String METHOD_CSS_SET_STYLE_SHEET_TEXT = "CSS.setStyleSheetText";
private static final String METHOD_CONSOLE_ENABLE = "Console.enable";
private static final String METHOD_CONSOLE_MESSAGE_ADDED = "Console.messageAdded";
private static final String METHOD_CONSOLE_MESSAGE_REPEAT_COUNT_UPDATED =
"Console.messageRepeatCountUpdated";
private static final String METHOD_CONSOLE_MESSAGES_CLEARED = "Console.messagesCleared";
private static final String METHOD_DEBUGGER_ENABLE = "Debugger.enable";
private static final String METHOD_DEBUGGER_SET_BREAKPOINT_BY_URL = "Debugger.setBreakpointByUrl";
private static final String METHOD_DEBUGGER_REMOVE_BREAKPOINT = "Debugger.removeBreakpoint";
private static final String METHOD_DEBUGGER_SET_BREAKPOINTS_ACTIVE =
"Debugger.setBreakpointsActive";
private static final String METHOD_DEBUGGER_SET_PAUSE_ON_EXCEPTIONS =
"Debugger.setPauseOnExceptions";
private static final String METHOD_DEBUGGER_PAUSE = "Debugger.pause";
private static final String METHOD_DEBUGGER_RESUME = "Debugger.resume";
private static final String METHOD_DEBUGGER_STEP_INTO = "Debugger.stepInto";
private static final String METHOD_DEBUGGER_STEP_OUT = "Debugger.stepOut";
private static final String METHOD_DEBUGGER_STEP_OVER = "Debugger.stepOver";
private static final String METHOD_DEBUGGER_EVALUATE_ON_CALL_FRAME =
"Debugger.evaluateOnCallFrame";
private static final String METHOD_RUNTIME_CALL_FUNCTION_ON = "Runtime.callFunctionOn";
private static final String METHOD_RUNTIME_EVALUATE = "Runtime.evaluate";
private static final String METHOD_RUNTIME_GET_PROPERTIES = "Runtime.getProperties";
private static final String EVENT_DEBUGGER_BREAKPOINT_RESOLVED = "Debugger.breakpointResolved";
private static final String EVENT_DEBUGGER_SCRIPT_PARSED = "Debugger.scriptParsed";
private static final String EVENT_DEBUGGER_PAUSED = "Debugger.paused";
private static final String EVENT_DEBUGGER_RESUMED = "Debugger.resumed";
private final JsonArray<DebuggerResponseListener> debuggerResponseListeners =
JsonCollections.createArray();
private final EventRemover debuggerExtensionListenerRemover;
private int lastUsedId = 0;
private JsonStringMap<JsIntegerMap<Integer>> idToCustomMessageIds = JsonCollections.createMap();
private JsonStringMap<JsIntegerMap<Callback>> callbacks = JsonCollections.createMap();
private final EventListener debuggerExtensionResponseListener = new EventListener() {
@Override
public void handleEvent(Event evt) {
Object detail = ((CustomEvent) evt).getDetail();
if (detail != null) {
handleDebuggerExtensionResponse(new ExtensionResponse(Jso.deserialize(detail.toString())));
}
}
};
private static class ExtensionResponse {
private final Jso response;
ExtensionResponse(Jso response) {
this.response = response;
}
int messageId() {
return response.getFieldCastedToInteger("id");
}
String sessionId() {
return response.getStringField("target");
}
String methodName() {
return response.getStringField("method");
}
Jso request() {
return response.getJsObjectField("request").cast();
}
Jso result() {
return response.getJsObjectField("result").cast();
}
String errorMessage() {
return response.getStringField("error");
}
boolean isError() {
return !StringUtils.isNullOrEmpty(errorMessage());
}
}
private interface DebuggerResponseDispatcher {
void dispatch(DebuggerResponseListener responseListener);
}
private interface Callback {
void run(ExtensionResponse response);
}
public DebuggerChromeApi() {
debuggerExtensionListenerRemover = Browser.getWindow().addEventListener(
DEBUGGER_EXTENSION_RESPONSE_EVENT, debuggerExtensionResponseListener, false);
}
public void teardown() {
debuggerResponseListeners.clear();
debuggerExtensionListenerRemover.remove();
idToCustomMessageIds = JsonCollections.createMap();
callbacks = JsonCollections.createMap();
}
@Override
public final native boolean isDebuggerAvailable() /*-{
try {
return !!top["__DebuggerExtensionInstalled"];
} catch (e) {
}
return false;
}-*/;
@Override
public String getDebuggingExtensionUrl() {
return EXTENSION_URL;
}
@Override
public void runDebugger(String sessionId, String url) {
Jso params = Jso.create();
params.addField("url", url);
sendCustomEvent(sessionId, METHOD_WINDOW_OPEN, params);
sendCustomEvent(sessionId, METHOD_DEBUGGER_ENABLE, null);
sendCustomEvent(sessionId, METHOD_CONSOLE_ENABLE, null);
}
@Override
public void shutdownDebugger(String sessionId) {
sendCustomEvent(sessionId, METHOD_WINDOW_CLOSE, null);
}
@Override
public void setBreakpointByUrl(String sessionId, BreakpointInfo breakpointInfo) {
String condition = breakpointInfo.getCondition();
Jso params = Jso.create();
if (breakpointInfo.getUrl() != null) {
params.addField("url", breakpointInfo.getUrl());
} else {
params.addField("urlRegex", breakpointInfo.getUrlRegex());
}
params.addField("lineNumber", breakpointInfo.getLineNumber());
params.addField("columnNumber", breakpointInfo.getColumnNumber());
params.addField("condition", condition == null ? "" : condition);
sendCustomEvent(sessionId, METHOD_DEBUGGER_SET_BREAKPOINT_BY_URL, params);
}
@Override
public void removeBreakpoint(String sessionId, String breakpointId) {
Jso params = Jso.create();
params.addField("breakpointId", breakpointId);
sendCustomEvent(sessionId, METHOD_DEBUGGER_REMOVE_BREAKPOINT, params);
}
@Override
public void setBreakpointsActive(String sessionId, boolean active) {
Jso params = Jso.create();
params.addField("active", active);
sendCustomEvent(sessionId, METHOD_DEBUGGER_SET_BREAKPOINTS_ACTIVE, params);
}
@Override
public void setPauseOnExceptions(String sessionId, PauseOnExceptionsState state) {
Jso params = Jso.create();
params.addField("state", state.toString().toLowerCase());
sendCustomEvent(sessionId, METHOD_DEBUGGER_SET_PAUSE_ON_EXCEPTIONS, params);
}
@Override
public void pause(String sessionId) {
sendCustomEvent(sessionId, METHOD_DEBUGGER_PAUSE, null);
}
@Override
public void resume(String sessionId) {
sendCustomEvent(sessionId, METHOD_DEBUGGER_RESUME, null);
}
@Override
public void stepInto(String sessionId) {
sendCustomEvent(sessionId, METHOD_DEBUGGER_STEP_INTO, null);
}
@Override
public void stepOut(String sessionId) {
sendCustomEvent(sessionId, METHOD_DEBUGGER_STEP_OUT, null);
}
@Override
public void stepOver(String sessionId) {
sendCustomEvent(sessionId, METHOD_DEBUGGER_STEP_OVER, null);
}
@Override
public void requestRemoteObjectProperties(String sessionId, RemoteObjectId remoteObjectId) {
Jso params = Jso.create();
params.addField("objectId", remoteObjectId.toString());
params.addField("ownProperties", true);
sendCustomEvent(sessionId, METHOD_RUNTIME_GET_PROPERTIES, params);
}
@Override
public void setRemoteObjectProperty(String sessionId, RemoteObjectId remoteObjectId,
String propertyName, String propertyValueExpression) {
String expression = preparePropertyValueExpression(propertyValueExpression);
evaluateExpression(sessionId, expression,
createSetRemoteObjectPropertyCallback(sessionId, remoteObjectId, propertyName));
}
@Override
public void setRemoteObjectPropertyEvaluatedOnCallFrame(String sessionId, CallFrame callFrame,
RemoteObjectId remoteObjectId, String propertyName, String propertyValueExpression) {
String expression = preparePropertyValueExpression(propertyValueExpression);
evaluateExpressionOnCallFrame(sessionId, callFrame, expression,
createSetRemoteObjectPropertyCallback(sessionId, remoteObjectId, propertyName));
}
/**
* This will wrap the given expression into braces if it looks like a
* JavaScript object syntax.
*
* <p>We do this just for the user's convenience overriding the semantics of
* the {@code window.eval} method, that evaluates the <i>"{}"</i> into
* {@code undefined}, and <i>"{foo:123}"</i> into {@code 123}.
*/
private String preparePropertyValueExpression(String expression) {
if (StringUtils.firstNonWhitespaceCharacter(expression) == '{'
&& StringUtils.lastNonWhitespaceCharacter(expression) == '}') {
return "(" + expression + ")";
}
return expression;
}
private Callback createSetRemoteObjectPropertyCallback(final String sessionId,
final RemoteObjectId remoteObjectId, final String propertyName) {
return new Callback() {
@Override
public void run(ExtensionResponse evaluationResponse) {
OnEvaluateExpressionResponse evaluationParsedResponse =
DebuggerChromeApiUtils.parseOnEvaluateExpressionResponse(
evaluationResponse.request(), evaluationResponse.result());
boolean isError = evaluationResponse.isError()
|| evaluationParsedResponse == null
|| evaluationParsedResponse.wasThrown()
|| evaluationParsedResponse.getResult() == null;
final RemoteObject evaluationResult = isError ? null : evaluationParsedResponse.getResult();
Jso params = Jso.create();
if (!isError && DebuggerApiUtils.isNonFiniteNumber(evaluationResult)) {
params.addField("functionDeclaration", "function(a) {"
+ " this[a] = " + evaluationResult.getDescription() + ";"
+ " return this[a];"
+ "}");
params.addField("objectId", remoteObjectId.toString());
JsonArray<Jso> arguments = JsonCollections.createArray();
arguments.add(Jso.create());
arguments.get(0).addField("value", propertyName);
params.addField("arguments", arguments);
} else if (!isError) {
params.addField("functionDeclaration", "function(a, b) { this[a] = b; return this[a]; }");
params.addField("objectId", remoteObjectId.toString());
JsonArray<Jso> arguments = JsonCollections.createArray();
arguments.add(Jso.create());
arguments.add(Jso.create());
arguments.get(0).addField("value", propertyName);
if (evaluationResult.getObjectId() == null) {
if (!DebuggerApiUtils.addPrimitiveJsoField(
arguments.get(1), "value", evaluationResult)) {
isError = true;
}
} else {
arguments.get(1).addField("objectId", evaluationResult.getObjectId().toString());
}
params.addField("arguments", arguments);
}
if (isError) {
// We do not know the property value. Just dispatch the error event.
OnRemoteObjectPropertyChanged parsedResponse =
DebuggerChromeApiUtils.createOnEditRemoteObjectPropertyResponse(
remoteObjectId, propertyName, null, true);
dispatchOnRemoteObjectPropertyChanged(sessionId, parsedResponse);
return;
}
sendCustomEvent(sessionId, METHOD_RUNTIME_CALL_FUNCTION_ON, params, new Callback() {
@Override
public void run(ExtensionResponse response) {
RemoteObject newValue =
DebuggerChromeApiUtils.parseCallFunctionOnResult(response.result());
boolean isError = response.isError()
|| newValue == null
|| !DebuggerApiUtils.equal(evaluationResult, newValue);
OnRemoteObjectPropertyChanged parsedResponse =
DebuggerChromeApiUtils.createOnEditRemoteObjectPropertyResponse(
remoteObjectId, propertyName, newValue, isError);
dispatchOnRemoteObjectPropertyChanged(sessionId, parsedResponse);
}
});
}
};
}
@Override
public void removeRemoteObjectProperty(final String sessionId,
final RemoteObjectId remoteObjectId, final String propertyName) {
Jso params = Jso.create();
params.addField("functionDeclaration", "function(a) { delete this[a]; return !(a in this); }");
params.addField("objectId", remoteObjectId.toString());
JsonArray<Jso> arguments = JsonCollections.createArray();
arguments.add(Jso.create());
arguments.get(0).addField("value", propertyName);
params.addField("arguments", arguments);
sendCustomEvent(sessionId, METHOD_RUNTIME_CALL_FUNCTION_ON, params, new Callback() {
@Override
public void run(ExtensionResponse response) {
boolean isError = response.isError() || !DebuggerApiUtils.castToBoolean(
DebuggerChromeApiUtils.parseCallFunctionOnResult(response.result()));
OnRemoteObjectPropertyChanged parsedResponse =
DebuggerChromeApiUtils.createOnRemoveRemoteObjectPropertyResponse(
remoteObjectId, propertyName, isError);
dispatchOnRemoteObjectPropertyChanged(sessionId, parsedResponse);
}
});
}
@Override
public void renameRemoteObjectProperty(final String sessionId,
final RemoteObjectId remoteObjectId, final String oldName, final String newName) {
Jso params = Jso.create();
params.addField("functionDeclaration", "function(a, b) {"
+ " if (a === b) return true;"
+ " this[b] = this[a];"
+ " if (this[b] !== this[a]) return false;"
+ " delete this[a];"
+ " return !(a in this);"
+ "}");
params.addField("objectId", remoteObjectId.toString());
JsonArray<Jso> arguments = JsonCollections.createArray();
arguments.add(Jso.create());
arguments.add(Jso.create());
arguments.get(0).addField("value", oldName);
arguments.get(1).addField("value", newName);
params.addField("arguments", arguments);
sendCustomEvent(sessionId, METHOD_RUNTIME_CALL_FUNCTION_ON, params, new Callback() {
@Override
public void run(ExtensionResponse response) {
boolean isError = response.isError() || !DebuggerApiUtils.castToBoolean(
DebuggerChromeApiUtils.parseCallFunctionOnResult(response.result()));
OnRemoteObjectPropertyChanged parsedResponse =
DebuggerChromeApiUtils.createOnRenameRemoteObjectPropertyResponse(
remoteObjectId, oldName, newName, isError);
dispatchOnRemoteObjectPropertyChanged(sessionId, parsedResponse);
}
});
}
@Override
public void evaluateExpression(String sessionId, String expression) {
evaluateExpression(sessionId, expression, null);
}
private void evaluateExpression(String sessionId, String expression, Callback callback) {
Jso params = Jso.create();
params.addField("expression", expression);
params.addField("doNotPauseOnExceptions", true);
sendCustomEvent(sessionId, METHOD_RUNTIME_EVALUATE, params, callback);
}
@Override
public void evaluateExpressionOnCallFrame(String sessionId, CallFrame callFrame,
String expression) {
evaluateExpressionOnCallFrame(sessionId, callFrame, expression, null);
}
private void evaluateExpressionOnCallFrame(String sessionId, CallFrame callFrame,
String expression, Callback callback) {
Jso params = Jso.create();
params.addField("expression", expression);
params.addField("callFrameId", callFrame.getId());
sendCustomEvent(sessionId, METHOD_DEBUGGER_EVALUATE_ON_CALL_FRAME, params, callback);
}
@Override
public void requestAllCssStyleSheets(String sessionId) {
sendCustomEvent(sessionId, METHOD_CSS_GET_ALL_STYLE_SHEETS, null);
}
@Override
public void setStyleSheetText(String sessionId, String styleSheetId, String text) {
Jso params = Jso.create();
params.addField("styleSheetId", styleSheetId);
params.addField("text", text);
sendCustomEvent(sessionId, METHOD_CSS_SET_STYLE_SHEET_TEXT, params);
}
@Override
public void sendCustomMessage(String sessionId, String message) {
Jso messageObject = Jso.deserialize(message);
if (messageObject == null) {
return;
}
JsIntegerMap<Integer> map = idToCustomMessageIds.get(sessionId);
if (map == null) {
map = JsIntegerMap.create();
idToCustomMessageIds.put(sessionId, map);
}
map.put(lastUsedId + 1, messageObject.getIntField("id"));
sendCustomEvent(sessionId, messageObject.getStringField("method"),
(Jso) messageObject.getObjectField("params"));
}
@Override
public void addDebuggerResponseListener(DebuggerResponseListener debuggerResponseListener) {
debuggerResponseListeners.add(debuggerResponseListener);
}
@Override
public void removeDebuggerResponseListener(DebuggerResponseListener debuggerResponseListener) {
debuggerResponseListeners.remove(debuggerResponseListener);
}
private void sendCustomEvent(String sessionId, String methodName, Jso params) {
sendCustomEvent(sessionId, methodName, params, null);
}
private void sendCustomEvent(String sessionId, String methodName, Jso params, Callback callback) {
Log.debug(getClass(), "Sending message to the debugger: " + methodName);
final int id = ++lastUsedId;
Jso data = Jso.create();
data.addField("id", id);
data.addField("target", sessionId);
data.addField("method", methodName);
if (params != null) {
data.addField("params", params);
}
if (callback != null) {
JsIntegerMap<Callback> map = callbacks.get(sessionId);
if (map == null) {
map = JsIntegerMap.create();
callbacks.put(sessionId, map);
}
map.put(id, callback);
}
CustomEvent evt = (CustomEvent) Browser.getDocument().createEvent("CustomEvent");
evt.initCustomEvent(DEBUGGER_EXTENSION_REQUEST_EVENT, true, true, Jso.serialize(data));
Browser.getWindow().dispatchEvent(evt);
}
private void handleDebuggerExtensionResponse(ExtensionResponse response) {
if (maybeInvokeCallbackForResponse(response)) {
return;
}
if (maybeHandleDebuggerCustomMessageResponse(response)) {
return;
}
final String sessionId = response.sessionId();
final String methodName = response.methodName();
final Jso request = response.request();
final Jso result = response.result();
Log.debug(getClass(), "Received debugger message: " + methodName);
if (response.isError()) {
Log.debug(getClass(), "Received debugger error message: " + response.errorMessage());
handleDebuggerExtensionErrorResponse(sessionId, methodName);
return;
}
if (EVENT_DEBUGGER_SCRIPT_PARSED.equals(methodName)) { // The most frequent event.
final OnScriptParsedResponse parsedResponse =
DebuggerChromeApiUtils.parseOnScriptParsedResponse(result);
if (parsedResponse != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onScriptParsed(sessionId, parsedResponse);
}
});
}
} else if (METHOD_CONSOLE_MESSAGE_ADDED.equals(methodName)) {
final ConsoleMessage consoleMessage =
DebuggerChromeApiUtils.parseOnConsoleMessageReceived(result);
if (consoleMessage != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onConsoleMessage(sessionId, consoleMessage);
}
});
}
} else if (METHOD_CONSOLE_MESSAGE_REPEAT_COUNT_UPDATED.equals(methodName)) {
final int repeatCount =
DebuggerChromeApiUtils.parseOnConsoleMessageRepeatCountUpdated(result);
if (repeatCount != -1) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onConsoleMessageRepeatCountUpdated(sessionId, repeatCount);
}
});
}
} else if (EVENT_DEBUGGER_PAUSED.equals(methodName)) {
final OnPausedResponse parsedResponse = DebuggerChromeApiUtils.parseOnPausedResponse(result);
if (parsedResponse != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onPaused(sessionId, parsedResponse);
}
});
}
} else if (EVENT_DEBUGGER_RESUMED.equals(methodName)) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onResumed(sessionId);
}
});
} else if (METHOD_DEBUGGER_SET_BREAKPOINT_BY_URL.equals(methodName)
|| EVENT_DEBUGGER_BREAKPOINT_RESOLVED.equals(methodName)) {
final OnBreakpointResolvedResponse parsedResponse =
DebuggerChromeApiUtils.parseOnBreakpointResolvedResponse(request, result);
if (parsedResponse != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onBreakpointResolved(sessionId, parsedResponse);
}
});
}
} else if (METHOD_DEBUGGER_REMOVE_BREAKPOINT.equals(methodName)) {
final String breakpointId = DebuggerChromeApiUtils.parseOnRemoveBreakpointResponse(request);
if (breakpointId != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onBreakpointRemoved(sessionId, breakpointId);
}
});
}
} else if (METHOD_ON_ATTACH.equals(methodName) || METHOD_WINDOW_OPEN.equals(methodName)) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onDebuggerAttached(sessionId);
}
});
} else if (METHOD_ON_DETACH.equals(methodName) || METHOD_WINDOW_CLOSE.equals(methodName)) {
dispatchOnDebuggerDetachedEvent(sessionId);
} else if (METHOD_ON_GLOBAL_OBJECT_CHANGED.equals(methodName)) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onGlobalObjectChanged(sessionId);
}
});
} else if (METHOD_RUNTIME_GET_PROPERTIES.equals(methodName)) {
final OnRemoteObjectPropertiesResponse parsedResponse =
DebuggerChromeApiUtils.parseOnRemoteObjectPropertiesResponse(request, result);
if (parsedResponse != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onRemoteObjectPropertiesResponse(sessionId, parsedResponse);
}
});
}
} else if (METHOD_RUNTIME_EVALUATE.equals(methodName)
|| METHOD_DEBUGGER_EVALUATE_ON_CALL_FRAME.equals(methodName)) {
final OnEvaluateExpressionResponse parsedResponse =
DebuggerChromeApiUtils.parseOnEvaluateExpressionResponse(request, result);
if (parsedResponse != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onEvaluateExpressionResponse(sessionId, parsedResponse);
}
});
}
} else if (METHOD_CSS_GET_ALL_STYLE_SHEETS.equals(methodName)) {
final OnAllCssStyleSheetsResponse parsedResponse =
DebuggerChromeApiUtils.parseOnAllCssStyleSheetsResponse(result);
if (parsedResponse != null) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onAllCssStyleSheetsResponse(sessionId, parsedResponse);
}
});
}
} else if (METHOD_ON_EXTENSION_INSTALLED_CHANGED.equals(methodName)) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onDebuggerAvailableChanged();
}
});
} else if (METHOD_CONSOLE_MESSAGES_CLEARED.equals(methodName)) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onConsoleMessagesCleared(sessionId);
}
});
} else {
Log.warn(getClass(), "Ignoring debugger message: " + methodName);
}
}
private boolean maybeInvokeCallbackForResponse(ExtensionResponse response) {
final int messageId = response.messageId();
final String sessionId = response.sessionId();
if (callbacks.get(sessionId) != null && callbacks.get(sessionId).hasKey(messageId)) {
JsIntegerMap<Callback> map = callbacks.get(sessionId);
Callback callback = map.get(messageId);
map.erase(messageId);
callback.run(response);
return true;
}
return false;
}
private boolean maybeHandleDebuggerCustomMessageResponse(ExtensionResponse response) {
final int messageId = response.messageId();
final String sessionId = response.sessionId();
final String methodName = response.methodName();
final Jso result = response.result();
if (idToCustomMessageIds.get(sessionId) != null
&& idToCustomMessageIds.get(sessionId).hasKey(messageId)) {
JsIntegerMap<Integer> map = idToCustomMessageIds.get(sessionId);
Jso customResponse = Jso.create();
customResponse.addField("id", map.get(messageId).intValue());
customResponse.addField("result", result);
if (response.isError()) {
customResponse.addField("error", response.errorMessage());
}
final String customResponseSerialized = Jso.serialize(customResponse);
map.erase(messageId);
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onCustomMessageResponse(sessionId, customResponseSerialized);
}
});
return true;
} else if (methodName.startsWith("DOM.")) {
Jso customResponse = Jso.create();
customResponse.addField("method", methodName);
customResponse.addField("params", result);
if (response.isError()) {
customResponse.addField("error", response.errorMessage());
}
final String customResponseSerialized = Jso.serialize(customResponse);
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onCustomMessageResponse(sessionId, customResponseSerialized);
}
});
return true;
}
return false;
}
private void handleDebuggerExtensionErrorResponse(String sessionId, String methodName) {
// Dispatch the onDebuggerDetached internal event for each failed method
// that might have actually detached the remote debugger.
if (METHOD_ON_ATTACH.equals(methodName) || METHOD_WINDOW_OPEN.equals(methodName)
|| METHOD_ON_DETACH.equals(methodName) || METHOD_WINDOW_CLOSE.equals(methodName)) {
dispatchOnDebuggerDetachedEvent(sessionId);
}
}
private void dispatchOnDebuggerDetachedEvent(final String sessionId) {
// Clear cached data for the debugger session.
idToCustomMessageIds.remove(sessionId);
callbacks.remove(sessionId);
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onDebuggerDetached(sessionId);
}
});
}
private void dispatchOnRemoteObjectPropertyChanged(final String sessionId,
final OnRemoteObjectPropertyChanged parsedResponse) {
dispatchDebuggerResponse(new DebuggerResponseDispatcher() {
@Override
public void dispatch(DebuggerResponseListener responseListener) {
responseListener.onRemoteObjectPropertyChanged(sessionId, parsedResponse);
}
});
}
private void dispatchDebuggerResponse(DebuggerResponseDispatcher dispatcher) {
JsonArray<DebuggerResponseListener> copy = debuggerResponseListeners.copy();
for (int i = 0, n = copy.size(); i < n; i++) {
dispatcher.dispatch(copy.get(i));
}
}
}