@@ -7,89 +7,66 @@ package kotlinx.coroutines.internal
7
7
import kotlinx.coroutines.*
8
8
import kotlin.coroutines.*
9
9
10
- internal expect val ANDROID_DETECTED : Boolean
11
-
12
- internal expect fun propagateExceptionToPlatform (context : CoroutineContext , exception : Throwable )
13
-
14
10
/* *
15
- * If [addOnExceptionCallback] is called, the provided callback will be evaluated each time
16
- * [handleCoroutineException] is executed and can't find a [CoroutineExceptionHandler] to
17
- * process the exception.
18
- *
19
- * When a callback is registered once, even if it's later removed, the system starts to assume that
20
- * other callbacks will eventually be registered, and so collects the exceptions.
21
- * Once a new callback is registered, the collected exceptions are used with it.
22
- *
23
- * The callbacks in this object are the last resort before relying on platform-dependent
24
- * ways to report uncaught exceptions from coroutines.
11
+ * The list of globally installed [CoroutineExceptionHandler] instances that will be notified of any exceptions that
12
+ * were not processed in any other manner.
25
13
*/
26
- @PublishedApi
27
- internal object ExceptionCollector {
28
- private val lock = SynchronizedObject ()
29
- private var enabled = false
30
- private var unprocessedExceptions = mutableListOf<Throwable >()
31
- private val callbacks = mutableMapOf<Any , (Throwable ) - > Unit > ()
14
+ internal expect val platformExceptionHandlers: Collection <CoroutineExceptionHandler >
32
15
33
- /* *
34
- * Registers [callback] to be executed when an uncaught exception happens.
35
- * [owner] is a key by which to distinguish different callbacks.
36
- */
37
- fun addOnExceptionCallback (owner : Any , callback : (Throwable ) -> Unit ) = synchronized(lock) {
38
- enabled = true // never becomes `false` again
39
- val previousValue = callbacks.put(owner, callback)
40
- assert { previousValue == = null }
41
- // try to process the exceptions using the newly-registered callback
42
- unprocessedExceptions.forEach { reportException(it) }
43
- unprocessedExceptions = mutableListOf ()
44
- }
45
-
46
- /* *
47
- * Unregisters the callback associated with [owner].
48
- */
49
- fun removeOnExceptionCallback (owner : Any ) = synchronized(lock) {
50
- val existingValue = callbacks.remove(owner)
51
- assert { existingValue != = null }
52
- }
16
+ /* *
17
+ * Ensures that the given [callback] is present in the [platformExceptionHandlers] list.
18
+ */
19
+ internal expect fun ensurePlatformExceptionHandlerLoaded (callback : CoroutineExceptionHandler )
53
20
54
- /* *
55
- * Tries to handle the exception by propagating it to an interested consumer.
56
- * Returns `true` if the exception does not need further processing.
57
- *
58
- * Doesn't throw.
59
- */
60
- fun handleException (exception : Throwable ): Boolean = synchronized(lock) {
61
- if (! enabled) return false
62
- if (reportException(exception)) return true
63
- /* * we don't return the result of the `add` function because we don't have a guarantee
64
- * that a callback will eventually appear and collect the unprocessed exceptions, so
65
- * we can't consider [exception] to be properly handled. */
66
- unprocessedExceptions.add(exception)
67
- return false
68
- }
21
+ /* *
22
+ * The platform-dependent global exception handler, used so that the exception is logged at least *somewhere*.
23
+ */
24
+ internal expect fun propagateExceptionFinalResort (exception : Throwable )
69
25
70
- /* *
71
- * Try to report [exception] to the existing callbacks.
72
- */
73
- private fun reportException (exception : Throwable ): Boolean {
74
- var executedACallback = false
75
- for (callback in callbacks.values) {
76
- callback(exception)
77
- executedACallback = true
78
- /* * We don't leave the function here because we want to fan-out the exceptions to every interested consumer,
79
- * it's not enough to have the exception processed by one of them.
80
- * The reason is, it's less big of a deal to observe multiple concurrent reports of bad behavior than not
81
- * to observe the report in the exact callback that is connected to that bad behavior. */
26
+ /* *
27
+ * Deal with exceptions that happened in coroutines and weren't programmatically dealt with.
28
+ *
29
+ * First, it notifies every [CoroutineExceptionHandler] in the [platformExceptionHandlers] list.
30
+ * If one of them throws [ExceptionSuccessfullyProcessed], it means that that handler believes that the exception was
31
+ * dealt with sufficiently well and doesn't need any further processing.
32
+ * Otherwise, the platform-dependent global exception handler is also invoked.
33
+ */
34
+ internal fun handleUncaughtCoroutineException (context : CoroutineContext , exception : Throwable ) {
35
+ // use additional extension handlers
36
+ for (handler in platformExceptionHandlers) {
37
+ try {
38
+ handler.handleException(context, exception)
39
+ } catch (_: ExceptionSuccessfullyProcessed ) {
40
+ return
41
+ } catch (t: Throwable ) {
42
+ propagateExceptionFinalResort(handlerException(exception, t))
82
43
}
83
- return executedACallback
84
44
}
85
- }
86
45
87
- internal fun handleUncaughtCoroutineException (context : CoroutineContext , exception : Throwable ) {
88
- /* * this check is purely for the whole [ExceptionCollector] to be eliminated when an Android app is minified. */
89
- if (ANDROID_DETECTED ) {
90
- propagateExceptionToPlatform(context, exception)
91
- } else {
92
- if (! ExceptionCollector .handleException(exception))
93
- propagateExceptionToPlatform(context, exception)
46
+ try {
47
+ exception.addSuppressed(DiagnosticCoroutineContextException (context))
48
+ } catch (e: Throwable ) {
49
+ // addSuppressed is never user-defined and cannot normally throw with the only exception being OOM
50
+ // we do ignore that just in case to definitely deliver the exception
94
51
}
52
+ propagateExceptionFinalResort(exception)
95
53
}
54
+
55
+ /* *
56
+ * Private exception that is added to suppressed exceptions of the original exception
57
+ * when it is reported to the last-ditch current thread 'uncaughtExceptionHandler'.
58
+ *
59
+ * The purpose of this exception is to add an otherwise inaccessible diagnostic information and to
60
+ * be able to poke the context of the failing coroutine in the debugger.
61
+ */
62
+ internal expect class DiagnosticCoroutineContextException (context : CoroutineContext ) : RuntimeException
63
+
64
+ /* *
65
+ * A dummy exception that signifies that the exception was successfully processed by the handler and no further
66
+ * action is required.
67
+ *
68
+ * Would be nicer if [CoroutineExceptionHandler] could return a boolean, but that would be a breaking change.
69
+ * For now, we will take solace in knowledge that such exceptions are exceedingly rare, even rarer than globally
70
+ * uncaught exceptions in general.
71
+ */
72
+ internal object ExceptionSuccessfullyProcessed: Exception()
0 commit comments