/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.inspector.protocol.module;
import android.content.Context;
import com.facebook.stetho.Stetho;
import com.facebook.stetho.common.LogUtil;
import com.facebook.stetho.inspector.console.RuntimeRepl;
import com.facebook.stetho.inspector.console.RuntimeReplFactory;
import com.facebook.stetho.inspector.helper.ObjectIdMapper;
import com.facebook.stetho.inspector.jsonrpc.DisconnectReceiver;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcException;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult;
import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod;
import com.facebook.stetho.inspector.runtime.RhinoDetectingRuntimeReplFactory;
import com.facebook.stetho.json.ObjectMapper;
import com.facebook.stetho.json.annotation.JsonProperty;
import com.facebook.stetho.json.annotation.JsonValue;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class Runtime implements ChromeDevtoolsDomain {
private final ObjectMapper mObjectMapper = new ObjectMapper();
private static final Map<JsonRpcPeer, Session> sSessions =
Collections.synchronizedMap(new HashMap<JsonRpcPeer, Session>());
private final RuntimeReplFactory mReplFactory;
/**
* @deprecated Provided for ABI compatibility
*
* @see #Runtime(RuntimeReplFactory)
* @see Stetho.DefaultInspectorModulesBuilder#runtimeRepl(RuntimeReplFactory)
*/
@Deprecated
public Runtime() {
this(new RuntimeReplFactory() {
@Override
public RuntimeRepl newInstance() {
return new RuntimeRepl() {
@Override
public Object evaluate(String expression) throws Throwable {
return "Not supported with legacy Runtime module";
}
};
}
});
}
/**
* @deprecated This was a transitionary API that was replaced by
* {@link com.facebook.stetho.Stetho.DefaultInspectorModulesBuilder#runtimeRepl}
*/
public Runtime(Context context) {
this(new RhinoDetectingRuntimeReplFactory(context));
}
public Runtime(RuntimeReplFactory replFactory) {
mReplFactory = replFactory;
}
public static int mapObject(JsonRpcPeer peer, Object object) {
return getSession(peer).getObjects().putObject(object);
}
@Nonnull
private static synchronized Session getSession(final JsonRpcPeer peer) {
Session session = sSessions.get(peer);
if (session == null) {
session = new Session();
sSessions.put(peer, session);
peer.registerDisconnectReceiver(new DisconnectReceiver() {
@Override
public void onDisconnect() {
sSessions.remove(peer);
}
});
}
return session;
}
@ChromeDevtoolsMethod
public void releaseObject(JsonRpcPeer peer, JSONObject params) throws JSONException {
String objectId = params.getString("objectId");
getSession(peer).getObjects().removeObjectById(Integer.parseInt(objectId));
}
@ChromeDevtoolsMethod
public void releaseObjectGroup(JsonRpcPeer peer, JSONObject params) {
LogUtil.w("Ignoring request to releaseObjectGroup: " + params);
}
@ChromeDevtoolsMethod
public CallFunctionOnResponse callFunctionOn(JsonRpcPeer peer, JSONObject params)
throws JsonRpcException {
CallFunctionOnRequest args = mObjectMapper.convertValue(params, CallFunctionOnRequest.class);
Session session = getSession(peer);
Object object = session.getObjectOrThrow(args.objectId);
// The DevTools UI thinks it can run arbitrary JavaScript against us in order to figure out
// the class structure of an object. That obviously won't fly, and there's no way to
// translate without building a crude JavaScript parser so let's just go ahead and guess
// what this function does by name.
if (!args.functionDeclaration.startsWith("function protoList(")) {
throw new JsonRpcException(
new JsonRpcError(
JsonRpcError.ErrorCode.INTERNAL_ERROR,
"Expected protoList, got: " + args.functionDeclaration,
null /* data */));
}
// Since this is really a function call we have to create this fake object to hold the
// "result" of the function.
ObjectProtoContainer objectContainer = new ObjectProtoContainer(object);
RemoteObject result = new RemoteObject();
result.type = ObjectType.OBJECT;
result.subtype = ObjectSubType.NODE;
result.className = object.getClass().getName();
result.description = getPropertyClassName(object);
result.objectId = String.valueOf(session.getObjects().putObject(objectContainer));
CallFunctionOnResponse response = new CallFunctionOnResponse();
response.result = result;
response.wasThrown = false;
return response;
}
@ChromeDevtoolsMethod
public JsonRpcResult evaluate(JsonRpcPeer peer, JSONObject params) {
return getSession(peer).evaluate(mReplFactory, params);
}
@ChromeDevtoolsMethod
public JsonRpcResult getProperties(JsonRpcPeer peer, JSONObject params) throws JsonRpcException {
return getSession(peer).getProperties(params);
}
private static String getPropertyClassName(Object o) {
String name = o.getClass().getSimpleName();
if (name == null || name.length() == 0) {
// Looks better for anonymous classes.
name = o.getClass().getName();
}
return name;
}
private static class ObjectProtoContainer {
public final Object object;
public ObjectProtoContainer(Object object) {
this.object = object;
}
}
/**
* Object representing a session with a single client.
*
* <p>Clients inherently leak object references because they can expand any object in the UI
* at any time. Grouping references by client allows us to drop them when the client
* disconnects.
*/
private static class Session {
private final ObjectIdMapper mObjects = new ObjectIdMapper();
private final ObjectMapper mObjectMapper = new ObjectMapper();
@Nullable
private RuntimeRepl mRepl;
public ObjectIdMapper getObjects() {
return mObjects;
}
public Object getObjectOrThrow(String objectId) throws JsonRpcException {
Object object = getObjects().getObjectForId(Integer.parseInt(objectId));
if (object == null) {
throw new JsonRpcException(new JsonRpcError(
JsonRpcError.ErrorCode.INVALID_REQUEST,
"No object found for " + objectId,
null /* data */));
}
return object;
}
public RemoteObject objectForRemote(Object value) {
RemoteObject result = new RemoteObject();
if (value == null) {
result.type = ObjectType.OBJECT;
result.subtype = ObjectSubType.NULL;
result.value = JSONObject.NULL;
} else if (value instanceof Boolean) {
result.type = ObjectType.BOOLEAN;
result.value = value;
} else if (value instanceof Number) {
result.type = ObjectType.NUMBER;
result.value = value;
} else if (value instanceof Character) {
// Unclear whether we should expose these as strings, numbers, or something else.
result.type = ObjectType.NUMBER;
result.value = Integer.valueOf(((Character)value).charValue());
} else if (value instanceof String) {
result.type = ObjectType.STRING;
result.value = String.valueOf(value);
} else {
result.type = ObjectType.OBJECT;
result.className = "What??"; // I have no idea where this is used.
result.objectId = String.valueOf(mObjects.putObject(value));
if (value.getClass().isArray()) {
result.description = "array";
} else if (value instanceof List) {
result.description = "List";
} else if (value instanceof Set) {
result.description = "Set";
} else if (value instanceof Map) {
result.description = "Map";
} else {
result.description = getPropertyClassName(value);
}
}
return result;
}
public EvaluateResponse evaluate(RuntimeReplFactory replFactory, JSONObject params) {
EvaluateRequest request = mObjectMapper.convertValue(params, EvaluateRequest.class);
try {
if (!request.objectGroup.equals("console")) {
return buildExceptionResponse("Not supported by FAB");
}
RuntimeRepl repl = getRepl(replFactory);
Object result = repl.evaluate(request.expression);
return buildNormalResponse(result);
} catch (Throwable t) {
return buildExceptionResponse(t);
}
}
@Nonnull
private synchronized RuntimeRepl getRepl(RuntimeReplFactory replFactory) {
if (mRepl == null) {
mRepl = replFactory.newInstance();
}
return mRepl;
}
private EvaluateResponse buildNormalResponse(Object retval) {
EvaluateResponse response = new EvaluateResponse();
response.wasThrown = false;
response.result = objectForRemote(retval);
return response;
}
private EvaluateResponse buildExceptionResponse(Object retval) {
EvaluateResponse response = new EvaluateResponse();
response.wasThrown = true;
response.result = objectForRemote(retval);
response.exceptionDetails = new ExceptionDetails();
response.exceptionDetails.text = retval.toString();
return response;
}
public GetPropertiesResponse getProperties(JSONObject params) throws JsonRpcException {
GetPropertiesRequest request = mObjectMapper.convertValue(params, GetPropertiesRequest.class);
if (!request.ownProperties) {
GetPropertiesResponse response = new GetPropertiesResponse();
response.result = new ArrayList<>();
return response;
}
Object object = getObjectOrThrow(request.objectId);
if (object.getClass().isArray()) {
object = arrayToList(object);
}
if (object instanceof ObjectProtoContainer) {
return getPropertiesForProtoContainer((ObjectProtoContainer) object);
} else if (object instanceof List) {
return getPropertiesForIterable((List) object, /* enumerate */ true);
} else if (object instanceof Set) {
return getPropertiesForIterable((Set) object, /* enumerate */ false);
} else if (object instanceof Map) {
return getPropertiesForMap(object);
} else {
return getPropertiesForObject(object);
}
}
private List<?> arrayToList(Object object) {
Class<?> type = object.getClass();
if (!type.isArray()) {
throw new IllegalArgumentException("Argument must be an array. Was " + type);
}
Class<?> component = type.getComponentType();
if (!component.isPrimitive()) {
return Arrays.asList((Object[]) object);
}
// Loop manually for primitives.
int length = Array.getLength(object);
List<Object> ret = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
ret.add(Array.get(object, i));
}
return ret;
}
// Normally JavaScript will return the full class hierarchy as a list. That seems less
// useful for Java since it's more natural (IMO) to see all available member variables in one
// big list.
private GetPropertiesResponse getPropertiesForProtoContainer(ObjectProtoContainer proto) {
Object target = proto.object;
RemoteObject protoRemote = new RemoteObject();
protoRemote.type = ObjectType.OBJECT;
protoRemote.subtype = ObjectSubType.NODE;
protoRemote.className = target.getClass().getName();
protoRemote.description = getPropertyClassName(target);
protoRemote.objectId = String.valueOf(mObjects.putObject(target));
PropertyDescriptor descriptor = new PropertyDescriptor();
descriptor.name = "1";
descriptor.value = protoRemote;
GetPropertiesResponse response = new GetPropertiesResponse();
response.result = new ArrayList<>(1);
response.result.add(descriptor);
return response;
}
private GetPropertiesResponse getPropertiesForIterable(Iterable<?> object, boolean enumerate) {
GetPropertiesResponse response = new GetPropertiesResponse();
List<PropertyDescriptor> properties = new ArrayList<>();
int index = 0;
for (Object value : object) {
PropertyDescriptor property = new PropertyDescriptor();
property.name = enumerate ? String.valueOf(index++) : null;
property.value = objectForRemote(value);
properties.add(property);
}
response.result = properties;
return response;
}
private GetPropertiesResponse getPropertiesForMap(Object object) {
GetPropertiesResponse response = new GetPropertiesResponse();
List<PropertyDescriptor> properties = new ArrayList<>();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
PropertyDescriptor property = new PropertyDescriptor();
property.name = String.valueOf(entry.getKey());
property.value = objectForRemote(entry.getValue());
properties.add(property);
}
response.result = properties;
return response;
}
private GetPropertiesResponse getPropertiesForObject(Object object) {
GetPropertiesResponse response = new GetPropertiesResponse();
List<PropertyDescriptor> properties = new ArrayList<>();
for (
Class<?> declaringClass = object.getClass();
declaringClass != null;
declaringClass = declaringClass.getSuperclass()
) {
// Reverse the list of fields while going up the superclass chain.
// When we're done, we'll reverse the full list so that the superclasses
// appear at the top, but within each class they properties are in declared order.
List<Field> fields =
new ArrayList<Field>(Arrays.asList(declaringClass.getDeclaredFields()));
Collections.reverse(fields);
String prefix = declaringClass == object.getClass()
? ""
: declaringClass.getSimpleName() + ".";
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
try {
Object fieldValue = field.get(object);
PropertyDescriptor property = new PropertyDescriptor();
property.name = prefix + field.getName();
property.value = objectForRemote(fieldValue);
properties.add(property);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Collections.reverse(properties);
response.result = properties;
return response;
}
}
private static class CallFunctionOnRequest {
@JsonProperty
public String objectId;
@JsonProperty
public String functionDeclaration;
@JsonProperty
public List<CallArgument> arguments;
@JsonProperty(required = false)
public Boolean doNotPauseOnExceptionsAndMuteConsole;
@JsonProperty(required = false)
public Boolean returnByValue;
@JsonProperty(required = false)
public Boolean generatePreview;
}
private static class CallFunctionOnResponse implements JsonRpcResult {
@JsonProperty
public RemoteObject result;
@JsonProperty(required = false)
public Boolean wasThrown;
}
private static class CallArgument {
@JsonProperty(required = false)
public Object value;
@JsonProperty(required = false)
public String objectId;
@JsonProperty(required = false)
public ObjectType type;
}
private static class GetPropertiesRequest implements JsonRpcResult {
@JsonProperty(required = true)
public boolean ownProperties;
@JsonProperty(required = true)
public String objectId;
}
private static class GetPropertiesResponse implements JsonRpcResult {
@JsonProperty(required = true)
public List<PropertyDescriptor> result;
}
private static class EvaluateRequest implements JsonRpcResult {
@JsonProperty(required = true)
public String objectGroup;
@JsonProperty(required = true)
public String expression;
}
private static class EvaluateResponse implements JsonRpcResult {
@JsonProperty(required = true)
public RemoteObject result;
@JsonProperty(required = true)
public boolean wasThrown;
@JsonProperty
public ExceptionDetails exceptionDetails;
}
private static class ExceptionDetails {
@JsonProperty(required = true)
public String text;
}
public static class RemoteObject {
@JsonProperty(required = true)
public ObjectType type;
@JsonProperty
public ObjectSubType subtype;
@JsonProperty
public Object value;
@JsonProperty
public String className;
@JsonProperty
public String description;
@JsonProperty
public String objectId;
}
private static class PropertyDescriptor {
@JsonProperty(required = true)
public String name;
@JsonProperty(required = true)
public RemoteObject value;
@JsonProperty(required = true)
public final boolean isOwn = true;
@JsonProperty(required = true)
public final boolean configurable = false;
@JsonProperty(required = true)
public final boolean enumerable = true;
@JsonProperty(required = true)
public final boolean writable = false;
}
public static enum ObjectType {
OBJECT("object"),
FUNCTION("function"),
UNDEFINED("undefined"),
STRING("string"),
NUMBER("number"),
BOOLEAN("boolean"),
SYMBOL("symbol");
private final String mProtocolValue;
private ObjectType(String protocolValue) {
mProtocolValue = protocolValue;
}
@JsonValue
public String getProtocolValue() {
return mProtocolValue;
}
}
public static enum ObjectSubType {
ARRAY("array"),
NULL("null"),
NODE("node"),
REGEXP("regexp"),
DATE("date"),
MAP("map"),
SET("set"),
ITERATOR("iterator"),
GENERATOR("generator"),
ERROR("error");
private final String mProtocolValue;
private ObjectSubType(String protocolValue) {
mProtocolValue = protocolValue;
}
@JsonValue
public String getProtocolValue() {
return mProtocolValue;
}
}
}