Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow exporting DOM elements to the Console #247

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ class JsRuntimeRepl implements RuntimeRepl {
return Context.jsToJava(result, Object.class);
}

@Override
public boolean assignVariable(String varName, Object value) throws Throwable {
enterJsContext();
try {
Object jsValue = Context.javaToJS(value, mJsScope);
ScriptableObject.putProperty(mJsScope, varName, jsValue);
return true;
} finally {
Context.exit();
}
}

/**
* Setups a proper javascript context so that it can run javascript code properly under android.
* For android we need to disable bytecode generation since the android vms don't understand JVM bytecode.
Expand Down
12 changes: 9 additions & 3 deletions stetho/src/main/java/com/facebook/stetho/Stetho.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,25 @@ public static InspectorModulesProvider defaultInspectorModulesProvider(final Con
@Override
public Iterable<ChromeDevtoolsDomain> get() {
ArrayList<ChromeDevtoolsDomain> modules = new ArrayList<ChromeDevtoolsDomain>();
modules.add(new Console());
modules.add(new CSS());
modules.add(new Debugger());
Runtime runtime = new Runtime();
modules.add(runtime);
if (Build.VERSION.SDK_INT >= AndroidDOMConstants.MIN_API_LEVEL) {
modules.add(new DOM(new AndroidDOMProviderFactory((Application)context.getApplicationContext())));
DOM dom = new DOM(
new AndroidDOMProviderFactory(
(Application)context.getApplicationContext()));
modules.add(dom);
modules.add(new Console(dom, runtime));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably talk with @ichub about this. In his refactoring PR #237 we are splitting DOM into two classes, DOM and Document, with the intent that CSS will have a reference to Document in order to avoid having a reference to DOM. In this case you're giving Console access to DOM and Runtime and that may not be the right approach, especially since you don't actually need DOM -- you need the ObjectIdMapper.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm giving them access because this is a public API contract and I don't want to be discrete about what is needed for fear of having frequent and possibly confusing changes to the API. There are solutions of course, but we should consider public API surface area and the fact that this is currently our only "configuration" mechanism we offer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now the dependency from Console to DOM and Runtime is part of the public contract? You say you want to avoid frequent and possibly confusing changes to the API, but I think that's precisely what you're introducing here. Why do I need to create DOM and Runtime and then plug those into Console? What happens when we change our mind and Console no longer needs either of these? Do I need to supply the same DOM to Console that I already plugged into a call to modules.add(), or should I add a new one? This all sounds pretty confusing and arbitrary to me.

I think it'd be better to handle this internally somehow. For example, DescriptorMap does initialization of Descriptors in 2 phases. First you register Descriptors, and then in phase 2 they all get linked together by Descriptormap.

So maybe ChromeDevtoolsDomain could have two new methods, Class<?>[] getDependencies() and void provideDependency(Class<?>, Object). Console's implementation of getDependencies would return { DOM.class, Runtime.class } and be given the appropriate stuff via multiple calls to provideDependency when we build().

It's hardly a wonderful or elegant solution but it keeps this dependency management as an internal Stetho problem.

} else {
modules.add(new Console());
}
modules.add(new DOMStorage(context));
modules.add(new HeapProfiler());
modules.add(new Inspector());
modules.add(new Network(context));
modules.add(new Page(context));
modules.add(new Profiler());
modules.add(new Runtime());
modules.add(new Worker());
if (Build.VERSION.SDK_INT >= DatabaseConstants.MIN_API_LEVEL) {
modules.add(new Database(context, new DefaultDatabaseFilesProvider(context)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,15 @@

public interface RuntimeRepl {
Object evaluate(String expression) throws Throwable;

/**
* Assign the variable in the evaluation scope such that it can be accessed by subsequent
* evaluations. The value object must be strong referenced by the implementor!

* @param varName Variable name to assign
* @param value Value of the object
* @return True if assignment is supported; false otherwise
* @throws Throwable Thrown if there is an error evaluating the variable name
*/
boolean assignVariable(String varName, Object value) throws Throwable;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,40 @@
import android.annotation.SuppressLint;

import com.facebook.stetho.inspector.console.ConsolePeerManager;
import com.facebook.stetho.inspector.console.RuntimeReplFactory;
import com.facebook.stetho.inspector.helper.ObjectIdMapper;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcException;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer;
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.json.ObjectMapper;
import com.facebook.stetho.json.annotation.JsonProperty;
import com.facebook.stetho.json.annotation.JsonValue;

import org.json.JSONObject;

import javax.annotation.Nullable;

public class Console implements ChromeDevtoolsDomain {
@Nullable
private final ObjectIdMapper mDomObjectIdMapper;
private final RuntimeReplFactory mRuntimeReplFactory;

private final ObjectMapper mObjectMapper = new ObjectMapper();

/**
* @deprecated See {@link #Console(DOM, Runtime)}
*/
@Deprecated
public Console() {
mDomObjectIdMapper = null;
mRuntimeReplFactory = null;
}

public Console(DOM dom, Runtime runtime) {
mDomObjectIdMapper = dom.getObjectIdMapper();
mRuntimeReplFactory = runtime.getReplFactory();
}

@ChromeDevtoolsMethod
Expand All @@ -34,6 +58,38 @@ public void disable(JsonRpcPeer peer, JSONObject params) {
ConsolePeerManager.getOrCreateInstance().removePeer(peer);
}

@ChromeDevtoolsMethod
public void addInspectedNode(JsonRpcPeer peer, JSONObject params) throws JsonRpcException {
if (mDomObjectIdMapper == null) {
throw new JsonRpcException(
new JsonRpcError(
JsonRpcError.ErrorCode.INTERNAL_ERROR,
"No DOM object mapper present",
null /* data */));
}

AddInspectedNodeRequest request =
mObjectMapper.convertValue(params, AddInspectedNodeRequest.class);
Object object = mDomObjectIdMapper.getObjectForId(request.nodeId);
if (object == null) {
throw new JsonRpcException(
new JsonRpcError(
JsonRpcError.ErrorCode.INVALID_PARAMS,
"No known nodeId=" + request.nodeId,
null /* data */));
}

try {
Runtime.addInspectedNode(peer, mRuntimeReplFactory, object);
} catch (Throwable t) {
throw new JsonRpcException(
new JsonRpcError(
JsonRpcError.ErrorCode.INTERNAL_ERROR,
t.toString(),
null /* data */));
}
}

@SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" })
public static class MessageAddedRequest {
@JsonProperty(required = true)
Expand Down Expand Up @@ -118,4 +174,9 @@ public CallFrame(String functionName, String url, int lineNumber, int columnNumb
this.columnNumber = columnNumber;
}
}

private static class AddInspectedNodeRequest {
@JsonProperty(required = true)
public int nodeId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ public DOM(DOMProvider.Factory providerFactory) {
mResultCounter = new AtomicInteger(0);
}

ObjectIdMapper getObjectIdMapper() {
return mObjectIdMapper;
}

@ChromeDevtoolsMethod
public void enable(JsonRpcPeer peer, JSONObject params) {
mPeerManager.addPeer(peer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,22 @@ public Runtime(RuntimeReplFactory replFactory) {
mReplFactory = replFactory;
}

RuntimeReplFactory getReplFactory() {
return mReplFactory;
}

public static int mapObject(JsonRpcPeer peer, Object object) {
return getSession(peer).getObjects().putObject(object);
}

public static void addInspectedNode(
JsonRpcPeer peer,
RuntimeReplFactory replFactory,
Object domObject)
throws Throwable {
getSession(peer).addInspectedNode(replFactory, domObject);
}

@Nonnull
private static synchronized Session getSession(final JsonRpcPeer peer) {
Session session = sSessions.get(peer);
Expand Down Expand Up @@ -237,6 +249,17 @@ public EvaluateResponse evaluate(RuntimeReplFactory replFactory, JSONObject para
}
}

public void addInspectedNode(RuntimeReplFactory replFactory, Object domObject)
throws Throwable {
RuntimeRepl repl = getRepl(replFactory);
boolean success = repl.assignVariable("$0", domObject);
if (!success) {
LogUtil.d("Cannot assign $0 to " +
domObject.getClass().getSimpleName() +
"{" + System.identityHashCode(domObject) + "}");
}
}

@Nonnull
private synchronized RuntimeRepl getRepl(RuntimeReplFactory replFactory) {
if (mRepl == null) {
Expand Down Expand Up @@ -568,6 +591,11 @@ public RuntimeRepl newInstance() {
public Object evaluate(String expression) throws Exception {
return "Not supported";
}

@Override
public boolean assignVariable(String varName, Object value) throws Throwable {
return false;
}
};
}
}
Expand Down