Skip to content

Commit

Permalink
Implemented displaying data from local variables and arguments in Chr…
Browse files Browse the repository at this point in the history
…ome DevTools when V8 stopped on debug break point:

 1. On break - create Single Synthetic Scope with object id as unique key for frame's variables. This key is used latter by Chrome DevTools for retrieving local variables and arguments.
 2. On break event - store local variables and arguments in Runtime. Later (when Debugger.paused is sent) Chrome DevTools asks Runtime for them and displays on UI.
 3. Store connected JsonRpcPeer from Debugger.enable as it's required later for storing local variables and arguments in Runtime.
 4. Replace Runtime domain with custom class, which override .getProperties() and changes  "ownProperties" to true so that stored local variables could be read by Chrome DevTools and displayed on UI.
 5.
  • Loading branch information
AlexTrotsenko committed Aug 15, 2018
1 parent 49a9cc7 commit fc2987d
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 40 deletions.
Expand Up @@ -13,14 +13,14 @@ class SimpleScriptProvider @Inject constructor() : ScriptSourceProvider {

override fun getSource(scriptId: String): String {
val jsScript = ("""
|function main() {
|function main(payload) {
| var hello = 'hello, ';
| var world = 'world!';
| var world = 'world';
|
| return hello.concat(world);
| return hello + world + " with " + payload + " !";
|}
|
|main()
|main("simple payload")
""").trimMargin()

if (scriptId == scriptName) return jsScript
Expand Down
57 changes: 34 additions & 23 deletions j2v8-debugger/src/main/java/com/alexii/j2v8debugger/Debugger.kt
Expand Up @@ -5,6 +5,7 @@ import com.alexii.j2v8debugger.utils.LogUtils
import com.alexii.j2v8debugger.utils.logger
import com.eclipsesource.v8.V8Object
import com.eclipsesource.v8.debug.*
import com.eclipsesource.v8.debug.mirror.Frame
import com.eclipsesource.v8.debug.mirror.Scope
import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer
import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult
Expand Down Expand Up @@ -54,6 +55,9 @@ class Debugger(
* XXX: consider using ThreadBound from Facebook with an implementation, which uses Executor.
*/
private var v8Executor: ExecutorService? = null

private var connectedPeer: JsonRpcPeer? = null

/** Whether Chrome DevTools is connected to the Stetho (in general) and this debugger (particularly). */
private var isDebuggingOn = false

Expand All @@ -65,7 +69,7 @@ class Debugger(
this.v8Debugger = v8Debugger
this.v8Executor = v8Executor

v8Debugger.addBreakHandler(V8ToChromeDevToolsBreakHandler());
v8Debugger.addBreakHandler(V8ToChromeDevToolsBreakHandler(::connectedPeer));
}

private fun validateV8Initialized() {
Expand All @@ -77,6 +81,7 @@ class Debugger(
@ChromeDevtoolsMethod
override fun enable(peer: JsonRpcPeer, params: JSONObject?) {
LogUtils.logMethodCalled()
connectedPeer = peer

scriptSourceProvider.allScriptIds
.map { ScriptParsedEvent(it) }
Expand All @@ -87,7 +92,9 @@ class Debugger(
override fun disable(peer: JsonRpcPeer, params: JSONObject?) {
LogUtils.logMethodCalled()

//check what's needed to be done here
connectedPeer = null

//check what else is needed to be done here
}

@ChromeDevtoolsMethod
Expand Down Expand Up @@ -334,7 +341,7 @@ class Debugger(
)
}

private class V8ToChromeDevToolsBreakHandler : BreakHandler {
private class V8ToChromeDevToolsBreakHandler(private val currentPeerProvider: () -> JsonRpcPeer?) : BreakHandler {
override fun onBreak(event: DebugHandler.DebugEvent?, state: ExecutionState?, eventData: EventData?, data: V8Object?) {
//XXX: optionally consider adding logging or throwing exceptions
if (event != DebugHandler.DebugEvent.Break) return
Expand All @@ -357,30 +364,24 @@ private class V8ToChromeDevToolsBreakHandler : BreakHandler {

val location = Debugger.Location(scriptId, eventData.sourceLine, eventData.sourceColumn)

val scopes = (0 until frame.scopeCount).map {
val scope = frame.getScope(it)
//j2v8 has api to access only local variables. Scope class has no get-, but only .setVariableValue() method
val knowVariables = frame.getKnownVariables()

val objectClassName = when (scope.type) {
Scope.ScopeType.Local -> "Object"
Scope.ScopeType.Global -> "Window"
else -> ""
}
//todo store id and remove on Resume
val storedVariablesId = Runtime.mapObject(currentPeerProvider(), knowVariables)

//consider using like Runtime.Session.objectForRemote()
val remoteObject = RemoteObject()
//check and use Runtime class here
.apply { objectId = storedVariablesId.toString() }
.apply { type = Runtime.ObjectType.OBJECT }
.apply { className = "Object" }
.apply { description = "Object" }

//consider using like Runtime.Session.objectForRemote()
val remoteObject = RemoteObject()
//check and use Runtime class here
.apply { objectId = it.toString() }
.apply { type = Runtime.ObjectType.OBJECT }
.apply { className = objectClassName }
.apply { description = objectClassName }
//xxx: check what's exactly needed here, pick UNDEFINED or OBJECT for now
val scopeName = Scope.ScopeType.Local.name.toLowerCase(Locale.ENGLISH)
val syntheticScope = Debugger.Scope(scopeName, remoteObject)

val scopeTypeString = scope.type.name.toLowerCase(Locale.ENGLISH)
Debugger.Scope(scopeTypeString, remoteObject)
}

val callFrame = Debugger.CallFrame(it.toString(), frame.function.name, location, scriptIdToUrl(scriptId), scopes)
val callFrame = Debugger.CallFrame(it.toString(), frame.function.name, location, scriptIdToUrl(scriptId), listOf(syntheticScope))
callFrame
}

Expand All @@ -394,4 +395,14 @@ private class V8ToChromeDevToolsBreakHandler : BreakHandler {
logger.w(Debugger.TAG, "Unable to forward break event to Chrome DevTools at ${eventData.sourceLine}, source: ${eventData.sourceLineText}")
}
}

/**
* @return local variables and function arguments if any.
*/
private fun Frame.getKnownVariables(): Map<String, Any> {
val args = (0 until argumentCount).associateBy({ getArgumentName(it) }, { getArgumentValue(it).value })
val localJsVars = (0 until localCount).associateBy({ getLocalName(it) }, { getLocalValue(it).value })

return args + localJsVars;
}
}
50 changes: 50 additions & 0 deletions j2v8-debugger/src/main/java/com/alexii/j2v8debugger/Runtime.kt
@@ -0,0 +1,50 @@
package com.alexii.j2v8debugger

import com.facebook.stetho.inspector.console.RuntimeReplFactory
import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer
import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod
import org.json.JSONObject
import com.facebook.stetho.inspector.protocol.module.Runtime as FacebookRuntimeBase

/**
* V8 JS Debugger. Name of the class and methods must match names defined in Chrome Dev Tools protocol.
*
* [initialize] must be called before actual debugging (adding breakpoints in Chrome DevTools).
* Otherwise setting breakpoint, etc. makes no effect.
*/
class Runtime(
replFactory: RuntimeReplFactory?
) : FacebookRuntimeBase(replFactory) {

@ChromeDevtoolsMethod
override fun getProperties(peer: JsonRpcPeer?, params: JSONObject?): JsonRpcResult {
/**
* hack needed to return local variables: Runtime.getProperties called after Debugger.paused.
* https://github.com/facebook/stetho/issues/611
* xxx: check if it should be conditional for requested related to Debugger only
*/

params?.put("ownProperties", true)

val result = super.getProperties(peer, params)

return result
}

@ChromeDevtoolsMethod
override fun releaseObject(peer: JsonRpcPeer?, params: JSONObject?) = super.releaseObject(peer, params)

@ChromeDevtoolsMethod
override fun releaseObjectGroup(peer: JsonRpcPeer?, params: JSONObject?) = super.releaseObjectGroup(peer, params)

/**
* Replaces [FacebookRuntimeBase.callFunctionOn] as override can't be used: CallFunctionOnResponse is private (return type)
*/
//FIXME check why when we add following - Stetho is not working completely
// @ChromeDevtoolsMethod
// fun callFunctionOn(peer: JsonRpcPeer?, params: Any?): JsonRpcResult? = super.callFunctionOn(peer, params as JSONObject?)

@ChromeDevtoolsMethod
override fun evaluate(peer: JsonRpcPeer?, params: JSONObject?): JsonRpcResult = super.evaluate(peer, params)
}
41 changes: 28 additions & 13 deletions j2v8-debugger/src/main/java/com/alexii/j2v8debugger/StethoHelper.kt
Expand Up @@ -7,11 +7,13 @@ import com.eclipsesource.v8.V8
import com.eclipsesource.v8.debug.DebugHandler
import com.facebook.stetho.InspectorModulesProvider
import com.facebook.stetho.Stetho
import com.facebook.stetho.inspector.console.RuntimeReplFactory
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ExecutorService
import com.facebook.stetho.inspector.protocol.module.Debugger as FacebookDebuggerStub
import com.facebook.stetho.inspector.protocol.module.Runtime as FacebookRuntimeBase

object StethoHelper {
var debugger: Debugger? = null
Expand All @@ -26,29 +28,36 @@ object StethoHelper {
*/
@JvmStatic
fun defaultInspectorModulesProvider(context: Context, scriptSourceProvider: ScriptSourceProvider): InspectorModulesProvider {
return InspectorModulesProvider {
try {
getDefaultInspectorModulesWithDebugger(context, scriptSourceProvider)
} catch (e: Throwable) { //v8 throws Error instead of Exception on wrong thread access, etc.
Log.e(Debugger.TAG, "Unable to init Stetho with V8 Debugger. Default set-up will be used", e)
return InspectorModulesProvider { getInspectorModules(context, scriptSourceProvider) }
}

getDefaultInspectorModules(context)
}
@JvmOverloads
private fun getInspectorModules(context: Context, scriptSourceProvider: ScriptSourceProvider, factory: RuntimeReplFactory? = null): Iterable<ChromeDevtoolsDomain> {
return try {
getDefaultInspectorModulesWithDebugger(context, scriptSourceProvider, factory)
} catch (e: Throwable) { //v8 throws Error instead of Exception on wrong thread access, etc.
Log.e(Debugger.TAG, "Unable to init Stetho with V8 Debugger. Default set-up will be used", e)

getDefaultInspectorModules(context, factory)
}
}

@VisibleForTesting
fun getDefaultInspectorModulesWithDebugger(context: Context, scriptSourceProvider: ScriptSourceProvider): Iterable<ChromeDevtoolsDomain> {
val defaultInspectorModules = getDefaultInspectorModules(context)
fun getDefaultInspectorModulesWithDebugger(context: Context, scriptSourceProvider: ScriptSourceProvider, factory: RuntimeReplFactory? = null): Iterable<ChromeDevtoolsDomain> {
val defaultInspectorModules = getDefaultInspectorModules(context, factory)

//remove work-around when https://github.com/facebook/stetho/pull/600 is merged
val inspectorModules = ArrayList<ChromeDevtoolsDomain>()
for (defaultModule in defaultInspectorModules) {
if (FacebookDebuggerStub::class.java != defaultModule.javaClass) inspectorModules.add(defaultModule)
if (FacebookDebuggerStub::class != defaultModule::class
&& FacebookRuntimeBase::class != defaultModule::class) {
inspectorModules.add(defaultModule)
}
}

debugger = Debugger(scriptSourceProvider)
inspectorModules.add(debugger!!)
inspectorModules.add(Runtime(factory))

bindV8ToChromeDebuggerIfReady()

Expand All @@ -73,7 +82,7 @@ object StethoHelper {
val v8DebuggerInitialized = v8Debugger != null && v8Executor != null

if (v8DebuggerInitialized && chromeDebuggerAttached) {
v8Executor!!.execute { bindV8DebuggerToChromeDebugger(debugger!!, v8Debugger!!, v8Executor!!) }
v8Executor!!.execute { bindV8DebuggerToChromeDebugger(debugger!!, v8Debugger!!, v8Executor) }
}
}

Expand All @@ -85,8 +94,14 @@ object StethoHelper {
chromeDebugger.initialize(v8Debugger, v8Executor)
}

private fun getDefaultInspectorModules(context: Context): Iterable<ChromeDevtoolsDomain> {
return Stetho.DefaultInspectorModulesBuilder(context)
/**
* @return default Stetho.DefaultInspectorModulesBuilder
*
* @param context Android context, which is required to access android resources by Stetho.
* @param factory copies behaviour of [Stetho.DefaultInspectorModulesBuilder.runtimeRepl] using [Runtime]
*/
private fun getDefaultInspectorModules(context: Context, factory: RuntimeReplFactory?): Iterable<ChromeDevtoolsDomain> {
return Stetho.DefaultInspectorModulesBuilder(context).runtimeRepl(factory)
.finish()
}
}
Expand Up @@ -8,6 +8,7 @@ import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.concurrent.ExecutorService
import com.facebook.stetho.inspector.protocol.module.Debugger as FacebookDebuggerStub
import com.facebook.stetho.inspector.protocol.module.Runtime as FacebookRuntimeBase

/**
* Example local unit test, which will execute on the development machine (host).
Expand All @@ -28,6 +29,18 @@ class StethoHelperTest {
assertFalse("Stetho Debugger present", domains.any { it.javaClass == FacebookDebuggerStub::class.java} )
}

@Test
fun `returns custom Runtime and no Stetho base Runtime`() {
val scriptSourceProviderMock = mock<ScriptSourceProvider> {}
val contextMock = mock<Application> {}
whenever(contextMock.applicationContext).thenReturn(contextMock)

val domains = StethoHelper.getDefaultInspectorModulesWithDebugger(contextMock, scriptSourceProviderMock)

assertTrue("No Debugger present", domains.any { it.javaClass == Runtime::class.java })
assertFalse("Stetho Debugger present", domains.any { it.javaClass == FacebookRuntimeBase::class.java} )
}

@Test
fun `initialized when Stetho created before v8`() {
val scriptSourceProviderMock = mock<ScriptSourceProvider> {}
Expand All @@ -45,6 +58,7 @@ class StethoHelperTest {
assertTrue(StethoHelper.isStethoAndV8DebuggerFullyInitialized)
}

//xxx: check why test is failing if run together with other, but ok when run separately
@Test
fun `initialized when v8 created before Stetho`() {
val v8DebugHandlerMock = mock<DebugHandler>()
Expand Down

0 comments on commit fc2987d

Please sign in to comment.