diff --git a/compiler/testData/debug/stepping/tryCatchFinally.kt b/compiler/testData/debug/stepping/tryCatchFinally.kt index cfab0cb70cf..bb741e8a739 100644 --- a/compiler/testData/debug/stepping/tryCatchFinally.kt +++ b/compiler/testData/debug/stepping/tryCatchFinally.kt @@ -1,4 +1,4 @@ -// IGNORE_BACKEND: JS_IR + // FILE: test.kt fun foo() { @@ -93,3 +93,28 @@ fun box() { // test.kt:39 box // EXPECTATIONS JS_IR +// test.kt:34 box +// test.kt:6 foo +// test.kt:26 mightThrow +// test.kt:14 foo +// test.kt:30 mightThrow2 +// test.kt:13 foo +// test.kt:35 box +// test.kt:36 box +// test.kt:6 foo +// test.kt:26 mightThrow +// test.kt:14 foo +// test.kt:30 mightThrow2 +// test.kt:30 mightThrow2 +// test.kt:16 foo +// test.kt:13 foo +// test.kt:37 box +// test.kt:38 box +// test.kt:6 foo +// test.kt:26 mightThrow +// test.kt:26 mightThrow +// test.kt:14 foo +// test.kt:30 mightThrow2 +// test.kt:30 mightThrow2 +// test.kt:16 foo +// test.kt:13 foo diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt index 6612093cc68..6b87f856a93 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt @@ -13,10 +13,7 @@ import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.jetbrains.kotlin.utils.addToStdlib.cast -import java.util.logging.ConsoleHandler -import java.util.logging.Level import java.util.logging.Logger -import java.util.logging.MemoryHandler import kotlin.coroutines.* /** @@ -59,21 +56,32 @@ class NodeJsInspectorClient(val scriptPath: String, val args: List) { } }) - context.listenForMessages { message -> - when (val response = decodeCDPResponse(message) { context.messageContinuations[it]!!.first }) { - is CDPResponse.Event -> onDebuggerEventCallback?.invoke(response.event) - is CDPResponse.MethodInvocationResult -> context.messageContinuations.remove(response.id)!!.second.resume(response.result) - is CDPResponse.Error -> context.messageContinuations[response.id]!!.second.resumeWithException( - IllegalStateException("error ${response.error.code}" + (response.error.message?.let { ": $it" } ?: "")) - ) - } - context.waitingOnPredicate?.let { (predicate, continuation) -> - if (predicate()) { - context.waitingOnPredicate = null - continuation.resume(Unit) + try { + context.listenForMessages { message -> + when (val response = decodeCDPResponse(message) { context.messageContinuations[it]!!.encodingInfo }) { + is CDPResponse.Event -> onDebuggerEventCallback?.invoke(response.event) + is CDPResponse.MethodInvocationResult -> context.messageContinuations.remove(response.id)!!.continuation.resume(response.result) + is CDPResponse.Error -> context.messageContinuations[response.id]!!.let { (_, continuation, stackTrace) -> + continuation.resumeWithException( + IllegalStateException("error ${response.error.code}" + (response.error.message?.let { ": $it" } ?: "")).apply { + this.stackTrace = stackTrace + } + ) + } } + context.waitingOnPredicate?.let { (predicate, continuation, _) -> + if (predicate()) { + context.waitingOnPredicate = null + continuation.resume(Unit) + } + } + blockResult != null } - blockResult != null + } catch (e: Exception) { + val callerStackTrace = context.messageContinuations.values.singleOrNull()?.stackTrace ?: context.waitingOnPredicate?.stackTrace + if (callerStackTrace != null) + e.stackTrace = callerStackTrace + throw e } return blockResult!!.getOrThrow() @@ -94,16 +102,7 @@ private const val NODE_WS_DEBUG_URL_PREFIX = "Debugger listening on ws://" */ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : NodeJsInspectorClientContext, CDPRequestEvaluator { - private val consoleLoggingHandler = ConsoleHandler().apply { - level = Level.FINER - } - - private val memoryLoggingHandler = MemoryHandler(consoleLoggingHandler, 5000, Level.WARNING) - - private val logger = Logger.getLogger(this::class.java.name).apply { - level = Level.FINER - addHandler(memoryLoggingHandler) - } + private val logger = Logger.getLogger(this::class.java.name) private val nodeProcess = ProcessBuilder( System.getProperty("javascript.engine.path.NodeJs"), @@ -130,16 +129,31 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : private val webSocketClient = HttpClient(CIO) { install(WebSockets) + engine { + requestTimeout = 0 + } } private var webSocketSession: DefaultClientWebSocketSession? = null - val messageContinuations = mutableMapOf>>() + data class MessageContinuation( + val encodingInfo: CDPMethodCallEncodingInfo, + val continuation: Continuation, + val stackTrace: Array + ) + + val messageContinuations = mutableMapOf() + + data class WaitingOnPredicate( + val predicate: () -> Boolean, + val continuation: Continuation, + val stackTrace: Array, + ) /** * See [waitForConditionToBecomeTrue]. */ - var waitingOnPredicate: Pair<(() -> Boolean), Continuation>? = null + var waitingOnPredicate: WaitingOnPredicate? = null private var nextMessageId = 0 @@ -165,14 +179,9 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : suspend fun listenForMessages(receiveMessage: (String) -> Boolean) { val session = webSocketSession ?: error("Session closed") do { - val message = try { - when (val frame = session.incoming.receive()) { - is Frame.Text -> frame.readText() - else -> error("Unexpected frame kind: $frame") - } - } catch (e: Exception) { - logger.log(Level.SEVERE, "Could not receive message", e) - throw e + val message = when (val frame = session.incoming.receive()) { + is Frame.Text -> frame.readText() + else -> error("Unexpected frame kind: $frame") } logger.finer { "Received message:\n${prettyPrintJson(message)}" @@ -186,9 +195,13 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : override suspend fun waitForConditionToBecomeTrue(predicate: () -> Boolean) { if (predicate()) return + + // Save the stack trace to show it later. If the condition never becomes true due to an exception, + // this stack trace will be shown instead of some obscure coroutine-related one, which will make debugging easier. + val stacktrace = Thread.currentThread().stackTrace suspendCoroutine { continuation -> require(waitingOnPredicate == null) { "already waiting!" } - waitingOnPredicate = predicate to continuation + waitingOnPredicate = WaitingOnPredicate(predicate, continuation, stacktrace) } } @@ -203,9 +216,14 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : @Deprecated("Only for debugging purposes", level = DeprecationLevel.WARNING) override suspend fun sendPlainTextMessage(methodName: String, paramsJson: String): String { val messageId = nextMessageId++ + + // Save the stack trace to show it later. If we won't be able to receive a response for this message due to an exception, + // this stack trace will be shown instead of some obscure coroutine-related one, which will make debugging easier. + val stacktrace = Thread.currentThread().stackTrace + sendPlainTextMessage("""{"id":$messageId,"method":$methodName,"params":$paramsJson}""") return suspendCoroutine { continuation -> - messageContinuations[messageId] = CDPMethodCallEncodingInfoPlainText to continuation + messageContinuations[messageId] = MessageContinuation(CDPMethodCallEncodingInfoPlainText, continuation, stacktrace) }.cast().string } @@ -213,10 +231,15 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : encodeMethodCallWithMessageId: (Int) -> Pair ): CDPMethodInvocationResult { val messageId = nextMessageId++ + + // Save the stack trace to show it later. If we won't be able to receive a response for this message due to an exception, + // this stack trace will be shown instead of some obscure coroutine-related one, which will make debugging easier. + val stacktrace = Thread.currentThread().stackTrace + val (encodedMessage, encodingInfo) = encodeMethodCallWithMessageId(messageId) sendPlainTextMessage(encodedMessage) return suspendCoroutine { continuation -> - messageContinuations[messageId] = encodingInfo to continuation + messageContinuations[messageId] = MessageContinuation(encodingInfo, continuation, stacktrace) } } diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js new file mode 100644 index 00000000000..776f2e694f3 --- /dev/null +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +const vm = require('vm'); +const fs = require('fs'); + +var testFilePath = process.argv[2]; + +const sandbox = {}; +vm.createContext(sandbox); +const code = fs.readFileSync(testFilePath, 'utf-8'); + +try { + vm.runInContext(code, sandbox, testFilePath); + vm.runInContext('main.box()', sandbox); +} catch (e) { + // Ignore any exceptions +} diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt index e32e6bae596..258113ab77e 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt @@ -4,6 +4,7 @@ */ package org.jetbrains.kotlin.js.test.handlers +import kotlinx.coroutines.withTimeout import org.jetbrains.kotlin.KtPsiSourceFileLinesMapping import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode import org.jetbrains.kotlin.js.parser.sourcemaps.* @@ -104,6 +105,8 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t debugger.stepInto() waitForResumeEvent() } + debugger.resume() + waitForResumeEvent() } } checkSteppingTestResult( @@ -216,7 +219,8 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t */ private class NodeJsDebuggerFacade(jsFilePath: String) { - private val inspector = NodeJsInspectorClient("js/js.translator/testData/runIrTestInNode.js", listOf(jsFilePath)) + private val inspector = + NodeJsInspectorClient("js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js", listOf(jsFilePath)) private val scriptUrls = mutableMapOf() @@ -228,12 +232,15 @@ private class NodeJsDebuggerFacade(jsFilePath: String) { is Debugger.Event.ScriptParsed -> { scriptUrls[event.scriptId] = event.url } + is Debugger.Event.Paused -> { pausedEvent = event } + is Debugger.Event.Resumed -> { pausedEvent = null } + else -> {} } } @@ -248,7 +255,9 @@ private class NodeJsDebuggerFacade(jsFilePath: String) { runtime.runIfWaitingForDebugger() waitForPauseEvent { it.reason == Debugger.PauseReason.BREAK_ON_START } - body() + withTimeout(30000) { + body() + } } fun scriptUrlByScriptId(scriptId: Runtime.ScriptId) = scriptUrls[scriptId] ?: error("unknown scriptId")