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.test.internal 6 7 import kotlinx.coroutines.* 8 import kotlinx.coroutines.internal.* 9 import kotlin.coroutines.* 10 11 /** 12 * If [addOnExceptionCallback] is called, the provided callback will be evaluated each time 13 * [handleCoroutineException] is executed and can't find a [CoroutineExceptionHandler] to 14 * process the exception. 15 * 16 * When a callback is registered once, even if it's later removed, the system starts to assume that 17 * other callbacks will eventually be registered, and so collects the exceptions. 18 * Once a new callback is registered, the collected exceptions are used with it. 19 * 20 * The callbacks in this object are the last resort before relying on platform-dependent 21 * ways to report uncaught exceptions from coroutines. 22 */ 23 internal object ExceptionCollector : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { 24 private val lock = SynchronizedObject() 25 private var enabled = false 26 private val unprocessedExceptions = mutableListOf<Throwable>() 27 private val callbacks = mutableMapOf<Any, (Throwable) -> Unit>() 28 29 /** 30 * Registers [callback] to be executed when an uncaught exception happens. 31 * [owner] is a key by which to distinguish different callbacks. 32 */ <lambda>null33 fun addOnExceptionCallback(owner: Any, callback: (Throwable) -> Unit) = synchronized(lock) { 34 enabled = true // never becomes `false` again 35 val previousValue = callbacks.put(owner, callback) 36 check(previousValue === null) 37 // try to process the exceptions using the newly-registered callback 38 unprocessedExceptions.forEach { reportException(it) } 39 unprocessedExceptions.clear() 40 } 41 42 /** 43 * Unregisters the callback associated with [owner]. 44 */ <lambda>null45 fun removeOnExceptionCallback(owner: Any) = synchronized(lock) { 46 if (enabled) { 47 val existingValue = callbacks.remove(owner) 48 check(existingValue !== null) 49 } 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 */ <lambda>null58 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 */ reportExceptionnull71 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 @Suppress("INVISIBLE_MEMBER") handleExceptionnull85 override fun handleException(context: CoroutineContext, exception: Throwable) { 86 if (handleException(exception)) { 87 throw ExceptionSuccessfullyProcessed 88 } 89 } 90 equalsnull91 override fun equals(other: Any?): Boolean = other is ExceptionCollector || other is ExceptionCollectorAsService 92 } 93 94 /** 95 * A workaround for being unable to treat an object as a `ServiceLoader` service. 96 */ 97 internal class ExceptionCollectorAsService: CoroutineExceptionHandler by ExceptionCollector { 98 override fun equals(other: Any?): Boolean = other is ExceptionCollectorAsService || other is ExceptionCollector 99 override fun hashCode(): Int = ExceptionCollector.hashCode() 100 } 101