From bcce63c09d12fece39f175ff9e70017e1e04e7bc Mon Sep 17 00:00:00 2001 From: Ian Bull Date: Wed, 3 Sep 2014 15:37:58 -0700 Subject: [PATCH] Add debug support to J2V8 Debug Support allows the user to specify a debug port on which to attach an external debugger. The API require the user to register a callback, which will be invoked whenever a debugger is attached. It is the responsibility of this callback to invoke V8.processDebugMessages() from the proper V8 Thread. Debug support can be registered with an option to halt the VM on the first instruction and wait for a debugger to connect. --- jni/com_eclipsesource_v8_V8Impl.cpp | 77 +++++++++++++++++- jni/com_eclipsesource_v8_V8Impl.h | 24 ++++++ src/main/java/com/eclipsesource/v8/V8.java | 74 ++++++++++++++++-- .../eclipsesource/v8/tests/V8ArrayTest.java | 3 + .../v8/tests/V8CallbackTest.java | 3 + .../v8/tests/V8JSFunctionCallTest.java | 3 + .../eclipsesource/v8/tests/V8ObjectTest.java | 3 + .../com/eclipsesource/v8/tests/V8Test.java | 78 +++++++++++++++++++ 8 files changed, 253 insertions(+), 12 deletions(-) diff --git a/jni/com_eclipsesource_v8_V8Impl.cpp b/jni/com_eclipsesource_v8_V8Impl.cpp index e2c648b19..c3a71a3ad 100644 --- a/jni/com_eclipsesource_v8_V8Impl.cpp +++ b/jni/com_eclipsesource_v8_V8Impl.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include "com_eclipsesource_v8_V8Impl.h" @@ -20,6 +21,7 @@ class V8Runtime { }; std::map v8Isolates; +JavaVM* jvm = NULL; void throwError( JNIEnv *env, const char *message ); void throwExecutionException( JNIEnv *env, const char *message ); @@ -27,9 +29,78 @@ void throwResultUndefinedException( JNIEnv *env, const char *message ); Isolate* getIsolate(JNIEnv *env, int handle); void setupJNIContext(int v8RuntimeHandle, JNIEnv *env, jobject v8 ); +void debugHandler() { + JNIEnv * g_env; + // double check it's all ok + int getEnvStat = jvm->GetEnv((void **) &g_env, JNI_VERSION_1_6); + if (getEnvStat == JNI_EDETACHED) { + if (jvm->AttachCurrentThread((void **) &g_env, NULL) != 0) { + std::cout << "Failed to attach" << std::endl; + } + } else if (getEnvStat == JNI_OK) { + // + } else if (getEnvStat == JNI_EVERSION) { + std::cout << "GetEnv: version not supported" << std::endl; + } + + jclass cls = g_env->FindClass("com/eclipsesource/v8/V8"); + jmethodID processDebugMessage = g_env->GetStaticMethodID(cls, "debugMessageReceived", "()V"); + + g_env->CallStaticVoidMethod(cls, processDebugMessage); + + if (g_env->ExceptionCheck()) { + g_env->ExceptionDescribe(); + } + + jvm->DetachCurrentThread(); +} + +JNIEXPORT jboolean JNICALL Java_com_eclipsesource_v8_V8__1enableDebugSupport + (JNIEnv *env, jobject, jint v8RuntimeHandle, jint port, jboolean waitForConnection) { + Isolate* isolate = getIsolate(env, v8RuntimeHandle); + if ( isolate == NULL ) { + return false; + } + v8::Isolate::Scope isolateScope(isolate); + HandleScope handle_scope(isolate); + v8::Local context = v8::Local::New(isolate,v8Isolates[v8RuntimeHandle]->context_); + Context::Scope context_scope(context); + bool result = v8::Debug::EnableAgent("j2v8", port, waitForConnection); + v8::Debug::SetDebugMessageDispatchHandler(&debugHandler); + return result; +} + +JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1disableDebugSupport + (JNIEnv *env, jobject, jint v8RuntimeHandle) { + Isolate* isolate = getIsolate(env, v8RuntimeHandle); + if ( isolate == NULL ) { + return; + } + v8::Isolate::Scope isolateScope(isolate); + HandleScope handle_scope(isolate); + v8::Local context = v8::Local::New(isolate,v8Isolates[v8RuntimeHandle]->context_); + Context::Scope context_scope(context); + v8::Debug::DisableAgent(); +} + +JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1processDebugMessages + (JNIEnv *env, jobject, jint v8RuntimeHandle) { + Isolate* isolate = getIsolate(env, v8RuntimeHandle); + if ( isolate == NULL ) { + return; + } + v8::Isolate::Scope isolateScope(isolate); + HandleScope handle_scope(isolate); + v8::Local context = v8::Local::New(isolate,v8Isolates[v8RuntimeHandle]->context_); + Context::Scope context_scope(context); + v8::Debug::ProcessDebugMessages(); +} JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1createIsolate - (JNIEnv *, jobject, jint handle) { + (JNIEnv *env, jobject, jint handle) { + if (jvm == NULL ) { + env->GetJavaVM(&jvm); + } v8Isolates[handle] = new V8Runtime(); v8Isolates[handle]->isolate = Isolate::New(); v8Isolates[handle]->isolate_scope = new Isolate::Scope(v8Isolates[handle]->isolate); @@ -613,7 +684,6 @@ JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1executeVoidFunction v8::Local context = v8::Local::New(isolate,v8Isolates[v8RuntimeHandle]->context_); Context::Scope context_scope(context); Handle parentObject = Local::New(isolate, *v8Isolates[v8RuntimeHandle]->objects[objectHandle]); - int size = 0; Handle* args = NULL; if ( parameterHandle >= 0 ) { @@ -624,8 +694,7 @@ JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1executeVoidFunction args[i] = parameters->Get(i); } } - - Handle value = parentObject->Get(v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), functionName)); + Handle value = parentObject->Get(v8::String::NewFromUtf8(isolate, functionName)); Handle func = v8::Handle::Cast(value); func->Call(parentObject, size, args); env->ReleaseStringUTFChars(jfunctionName, functionName); diff --git a/jni/com_eclipsesource_v8_V8Impl.h b/jni/com_eclipsesource_v8_V8Impl.h index 05792d28f..475e0930a 100644 --- a/jni/com_eclipsesource_v8_V8Impl.h +++ b/jni/com_eclipsesource_v8_V8Impl.h @@ -437,6 +437,30 @@ JNIEXPORT jint JNICALL Java_com_eclipsesource_v8_V8__1getType__III JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1setPrototype (JNIEnv *, jobject, jint, jint, jint); +/* + * Class: com_eclipsesource_v8_V8 + * Method: _enableDebugSupport + * Signature: (IIZ)Z + */ +JNIEXPORT jboolean JNICALL Java_com_eclipsesource_v8_V8__1enableDebugSupport + (JNIEnv *, jobject, jint, jint, jboolean); + +/* + * Class: com_eclipsesource_v8_V8 + * Method: _disableDebugSupport + * Signature: (I)V + */ +JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1disableDebugSupport + (JNIEnv *, jobject, jint); + +/* + * Class: com_eclipsesource_v8_V8 + * Method: _processDebugMessages + * Signature: (I)V + */ +JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__1processDebugMessages + (JNIEnv *, jobject, jint); + #ifdef __cplusplus } #endif diff --git a/src/main/java/com/eclipsesource/v8/V8.java b/src/main/java/com/eclipsesource/v8/V8.java index eeaedd206..4c64f8668 100644 --- a/src/main/java/com/eclipsesource/v8/V8.java +++ b/src/main/java/com/eclipsesource/v8/V8.java @@ -2,17 +2,22 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class V8 extends V8Object { private static int v8InstanceCounter; - private int methodReferenceCounter = 0; + private static Thread thread = null; + private static List runtimes = new ArrayList<>(); + private static Runnable debugHandler = null; - private int v8RuntimeHandle; - static Thread thread = null; - long objectReferences = 0; + private int methodReferenceCounter = 0; + private int v8RuntimeHandle; + private boolean debugEnabled = false; + long objectReferences = 0; class MethodDescriptor { Object object; @@ -29,15 +34,51 @@ public synchronized static V8 createV8Runtime() { if (thread == null) { thread = Thread.currentThread(); } - return new V8(); + V8 runtime = new V8(); + runtimes.add(runtime); + return runtime; } - private V8() { + protected V8() { checkThread(); v8RuntimeHandle = v8InstanceCounter++; _createIsolate(v8RuntimeHandle); } + public boolean enableDebugSupport(final int port, final boolean waitForConnection) { + checkThread(); + debugEnabled = _enableDebugSupport(getHandle(), port, waitForConnection); + return debugEnabled; + } + + public boolean enableDebugSupport(final int port) { + checkThread(); + debugEnabled = true; + debugEnabled = _enableDebugSupport(getV8RuntimeHandle(), port, false); + return debugEnabled; + } + + public void disableDebugSupport() { + checkThread(); + _disableDebugSupport(getV8RuntimeHandle()); + debugEnabled = false; + } + + public static void processDebugMessages() { + checkThread(); + for (V8 v8 : runtimes) { + v8._processDebugMessages(v8.getV8RuntimeHandle()); + } + } + + public static int getActiveRuntimes() { + return runtimes.size(); + } + + public static void registerDebugHandler(final Runnable handler) { + debugHandler = handler; + } + public int getV8RuntimeHandle() { return v8RuntimeHandle; } @@ -45,12 +86,17 @@ public int getV8RuntimeHandle() { @Override public void release() { checkThread(); + if (debugEnabled) { + disableDebugSupport(); + } + runtimes.remove(this); + _releaseRuntime(v8RuntimeHandle); if (objectReferences > 0) { throw new IllegalStateException(objectReferences + " Object(s) still exist in runtime"); } - _releaseRuntime(v8RuntimeHandle); } + public int executeIntScript(final String script) throws V8RuntimeException { checkThread(); return _executeIntScript(v8RuntimeHandle, script); @@ -101,7 +147,7 @@ public void executeVoidScript(final String script) throws V8RuntimeException { } static void checkThread() { - if (thread != Thread.currentThread()) { + if ((thread != null) && (thread != Thread.currentThread())) { throw new Error("Invalid V8 thread access."); } } @@ -163,6 +209,12 @@ private Object getArrayItem(final V8Array array, final int index) { return null; } + protected static void debugMessageReceived() { + if (debugHandler != null) { + debugHandler.run(); + } + } + protected native void _initExistingV8Object(int v8RuntimeHandle, int parentHandle, String objectKey, int objectHandle); @@ -275,6 +327,12 @@ protected native void _arrayGetObject(final int v8RuntimeHandle, final int array protected native void _setPrototype(int v8RuntimeHandle, int objectHandle, int prototypeHandle); + protected native boolean _enableDebugSupport(int v8RuntimeHandle, int port, boolean waitForConnection); + + protected native void _disableDebugSupport(int v8RuntimeHandle); + + protected native void _processDebugMessages(int v8RuntimeHandle); + void addObjRef() { objectReferences++; } diff --git a/src/test/java/com/eclipsesource/v8/tests/V8ArrayTest.java b/src/test/java/com/eclipsesource/v8/tests/V8ArrayTest.java index 140e089ad..c93cfd5e5 100644 --- a/src/test/java/com/eclipsesource/v8/tests/V8ArrayTest.java +++ b/src/test/java/com/eclipsesource/v8/tests/V8ArrayTest.java @@ -26,6 +26,9 @@ public void seutp() { public void tearDown() { try { v8.release(); + if (V8.getActiveRuntimes() != 0) { + throw new IllegalStateException("V8Runtimes not properly released."); + } } catch (IllegalStateException e) { System.out.println(e.getMessage()); } diff --git a/src/test/java/com/eclipsesource/v8/tests/V8CallbackTest.java b/src/test/java/com/eclipsesource/v8/tests/V8CallbackTest.java index 5bb10c810..e2977f306 100644 --- a/src/test/java/com/eclipsesource/v8/tests/V8CallbackTest.java +++ b/src/test/java/com/eclipsesource/v8/tests/V8CallbackTest.java @@ -31,6 +31,9 @@ public void seutp() { public void tearDown() { try { v8.release(); + if (V8.getActiveRuntimes() != 0) { + throw new IllegalStateException("V8Runtimes not properly released."); + } } catch (IllegalStateException e) { System.out.println(e.getMessage()); } diff --git a/src/test/java/com/eclipsesource/v8/tests/V8JSFunctionCallTest.java b/src/test/java/com/eclipsesource/v8/tests/V8JSFunctionCallTest.java index a7e40c107..09aec95a0 100644 --- a/src/test/java/com/eclipsesource/v8/tests/V8JSFunctionCallTest.java +++ b/src/test/java/com/eclipsesource/v8/tests/V8JSFunctionCallTest.java @@ -25,6 +25,9 @@ public void seutp() { public void tearDown() { try { v8.release(); + if (V8.getActiveRuntimes() != 0) { + throw new IllegalStateException("V8Runtimes not properly released."); + } } catch (IllegalStateException e) { System.out.println(e.getMessage()); } diff --git a/src/test/java/com/eclipsesource/v8/tests/V8ObjectTest.java b/src/test/java/com/eclipsesource/v8/tests/V8ObjectTest.java index 92abe7d28..908e36e10 100644 --- a/src/test/java/com/eclipsesource/v8/tests/V8ObjectTest.java +++ b/src/test/java/com/eclipsesource/v8/tests/V8ObjectTest.java @@ -28,6 +28,9 @@ public void seutp() { public void tearDown() { try { v8.release(); + if (V8.getActiveRuntimes() != 0) { + throw new IllegalStateException("V8Runtimes not properly released."); + } } catch (IllegalStateException e) { System.out.println(e.getMessage()); } diff --git a/src/test/java/com/eclipsesource/v8/tests/V8Test.java b/src/test/java/com/eclipsesource/v8/tests/V8Test.java index e1ff77b4b..2728c7fed 100644 --- a/src/test/java/com/eclipsesource/v8/tests/V8Test.java +++ b/src/test/java/com/eclipsesource/v8/tests/V8Test.java @@ -1,5 +1,8 @@ package com.eclipsesource.v8.tests; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; import java.util.Arrays; import java.util.List; @@ -17,6 +20,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; public class V8Test { @@ -31,6 +36,9 @@ public void seutp() { public void tearDown() { try { v8.release(); + if (V8.getActiveRuntimes() != 0) { + throw new IllegalStateException("V8Runtimes not properly released."); + } } catch (IllegalStateException e) { System.out.println(e.getMessage()); } @@ -692,6 +700,7 @@ public void testGetBooleanDoesNotExist() { v8.getBoolean("x"); } + @Test public void testAddGet() { v8.add("string", "string"); @@ -889,4 +898,73 @@ public void testWindowAliasForGlobalScope() { assertTrue(v8.executeBooleanScript("window.hasOwnProperty( \"Object\" )")); } + /*** Debug Tests ***/ + @Test + public void testSetupDebugHandler() { + int port = 9991; + + v8.enableDebugSupport(port); + + assertTrue(debugEnabled(port)); + } + + @Test + public void testRemoveDebugHandler() { + int port = 9991; + v8.enableDebugSupport(port); + + v8.disableDebugSupport(); + + assertFalse(debugEnabled(port)); + } + + @Test + public void testMultipleDebugHandlers() { + V8 v8_2 = V8.createV8Runtime(); + + v8.enableDebugSupport(9991); + v8_2.enableDebugSupport(9992); + + assertTrue(debugEnabled(9991)); + assertTrue(debugEnabled(9992)); + v8_2.disableDebugSupport(); + v8_2.release(); + assertTrue(debugEnabled(9991)); + } + + static class SubV8 extends V8 { + public static void debugMessageReceived() { + V8.debugMessageReceived(); + } + } + + @Test + public void testHandlerCalled() { + Runnable runnable = mock(Runnable.class); + V8.registerDebugHandler(runnable); + + SubV8.debugMessageReceived(); + + verify(runnable).run(); + V8.registerDebugHandler(null); + } + + private boolean debugEnabled(final int port) { + Socket socket = new Socket(); + InetSocketAddress endPoint = new InetSocketAddress("localhost", port); + try { + socket.connect(endPoint); + return true; + } catch (IOException e) { + return false; + } finally { + try { + socket.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + } + } \ No newline at end of file