4
4
5
5
package kotlinx.coroutines
6
6
7
+ import kotlinx.coroutines.internal.*
7
8
import kotlin.coroutines.*
8
9
9
- internal expect fun handleCoroutineExceptionImpl (context : CoroutineContext , exception : Throwable )
10
+ internal expect fun propagateExceptionToPlatform (context : CoroutineContext , exception : Throwable )
11
+
12
+ /* *
13
+ * If [addOnExceptionCallback] is called, the provided callback will be evaluated each time
14
+ * [handleCoroutineException] is executed and can't find a [CoroutineExceptionHandler] to
15
+ * process the exception.
16
+ *
17
+ * When a callback is registered once, even if it's later removed, the system starts to assume that
18
+ * other callbacks will eventually be registered, and so collects the exceptions.
19
+ * Once a new callback is registered, it tries.
20
+ *
21
+ * The callbacks in this object are the last resort before relying on platform-dependent
22
+ * ways to report uncaught exceptions from coroutines.
23
+ */
24
+ @PublishedApi
25
+ internal object ExceptionCollector {
26
+ private val lock = SynchronizedObject ()
27
+ private var enabled = false
28
+ private var unprocessedExceptions = mutableListOf<Throwable >()
29
+ private val callbacks = mutableMapOf<Any , (Throwable ) - > Unit > ()
30
+
31
+ /* *
32
+ * Registers [callback] to be executed when an uncaught exception happens.
33
+ * [owner] is a key by which to distinguish different callbacks.
34
+ */
35
+ fun addOnExceptionCallback (owner : Any , callback : (Throwable ) -> Unit ) = synchronized(lock) {
36
+ enabled = true // never becomes `false` again
37
+ val previousValue = callbacks.put(owner, callback)
38
+ assert { previousValue == = null }
39
+ // try to process the exceptions using the newly-registered callback
40
+ unprocessedExceptions.forEach { reportException(it) }
41
+ unprocessedExceptions = mutableListOf ()
42
+ }
43
+
44
+ /* *
45
+ * Unregisters the callback associated with [owner].
46
+ */
47
+ fun removeOnExceptionCallback (owner : Any ) = synchronized(lock) {
48
+ val existingValue = callbacks.remove(owner)
49
+ assert { existingValue != = null }
50
+ }
51
+
52
+ /* *
53
+ * Tries to handle the exception by propagating it to an interested consumer.
54
+ * Returns `true` if the exception does not need further processing.
55
+ *
56
+ * Doesn't throw.
57
+ */
58
+ fun handleException (exception : Throwable ): Boolean = synchronized(lock) {
59
+ if (! enabled) return false
60
+ if (reportException(exception)) return true
61
+ /* * we don't return the result of the `add` function because we don't have a guarantee
62
+ * that a callback will eventually appear and collect the unprocessed exceptions, so
63
+ * we can't consider [exception] to be properly handled. */
64
+ unprocessedExceptions.add(exception)
65
+ return false
66
+ }
67
+
68
+ /* *
69
+ * Try to report [exception] to the existing callbacks.
70
+ */
71
+ private fun reportException (exception : Throwable ): Boolean {
72
+ var executedACallback = false
73
+ for (callback in callbacks.values) {
74
+ callback(exception)
75
+ executedACallback = true
76
+ /* * We don't leave the function here because we want to fan-out the exceptions to every interested consumer,
77
+ * it's not enough to have the exception processed by one of them.
78
+ * The reason is, it's less big of a deal to observe multiple concurrent reports of bad behavior than not
79
+ * to observe the report in the exact callback that is connected to that bad behavior. */
80
+ }
81
+ return executedACallback
82
+ }
83
+ }
84
+
85
+ internal fun handleUncaughtCoroutineException (context : CoroutineContext , exception : Throwable ) {
86
+ // TODO: if ANDROID_DETECTED
87
+ if (! ExceptionCollector .handleException(exception))
88
+ propagateExceptionToPlatform(context, exception)
89
+ }
10
90
11
91
/* *
12
92
* Helper function for coroutine builder implementations to handle uncaught and unexpected exceptions in coroutines,
@@ -26,11 +106,11 @@ public fun handleCoroutineException(context: CoroutineContext, exception: Throwa
26
106
return
27
107
}
28
108
} catch (t: Throwable ) {
29
- handleCoroutineExceptionImpl (context, handlerException(exception, t))
109
+ handleUncaughtCoroutineException (context, handlerException(exception, t))
30
110
return
31
111
}
32
112
// If a handler is not present in the context or an exception was thrown, fallback to the global handler
33
- handleCoroutineExceptionImpl (context, exception)
113
+ handleUncaughtCoroutineException (context, exception)
34
114
}
35
115
36
116
internal fun handlerException (originalException : Throwable , thrownException : Throwable ): Throwable {
0 commit comments