diff --git a/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt b/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt index 5122aa0df..0ec538bb4 100644 --- a/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt +++ b/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt @@ -35,6 +35,7 @@ import net.mamoe.mirai.console.internal.data.builtins.DataScope import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.internal.permission.BuiltInPermissionService import net.mamoe.mirai.console.internal.permission.getPermittedPermissionsAndSource +import net.mamoe.mirai.console.internal.plugin.JvmPluginInternal import net.mamoe.mirai.console.internal.pluginManagerImpl import net.mamoe.mirai.console.internal.util.runIgnoreException import net.mamoe.mirai.console.permission.Permission @@ -617,7 +618,13 @@ public object BuiltInCommands { if (plugin.isEnabled) { green().append(plugin.name).reset().append(" v").gold() } else { - red().append(plugin.name).append("(disabled)").reset().append(" v").gold() + red().append(plugin.name) + if (plugin is JvmPluginInternal) { + append("(").append(plugin.currentPluginStatus.name.lowercase()).append(")") + } else { + append("(disabled)") + } + reset().append(" v").gold() } plugin.version.toString() } diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt index 22bebf61e..7d6c506f0 100644 --- a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt +++ b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.console.internal.plugin import kotlinx.atomicfu.AtomicLong +import kotlinx.atomicfu.atomic import kotlinx.atomicfu.locks.withLock import kotlinx.coroutines.* import net.mamoe.mirai.console.MiraiConsole @@ -25,6 +26,7 @@ import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader import net.mamoe.mirai.console.plugin.ResourceContainer.Companion.asResourceContainer +import net.mamoe.mirai.console.plugin.id import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin.Companion.onLoad @@ -47,6 +49,73 @@ internal val T.job: Job where T : CoroutineScope, T : Plugin get() = this.co internal abstract class JvmPluginInternal( parentCoroutineContext: CoroutineContext, ) : JvmPlugin, CoroutineScope { + internal enum class PluginStatus { + ALLOCATED, + + CRASHED_LOAD_ERROR(Flags.ALLOW_SWITCH_TO_DISABLE), + CRASHED_ENABLE_ERROR(Flags.ALLOW_SWITCH_TO_DISABLE), + CRASHED_DISABLE_ERROR, + + LOAD_PENDING, + LOAD_LOADING, + LOAD_LOAD_DONE, + + ENABLE_PENDING, + ENABLE_ENABLING, + ENABLED(Flags.ALLOW_SWITCH_TO_DISABLE), + + DISABLE_PENDING, + DISABLE_DISABLING, + DISABLED, + + ; + + private val flags: Int + + constructor() : this(0) + constructor(flags: Int) { + this.flags = flags + } + + internal object Flags { // compiler bug: [UNINITIALIZED_VARIABLE] Variable 'FLAG_ALLOW_SWITCH_TO_DISABLE' must be initialized + internal const val ALLOW_SWITCH_TO_DISABLE = 1 shl 0 + } + + fun hasFlag(flag: Int): Boolean = flags.and(flag) != 0 + } + + private val pluginStatus = atomic(PluginStatus.ALLOCATED) + + @get:JvmSynthetic + internal val currentPluginStatus: PluginStatus get() = pluginStatus.value + + final override val isEnabled: Boolean + get() = pluginStatus.value === PluginStatus.ENABLED + + @JvmSynthetic + internal fun switchStatusOrFail(expectFlag: Int, update: PluginStatus) { + val nowStatus = pluginStatus.value + if (nowStatus.hasFlag(expectFlag)) { + if (pluginStatus.compareAndSet(expect = nowStatus, update = update)) { + return + } + error("Failed to switch plugin '$id' status from $nowStatus to $update, current status = ${pluginStatus.value}") + } + error("Failed to switch plugin '$id' status to $update because current status $nowStatus doesn't contain flag ${Integer.toBinaryString(expectFlag)}") + } + + @JvmSynthetic + internal fun switchStatusOrFail(expect: PluginStatus, update: PluginStatus) { + val nowStatus = pluginStatus.value + if (nowStatus === expect) { + if (pluginStatus.compareAndSet(expect = expect, update = update)) { + return + } + error("Failed to switch plugin '$id' status from $expect to $update, current status=${pluginStatus.value}") + } + error("Failed to switch plugin '$id' status from $expect to $update, current status = $nowStatus") + } + final override val parentPermission: Permission by lazy { PermissionService.INSTANCE.register( @@ -55,8 +124,6 @@ internal abstract class JvmPluginInternal( ) } - final override var isEnabled: Boolean = false - internal set private val resourceContainerDelegate by lazy { this::class.java.classLoader.asResourceContainer() } final override fun getResourceAsStream(path: String): InputStream? = @@ -88,20 +155,31 @@ internal abstract class JvmPluginInternal( } internal fun internalOnDisable() { + + switchStatusOrFail( + expectFlag = PluginStatus.Flags.ALLOW_SWITCH_TO_DISABLE, + update = PluginStatus.DISABLE_PENDING, + ) + firstRun = false kotlin.runCatching { val crtThread = Thread.currentThread() ShutdownDaemon.pluginDisablingThreads.add(crtThread) try { + pluginStatus.value = PluginStatus.DISABLE_DISABLING onDisable() } finally { ShutdownDaemon.pluginDisablingThreads.remove(crtThread) } }.fold( onSuccess = { + pluginStatus.value = PluginStatus.DISABLED + cancel(CancellationException("plugin disabled")) }, onFailure = { err -> + pluginStatus.value = PluginStatus.CRASHED_DISABLE_ERROR + cancel(CancellationException("Exception while disabling plugin", err)) // @TestOnly @@ -112,17 +190,32 @@ internal abstract class JvmPluginInternal( } } ) - isEnabled = false } @Throws(Throwable::class) internal fun internalOnLoad() { - val componentStorage = PluginComponentStorage(this) - onLoad(componentStorage) - GlobalComponentStorage.mergeWith(componentStorage) + switchStatusOrFail(PluginStatus.ALLOCATED, PluginStatus.LOAD_PENDING) + + try { + pluginStatus.value = PluginStatus.LOAD_LOADING + + val componentStorage = PluginComponentStorage(this) + onLoad(componentStorage) + + pluginStatus.value = PluginStatus.LOAD_LOAD_DONE + GlobalComponentStorage.mergeWith(componentStorage) + + } catch (e: Throwable) { + pluginStatus.value = PluginStatus.CRASHED_LOAD_ERROR + + cancel(CancellationException("Exception while loading plugin", e)) + } } + internal fun internalOnEnable(): Boolean { + switchStatusOrFail(PluginStatus.LOAD_LOAD_DONE, PluginStatus.ENABLE_PENDING) + parentPermission if (!firstRun) refreshCoroutineContext() @@ -133,6 +226,7 @@ internal abstract class JvmPluginInternal( } kotlin.runCatching { + pluginStatus.value = PluginStatus.ENABLE_ENABLING onEnable() }.fold( onSuccess = { @@ -142,10 +236,12 @@ internal abstract class JvmPluginInternal( logger.error(msg) throw AssertionError(msg) } - isEnabled = true + pluginStatus.value = PluginStatus.ENABLED return true }, onFailure = { err -> + pluginStatus.value = PluginStatus.CRASHED_ENABLE_ERROR + cancel(CancellationException("Exception while enabling plugin", err)) logger.error(err) @@ -189,6 +285,7 @@ internal abstract class JvmPluginInternal( // for future use @Suppress("PropertyName") + @get:JvmSynthetic internal val _intrinsicCoroutineContext: CoroutineContext by lazy { this as AbstractJvmPlugin CoroutineName("Plugin $dataHolderName") @@ -211,6 +308,7 @@ internal abstract class JvmPluginInternal( } @JvmField + @JvmSynthetic internal val coroutineContextInitializer = { CoroutineExceptionHandler { context, throwable -> if (throwable.rootCauseOrSelf !is CancellationException) logger.error( @@ -228,7 +326,9 @@ internal abstract class JvmPluginInternal( job.invokeOnCompletion { e -> if (e != null) { if (e !is CancellationException) logger.error(e) - if (this.isEnabled) safeLoader.disable(this) + if (pluginStatus.value == PluginStatus.ENABLED) { + safeLoader.disable(this) + } } } }