diff --git a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java index 5207b1af4..cc1242780 100644 --- a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java +++ b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java @@ -1,7 +1,10 @@ package io.izzel.incision.bridge; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; /** * Incision 字节码桥 — 所有被 incision 织入的 INVOKESTATIC 调用都指向这里。 @@ -17,8 +20,8 @@ * * 解析顺序: *
    - *
  1. 按被织入类的 defining ClassLoader 精确查找本地 TheatreDispatcher;
  2. - *
  3. 目标是服务端类且当前只有一个本地 lease 时,允许唯一回退;
  4. + *
  5. 按目标签名查找声明该切术的 TheatreDispatcher;
  6. + *
  7. 旧调用方未登记目标时,才按 defining ClassLoader 或唯一 lease 回退;
  8. *
  9. 本地没有可用路由时再交给系统 ClassLoader 上的 Gate。
  10. *
* @@ -39,6 +42,19 @@ private IncisionBridge() {} /** ClassLoader → 本地 TheatreDispatcher.dispatch Method 缓存(单插件 fallback 路径) */ private static final ConcurrentHashMap localCache = new ConcurrentHashMap(); + /** + * 运行时目标签名 → 声明该目标的 dispatcher。 + * + * 被织入类可能属于 Leaf 的服务端 URLClassLoader,也可能属于 AuraSkills 等第三方插件; + * 它的 defining loader 与切术声明方没有必然关系,因此 loader 绝不能作为正常路由依据。 + */ + private static final ConcurrentHashMap> targetRoutes = + new ConcurrentHashMap>(); + + /** 多插件声明同一目标的诊断去重;真正的跨插件优先级聚合必须由 Gate 完成。 */ + private static final ConcurrentHashMap routeConflictWarnings = + new ConcurrentHashMap(); + /** * 供 weaver 注入的 INVOKESTATIC 目标。 * @@ -54,16 +70,26 @@ private IncisionBridge() {} */ public static Object dispatch(Class ownerClass, String targetSignature, Object self, Object[] args) { ClassLoader definingLoader = ownerClass == null ? null : ownerClass.getClassLoader(); - Method local = resolveLocalDispatch(definingLoader); - if (local != null) { - try { - if (local.getParameterCount() == 4) { - return local.invoke(null, targetSignature, self, args, null); + List locals = resolveLocalDispatches(definingLoader, targetSignature); + if (!locals.isEmpty()) { + Object result = null; + boolean invoked = false; + for (Method local : locals) { + try { + Object localResult; + if (local.getParameterCount() == 4) { + localResult = local.invoke(null, targetSignature, self, args, null); + } else { + localResult = local.invoke(null, targetSignature, self, args); + } + // 未持有该 target 的 dispatcher 以 null 表示未命中,不能覆盖前一个插件的有效结果。 + if (localResult != null) result = localResult; + invoked = true; + } catch (Throwable t) { + System.err.println("[Incision][Bridge] local dispatch failed: " + t); } - return local.invoke(null, targetSignature, self, args); - } catch (Throwable t) { - System.err.println("[Incision][Bridge] local dispatch failed: " + t); } + if (invoked) return result; } Method m = systemDispatch; Object host = systemHost; @@ -77,15 +103,21 @@ public static Object dispatch(Class ownerClass, String targetSignature, Objec // 精确 loader 路由失败属于生命周期错误,不能静默伪装成 advice 未命中。 System.err.println("[Incision][Bridge] dispatch unavailable: owner=" + (ownerClass == null ? "null" : ownerClass.getName()) + " loader=" + definingLoader + - " localLeases=" + localCache.size() + " target=" + targetSignature); + " localLeases=" + localCache.size() + " targetRoutes=" + targetRoutes.size() + + " target=" + targetSignature); return null; } public static Object dispatchBypass(Class ownerClass, String targetSignature, Object self, Object[] args) { - Method local = resolveLocalSibling(ownerClass == null ? null : ownerClass.getClassLoader(), "dispatchBypass"); - if (local != null) { + List dispatches = resolveLocalDispatches( + ownerClass == null ? null : ownerClass.getClassLoader(), targetSignature + ); + for (Method dispatch : dispatches) { + Method local = resolveLocalSibling(dispatch, "dispatchBypass"); + if (local == null) continue; try { - return local.invoke(null, targetSignature, self, args); + Object result = local.invoke(null, targetSignature, self, args); + if (!isBypassMiss(result)) return result; } catch (Throwable t) { System.err.println("[Incision][Bridge] local bypass dispatch failed: " + t); } @@ -116,6 +148,11 @@ public static boolean hasSystemHost() { return systemHost != null && systemDispatch != null; } + /** JVM 级 lease 数量;Gate holder 只能在该值归零后释放共享 delegate。 */ + public static int localLeaseCount() { + return localCache.size(); + } + // ----------------------------------------------------------------- private static Object findSystemHost() { @@ -141,14 +178,63 @@ private static Method resolveDispatch(Object host) { } private static Method resolveLocalDispatch(ClassLoader definingLoader) { - Method cached = localCache.get(definingLoader); + Method cached = definingLoader == null ? null : localCache.get(definingLoader); if (cached != null) return cached; - // 服务端类通常由 bootstrap/system loader 定义。只有一个插件持有 lease 时路由没有歧义; - // 多插件场景必须交给 Gate,禁止退化为“最后注册 dispatcher”。 + // 兼容未登记 target 的旧调用方:只有一个插件持有 lease 时路由没有歧义。 + // 多 lease 的广播由 resolveLocalDispatches 生成快照,禁止退化为“最后注册 dispatcher”。 if (localCache.size() == 1) return localCache.values().iterator().next(); return null; } + private static List resolveLocalDispatches(ClassLoader definingLoader, String targetSignature) { + CopyOnWriteArrayList routed = targetRoutes.get(baseSignature(targetSignature)); + if (routed != null && !routed.isEmpty()) return new ArrayList(routed); + Method legacy = resolveLocalDispatch(definingLoader); + if (legacy != null) return java.util.Collections.singletonList(legacy); + // 旧版调用方不会登记 target。owner loader 又可能属于 Leaf 或第三方插件, + // 此时广播给快照中的 dispatcher,由各自的 chain 表自行判定是否命中,禁止再因多 lease 直接断链。 + return new ArrayList(localCache.values()); + } + + /** + * 登记目标的真实声明方。相位与 Site advice id 属于调用后缀,不参与路由键。 + */ + public static void registerLocalTarget(Class dispatcherClass, String targetSignature) { + if (dispatcherClass == null || targetSignature == null) return; + Method dispatch = pickDispatchMethod(dispatcherClass); + if (dispatch == null) return; + String base = baseSignature(targetSignature); + CopyOnWriteArrayList routes = targetRoutes.computeIfAbsent( + base, ignored -> new CopyOnWriteArrayList() + ); + for (Method route : routes) { + if (route.getDeclaringClass() == dispatcherClass) return; + } + routes.add(dispatch); + if (routes.size() > 1 && routeConflictWarnings.putIfAbsent(base, Boolean.TRUE) == null) { + System.err.println("[Incision][Bridge] multiple dispatchers registered for target=" + base + + " routes=" + routes.size() + " (cross-plugin priority requires Gate aggregation)"); + } + } + + /** 仅移除当前插件对指定目标的路由,不影响其他同时安装 Incision 的插件。 */ + public static void unregisterLocalTarget(ClassLoader classLoader, String targetSignature) { + if (classLoader == null || targetSignature == null) return; + String base = baseSignature(targetSignature); + CopyOnWriteArrayList routes = targetRoutes.get(base); + if (routes == null) return; + routes.removeIf(method -> method.getDeclaringClass().getClassLoader() == classLoader); + if (routes.isEmpty()) targetRoutes.remove(base, routes); + if (routes.size() <= 1) routeConflictWarnings.remove(base); + } + + private static String baseSignature(String targetSignature) { + int hash = targetSignature.indexOf('#'); + String withoutAdvice = hash < 0 ? targetSignature : targetSignature.substring(0, hash); + int phase = withoutAdvice.lastIndexOf('@'); + return phase < 0 ? withoutAdvice : withoutAdvice.substring(0, phase); + } + /** 由 IncisionBootstrap 在 CONST 阶段调用,显式注册经过重定向后的 dispatcher 类 */ public static void registerLocalDispatcher(Class dispatcherClass) { if (dispatcherClass == null) return; @@ -177,8 +263,7 @@ private static Method pickDispatchMethod(Class cls) { return anyShape4 != null ? anyShape4 : anyShape3; } - private static Method resolveLocalSibling(ClassLoader cl, String methodName) { - Method local = resolveLocalDispatch(cl); + private static Method resolveLocalSibling(Method local, String methodName) { if (local == null) return null; try { return local.getDeclaringClass().getMethod(methodName, String.class, Object.class, Object[].class); @@ -191,5 +276,10 @@ private static Method resolveLocalSibling(ClassLoader cl, String methodName) { public static void unregisterLocalDispatcher(ClassLoader cl) { if (cl == null) return; localCache.remove(cl); + for (String target : new ArrayList(targetRoutes.keySet())) { + unregisterLocalTarget(cl, target); + } + // 最后一个插件退出后必须断开 Gate 对首个插件 ClassLoader 的强引用;更早解绑会破坏其他 lease。 + if (localCache.isEmpty()) unbindSystemHost(); } } diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt index 17edf8ec2..5b4fc9226 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt @@ -1,6 +1,5 @@ package taboolib.module.incision -import io.izzel.incision.bridge.IncisionBridge import taboolib.common.Inject import taboolib.common.LifeCycle import taboolib.common.env.RuntimeDependencies @@ -19,6 +18,7 @@ import taboolib.module.incision.loader.PipelineBackend import taboolib.module.incision.reflex.IncisionReflex import taboolib.module.incision.remap.RemapRouter import taboolib.module.incision.remap.TabooLibNmsResolver +import taboolib.module.incision.runtime.CanonicalBridge import taboolib.module.incision.runtime.SurgeryRegistry import taboolib.module.incision.runtime.TheatreDispatcher @@ -50,13 +50,7 @@ object IncisionBootstrap { TabooLibNmsResolver.installIfAvailable() // 2. 安装反射穿透适配器,让 TabooLib reflex 在 invoke 时自动 withoutIncision IncisionReflex.installReflexAdapter() - // 3. 把经 gradle 重定位后真实的 TheatreDispatcher 类名注册进桥(插件 CL 版本) - try { - IncisionBridge.registerLocalDispatcher(TheatreDispatcher::class.java) - } catch (t: Throwable) { - Forensics.warn("Incision bridge registerLocalDispatcher failed: ${t.message}") - } - // 4. 注入 IncisionBridge 到 bootstrap ClassLoader,使跨 CL 目标(NMS、Bukkit API)能解析桥类 + // 3. 注入 IncisionBridge 到 bootstrap ClassLoader,使跨 CL 目标(NMS、Bukkit API)能解析桥类 // 必须在任何 installWeaver/retransform 之前完成 injectBridgeIntoSystemClassLoader() Forensics.info("Incision CONST: api=$API_VERSION resolver=${RemapRouter.name()}") @@ -70,7 +64,7 @@ object IncisionBootstrap { // 1. 接入 / 创建 IncisionGate try { val gate = IncisionGateLocator.locateOrCreate(API_VERSION) - IncisionBridge.bindSystemHost(gate) + CanonicalBridge.bindSystemHost(gate) Forensics.info("Incision Gate online: api=${gate.apiVersion()}") } catch (t: Throwable) { Forensics.warn("Incision Gate 接入失败(将使用本地 dispatcher 兜底):${t.javaClass.name}: ${t.message}") @@ -104,10 +98,7 @@ object IncisionBootstrap { PipelineBackend.clear() InstrumentationBackend.clearTransformers() // 解绑本插件在桥上的本地 dispatcher 缓存 - runCatching { - IncisionBridge.unregisterLocalDispatcher(IncisionBootstrap::class.java.classLoader) - } - IncisionBridge.unbindSystemHost() + CanonicalBridge.unregisterDispatcher(IncisionBootstrap::class.java.classLoader) GateBootstrapper.release(IncisionBootstrap::class.java.classLoader) JvmtiBackend.dispose() } @@ -165,8 +156,8 @@ object IncisionBootstrap { } catch (_: Throwable) { null } - if (existing != null && existing.classLoader == null) { - Forensics.info("IncisionBridge 已存在于 bootstrap ClassLoader") + if (existing != null && (existing.classLoader == null || existing.classLoader === sysCL)) { + Forensics.info("IncisionBridge 已存在于 ${existing.classLoader ?: "bootstrap"} ClassLoader") registerDispatcherOn(existing) return } @@ -208,13 +199,17 @@ object IncisionBootstrap { } } // 路径 3: fallback — 仅在插件 CL 中,跨 CL 目标无法使用 + runCatching { + Class.forName(bridgeClassName, true, IncisionBootstrap::class.java.classLoader) + }.getOrNull()?.let(::registerDispatcherOn) Forensics.warn("IncisionBridge 未能注入 bootstrap/system CL — 跨 ClassLoader 目标(NMS、Bukkit API)将不可用") } private fun registerDispatcherOn(bridgeClass: Class<*>) { try { - val regMethod = bridgeClass.getMethod("registerLocalDispatcher", Class::class.java) - regMethod.invoke(null, TheatreDispatcher::class.java) + // 后续 target 注册、host 绑定与卸载必须复用同一个类句柄,不能再直接链接插件内同名 Bridge。 + CanonicalBridge.bind(bridgeClass) + CanonicalBridge.registerDispatcher(TheatreDispatcher::class.java) Forensics.info("IncisionBridge dispatcher 已注册 (CL=${bridgeClass.classLoader ?: "bootstrap"})") } catch (t: Throwable) { Forensics.warn("IncisionBridge registerLocalDispatcher 失败: ${t.message}") diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt index 9802869ae..be0fbd78f 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt @@ -211,6 +211,7 @@ object Scalpel { continue } activeTokens[resolvedOwner] = token + syncRuntimeAliases(resolvedOwner, ownerEntries.values.toList()) Forensics.debug("installWeaver status=${installation.status} owner=$owner resolved=$resolvedOwner advices=${group.size} total=${ownerEntries.size} backend=${backend.name}") } } @@ -256,14 +257,14 @@ object Scalpel { val token = installation.token ?: return false if (installation.status !in setOf(Backend.InstallStatus.INSTALLED, Backend.InstallStatus.PENDING_LOAD)) return false activeTokens[owner] = token + syncRuntimeAliases(owner, entries) return true } /** 把逻辑声明统一翻译为运行时坐标;宿主与 Site 必须经过同一条映射链。 */ private fun buildRuntimeTargets(resolvedOwner: String, entries: List): List { return entries.groupBy { it.target }.map { (target, targetEntries) -> - val (resolvedName, resolvedDescriptor) = RemapRouter.resolveMethod(target.owner, target.name, target.descriptor) - val runtimeTarget = target.copy(owner = resolvedOwner, name = resolvedName, descriptor = resolvedDescriptor) + val runtimeTarget = resolveRuntimeTarget(resolvedOwner, target) ScalpelWeaver.AdviceTargetSpec( target = runtimeTarget, kinds = targetEntries.map { it.kind }.toSet(), @@ -294,6 +295,20 @@ object Scalpel { } } + /** 后端确认接受计划后才发布别名,失败安装不得留下一个永远不会被字节码调用的幽灵 chain。 */ + private fun syncRuntimeAliases(resolvedOwner: String, entries: List) { + entries.groupBy { it.target }.forEach { (logicalTarget, targetEntries) -> + val runtimeTarget = resolveRuntimeTarget(resolvedOwner, logicalTarget) + TheatreDispatcher.registerRuntimeAlias(runtimeTarget, targetEntries) + } + } + + /** 宿主坐标只允许经过这一条解析链,保证 weave key、Bridge route 与 dispatcher alias 完全一致。 */ + private fun resolveRuntimeTarget(resolvedOwner: String, target: taboolib.module.incision.api.MethodCoordinate): taboolib.module.incision.api.MethodCoordinate { + val (resolvedName, resolvedDescriptor) = RemapRouter.resolveMethod(target.owner, target.name, target.descriptor) + return target.copy(owner = resolvedOwner, name = resolvedName, descriptor = resolvedDescriptor) + } + /** * 插件卸载边界:移除全部 transformer 与累计计划,避免下一 ClassLoader 再次叠加旧织入。 */ diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt b/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt index 02548298e..cc3f08110 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt @@ -3,6 +3,7 @@ package taboolib.module.incision.gate import taboolib.module.incision.diagnostic.Forensics import taboolib.module.incision.diagnostic.Trauma import taboolib.module.incision.loader.InstrumentationBackend +import taboolib.module.incision.runtime.CanonicalBridge import taboolib.platform.bukkit.Exchanges import java.io.File import java.io.FileOutputStream @@ -41,10 +42,14 @@ object GateBootstrapper { val gate = bound ?: return runCatching { gate.healByClassLoader(classLoader) } bound = null - val holder = runCatching { - Class.forName("taboolib.incision.gate.IncisionGate\$V${gate.apiVersion()}", false, ClassLoader.getSystemClassLoader()) - }.getOrNull() - runCatching { holder?.getMethod("setDelegate", Object::class.java)?.invoke(null, null) } + // holder 是 JVM 共享对象。任一插件单独 disable 都不能清掉其他 lease 正在使用的 delegate; + // canonical Bridge 在最后一个 dispatcher 注销时同步断开 systemHost,二者必须一起归零。 + if (CanonicalBridge.localLeaseCount() == 0) { + val holder = runCatching { + Class.forName("taboolib.incision.gate.IncisionGate\$V${gate.apiVersion()}", false, ClassLoader.getSystemClassLoader()) + }.getOrNull() + runCatching { holder?.getMethod("setDelegate", Object::class.java)?.invoke(null, null) } + } } fun bootstrap(apiVersion: Int): IncisionGateApi { diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt index 12599c64b..ffc422f96 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt @@ -47,6 +47,8 @@ class AdviceChain(val target: MethodCoordinate) { private val entries = java.util.concurrent.CopyOnWriteArrayList() fun add(entry: AdviceEntry) { + // 聚合计划重装会再次同步逻辑/运行时别名;同一 id 必须替换而不是重复执行。 + entries.removeIf { it.id == entry.id } entries.add(entry) entries.sortByDescending { it.priority } } diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/CanonicalBridge.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/CanonicalBridge.kt new file mode 100644 index 000000000..91f7cbc8e --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/CanonicalBridge.kt @@ -0,0 +1,80 @@ +package taboolib.module.incision.runtime + +import taboolib.module.incision.diagnostic.Forensics + +/** + * 当前插件对 JVM 唯一 IncisionBridge 的反射句柄。 + * + * 同名 Bridge 可能同时存在于插件与 bootstrap ClassLoader;业务代码若直接调用静态方法, + * JVM 会在链接时永久绑定到其中一个版本。这里强制所有 lease 与目标路由操作都落到启动期 + * 选定的 canonical 类,避免出现日志显示两个 lease、实际却写入另一份静态表的分裂状态。 + */ +internal object CanonicalBridge { + + /** 仅在 Bridge 注入彻底失败时使用;该场景本来就不支持跨 ClassLoader 织入。 */ + private val fallbackBypassMiss = Any() + + @Volatile + private var bridgeClass: Class<*>? = null + + /** 只能在 Bridge 注入完成后绑定;重复绑定同一个类是幂等的。 */ + fun bind(type: Class<*>) { + bridgeClass = type + } + + fun registerDispatcher(dispatcherClass: Class<*>) { + invoke("registerLocalDispatcher", arrayOf>(Class::class.java), dispatcherClass) + } + + fun unregisterDispatcher(classLoader: ClassLoader) { + invoke("unregisterLocalDispatcher", arrayOf>(ClassLoader::class.java), classLoader) + } + + fun registerTarget(dispatcherClass: Class<*>, targetSignature: String) { + invoke( + "registerLocalTarget", + arrayOf>(Class::class.java, String::class.java), + dispatcherClass, + targetSignature, + ) + } + + fun unregisterTarget(classLoader: ClassLoader, targetSignature: String) { + invoke( + "unregisterLocalTarget", + arrayOf>(ClassLoader::class.java, String::class.java), + classLoader, + targetSignature, + ) + } + + fun bindSystemHost(host: Any) { + invoke("bindSystemHost", arrayOf>(Any::class.java), host) + } + + /** 返回 JVM canonical Bridge 当前持有的插件 lease 数,而不是当前重定位副本的本地状态。 */ + fun localLeaseCount(): Int = + (invokeResult("localLeaseCount", emptyArray>()) as? Number)?.toInt() ?: 0 + + /** Bypass sentinel 必须来自 canonical Bridge,引用身份才能跨插件 ClassLoader 保持一致。 */ + fun bypassMiss(): Any = + invokeResult("bypassMiss", emptyArray>()) ?: fallbackBypassMiss + + private fun invoke(name: String, parameterTypes: Array>, vararg args: Any?) { + invokeResult(name, parameterTypes, *args) + } + + private fun invokeResult(name: String, parameterTypes: Array>, vararg args: Any?): Any? { + val type = bridgeClass + if (type == null) { + Forensics.warn("Incision canonical Bridge 尚未绑定,忽略操作: $name") + return null + } + return try { + type.getMethod(name, *parameterTypes).invoke(null, *args) + } catch (t: Throwable) { + Forensics.warn("Incision canonical Bridge 调用失败: method=$name reason=${t.message}") + null + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt index dfb1c1485..1c96f4c59 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt @@ -1,6 +1,5 @@ package taboolib.module.incision.runtime -import io.izzel.incision.bridge.IncisionBridge import taboolib.module.incision.api.MethodCoordinate import taboolib.module.incision.api.Resume import taboolib.module.incision.api.Theatre @@ -20,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap object TheatreDispatcher { @JvmField - val BYPASS_MISS: Any = IncisionBridge.bypassMiss() + val BYPASS_MISS: Any = CanonicalBridge.bypassMiss() private val chains = ConcurrentHashMap() private val wildcardEntries = ConcurrentHashMap>() @@ -108,6 +107,8 @@ object TheatreDispatcher { chains.computeIfAbsent(target.signature) { AdviceChain(target) } fun register(entry: AdviceEntry) { + // owner 的 defining loader 可能属于 Leaf、AuraSkills 或其他第三方插件;按签名登记声明方才能精确回调。 + CanonicalBridge.registerTarget(TheatreDispatcher::class.java, entry.target.signature) val desc = entry.target.descriptor if (desc.contains('*')) { val ownerName = "${entry.target.owner}.${entry.target.name}" @@ -128,17 +129,34 @@ object TheatreDispatcher { } } + /** + * 为 remap 后的运行时坐标建立同一 advice 链的别名。 + * + * Weaver 写入字节码的是运行时 owner/name/descriptor;dispatcher 若只保存 Mojang/Spigot + * 声明坐标,会在旧版 NMS 或成员改名时路由成功却找不到 chain。 + */ + internal fun registerRuntimeAlias(runtimeTarget: MethodCoordinate, entries: List) { + if (entries.isEmpty()) return + CanonicalBridge.registerTarget(TheatreDispatcher::class.java, runtimeTarget.signature) + val chain = chainOf(runtimeTarget) + entries.forEach(chain::add) + } + fun unregister(target: MethodCoordinate, id: String): Boolean { - var removed = chains[target.signature]?.remove(id) ?: false + var removed = false + // 一个 entry 可能同时存在于声明坐标和 remap 后运行时坐标;按 id 清理全部投影。 + for ((signature, chain) in chains) { + removed = chain.remove(id) || removed + if (chain.isEmpty() && chains.remove(signature, chain)) { + CanonicalBridge.unregisterTarget(TheatreDispatcher::class.java.classLoader, signature) + bodyInvokerCache.keys.removeIf { it.baseSig == signature } + } + } val ownerName = "${target.owner}.${target.name}" wildcardEntries[ownerName]?.let { entries -> removed = entries.removeIf { it.id == id } || removed if (entries.isEmpty()) wildcardEntries.remove(ownerName, entries) } - if (target.descriptor.contains('*')) { - // 通配 advice 注册时会被复制进已存在的具体 chain;卸载必须按 id 全量清除这些投影。 - chains.values.forEach { chain -> removed = chain.remove(id) || removed } - } predicateWarnedIds.remove(id) bodyInvokerCache.keys.removeIf { it.baseSig == target.signature } return removed diff --git a/module/incision/src/test/java/taboolib/module/incision/bridge/BridgeFixtureDispatcher.java b/module/incision/src/test/java/taboolib/module/incision/bridge/BridgeFixtureDispatcher.java new file mode 100644 index 000000000..ef914a057 --- /dev/null +++ b/module/incision/src/test/java/taboolib/module/incision/bridge/BridgeFixtureDispatcher.java @@ -0,0 +1,23 @@ +package taboolib.module.incision.bridge; + +/** + * Bridge 路由测试夹具;测试会把同一份字节码定义到两个隔离 ClassLoader,模拟两个插件 lease。 + */ +public final class BridgeFixtureDispatcher { + + private static String acceptedTarget; + + private BridgeFixtureDispatcher() {} + + public static Object dispatch(String targetSignature, Object self, Object[] args) { + return targetSignature.startsWith(acceptedTarget) ? BridgeFixtureDispatcher.class.getClassLoader() : null; + } + + public static Object dispatchBypass(String targetSignature, Object self, Object[] args) { + return dispatch(targetSignature, self, args); + } + + public static void configure(String targetSignature) { + acceptedTarget = targetSignature; + } +} diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeTargetRoutingTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeTargetRoutingTest.kt new file mode 100644 index 000000000..38128e952 --- /dev/null +++ b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeTargetRoutingTest.kt @@ -0,0 +1,87 @@ +package taboolib.module.incision.bridge + +import io.izzel.incision.bridge.IncisionBridge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * 覆盖 Leaf 服务端类和第三方插件类均无法用 defining loader 找到切术声明方的场景。 + */ +@DisplayName("IncisionBridge 多 lease 目标签名路由") +class IncisionBridgeTargetRoutingTest { + + @Test + @DisplayName("两个隔离 lease 存在时按目标签名选择 dispatcher") + fun routesByTargetInsteadOfOwnerLoader() { + val fixtureName = BridgeFixtureDispatcher::class.java.name + val fixturePath = fixtureName.replace('.', '/') + ".class" + val bytes = BridgeFixtureDispatcher::class.java.classLoader + .getResourceAsStream(fixturePath)!!.use { it.readBytes() } + val loaderA = FixtureLoader(fixtureName, bytes) + val loaderB = FixtureLoader(fixtureName, bytes) + val dispatcherA = Class.forName(fixtureName, true, loaderA) + val dispatcherB = Class.forName(fixtureName, true, loaderB) + val targetA = "example/Target.method()Ljava/lang/Object;" + val targetB = "example/Other.method()Ljava/lang/Object;" + + try { + dispatcherA.getMethod("configure", String::class.java).invoke(null, targetA) + dispatcherB.getMethod("configure", String::class.java).invoke(null, targetB) + IncisionBridge.registerLocalDispatcher(dispatcherA) + IncisionBridge.registerLocalDispatcher(dispatcherB) + IncisionBridge.registerLocalTarget(dispatcherA, targetA) + IncisionBridge.registerLocalTarget(dispatcherB, targetB) + + val result = IncisionBridge.dispatch(dispatcherB, "$targetA@SPLICE", null, emptyArray()) + + assertSame(loaderA, result) + + // 兼容尚未调用 registerLocalTarget 的旧插件:owner loader 不属于任一 lease 时广播快照。 + val legacyTarget = "legacy/Target.method()Ljava/lang/Object;" + dispatcherA.getMethod("configure", String::class.java).invoke(null, legacyTarget) + val legacyResult = IncisionBridge.dispatch( + IncisionBridgeTargetRoutingTest::class.java, + "$legacyTarget@SPLICE", + null, + emptyArray(), + ) + assertSame(loaderA, legacyResult) + + IncisionBridge.bindSystemHost(FakeHost()) + IncisionBridge.unregisterLocalDispatcher(loaderA) + assertEquals(1, IncisionBridge.localLeaseCount()) + assertTrue(IncisionBridge.hasSystemHost()) + IncisionBridge.unregisterLocalDispatcher(loaderB) + assertEquals(0, IncisionBridge.localLeaseCount()) + assertFalse(IncisionBridge.hasSystemHost()) + } finally { + IncisionBridge.unregisterLocalDispatcher(loaderA) + IncisionBridge.unregisterLocalDispatcher(loaderB) + } + } + + /** Gate 生命周期夹具;最后一个 lease 释放前 host 必须保持可用。 */ + class FakeHost { + fun dispatch(targetSignature: String, self: Any?, args: Array): Any? = null + } + + /** child-first 仅作用于夹具类,确保测试本身仍共享 JUnit 与 Bridge 类型。 */ + private class FixtureLoader( + private val fixtureName: String, + private val fixtureBytes: ByteArray, + ) : ClassLoader(BridgeFixtureDispatcher::class.java.classLoader) { + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + if (name != fixtureName) return super.loadClass(name, resolve) + synchronized(getClassLoadingLock(name)) { + val loaded = findLoadedClass(name) ?: defineClass(name, fixtureBytes, 0, fixtureBytes.size) + if (resolve) resolveClass(loaded) + return loaded + } + } + } +} diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/runtime/TheatreDispatcherRuntimeAliasTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/runtime/TheatreDispatcherRuntimeAliasTest.kt new file mode 100644 index 000000000..71a1c4b18 --- /dev/null +++ b/module/incision/src/test/kotlin/taboolib/module/incision/runtime/TheatreDispatcherRuntimeAliasTest.kt @@ -0,0 +1,42 @@ +package taboolib.module.incision.runtime + +import io.izzel.incision.bridge.IncisionBridge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import taboolib.module.incision.api.MethodCoordinate +import java.util.concurrent.atomic.AtomicInteger + +/** 验证 NMS remap 后字节码使用运行时签名时仍能命中逻辑声明的 advice。 */ +@DisplayName("TheatreDispatcher 运行时坐标别名") +class TheatreDispatcherRuntimeAliasTest { + + @Test + @DisplayName("运行时签名与逻辑签名不同时只执行一次") + fun dispatchesRemappedRuntimeAlias() { + CanonicalBridge.bind(IncisionBridge::class.java) + val calls = AtomicInteger() + val logical = MethodCoordinate("net/minecraft/Logical", "logicalName", "()V") + val runtime = MethodCoordinate("net/minecraft/Runtime", "runtimeName", "()V") + val entry = AdviceEntry( + id = "runtime-alias-test", + kind = AdviceKind.LEAD, + target = logical, + priority = 0, + handler = { calls.incrementAndGet(); null }, + ) + + try { + TheatreDispatcher.register(entry) + TheatreDispatcher.registerRuntimeAlias(runtime, listOf(entry)) + + TheatreDispatcher.dispatch("${runtime.signature}@LEAD", null, emptyArray()) + + assertEquals(1, calls.get()) + } finally { + TheatreDispatcher.unregister(logical, entry.id) + TheatreDispatcher.clear() + IncisionBridge.unregisterLocalDispatcher(TheatreDispatcher::class.java.classLoader) + } + } +}