diff --git a/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesConstants.kt b/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesConstants.kt index 8744b15..8a5aabb 100644 --- a/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesConstants.kt +++ b/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesConstants.kt @@ -1,20 +1,43 @@ package com.noxcrew.interfaces +import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher +import org.bukkit.Bukkit +import org.slf4j.LoggerFactory import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger /** Holds the shared scope used for any interfaces coroutines. */ public object InterfacesConstants { + private val EXCEPTION_LOGGER = LoggerFactory.getLogger("InterfacesExceptionHandler") + /** The [CoroutineScope] for any suspending operations performed by interfaces. */ public val SCOPE: CoroutineScope = CoroutineScope( CoroutineName("interfaces") + SupervisorJob() + + CoroutineExceptionHandler { context, exception -> + val details = context[InterfacesCoroutineDetails] + + if (details == null) { + EXCEPTION_LOGGER.error("An unknown error occurred in a coroutine!", exception) + } else { + val (player, reason) = details + EXCEPTION_LOGGER.error( + """ + An unknown error occurred in a coroutine! + - Player: ${player ?: "N/A"} (${player?.let(Bukkit::getPlayer)?.name ?: "offline"}) + - Launch reason: $reason + """.trimIndent(), + exception + ) + } + } + run { val threadNumber = AtomicInteger() val factory = { runnable: Runnable -> diff --git a/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesListeners.kt b/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesListeners.kt index bebe7b1..1f351b0 100644 --- a/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesListeners.kt +++ b/interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesListeners.kt @@ -10,6 +10,7 @@ import com.noxcrew.interfaces.click.ClickHandler import com.noxcrew.interfaces.click.CompletableClickHandler import com.noxcrew.interfaces.grid.GridPoint import com.noxcrew.interfaces.pane.PlayerPane +import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails import com.noxcrew.interfaces.view.AbstractInterfaceView import com.noxcrew.interfaces.view.ChestInterfaceView import com.noxcrew.interfaces.view.InterfaceView @@ -166,7 +167,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin) /** Re-opens the current background interface of [player]. */ public fun reopenInventory(player: Player) { getBackgroundPlayerInterface(player.uniqueId)?.also { - SCOPE.launch { + SCOPE.launch(InterfacesCoroutineDetails(player.uniqueId, "reopening background interface")) { it.open() } } @@ -272,7 +273,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin) // Saves any persistent items stored in the given inventory before we close it view.savePersistentItems(event.inventory) - SCOPE.launch { + SCOPE.launch(InterfacesCoroutineDetails(event.player.uniqueId, "handling inventory close")) { // Determine if we can re-open a previous interface val backgroundInterface = getBackgroundPlayerInterface(event.player.uniqueId) val shouldReopen = reason in REOPEN_REASONS && !event.player.isDead && backgroundInterface != null @@ -542,7 +543,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin) queries.invalidate(player.uniqueId) // Complete the query and re-open the view - SCOPE.launch { + SCOPE.launch(InterfacesCoroutineDetails(event.player.uniqueId, "completing chat query")) { if (query.onComplete(event.message())) { query.view.open() } @@ -726,7 +727,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin) // Remove the query, run the cancel handler, and re-open the view queries.invalidate(playerId) - SCOPE.launch { + SCOPE.launch(InterfacesCoroutineDetails(playerId, "cancelling chat query due to timeout")) { onCancel() view.open() } @@ -743,7 +744,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin) if (view != null && query.view != view) return queries.invalidate(playerId) - SCOPE.launch { + SCOPE.launch(InterfacesCoroutineDetails(playerId, "aborting chat query")) { // Run the cancellation handler query.onCancel() diff --git a/interfaces/src/main/kotlin/com/noxcrew/interfaces/grid/ChainGridPositionGenerator.kt b/interfaces/src/main/kotlin/com/noxcrew/interfaces/grid/ChainGridPositionGenerator.kt index 743b46f..ee25ee8 100644 --- a/interfaces/src/main/kotlin/com/noxcrew/interfaces/grid/ChainGridPositionGenerator.kt +++ b/interfaces/src/main/kotlin/com/noxcrew/interfaces/grid/ChainGridPositionGenerator.kt @@ -5,7 +5,7 @@ public data class ChainGridPositionGenerator( /** The first generator. */ private val first: GridPositionGenerator, /** The second generator. */ - private val second: GridPositionGenerator, + private val second: GridPositionGenerator ) : GridPositionGenerator { public companion object { diff --git a/interfaces/src/main/kotlin/com/noxcrew/interfaces/utilities/InterfacesCoroutineDetails.kt b/interfaces/src/main/kotlin/com/noxcrew/interfaces/utilities/InterfacesCoroutineDetails.kt new file mode 100644 index 0000000..35382ee --- /dev/null +++ b/interfaces/src/main/kotlin/com/noxcrew/interfaces/utilities/InterfacesCoroutineDetails.kt @@ -0,0 +1,14 @@ +package com.noxcrew.interfaces.utilities + +import java.util.UUID +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext + +/** Context element that contains details used for error handling and debugging. */ +internal data class InterfacesCoroutineDetails( + internal val player: UUID?, + internal val reason: String +) : AbstractCoroutineContextElement(InterfacesCoroutineDetails) { + /** Key this element. */ + internal companion object : CoroutineContext.Key +} diff --git a/interfaces/src/main/kotlin/com/noxcrew/interfaces/view/AbstractInterfaceView.kt b/interfaces/src/main/kotlin/com/noxcrew/interfaces/view/AbstractInterfaceView.kt index 8a2aa92..2d8ed06 100644 --- a/interfaces/src/main/kotlin/com/noxcrew/interfaces/view/AbstractInterfaceView.kt +++ b/interfaces/src/main/kotlin/com/noxcrew/interfaces/view/AbstractInterfaceView.kt @@ -14,6 +14,7 @@ import com.noxcrew.interfaces.pane.complete import com.noxcrew.interfaces.properties.Trigger import com.noxcrew.interfaces.transform.AppliedTransform import com.noxcrew.interfaces.utilities.CollapsablePaneMap +import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails import com.noxcrew.interfaces.utilities.forEachInGrid import kotlinx.coroutines.Job import kotlinx.coroutines.async @@ -240,7 +241,7 @@ public abstract class AbstractInterfaceView