mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Fix raw hooks: plugin-side hookRawMethod with Advice instrumentation
Remove the planted isContactsTabVisible() hook target and app-side visibility conditionals (that was cheating). Plugins now declare raw JVM targets directly via hookRawMethod on existing methods like MainScreenKt.selectMainPage, ContactsTabKt.ContactsTab, NavigationBarKt.NavigationBarItem, and Logger.d. Replace ByteBuddy MethodDelegation (schema-changing) with Advice-based ClassReloadingStrategy so hooks actually install at runtime. Fix Logger hook signature for Kotlin object instance methods. Add chat banner z-index fix and PluginEngine.hostRevision to recompose MainScreen after plugin load. Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
+2
-2
@@ -21,9 +21,9 @@ actual object PluginPlatform {
|
||||
private val pineHooks = mutableMapOf<String, MutableList<top.canyie.pine.Pine.HookRecord>>()
|
||||
|
||||
actual fun installSharedHook(registration: SharedHookRegistration) {
|
||||
val target = SharedMethodHookRegistry.targetFor(registration.hookId) ?: return
|
||||
val target = registration.target
|
||||
val clazz = Class.forName(target.className)
|
||||
val paramTypes = target.paramTypeNames.map { Class.forName(it) }.toTypedArray()
|
||||
val paramTypes = target.paramTypeNames.map { resolveJvmParameterType(it) }.toTypedArray()
|
||||
val method = clazz.getDeclaredMethod(target.methodName, *paramTypes)
|
||||
val record = Pine.hook(method, object : MethodHook() {
|
||||
override fun beforeCall(callFrame: Pine.CallFrame) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.fromchat.plugins.host
|
||||
|
||||
internal fun resolveJvmParameterType(typeName: String): Class<*> =
|
||||
when (typeName) {
|
||||
"boolean" -> java.lang.Boolean.TYPE
|
||||
"byte" -> java.lang.Byte.TYPE
|
||||
"char" -> java.lang.Character.TYPE
|
||||
"short" -> java.lang.Short.TYPE
|
||||
"int" -> Integer.TYPE
|
||||
"long" -> java.lang.Long.TYPE
|
||||
"float" -> java.lang.Float.TYPE
|
||||
"double" -> java.lang.Double.TYPE
|
||||
"void" -> java.lang.Void.TYPE
|
||||
else -> Class.forName(typeName)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ object HookRegistry {
|
||||
LOGGER_DEBUG to SharedHookTarget(
|
||||
className = "ru.fromchat.Logger",
|
||||
methodName = "d",
|
||||
paramTypeNames = arrayOf("java.lang.String", "java.lang.String"),
|
||||
paramTypeNames = arrayOf("java.lang.String", "java.lang.String", "java.lang.Throwable"),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ object HookRegistry {
|
||||
data class SharedHookRegistration(
|
||||
val pluginId: String,
|
||||
val hookId: String,
|
||||
val target: SharedHookTarget,
|
||||
val priority: Int,
|
||||
val before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
|
||||
val after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
|
||||
|
||||
@@ -23,6 +23,9 @@ object PluginEngine : PluginHostBridge {
|
||||
private val _installed = MutableStateFlow<List<PluginManifest>>(emptyList())
|
||||
val installed: StateFlow<List<PluginManifest>> = _installed.asStateFlow()
|
||||
|
||||
private val _hostRevision = MutableStateFlow(0)
|
||||
val hostRevision: StateFlow<Int> = _hostRevision.asStateFlow()
|
||||
|
||||
private val enabledIds = linkedSetOf<String>()
|
||||
private val pinnedIds = linkedSetOf<String>()
|
||||
private val loaded = mutableMapOf<String, LoadedPlugin>()
|
||||
@@ -51,6 +54,7 @@ object PluginEngine : PluginHostBridge {
|
||||
} else {
|
||||
loaded.keys.toList().forEach { unloadPlugin(it) }
|
||||
}
|
||||
bumpHostRevision()
|
||||
}
|
||||
|
||||
fun setDeveloperMode(enabled: Boolean) {
|
||||
@@ -69,6 +73,7 @@ object PluginEngine : PluginHostBridge {
|
||||
}
|
||||
persistEnabledState()
|
||||
refreshInstalledList()
|
||||
bumpHostRevision()
|
||||
}
|
||||
|
||||
fun isPluginEnabled(pluginId: String): Boolean = pluginId in enabledIds
|
||||
@@ -182,6 +187,10 @@ object PluginEngine : PluginHostBridge {
|
||||
PluginHookDispatcher.registerMenuItem(pluginId, item)
|
||||
}
|
||||
|
||||
override fun registerUiOverlay(pluginId: String, slot: String, title: String, message: String) {
|
||||
ru.fromchat.plugins.host.ui.PluginOverlayStore.register(pluginId, slot, title, message)
|
||||
}
|
||||
|
||||
override fun showBulletin(message: String) {
|
||||
PluginHookDispatcher.showBulletin(message)
|
||||
}
|
||||
@@ -193,7 +202,37 @@ object PluginEngine : PluginHostBridge {
|
||||
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
|
||||
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
|
||||
): () -> Unit {
|
||||
val registration = SharedHookRegistration(pluginId, hookId, priority, before, after)
|
||||
val target = SharedMethodHookRegistry.targetFor(hookId)
|
||||
?: run {
|
||||
log(pluginId, "Unknown shared hook id: $hookId")
|
||||
return {}
|
||||
}
|
||||
return registerSharedHook(pluginId, hookId, target, priority, before, after)
|
||||
}
|
||||
|
||||
override fun hookRawMethod(
|
||||
pluginId: String,
|
||||
className: String,
|
||||
methodName: String,
|
||||
paramTypeNames: Array<String>,
|
||||
priority: Int,
|
||||
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
|
||||
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
|
||||
): () -> Unit {
|
||||
val hookId = "raw:$className#$methodName"
|
||||
val target = SharedHookTarget(className, methodName, paramTypeNames)
|
||||
return registerSharedHook(pluginId, hookId, target, priority, before, after)
|
||||
}
|
||||
|
||||
private fun registerSharedHook(
|
||||
pluginId: String,
|
||||
hookId: String,
|
||||
target: SharedHookTarget,
|
||||
priority: Int,
|
||||
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
|
||||
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
|
||||
): () -> Unit {
|
||||
val registration = SharedHookRegistration(pluginId, hookId, target, priority, before, after)
|
||||
SharedMethodHookRegistry.register(registration)
|
||||
val unhook = {
|
||||
SharedMethodHookRegistry.unregisterPlugin(pluginId)
|
||||
@@ -215,6 +254,7 @@ object PluginEngine : PluginHostBridge {
|
||||
runCatching { loadedPlugin.instance.onPluginLoad() }
|
||||
.onFailure { log(manifest.id, "onPluginLoad failed: ${it.message}") }
|
||||
loaded[manifest.id] = loadedPlugin
|
||||
bumpHostRevision()
|
||||
}
|
||||
|
||||
private fun unloadPlugin(pluginId: String) {
|
||||
@@ -223,6 +263,11 @@ object PluginEngine : PluginHostBridge {
|
||||
PluginHookDispatcher.unregisterPlugin(pluginId)
|
||||
sharedUnhooks.remove(pluginId)
|
||||
PluginPlatform.current?.unloadPlugin(pluginId)
|
||||
bumpHostRevision()
|
||||
}
|
||||
|
||||
private fun bumpHostRevision() {
|
||||
_hostRevision.value++
|
||||
}
|
||||
|
||||
private fun loadSettingsFile(pluginId: String): MutableMap<String, String> {
|
||||
|
||||
+12
-1
@@ -4,6 +4,7 @@ import ru.fromchat.plugins.HookResult
|
||||
import ru.fromchat.plugins.HookStrategy
|
||||
import ru.fromchat.plugins.MenuItemData
|
||||
import ru.fromchat.plugins.SendMessageHookContext
|
||||
import ru.fromchat.plugins.host.ui.PluginOverlayStore
|
||||
|
||||
data class RegisteredSendHook(
|
||||
val pluginId: String,
|
||||
@@ -27,6 +28,7 @@ object PluginHookDispatcher {
|
||||
featureOverrides.values.forEach { list -> list.removeAll { it.first == pluginId } }
|
||||
menuItems.removeAll { it.first == pluginId }
|
||||
SharedMethodHookRegistry.unregisterPlugin(pluginId)
|
||||
PluginOverlayStore.unregisterPlugin(pluginId)
|
||||
}
|
||||
|
||||
fun registerFeatureOverride(pluginId: String, featureId: String, provider: () -> Boolean?) {
|
||||
@@ -37,8 +39,17 @@ object PluginHookDispatcher {
|
||||
menuItems += pluginId to item
|
||||
}
|
||||
|
||||
fun menuItemEntriesFor(type: ru.fromchat.plugins.MenuItemType): List<Pair<String, MenuItemData>> =
|
||||
menuItems.filter { it.second.menuType == type }.sortedByDescending { it.second.priority }
|
||||
|
||||
fun menuItemsFor(type: ru.fromchat.plugins.MenuItemType): List<MenuItemData> =
|
||||
menuItems.filter { it.second.menuType == type }.sortedByDescending { it.second.priority }.map { it.second }
|
||||
menuItemEntriesFor(type).map { it.second }
|
||||
|
||||
fun dispatchMenuItemClick(clickKey: String) {
|
||||
if (clickKey.isBlank()) return
|
||||
val owner = menuItems.firstOrNull { it.second.onClickKey == clickKey } ?: return
|
||||
PluginEngine.loadedPlugin(owner.first)?.instance?.onMenuItemClick(clickKey)
|
||||
}
|
||||
|
||||
fun featureOverrideValue(id: String): Boolean? {
|
||||
featureOverrides[id]?.asReversed()?.forEach { (_, provider) ->
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package ru.fromchat.plugins.host.ui
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
|
||||
data class PluginUiOverlay(
|
||||
val pluginId: String,
|
||||
val slot: String,
|
||||
val title: String,
|
||||
val message: String,
|
||||
)
|
||||
|
||||
object PluginOverlayStore {
|
||||
val overlays = mutableStateListOf<PluginUiOverlay>()
|
||||
|
||||
fun register(pluginId: String, slot: String, title: String, message: String) {
|
||||
overlays.removeAll { it.pluginId == pluginId && it.slot == slot }
|
||||
overlays += PluginUiOverlay(pluginId, slot, title, message)
|
||||
}
|
||||
|
||||
fun unregisterPlugin(pluginId: String) {
|
||||
overlays.removeAll { it.pluginId == pluginId }
|
||||
}
|
||||
|
||||
fun overlaysFor(slot: String): List<PluginUiOverlay> =
|
||||
overlays.filter { it.slot == slot }
|
||||
}
|
||||
@@ -32,10 +32,12 @@ fun PluginUiHost(content: @Composable () -> Unit) {
|
||||
val bulletins = remember { PluginBulletinStore.bulletins }
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
content()
|
||||
bulletins.lastOrNull()?.let { bulletin ->
|
||||
bulletins.firstOrNull()?.let { bulletin ->
|
||||
LaunchedEffect(bulletin.id) {
|
||||
delay(4_000)
|
||||
bulletins.remove(bulletin)
|
||||
if (bulletins.firstOrNull()?.id == bulletin.id) {
|
||||
bulletins.removeAt(0)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
package ru.fromchat.plugins.host
|
||||
|
||||
import net.bytebuddy.ByteBuddy
|
||||
import net.bytebuddy.agent.ByteBuddyAgent
|
||||
import net.bytebuddy.implementation.MethodDelegation
|
||||
import net.bytebuddy.implementation.bind.annotation.AllArguments
|
||||
import net.bytebuddy.implementation.bind.annotation.Origin
|
||||
import net.bytebuddy.implementation.bind.annotation.RuntimeType
|
||||
import net.bytebuddy.implementation.bind.annotation.SuperCall
|
||||
import net.bytebuddy.asm.Advice
|
||||
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy
|
||||
import net.bytebuddy.matcher.ElementMatchers
|
||||
import ru.fromchat.plugins.BasePlugin
|
||||
import ru.fromchat.plugins.HookResult
|
||||
import ru.fromchat.plugins.HookStrategy
|
||||
import ru.fromchat.plugins.PluginManifest
|
||||
import ru.fromchat.plugins.host.util.PluginLogger
|
||||
import java.io.File
|
||||
import java.lang.reflect.Method
|
||||
import java.net.URLClassLoader
|
||||
import java.util.concurrent.Callable
|
||||
import net.bytebuddy.ByteBuddy
|
||||
|
||||
actual object PluginPlatform {
|
||||
actual val current: PluginPlatform? get() = this
|
||||
@@ -26,14 +21,14 @@ actual object PluginPlatform {
|
||||
private var agentInstalled = false
|
||||
|
||||
actual fun installSharedHook(registration: SharedHookRegistration) {
|
||||
val target = SharedMethodHookRegistry.targetFor(registration.hookId) ?: return
|
||||
val handle = JvmSharedHookHandle(registration, target)
|
||||
val handle = JvmSharedHookHandle(registration, registration.target)
|
||||
handle.install()
|
||||
hookHandles.getOrPut(registration.pluginId) { mutableListOf() } += handle
|
||||
}
|
||||
|
||||
actual fun uninstallSharedHook(registration: SharedHookRegistration) {
|
||||
hookHandles[registration.pluginId]?.removeAll { it.registration == registration }
|
||||
HookAdviceBindings.unbind(registration)
|
||||
}
|
||||
|
||||
actual fun loadPlugin(manifest: PluginManifest, pluginDir: String): LoadedPlugin? {
|
||||
@@ -56,7 +51,7 @@ actual object PluginPlatform {
|
||||
|
||||
private fun ensureAgent() {
|
||||
if (agentInstalled) return
|
||||
runCatching { ByteBuddyAgent.install() }
|
||||
ByteBuddyAgent.install()
|
||||
agentInstalled = true
|
||||
}
|
||||
|
||||
@@ -71,52 +66,37 @@ actual object PluginPlatform {
|
||||
private val target: SharedHookTarget,
|
||||
) : SharedHookHandle {
|
||||
private var installed = false
|
||||
private var boundMethod: Method? = null
|
||||
|
||||
override fun install() {
|
||||
if (installed) return
|
||||
ensureAgent()
|
||||
val clazz = Class.forName(target.className)
|
||||
val paramTypes = target.paramTypeNames.map { Class.forName(it) }.toTypedArray()
|
||||
val method = clazz.getDeclaredMethod(target.methodName, *paramTypes)
|
||||
ByteBuddy()
|
||||
.redefine(clazz)
|
||||
.method(ElementMatchers.named(target.methodName))
|
||||
.intercept(MethodDelegation.to(JvmHookInterceptor(registration)))
|
||||
.make()
|
||||
.load(clazz.classLoader)
|
||||
installed = true
|
||||
runCatching {
|
||||
ensureAgent()
|
||||
val clazz = Class.forName(target.className)
|
||||
val paramTypes = target.paramTypeNames.map { resolveJvmParameterType(it) }.toTypedArray()
|
||||
val method = clazz.getDeclaredMethod(target.methodName, *paramTypes)
|
||||
HookAdviceBindings.bind(method, registration)
|
||||
boundMethod = method
|
||||
ByteBuddy()
|
||||
.redefine(clazz)
|
||||
.visit(Advice.to(SharedHookAdvice::class.java).on(ElementMatchers.`is`(method)))
|
||||
.make()
|
||||
.load(clazz.classLoader, ClassReloadingStrategy.fromInstalledAgent())
|
||||
installed = true
|
||||
}.onFailure {
|
||||
boundMethod?.let { HookAdviceBindings.unbind(registration) }
|
||||
boundMethod = null
|
||||
PluginLogger.log(
|
||||
registration.pluginId,
|
||||
"Raw hook install failed for ${target.className}.${target.methodName}: ${it.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun uninstall() {
|
||||
HookAdviceBindings.unbind(registration)
|
||||
boundMethod = null
|
||||
installed = false
|
||||
}
|
||||
}
|
||||
|
||||
class JvmHookInterceptor(private val registration: SharedHookRegistration) {
|
||||
@RuntimeType
|
||||
fun intercept(
|
||||
@AllArguments args: Array<Any?>,
|
||||
@Origin method: Method,
|
||||
@SuperCall callable: Callable<Any?>,
|
||||
): Any? {
|
||||
registration.before?.let { before ->
|
||||
val result = before(args)
|
||||
when (result.strategy) {
|
||||
HookStrategy.CANCEL -> return null
|
||||
HookStrategy.MODIFY, HookStrategy.MODIFY_FINAL -> result.value?.let { return@intercept callable.call() }
|
||||
HookStrategy.DEFAULT -> Unit
|
||||
}
|
||||
}
|
||||
val value = callable.call()
|
||||
registration.after?.let { after ->
|
||||
val result = after(args, value)
|
||||
when (result.strategy) {
|
||||
HookStrategy.CANCEL -> return null
|
||||
HookStrategy.MODIFY, HookStrategy.MODIFY_FINAL -> return result.value
|
||||
HookStrategy.DEFAULT -> Unit
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package ru.fromchat.plugins.host
|
||||
|
||||
import net.bytebuddy.asm.Advice
|
||||
import ru.fromchat.plugins.HookStrategy
|
||||
import java.lang.reflect.Method
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
internal object HookAdviceBindings {
|
||||
private val byMethodKey = ConcurrentHashMap<String, SharedHookRegistration>()
|
||||
|
||||
fun bind(method: Method, registration: SharedHookRegistration) {
|
||||
byMethodKey[key(method)] = registration
|
||||
}
|
||||
|
||||
fun unbind(registration: SharedHookRegistration) {
|
||||
byMethodKey.entries.removeIf { it.value == registration }
|
||||
}
|
||||
|
||||
fun registration(method: Method): SharedHookRegistration? = byMethodKey[key(method)]
|
||||
|
||||
private fun key(method: Method): String =
|
||||
buildString {
|
||||
append(method.declaringClass.name)
|
||||
append('#')
|
||||
append(method.name)
|
||||
append('(')
|
||||
method.parameterTypes.forEachIndexed { index, type ->
|
||||
if (index > 0) append(',')
|
||||
append(type.name)
|
||||
}
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
|
||||
internal class SharedHookAdvice {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue::class)
|
||||
fun onEnter(
|
||||
@Advice.AllArguments arguments: Array<Any?>,
|
||||
@Advice.Origin method: Method,
|
||||
): Boolean {
|
||||
val registration = HookAdviceBindings.registration(method) ?: return false
|
||||
registration.before?.let { before ->
|
||||
when (before(arguments).strategy) {
|
||||
HookStrategy.CANCEL -> return true
|
||||
HookStrategy.MODIFY, HookStrategy.MODIFY_FINAL, HookStrategy.DEFAULT -> Unit
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Advice.OnMethodExit(onThrowable = Throwable::class)
|
||||
fun onExit(
|
||||
@Advice.AllArguments arguments: Array<Any?>,
|
||||
@Advice.Origin method: Method,
|
||||
@Advice.Enter skipOriginal: Boolean,
|
||||
) {
|
||||
if (skipOriginal) return
|
||||
HookAdviceBindings.registration(method)?.after?.invoke(arguments, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user