Skip to content

Commit b9c3e77

Browse files
committed
Use ANDROID_DETECTED
1 parent 7bfac6d commit b9c3e77

File tree

7 files changed

+108
-90
lines changed

7 files changed

+108
-90
lines changed

kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt

-81
Original file line numberDiff line numberDiff line change
@@ -7,87 +7,6 @@ package kotlinx.coroutines
77
import kotlinx.coroutines.internal.*
88
import kotlin.coroutines.*
99

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-
}
90-
9110
/**
9211
* Helper function for coroutine builder implementations to handle uncaught and unexpected exceptions in coroutines,
9312
* that could not be otherwise handled in a normal way through structured concurrency, saving them to a future, and
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.internal
6+
7+
import kotlinx.coroutines.*
8+
import kotlin.coroutines.*
9+
10+
internal expect val ANDROID_DETECTED: Boolean
11+
12+
internal expect fun propagateExceptionToPlatform(context: CoroutineContext, exception: Throwable)
13+
14+
/**
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, it tries.
22+
*
23+
* The callbacks in this object are the last resort before relying on platform-dependent
24+
* ways to report uncaught exceptions from coroutines.
25+
*/
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>()
32+
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+
}
53+
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+
}
69+
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. */
82+
}
83+
return executedACallback
84+
}
85+
}
86+
87+
internal fun handleUncaughtCoroutineException(context: CoroutineContext, exception: Throwable) {
88+
if (ANDROID_DETECTED) {
89+
propagateExceptionToPlatform(context, exception)
90+
} else {
91+
if (!ExceptionCollector.handleException(exception))
92+
propagateExceptionToPlatform(context, exception)
93+
}
94+
}

kotlinx-coroutines-core/js/src/CoroutineExceptionHandlerImpl.kt renamed to kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/*
2-
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5-
package kotlinx.coroutines
5+
package kotlinx.coroutines.internal
66

77
import kotlin.coroutines.*
88

9+
internal actual val ANDROID_DETECTED = false
10+
911
internal actual fun propagateExceptionToPlatform(context: CoroutineContext, exception: Throwable) {
1012
// log exception
1113
console.error(exception)

kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt renamed to kotlinx-coroutines-core/jvm/src/internal/CoroutineExceptionHandlerImplJvm.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/*
2-
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5-
package kotlinx.coroutines
5+
package kotlinx.coroutines.internal
66

77
import java.util.*
88
import kotlin.coroutines.*
9+
import kotlinx.coroutines.*
910

1011
/**
1112
* A list of globally installed [CoroutineExceptionHandler] instances.

kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlin.collections.ArrayList
1414
/**
1515
* Don't use JvmField here to enable R8 optimizations via "assumenosideeffects"
1616
*/
17-
internal val ANDROID_DETECTED = runCatching { Class.forName("android.os.Build") }.isSuccess
17+
internal actual val ANDROID_DETECTED = runCatching { Class.forName("android.os.Build") }.isSuccess
1818

1919
/**
2020
* A simplified version of [ServiceLoader].

kotlinx-coroutines-core/native/src/CoroutineExceptionHandlerImpl.kt renamed to kotlinx-coroutines-core/native/src/internal/CoroutineExceptionHandlerImpl.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/*
2-
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5-
package kotlinx.coroutines
5+
package kotlinx.coroutines.internal
66

77
import kotlin.coroutines.*
88
import kotlin.native.*
99

10+
internal actual val ANDROID_DETECTED = false
11+
1012
@OptIn(ExperimentalStdlibApi::class)
1113
internal actual fun propagateExceptionToPlatform(context: CoroutineContext, exception: Throwable) {
1214
// log exception

kotlinx-coroutines-test/common/src/TestScope.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ internal class TestScopeImpl(context: CoroutineContext) :
227227
* after the previous one, and learning about such exceptions as soon is possible is nice. */
228228
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
229229
run {
230-
ExceptionCollector.addOnExceptionCallback(lock, this::reportException)
230+
kotlinx.coroutines.internal.ExceptionCollector.addOnExceptionCallback(lock, this::reportException)
231231
}
232232
uncaughtExceptions
233233
}
@@ -246,7 +246,7 @@ internal class TestScopeImpl(context: CoroutineContext) :
246246
/** After [finished] becomes `true`, it is no longer valid to have [reportException] as the callback. */
247247
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
248248
run {
249-
ExceptionCollector.removeOnExceptionCallback(lock)
249+
kotlinx.coroutines.internal.ExceptionCollector.removeOnExceptionCallback(lock)
250250
}
251251
finished = true
252252
uncaughtExceptions

0 commit comments

Comments
 (0)