From 31399efe40844037b176fd9c48401d212dcabb1c Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Wed, 10 Nov 2021 22:39:32 +0800 Subject: [PATCH] AbstractExternalResource (#1637) * AbstractExternalResource * typo * make `ResourceCleanCallback` `fun interface` * custom display name * update logic * Update docs * Update ExternalResource.kt --- ...binary-compatibility-validator-android.api | 23 ++ .../api/binary-compatibility-validator.api | 23 ++ .../internal/utils/ExternalImageImpls.kt | 2 +- .../utils/ExternalResourceLeakObserver.kt | 19 +- .../kotlin/utils/ExternalResource.kt | 242 +++++++++++++++++- 5 files changed, 302 insertions(+), 7 deletions(-) diff --git a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api index 68528b0bd..dce2a8373 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -5572,6 +5572,29 @@ public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : n public final class net/mamoe/mirai/network/WrongPasswordException : net/mamoe/mirai/network/LoginFailedException { } +public abstract class net/mamoe/mirai/utils/AbstractExternalResource : net/mamoe/mirai/utils/ExternalResource { + public fun ()V + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;)V + public synthetic fun (Ljava/lang/String;Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;)V + public synthetic fun (Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun close ()V + protected final fun dontRegisterLeakObserver ()V + public final fun getClosed ()Lkotlinx/coroutines/Deferred; + public fun getFormatName ()Ljava/lang/String; + public fun getMd5 ()[B + public fun getSha1 ()[B + public final fun inputStream ()Ljava/io/InputStream; + protected abstract fun inputStream0 ()Ljava/io/InputStream; + protected final fun registerToLeakObserver ()V + protected final fun setResourceCleanCallback (Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;)V +} + +public abstract interface class net/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback { + public abstract fun cleanup ()V +} + public class net/mamoe/mirai/utils/BotConfiguration { public static final field Companion Lnet/mamoe/mirai/utils/BotConfiguration$Companion; public fun ()V diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 2f4520159..e36e15859 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -5572,6 +5572,29 @@ public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : n public final class net/mamoe/mirai/network/WrongPasswordException : net/mamoe/mirai/network/LoginFailedException { } +public abstract class net/mamoe/mirai/utils/AbstractExternalResource : net/mamoe/mirai/utils/ExternalResource { + public fun ()V + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;)V + public synthetic fun (Ljava/lang/String;Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;)V + public synthetic fun (Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun close ()V + protected final fun dontRegisterLeakObserver ()V + public final fun getClosed ()Lkotlinx/coroutines/Deferred; + public fun getFormatName ()Ljava/lang/String; + public fun getMd5 ()[B + public fun getSha1 ()[B + public final fun inputStream ()Ljava/io/InputStream; + protected abstract fun inputStream0 ()Ljava/io/InputStream; + protected final fun registerToLeakObserver ()V + protected final fun setResourceCleanCallback (Lnet/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback;)V +} + +public abstract interface class net/mamoe/mirai/utils/AbstractExternalResource$ResourceCleanCallback { + public abstract fun cleanup ()V +} + public class net/mamoe/mirai/utils/BotConfiguration { public static final field Companion Lnet/mamoe/mirai/utils/BotConfiguration$Companion; public fun ()V diff --git a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt index edaef541a..a7add0f7b 100644 --- a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt @@ -18,7 +18,7 @@ import java.io.InputStream import java.io.RandomAccessFile -private fun InputStream.detectFileTypeAndClose(): String? { +internal fun InputStream.detectFileTypeAndClose(): String? { val buffer = ByteArray(COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE) return use { kotlin.runCatching { it.read(buffer) }.onFailure { return null } diff --git a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt index 0590d2cef..8ffb70525 100644 --- a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt @@ -24,11 +24,17 @@ internal object ExternalResourceLeakObserver : Runnable { MiraiLogger.Factory.create(ExternalResourceLeakObserver::class, "ExternalResourceLeakObserver") } - internal class ERReference( - resourceInternal: ExternalResourceInternal - ) : WeakReference(resourceInternal, queue) { + internal class ERReference : WeakReference { + constructor(resource: ExternalResourceInternal) : super(resource, queue) { + this.holder = resource.holder + } + + constructor(resource: ExternalResource, holder: ExternalResourceHolder) : super(resource, queue) { + this.holder = holder + } + @JvmField - internal val holder: ExternalResourceHolder = resourceInternal.holder + internal val holder: ExternalResourceHolder } class ExternalResourceCreateStackTrace : Throwable() { @@ -44,6 +50,11 @@ internal object ExternalResourceLeakObserver : Runnable { references.add(ERReference(resource)) } + @JvmStatic + fun register(resource: ExternalResource, holder: ExternalResourceHolder) { + references.add(ERReference(resource, holder)) + } + init { val thread = Thread(this, "Mirai ExternalResource Leak Observer Thread") thread.isDaemon = true diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt index 7334a9cd4..b4b701b66 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.utils +import kotlinx.atomicfu.atomic import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import net.mamoe.kjbb.JvmBlockingBridge @@ -20,12 +21,12 @@ import net.mamoe.mirai.contact.Contact.Companion.sendImage import net.mamoe.mirai.contact.Contact.Companion.uploadImage import net.mamoe.mirai.contact.FileSupported import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.internal.utils.ExternalResourceImplByByteArray -import net.mamoe.mirai.internal.utils.ExternalResourceImplByFile +import net.mamoe.mirai.internal.utils.* import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.FileMessage import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.sendTo +import net.mamoe.mirai.utils.AbstractExternalResource.ResourceCleanCallback import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage @@ -531,6 +532,243 @@ public interface ExternalResource : Closeable { } +/** + * 一个实现了基本方法的外部资源 + * + * ## 实现 + * + * [AbstractExternalResource] 实现了大部分必要的方法, + * 只有 [ExternalResource.inputStream], [ExternalResource.size] 还未实现 + * + * 其中 [ExternalResource.inputStream] 要求每次读取的内容都是一致的 + * + * Example: + * ``` + * class MyCustomExternalResource: AbstractExternalResource() { + * override fun inputStream0(): InputStream = FileInputStream("/test.txt") + * override val size: Long get() = File("/test.txt").length() + * } + * ``` + * + * ## 资源释放 + * + * 如同 mirai 内置的 [ExternalResource] 实现一样, + * [AbstractExternalResource] 也会被注册进入资源泄露监视器 + * (即意味着 [AbstractExternalResource] 也要求手动关闭) + * + * 为了确保逻辑正确性, [AbstractExternalResource] 不允许覆盖其 [close] 方法, + * 必须在构造 [AbstractExternalResource] 的时候给定一个 [ResourceCleanCallback] 以进行资源释放 + * + * 对于 [ResourceCleanCallback], 有以下要求 + * + * - 没有对 [AbstractExternalResource] 的访问 (即没有 [AbstractExternalResource] 的任何引用) + * + * Example: + * ``` + * class MyRes( + * cleanup: ResourceCleanCallback, + * val delegate: Closable, + * ): AbstractExternalResource(cleanup) { + * } + * + * // 错误, 该写法会导致 Resource 永远也不会被自动释放 + * lateinit var myRes: MyRes + * val cleanup = ResourceCleanCallback { + * myRes.delegate.close() + * } + * myRes = MyRes(cleanup, fetchDelegate()) + * + * // 正确 + * val delegate: Closable + * val cleanup = ResourceCleanCallback { + * delegate.close() + * } + * val myRes = MyRes(cleanup, delegate) + * ``` + * + * @since 2.9 + * + * @see ExternalResource + * @see AbstractExternalResource.setResourceCleanCallback + * @see AbstractExternalResource.registerToLeakObserver + */ +@Suppress("MemberVisibilityCanBePrivate") +public abstract class AbstractExternalResource +@JvmOverloads +public constructor( + displayName: String? = null, + cleanup: ResourceCleanCallback? = null, +) : ExternalResource { + + public constructor( + cleanup: ResourceCleanCallback? = null, + ): this(null, cleanup) + + public fun interface ResourceCleanCallback { + @Throws(IOException::class) + public fun cleanup() + } + + override val md5: ByteArray by lazy { inputStream().md5() } + override val sha1: ByteArray by lazy { inputStream().sha1() } + override val formatName: String by lazy { + inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME + } + + private val leakObserverRegistered = atomic(false) + + /** + * 注册 [ExternalResource] 资源泄露监视器 + * + * 受限于类继承构造器调用顺序, [AbstractExternalResource] 无法做到在完成初始化后马上注册监视器 + * + * 该方法以允许 实现类 在完成初始化后直接注册资源监视器以避免意外的资源泄露 + * + * 在不调用本方法的前提下, 如果没有相关的资源访问操作, `this` 可能会被意外泄露 + * + * 正确示例: + * ``` + * // Kotlin + * public class MyResource: AbstractExternalResource() { + * init { + * val res: SomeResource + * // 一些资源初始化 + * registerToLeakObserver() + * setResourceCleanCallback(Releaser(res)) + * } + * + * private class Releaser( + * private val res: SomeResource, + * ) : AbstractExternalResource.ResourceCleanCallback { + * override fun cleanup() = res.close() + * } + * } + * + * // Java + * public class MyResource extends AbstractExternalResource { + * public MyResource() throws IOException { + * SomeResource res; + * // 一些资源初始化 + * registerToLeakObserver(); + * setResourceCleanCallback(new Releaser(res)); + * } + * + * private static class Releaser implements ResourceCleanCallback { + * private final SomeResource res; + * Releaser(SomeResource res) { this.res = res; } + * + * public void cleanup() throws IOException { res.close(); } + * } + * } + * ``` + * + * @see setResourceCleanCallback + */ + protected fun registerToLeakObserver() { + // 用户自定义 AbstractExternalResource 也许会在 的时候失败 + // 于是在第一次使用 ExternalResource 相关的函数的时候注册 LeakObserver + if (leakObserverRegistered.compareAndSet(expect = false, update = true)) { + ExternalResourceLeakObserver.register(this, holder) + } + } + + /** + * 该方法用于告知 [AbstractExternalResource] 不需要注册资源泄露监视器。 + * **仅在我知道我在干什么的前提下调用此方法** + * + * 不建议取消注册监视器, 这可能带来意外的错误 + * + * @see registerToLeakObserver + */ + protected fun dontRegisterLeakObserver() { + leakObserverRegistered.value = true + } + + final override fun inputStream(): InputStream { + registerToLeakObserver() + return inputStream0() + } + + protected abstract fun inputStream0(): InputStream + + /** + * 修改 `this` 的资源释放回调。 + * **仅在我知道我在干什么的前提下调用此方法** + * + * ``` + * class MyRes { + * // region kotlin + * + * private inner class Releaser : ResourceCleanCallback + * + * private class NotInnerReleaser : ResourceCleanCallback + * + * init { + * // 错误, 内部类, Releaser 存在对 MyRes 的引用 + * setResourceCleanCallback(Releaser()) + * // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于编译器 + * setResourceCleanCallback(object : ResourceCleanCallback {}) + * // 正确, 无 inner 修饰, 等同于 java 的 private static class + * setResourceCleanCallback(NotInnerReleaser(directResource)) + * } + * + * // endregion kotlin + * + * // region java + * + * private class Releaser implements ResourceCleanCallback {} + * private static class StaticReleaser implements ResourceCleanCallback {} + * + * MyRes() { + * // 错误, 内部类, 存在对 MyRes 的引用 + * setResourceCleanCallback(new Releaser()); + * // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于 javac + * setResourceCleanCallback(new ResourceCleanCallback() {}); + * // 正确 + * setResourceCleanCallback(new StaticReleaser(directResource)); + * } + * + * // endregion java + * } + * ``` + * + * @see registerToLeakObserver + */ + protected fun setResourceCleanCallback(cleanup: ResourceCleanCallback?) { + holder.cleanup = cleanup + } + + private class UsrCustomResHolder( + @JvmField var cleanup: ResourceCleanCallback?, + private val resourceName: String, + ) : ExternalResourceHolder() { + + override val closed: Deferred = CompletableDeferred() + + override fun closeImpl() { + cleanup?.cleanup() + } + + // display on logger of ExternalResourceLeakObserver + override fun toString(): String = resourceName + } + + private val holder = UsrCustomResHolder(cleanup, displayName ?: buildString { + append("ExternalResourceHolder<") + append(this@AbstractExternalResource.javaClass.name) + append('@') + append(System.identityHashCode(this@AbstractExternalResource)) + append('>') + }) + + final override val closed: Deferred get() = holder.closed.also { registerToLeakObserver() } + + @Throws(IOException::class) + final override fun close() { + holder.close() + } +} + /** * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close]. *