|
|
@@ -11,104 +11,221 @@
|
|
|
package net.mamoe.mirai.console.internal.plugin
|
|
|
|
|
|
import net.mamoe.mirai.console.plugin.jvm.ExportManager
|
|
|
+import net.mamoe.mirai.utils.MiraiLogger
|
|
|
+import net.mamoe.mirai.utils.debug
|
|
|
+import net.mamoe.mirai.utils.verbose
|
|
|
+import org.eclipse.aether.artifact.Artifact
|
|
|
+import org.eclipse.aether.graph.DependencyFilter
|
|
|
import java.io.File
|
|
|
import java.net.URL
|
|
|
import java.net.URLClassLoader
|
|
|
import java.util.*
|
|
|
-import java.util.concurrent.ConcurrentHashMap
|
|
|
+import java.util.zip.ZipFile
|
|
|
|
|
|
-internal class JvmPluginClassLoader(
|
|
|
- val file: File,
|
|
|
+/*
|
|
|
+Class resolving:
|
|
|
+
|
|
|
+|
|
|
|
+`- Resolve standard classes: by super class loader.
|
|
|
+`- Resolve classes in shared libraries (Shared in all plugins)
|
|
|
+|
|
|
|
+|-===== SANDBOX =====
|
|
|
+|
|
|
|
+`- Resolve classes in plugin dependency shared libraries (Shared by depend-ed plugins)
|
|
|
+`- Resolve classes in independent libraries (Can only be loaded by current plugin)
|
|
|
+`- Resolve classes in current jar.
|
|
|
+`- Resolve classes from other plugin jar
|
|
|
+
|
|
|
+ */
|
|
|
+
|
|
|
+internal class JvmPluginsLoadingCtx(
|
|
|
+ val sharedLibrariesLoader: DynLibClassLoader,
|
|
|
+ val pluginClassLoaders: MutableList<JvmPluginClassLoaderN>,
|
|
|
+ val downloader: JvmPluginDependencyDownloader,
|
|
|
+) {
|
|
|
+ val sharedLibrariesDependencies = HashSet<String>()
|
|
|
+ val sharedLibrariesFilter: DependencyFilter = DependencyFilter { node, _ ->
|
|
|
+ return@DependencyFilter node.artifact.depId() !in sharedLibrariesDependencies
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+internal class DynLibClassLoader(
|
|
|
parent: ClassLoader?,
|
|
|
- val classLoaders: Collection<JvmPluginClassLoader>,
|
|
|
-) : URLClassLoader(arrayOf(file.toURI().toURL()), parent) {
|
|
|
- //// 只允许插件 getResource 时获取插件自身资源, #205
|
|
|
- override fun getResources(name: String?): Enumeration<URL> = findResources(name)
|
|
|
- override fun getResource(name: String?): URL? = findResource(name)
|
|
|
- // getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源
|
|
|
- // 因此无需 override getResourceAsStream
|
|
|
+) : URLClassLoader(arrayOf(), parent) {
|
|
|
+ companion object {
|
|
|
+ init {
|
|
|
+ ClassLoader.registerAsParallelCapable()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ internal fun addLib(url: URL) {
|
|
|
+ addURL(url)
|
|
|
+ }
|
|
|
+
|
|
|
+ internal fun addLib(file: File) {
|
|
|
+ addURL(file.toURI().toURL())
|
|
|
+ }
|
|
|
|
|
|
override fun toString(): String {
|
|
|
- return "JvmPluginClassLoader{source=$file}"
|
|
|
+ return "DynLibClassLoader@" + hashCode()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@Suppress("JoinDeclarationAndAssignment")
|
|
|
+internal class JvmPluginClassLoaderN : URLClassLoader {
|
|
|
+ val file: File
|
|
|
+ val ctx: JvmPluginsLoadingCtx
|
|
|
+
|
|
|
+ val dependencies: MutableCollection<JvmPluginClassLoaderN> = hashSetOf()
|
|
|
+
|
|
|
+ lateinit var pluginSharedCL: DynLibClassLoader
|
|
|
+ lateinit var pluginIndependentCL: DynLibClassLoader
|
|
|
+
|
|
|
+
|
|
|
+ private constructor(file: File, ctx: JvmPluginsLoadingCtx, unused: Unit) : super(
|
|
|
+ arrayOf(), ctx.sharedLibrariesLoader
|
|
|
+ ) {
|
|
|
+ this.file = file
|
|
|
+ this.ctx = ctx
|
|
|
+ init0()
|
|
|
+ }
|
|
|
+
|
|
|
+ private constructor(file: File, ctx: JvmPluginsLoadingCtx) : super(
|
|
|
+ file.name,
|
|
|
+ arrayOf(), ctx.sharedLibrariesLoader
|
|
|
+ ) {
|
|
|
+ this.file = file
|
|
|
+ this.ctx = ctx
|
|
|
+ init0()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun init0() {
|
|
|
+ ZipFile(file).use { zipFile ->
|
|
|
+ zipFile.entries().asSequence()
|
|
|
+ .filter { it.name.endsWith(".class") }
|
|
|
+ .map { it.name.substringBeforeLast('.') }
|
|
|
+ .map { it.removePrefix("/").replace('/', '.') }
|
|
|
+ .map { it.substringBeforeLast('.') }
|
|
|
+ .forEach { pkg ->
|
|
|
+ pluginMainPackages.add(pkg)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ pluginSharedCL = DynLibClassLoader(ctx.sharedLibrariesLoader)
|
|
|
+ pluginIndependentCL = DynLibClassLoader(pluginSharedCL)
|
|
|
+ addURL(file.toURI().toURL())
|
|
|
}
|
|
|
|
|
|
- private val cache = ConcurrentHashMap<String, Class<*>>()
|
|
|
+ private val pluginMainPackages: MutableSet<String> = HashSet()
|
|
|
internal var declaredFilter: ExportManager? = null
|
|
|
|
|
|
+ val sharedClLoadedDependencies = mutableSetOf<String>()
|
|
|
+ internal fun containsSharedDependency(
|
|
|
+ dependency: String
|
|
|
+ ): Boolean {
|
|
|
+ if (dependency in sharedClLoadedDependencies) return true
|
|
|
+ return dependencies.any { it.containsSharedDependency(dependency) }
|
|
|
+ }
|
|
|
+
|
|
|
+ internal fun linkPluginSharedLibraries(logger: MiraiLogger, dependencies: Collection<String>) {
|
|
|
+ linkLibraries(logger, dependencies, true)
|
|
|
+ }
|
|
|
+
|
|
|
+ internal fun linkPluginPrivateLibraries(logger: MiraiLogger, dependencies: Collection<String>) {
|
|
|
+ linkLibraries(logger, dependencies, false)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun linkLibraries(logger: MiraiLogger, dependencies: Collection<String>, shared: Boolean) {
|
|
|
+ if (dependencies.isEmpty()) return
|
|
|
+ val results = ctx.downloader.resolveDependencies(
|
|
|
+ dependencies, ctx.sharedLibrariesFilter,
|
|
|
+ DependencyFilter { node, _ ->
|
|
|
+ return@DependencyFilter !containsSharedDependency(node.artifact.depId())
|
|
|
+ })
|
|
|
+ val files = results.artifactResults.mapNotNull { result ->
|
|
|
+ result.artifact?.let { it to it.file }
|
|
|
+ }
|
|
|
+ val linkType = if (shared) "(shared)" else "(private)"
|
|
|
+ files.forEach { (artifact, lib) ->
|
|
|
+ logger.verbose { "Linking $lib $linkType" }
|
|
|
+ if (shared) {
|
|
|
+ pluginSharedCL.addLib(lib)
|
|
|
+ sharedClLoadedDependencies.add(artifact.depId())
|
|
|
+ } else {
|
|
|
+ pluginIndependentCL.addLib(lib)
|
|
|
+ }
|
|
|
+ logger.debug { "Linked $artifact $linkType" }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
companion object {
|
|
|
- val loadingLock = ConcurrentHashMap<String, Any>()
|
|
|
+ private val java9: Boolean
|
|
|
|
|
|
init {
|
|
|
ClassLoader.registerAsParallelCapable()
|
|
|
+ java9 = kotlin.runCatching { Class.forName("java.lang.Module") }.isSuccess
|
|
|
+ }
|
|
|
+
|
|
|
+ fun newLoader(file: File, ctx: JvmPluginsLoadingCtx): JvmPluginClassLoaderN {
|
|
|
+ return when {
|
|
|
+ java9 -> JvmPluginClassLoaderN(file, ctx)
|
|
|
+ else -> JvmPluginClassLoaderN(file, ctx, Unit)
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- override fun findClass(name: String): Class<*> {
|
|
|
- synchronized(kotlin.run {
|
|
|
- val lock = Any()
|
|
|
- loadingLock.putIfAbsent(name, lock) ?: lock
|
|
|
- }) {
|
|
|
- return findClass(name, false) ?: throw ClassNotFoundException(name)
|
|
|
+ internal fun resolvePluginSharedLibAndPluginClass(name: String): Class<*>? {
|
|
|
+ return try {
|
|
|
+ pluginSharedCL.loadClass(name)
|
|
|
+ } catch (e: ClassNotFoundException) {
|
|
|
+ resolvePluginPublicClass(name)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- internal fun findClass(name: String, disableGlobal: Boolean): Class<*>? {
|
|
|
- // First. Try direct load in cache.
|
|
|
- val cachedClass = cache[name]
|
|
|
- if (cachedClass != null) {
|
|
|
- if (disableGlobal) {
|
|
|
- val filter = declaredFilter
|
|
|
- if (filter != null && !filter.isExported(name)) {
|
|
|
- throw LoadingDeniedException(name)
|
|
|
- }
|
|
|
- }
|
|
|
- return cachedClass
|
|
|
+ internal fun resolvePluginPublicClass(name: String): Class<*>? {
|
|
|
+ if (pluginMainPackages.contains(name.pkgName())) {
|
|
|
+ if (declaredFilter?.isExported(name) == false) return null
|
|
|
+ return loadClass(name)
|
|
|
}
|
|
|
- if (disableGlobal) {
|
|
|
- // ==== Process Loading Request From JvmPluginClassLoader ====
|
|
|
- //
|
|
|
- // If load from other classloader,
|
|
|
- // means no other loaders are cached.
|
|
|
- // direct load
|
|
|
- return kotlin.runCatching {
|
|
|
- super.findClass(name).also { cache[name] = it }
|
|
|
- }.getOrElse {
|
|
|
- if (it is ClassNotFoundException) null
|
|
|
- else throw it
|
|
|
- }?.also {
|
|
|
- // This request is from other classloader,
|
|
|
- // so we need to check the class is exported or not.
|
|
|
- val filter = declaredFilter
|
|
|
- if (filter != null && !filter.isExported(name)) {
|
|
|
- throw LoadingDeniedException(name)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun findClass(name: String): Class<*> {
|
|
|
+ // Search dependencies first
|
|
|
+ dependencies.forEach { dependency ->
|
|
|
+ dependency.resolvePluginSharedLibAndPluginClass(name)?.let { return it }
|
|
|
+ }
|
|
|
+ // Search in independent class loader
|
|
|
+ // @context: pluginIndependentCL.parent = pluinSharedCL
|
|
|
+ try {
|
|
|
+ return pluginIndependentCL.loadClass(name)
|
|
|
+ } catch (ignored: ClassNotFoundException) {
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ return super.findClass(name)
|
|
|
+ } catch (error: ClassNotFoundException) {
|
|
|
+ // Finally, try search from other plugins
|
|
|
+ ctx.pluginClassLoaders.forEach { other ->
|
|
|
+ if (other !== this) {
|
|
|
+ other.resolvePluginPublicClass(name)?.let { return it }
|
|
|
}
|
|
|
}
|
|
|
+ throw error
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // ==== Process Loading Request From JDK ClassLoading System ====
|
|
|
+ internal fun loadedClass(name: String): Class<*>? = super.findLoadedClass(name)
|
|
|
|
|
|
- // First. scan other classLoaders's caches
|
|
|
- classLoaders.forEach { otherClassloader ->
|
|
|
- if (otherClassloader === this) return@forEach
|
|
|
- val filter = otherClassloader.declaredFilter
|
|
|
- if (otherClassloader.cache.containsKey(name)) {
|
|
|
- return if (filter == null || filter.isExported(name)) {
|
|
|
- otherClassloader.cache[name]
|
|
|
- } else throw LoadingDeniedException("$name was not exported by $otherClassloader")
|
|
|
- }
|
|
|
- }
|
|
|
- classLoaders.forEach { otherClassloader ->
|
|
|
- val other = kotlin.runCatching {
|
|
|
- if (otherClassloader === this) super.findClass(name).also { cache[name] = it }
|
|
|
- else otherClassloader.findClass(name, true)
|
|
|
- }.onFailure { err ->
|
|
|
- if (err is LoadingDeniedException || err !is ClassNotFoundException)
|
|
|
- throw err
|
|
|
- }.getOrNull()
|
|
|
- if (other != null) return other
|
|
|
- }
|
|
|
- throw ClassNotFoundException(name)
|
|
|
+ //// 只允许插件 getResource 时获取插件自身资源, https://github.com/mamoe/mirai-console/issues/205
|
|
|
+ override fun getResources(name: String?): Enumeration<URL> = findResources(name)
|
|
|
+ override fun getResource(name: String?): URL? = findResource(name)
|
|
|
+ // getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源
|
|
|
+ // 因此无需 override getResourceAsStream
|
|
|
+
|
|
|
+ override fun toString(): String {
|
|
|
+ return "JvmPluginClassLoader{${file.name}}"
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-internal class LoadingDeniedException(name: String) : ClassNotFoundException(name)
|
|
|
+private fun String.pkgName(): String = substringBeforeLast('.', "")
|
|
|
+internal fun Artifact.depId(): String = "$groupId:$artifactId"
|