Introduce private DiagnosticCoroutineContextException and add it to the original exception prior to passing it to the Thread.currentThread().uncaughtExceptionHandler() (#3170)

Fixes #3153
diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
index 4c8c81b..dd39210 100644
--- a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
+++ b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
@@ -27,6 +27,24 @@
     CoroutineExceptionHandler
 }
 
+/**
+ * Private exception without stacktrace that is added to suppressed exceptions of the original exception
+ * when it is reported to the last-ditch current thread 'uncaughtExceptionHandler'.
+ *
+ * The purpose of this exception is to add an otherwise inaccessible diagnostic information and to
+ * be able to poke the failing coroutine context in the debugger.
+ */
+private class DiagnosticCoroutineContextException(private val context: CoroutineContext) : RuntimeException() {
+    override fun getLocalizedMessage(): String {
+        return context.toString()
+    }
+
+    override fun fillInStackTrace(): Throwable {
+        // Prevent Android <= 6.0 bug, #1866
+        stackTrace = emptyArray()
+        return this
+    }
+}
 
 internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
     // use additional extension handlers
@@ -42,5 +60,8 @@
 
     // use thread's handler
     val currentThread = Thread.currentThread()
+    // addSuppressed is never user-defined and cannot normally throw with the only exception being OOM
+    // we do ignore that just in case to definitely deliver the exception
+    runCatching { exception.addSuppressed(DiagnosticCoroutineContextException(context)) }
     currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
 }
diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
index cea9713..2095f14 100644
--- a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
@@ -39,4 +39,16 @@
 
         finish(3)
     }
+
+    @Test
+    fun testLastDitchHandlerContainsContextualInformation() = runBlocking {
+        expect(1)
+        GlobalScope.launch(CoroutineName("last-ditch")) {
+            expect(2)
+            throw TestException()
+        }.join()
+        assertTrue(caughtException is TestException)
+        assertContains(caughtException.suppressed[0].toString(), "last-ditch")
+        finish(3)
+    }
 }