commit 392cd95dacaced5aa1c297fb06d34545f6c8c4c6 parent 7d6978f9618283715a9341a748e8dc57e21aead7 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 24 Dec 2023 17:29:19 +0100 feat(scripting): module system Diffstat:
25 files changed, 565 insertions(+), 375 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -118,7 +118,7 @@ class RemoteSideContext( } scriptManager.runtime.eachModule { - callFunction("module.onManagerLoad", androidContext) + callFunction("module.onSnapEnhanceLoad", androidContext) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -268,7 +268,7 @@ class ModDatabase( version = cursor.getStringOrNull("version")!!, description = cursor.getStringOrNull("description"), author = cursor.getStringOrNull("author"), - grantPermissions = null + grantedPermissions = emptyList() ) ) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -11,8 +11,8 @@ import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.scripting.impl.IPCListeners -import me.rhunk.snapenhance.scripting.impl.RemoteManagerIPC -import me.rhunk.snapenhance.scripting.impl.RemoteScriptConfig +import me.rhunk.snapenhance.scripting.impl.ManagerIPC +import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager import java.io.File import java.io.InputStream @@ -21,7 +21,9 @@ import kotlin.system.exitProcess class RemoteScriptManager( val context: RemoteSideContext, ) : IScripting.Stub() { - val runtime = ScriptRuntime(context.androidContext, context.log) + val runtime = ScriptRuntime(context.androidContext, context.log).apply { + scripting = this@RemoteScriptManager + } private var autoReloadListener: AutoReloadListener? = null private val autoReloadHandler by lazy { @@ -61,11 +63,11 @@ class RemoteScriptManager( fun init() { runtime.buildModuleObject = { module -> - module.extras["ipc"] = RemoteManagerIPC(module.moduleInfo, context.log, ipcListeners) - module.extras["im"] = InterfaceManager(module.moduleInfo, context.log) - module.extras["config"] = RemoteScriptConfig(this@RemoteScriptManager, module.moduleInfo, context.log).also { - it.load() - } + module.registerBindings( + ManagerIPC(ipcListeners), + InterfaceManager(), + ManagerScriptConfig(this@RemoteScriptManager) + ) } sync() @@ -74,12 +76,20 @@ class RemoteScriptManager( } } - fun loadScript(name: String) { - val content = getScriptContent(name) ?: return + fun getModulePath(name: String): String? { + return cachedModuleInfo.entries.find { it.value.name == name }?.key + } + + fun loadScript(path: String) { + val content = getScriptContent(path) ?: return if (context.config.root.scripting.autoReload.getNullable() != null) { - autoReloadHandler.addFile(getScriptsFolder()?.findFile(name) ?: return) + autoReloadHandler.addFile(getScriptsFolder()?.findFile(path) ?: return) } - runtime.load(name, content) + runtime.load(path, content) + } + + fun unloadScript(scriptPath: String) { + runtime.unload(scriptPath) } private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R { @@ -140,7 +150,7 @@ class RemoteScriptManager( value: String?, save: Boolean ): String? { - val scriptConfig = runtime.getModuleByName(module ?: return null)?.extras?.get("config") as? ConfigInterface ?: return null.also { + val scriptConfig = runtime.getModuleByName(module ?: return null)?.getBinding(ConfigInterface::class) ?: return null.also { context.log.warn("Failed to get config interface for $module") } val transactionType = ConfigTransactionType.fromKey(action) @@ -154,7 +164,7 @@ class RemoteScriptManager( ConfigTransactionType.SET -> set(key ?: return@runCatching null, value, save) ConfigTransactionType.SAVE -> save() ConfigTransactionType.LOAD -> load() - ConfigTransactionType.DELETE -> delete() + ConfigTransactionType.DELETE -> deleteConfig() else -> {} } null diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt @@ -0,0 +1,51 @@ +package me.rhunk.snapenhance.scripting.impl + +import android.os.DeadObjectException +import me.rhunk.snapenhance.bridge.scripting.IPCListener +import me.rhunk.snapenhance.common.scripting.impl.IPCInterface +import me.rhunk.snapenhance.common.scripting.impl.Listener +import java.util.concurrent.ConcurrentHashMap + +typealias IPCListeners = ConcurrentHashMap<String, MutableMap<String, MutableSet<IPCListener>>> // channel, eventName -> listeners + +class ManagerIPC( + private val ipcListeners: IPCListeners = ConcurrentHashMap(), +) : IPCInterface() { + companion object { + private const val TAG = "RemoteManagerIPC" + } + + override fun on(eventName: String, listener: Listener) { + onBroadcast(context.moduleInfo.name, eventName, listener) + } + + override fun emit(eventName: String, vararg args: String?) { + emit(context.moduleInfo.name, eventName, *args) + } + + override fun onBroadcast(channel: String, eventName: String, listener: Listener) { + ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(object: IPCListener.Stub() { + override fun onMessage(args: Array<out String?>) { + try { + listener(args.toList()) + } catch (doe: DeadObjectException) { + ipcListeners[channel]?.get(eventName)?.remove(this) + } catch (t: Throwable) { + context.runtime.logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG) + } + } + }) + } + + override fun broadcast(channel: String, eventName: String, vararg args: String?) { + ipcListeners[channel]?.get(eventName)?.toList()?.forEach { + try { + it.onMessage(args) + } catch (doe: DeadObjectException) { + ipcListeners[channel]?.get(eventName)?.remove(it) + } catch (t: Throwable) { + context.runtime.logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG) + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt @@ -0,0 +1,57 @@ +package me.rhunk.snapenhance.scripting.impl + +import com.google.gson.JsonObject +import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface +import me.rhunk.snapenhance.scripting.RemoteScriptManager +import java.io.File + +class ManagerScriptConfig( + private val remoteScriptManager: RemoteScriptManager +) : ConfigInterface() { + private val configFile by lazy { File(remoteScriptManager.getModuleDataFolder(context.moduleInfo.name), "config.json") } + private var config = JsonObject() + + override fun get(key: String, defaultValue: Any?): String? { + return config[key]?.asString ?: defaultValue?.toString() + } + + override fun set(key: String, value: Any?, save: Boolean) { + when (value) { + is Int -> config.addProperty(key, value) + is Double -> config.addProperty(key, value) + is Boolean -> config.addProperty(key, value) + is Long -> config.addProperty(key, value) + is Float -> config.addProperty(key, value) + is Byte -> config.addProperty(key, value) + is Short -> config.addProperty(key, value) + else -> config.addProperty(key, value?.toString()) + } + + if (save) save() + } + + override fun save() { + configFile.writeText(config.toString()) + } + + override fun load() { + runCatching { + if (!configFile.exists()) { + save() + return@runCatching + } + config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java) + }.onFailure { + context.runtime.logger.error("Failed to load config file", it) + save() + } + } + + override fun deleteConfig() { + configFile.delete() + } + + override fun onInit() { + load() + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteManagerIPC.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteManagerIPC.kt @@ -1,55 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl - -import android.os.DeadObjectException -import me.rhunk.snapenhance.bridge.scripting.IPCListener -import me.rhunk.snapenhance.common.logger.AbstractLogger -import me.rhunk.snapenhance.common.scripting.impl.IPCInterface -import me.rhunk.snapenhance.common.scripting.impl.Listener -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -import java.util.concurrent.ConcurrentHashMap - -typealias IPCListeners = ConcurrentHashMap<String, MutableMap<String, MutableSet<IPCListener>>> // channel, eventName -> listeners - -class RemoteManagerIPC( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger, - private val ipcListeners: IPCListeners = ConcurrentHashMap(), -) : IPCInterface() { - companion object { - private const val TAG = "RemoteManagerIPC" - } - - override fun on(eventName: String, listener: Listener) { - onBroadcast(moduleInfo.name, eventName, listener) - } - - override fun emit(eventName: String, vararg args: String?) { - emit(moduleInfo.name, eventName, *args) - } - - override fun onBroadcast(channel: String, eventName: String, listener: Listener) { - ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(object: IPCListener.Stub() { - override fun onMessage(args: Array<out String?>) { - try { - listener(args) - } catch (doe: DeadObjectException) { - ipcListeners[channel]?.get(eventName)?.remove(this) - } catch (t: Throwable) { - logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG) - } - } - }) - } - - override fun broadcast(channel: String, eventName: String, vararg args: String?) { - ipcListeners[channel]?.get(eventName)?.toList()?.forEach { - try { - it.onMessage(args) - } catch (doe: DeadObjectException) { - ipcListeners[channel]?.get(eventName)?.remove(it) - } catch (t: Throwable) { - logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG) - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt @@ -1,57 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl - -import com.google.gson.JsonObject -import me.rhunk.snapenhance.common.logger.AbstractLogger -import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -import me.rhunk.snapenhance.scripting.RemoteScriptManager -import java.io.File - -class RemoteScriptConfig( - private val remoteScriptManager: RemoteScriptManager, - moduleInfo: ModuleInfo, - private val logger: AbstractLogger, -) : ConfigInterface() { - private val configFile = File(remoteScriptManager.getModuleDataFolder(moduleInfo.name), "config.json") - private var config = JsonObject() - - override fun get(key: String, defaultValue: Any?): String? { - return config[key]?.asString ?: defaultValue?.toString() - } - - override fun set(key: String, value: Any?, save: Boolean) { - when (value) { - is Int -> config.addProperty(key, value) - is Double -> config.addProperty(key, value) - is Boolean -> config.addProperty(key, value) - is Long -> config.addProperty(key, value) - is Float -> config.addProperty(key, value) - is Byte -> config.addProperty(key, value) - is Short -> config.addProperty(key, value) - else -> config.addProperty(key, value?.toString()) - } - - if (save) save() - } - - override fun save() { - configFile.writeText(config.toString()) - } - - override fun load() { - runCatching { - if (!configFile.exists()) { - save() - return@runCatching - } - config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java) - }.onFailure { - logger.error("Failed to load config file", it) - save() - } - } - - override fun delete() { - configFile.delete() - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt @@ -1,13 +1,13 @@ package me.rhunk.snapenhance.scripting.impl.ui -import me.rhunk.snapenhance.common.logger.AbstractLogger -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.scripting.impl.ui.components.Node import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode -import org.mozilla.javascript.Context import org.mozilla.javascript.Function import org.mozilla.javascript.annotations.JSFunction @@ -73,27 +73,31 @@ class InterfaceBuilder { -class InterfaceManager( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger -) { +class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) { private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>() fun buildInterface(name: String): InterfaceBuilder? { return interfaces[name]?.invoke() } + override fun onDispose() { + interfaces.clear() + } + + @Suppress("unused") @JSFunction fun create(name: String, callback: Function) { interfaces[name] = { val interfaceBuilder = InterfaceBuilder() runCatching { - Context.enter() - callback.call(Context.getCurrentContext(), callback, callback, arrayOf(interfaceBuilder)) - Context.exit() + contextScope { + callback.call(this, callback, callback, arrayOf(interfaceBuilder)) + } interfaceBuilder }.onFailure { - logger.error("Failed to create interface $name for ${moduleInfo.name}", it) + context.runtime.logger.error("Failed to create interface $name for ${context.moduleInfo.name}", it) }.getOrNull() } } + + override fun getObject() = this } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.filled.LibraryBooks import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* @@ -14,6 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -70,12 +72,27 @@ class ScriptsSection : Section() { } Switch( checked = enabled, - onCheckedChange = { - context.modDatabase.setScriptEnabled(script.name, it) - if (it) { - context.scriptManager.loadScript(script.name) + onCheckedChange = { isChecked -> + context.modDatabase.setScriptEnabled(script.name, isChecked) + enabled = isChecked + runCatching { + val modulePath = context.scriptManager.getModulePath(script.name)!! + context.scriptManager.unloadScript(modulePath) + if (isChecked) { + context.scriptManager.loadScript(modulePath) + context.scriptManager.runtime.getModuleByName(script.name) + ?.callFunction("module.onSnapEnhanceLoad") + context.shortToast("Loaded script ${script.name}") + } else { + context.shortToast("Unloaded script ${script.name}") + } + }.onFailure { throwable -> + enabled = !isChecked + ("Failed to ${if (isChecked) "enable" else "disable"} script").let { + context.log.error(it, throwable) + context.shortToast(it) + } } - enabled = it } ) } @@ -130,7 +147,7 @@ class ScriptsSection : Section() { val settingsInterface = remember { val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null runCatching { - (module.extras["im"] as? InterfaceManager)?.buildInterface("settings") + (module.getBinding(InterfaceManager::class))?.buildInterface("settings") }.onFailure { settingsError = it }.getOrNull() @@ -228,4 +245,18 @@ class ScriptsSection : Section() { ) } } + + @Composable + override fun TopBarActions(rowScope: RowScope) { + rowScope.apply { + IconButton(onClick = { + context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply { + data = "https://github.com/SnapEnhance/docs".toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }) + }) { + Icon(imageVector = Icons.Default.LibraryBooks, contentDescription = "Documentation") + } + } + } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/logger/LogChannel.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/logger/LogChannel.kt @@ -5,6 +5,8 @@ enum class LogChannel( val shortName: String ) { CORE("SnapEnhanceCore", "core"), + COMMON("SnapEnhanceCommon", "common"), + SCRIPTING("Scripting", "scripting"), NATIVE("SnapEnhanceNative", "native"), MANAGER("SnapEnhanceManager", "manager"), XPOSED("LSPosed-Bridge", "xposed"); diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -2,6 +2,8 @@ package me.rhunk.snapenhance.common.scripting import android.os.Handler import android.widget.Toast +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.common.scripting.ktx.putFunction import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject @@ -12,15 +14,23 @@ import org.mozilla.javascript.ScriptableObject import org.mozilla.javascript.Undefined import org.mozilla.javascript.Wrapper import java.lang.reflect.Modifier +import kotlin.reflect.KClass class JSModule( val scriptRuntime: ScriptRuntime, val moduleInfo: ModuleInfo, val content: String, ) { - val extras = mutableMapOf<String, Any>() + private val moduleBindings = mutableMapOf<String, AbstractBinding>() private lateinit var moduleObject: ScriptableObject + private val moduleBindingContext by lazy { + BindingsContext( + moduleInfo = moduleInfo, + runtime = scriptRuntime + ) + } + fun load(block: ScriptableObject.() -> Unit) { contextScope { val classLoader = scriptRuntime.androidContext.classLoader @@ -33,7 +43,7 @@ class JSModule( putConst("author", this, moduleInfo.author) putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion) putConst("minSEVersion", this, moduleInfo.minSEVersion) - putConst("grantPermissions", this, moduleInfo.grantPermissions) + putConst("grantedPermissions", this, moduleInfo.grantedPermissions) }) }) @@ -62,12 +72,16 @@ class JSModule( moduleObject.putFunction("findClass") { val className = it?.get(0).toString() - classLoader.loadClass(className) + runCatching { + classLoader.loadClass(className) + }.onFailure { throwable -> + scriptRuntime.logger.error("Failed to load class $className", throwable) + }.getOrNull() } moduleObject.putFunction("type") { args -> val className = args?.get(0).toString() - val clazz = classLoader.loadClass(className) + val clazz = runCatching { classLoader.loadClass(className) }.getOrNull() ?: return@putFunction Undefined.instance scriptableObject("JavaClassWrapper") { putFunction("newInstance") newInstance@{ args -> @@ -95,12 +109,12 @@ class JSModule( } moduleObject.putFunction("logInfo") { args -> - scriptRuntime.logger.info(args?.joinToString(" ") { - when (it) { - is Wrapper -> it.unwrap().toString() - else -> it.toString() - } - } ?: "null") + scriptRuntime.logger.info(argsToString(args)) + Undefined.instance + } + + moduleObject.putFunction("logError") { args -> + scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.get(1) as? Throwable ?: Throwable()) Undefined.instance } @@ -116,16 +130,38 @@ class JSModule( Undefined.instance } } + block(moduleObject) - extras.forEach { (key, value) -> - moduleObject.putConst(key, moduleObject, value) + + moduleBindings.forEach { (_, instance) -> + instance.context = moduleBindingContext + + runCatching { + instance.onInit() + }.onFailure { + scriptRuntime.logger.error("Failed to init binding ${instance.name}", it) + } } + + moduleObject.putFunction("require") { args -> + val bindingName = args?.get(0).toString() + moduleBindings[bindingName]?.getObject() + } + evaluateString(moduleObject, content, moduleInfo.name, 1, null) } } fun unload() { callFunction("module.onUnload") + moduleBindings.entries.removeIf { (name, binding) -> + runCatching { + binding.onDispose() + }.onFailure { + scriptRuntime.logger.error("Failed to dispose binding $name", it) + } + true + } } fun callFunction(name: String, vararg args: Any?) { @@ -143,4 +179,25 @@ class JSModule( } } } + fun registerBindings(vararg bindings: AbstractBinding) { + bindings.forEach { + moduleBindings[it.name] = it.apply { + context = moduleBindingContext + } + } + } + + @Suppress("UNCHECKED_CAST") + fun <T : Any> getBinding(clazz: KClass<T>): T? { + return moduleBindings.values.find { clazz.isInstance(it) } as? T + } + + private fun argsToString(args: Array<out Any?>?): String { + return args?.joinToString(" ") { + when (it) { + is Wrapper -> it.unwrap().toString() + else -> it.toString() + } + } ?: "null" + } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.common.scripting import android.content.Context +import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import org.mozilla.javascript.ScriptableObject @@ -10,9 +11,13 @@ import java.io.InputStream open class ScriptRuntime( val androidContext: Context, - val logger: AbstractLogger, + logger: AbstractLogger, ) { + val logger = ScriptingLogger(logger) + + lateinit var scripting: IScripting var buildModuleObject: ScriptableObject.(JSModule) -> Unit = {} + private val modules = mutableMapOf<String, JSModule>() fun eachModule(f: JSModule.() -> Unit) { @@ -55,7 +60,7 @@ open class ScriptRuntime( author = properties["author"], minSnapchatVersion = properties["minSnapchatVersion"]?.toLong(), minSEVersion = properties["minSEVersion"]?.toLong(), - grantPermissions = properties["permissions"]?.split(",")?.map { it.trim() }, + grantedPermissions = properties["permissions"]?.split(",")?.map { it.trim() } ?: emptyList(), ) } @@ -63,19 +68,15 @@ open class ScriptRuntime( return readModuleInfo(inputStream.bufferedReader()) } - fun reload(path: String, content: String) { - unload(path) - load(path, content) - } - - private fun unload(path: String) { - val module = modules[path] ?: return + fun unload(scriptPath: String) { + val module = modules[scriptPath] ?: return + logger.info("Unloading module $scriptPath") module.unload() - modules.remove(path) + modules.remove(scriptPath) } - fun load(path: String, content: String): JSModule? { - logger.info("Loading module $path") + fun load(scriptPath: String, content: String): JSModule? { + logger.info("Loading module $scriptPath") return runCatching { JSModule( scriptRuntime = this, @@ -85,10 +86,10 @@ open class ScriptRuntime( load { buildModuleObject(this, this@apply) } - modules[path] = this + modules[scriptPath] = this } }.onFailure { - logger.error("Failed to load module $path", it) + logger.error("Failed to load module $scriptPath", it) }.getOrNull() } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptingLogger.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptingLogger.kt @@ -0,0 +1,40 @@ +package me.rhunk.snapenhance.common.scripting + +import me.rhunk.snapenhance.common.logger.AbstractLogger +import me.rhunk.snapenhance.common.logger.LogChannel + +class ScriptingLogger( + private val logger: AbstractLogger +) { + companion object { + private val TAG = LogChannel.SCRIPTING.channel + } + + fun debug(message: Any?, tag: String = TAG) { + logger.debug(message, tag) + } + + fun error(message: Any?, tag: String = TAG) { + logger.error(message, tag) + } + + fun error(message: Any?, throwable: Throwable, tag: String = TAG) { + logger.error(message, throwable, tag) + } + + fun info(message: Any?, tag: String = TAG) { + logger.info(message, tag) + } + + fun verbose(message: Any?, tag: String = TAG) { + logger.verbose(message, tag) + } + + fun warn(message: Any?, tag: String = TAG) { + logger.warn(message, tag) + } + + fun assert(message: Any?, tag: String = TAG) { + logger.assert(message, tag) + } +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.common.scripting.bindings + +abstract class AbstractBinding( + val name: String, + val side: BindingSide +) { + lateinit var context: BindingsContext + + open fun onInit() {} + + open fun onDispose() {} + + abstract fun getObject(): Any +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingSide.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingSide.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.common.scripting.bindings + +enum class BindingSide( + val key: String +) { + COMMON("common"), + CORE("core"), + MANAGER("manager"); + + companion object { + fun fromKey(key: String): BindingSide { + return entries.firstOrNull { it.key == key } ?: COMMON + } + } +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingsContext.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingsContext.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.common.scripting.bindings + +import me.rhunk.snapenhance.common.scripting.ScriptRuntime +import me.rhunk.snapenhance.common.scripting.type.ModuleInfo + +class BindingsContext( + val moduleInfo: ModuleInfo, + val runtime: ScriptRuntime +)+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/ConfigInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/ConfigInterface.kt @@ -1,5 +1,7 @@ package me.rhunk.snapenhance.common.scripting.impl +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import org.mozilla.javascript.annotations.JSFunction @@ -18,7 +20,8 @@ enum class ConfigTransactionType( } -abstract class ConfigInterface { +@Suppress("unused") +abstract class ConfigInterface : AbstractBinding("config", BindingSide.COMMON) { @JSFunction fun get(key: String): String? = get(key, null) @JSFunction abstract fun get(key: String, defaultValue: Any?): String? @@ -70,5 +73,7 @@ abstract class ConfigInterface { @JSFunction abstract fun save() @JSFunction abstract fun load() - @JSFunction abstract fun delete() + @JSFunction abstract fun deleteConfig() + + override fun getObject() = this } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/IPCInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/IPCInterface.kt @@ -1,8 +1,11 @@ package me.rhunk.snapenhance.common.scripting.impl -typealias Listener = (Array<out String?>) -> Unit +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide -abstract class IPCInterface { +typealias Listener = (List<String?>) -> Unit + +abstract class IPCInterface : AbstractBinding("ipc", BindingSide.COMMON) { abstract fun on(eventName: String, listener: Listener) abstract fun onBroadcast(channel: String, eventName: String, listener: Listener) @@ -13,5 +16,8 @@ abstract class IPCInterface { @Suppress("unused") fun emit(eventName: String) = emit(eventName, *emptyArray()) @Suppress("unused") - fun emit(channel: String, eventName: String) = broadcast(channel, eventName) + fun broadcast(channel: String, eventName: String) = + broadcast(channel, eventName, *emptyArray()) + + override fun getObject() = this } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt @@ -7,5 +7,5 @@ data class ModuleInfo( val author: String? = null, val minSnapchatVersion: Long? = null, val minSEVersion: Long? = null, - val grantPermissions: List<String>? = null, + val grantedPermissions: List<String>, ) \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -17,8 +17,8 @@ import me.rhunk.snapenhance.common.data.MessagingGroupInfo import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.bridge.loadFromBridge import me.rhunk.snapenhance.core.data.SnapClassCache -import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.util.LSPatchUpdater import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -142,7 +142,7 @@ class SnapEnhance { bridgeClient.registerMessagingBridge(messagingBridge) features.init() scriptRuntime.connect(bridgeClient.getScriptingInterface()) - scriptRuntime.eachModule { callFunction("module.onBeforeApplicationLoad", androidContext) } + scriptRuntime.eachModule { callFunction("module.onSnapApplicationLoad", androidContext) } syncRemote() } } @@ -151,7 +151,7 @@ class SnapEnhance { measureTimeMillis { with(appContext) { features.onActivityCreate() - scriptRuntime.eachModule { callFunction("module.onSnapActivity", mainActivity!!) } + scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", mainActivity!!) } } }.also { time -> appContext.log.verbose("onActivityCreate took $time") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt @@ -7,22 +7,21 @@ import me.rhunk.snapenhance.common.scripting.ScriptRuntime import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.scripting.impl.CoreIPC import me.rhunk.snapenhance.core.scripting.impl.CoreScriptConfig -import me.rhunk.snapenhance.core.scripting.impl.ScriptHooker +import me.rhunk.snapenhance.core.scripting.impl.CoreScriptHooker class CoreScriptRuntime( private val modContext: ModContext, logger: AbstractLogger, ): ScriptRuntime(modContext.androidContext, logger) { - private val scriptHookers = mutableListOf<ScriptHooker>() - fun connect(scriptingInterface: IScripting) { + scripting = scriptingInterface scriptingInterface.apply { buildModuleObject = { module -> - module.extras["ipc"] = CoreIPC(this@apply, module.moduleInfo) - module.extras["hooker"] = ScriptHooker(module.moduleInfo, logger, androidContext.classLoader).also { - scriptHookers.add(it) - } - module.extras["config"] = CoreScriptConfig(this@apply, module.moduleInfo) + module.registerBindings( + CoreScriptConfig(), + CoreIPC(), + CoreScriptHooker(), + ) } enabledScripts.forEach { path -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreIPC.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreIPC.kt @@ -1,32 +1,27 @@ package me.rhunk.snapenhance.core.scripting.impl import me.rhunk.snapenhance.bridge.scripting.IPCListener -import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.scripting.impl.IPCInterface import me.rhunk.snapenhance.common.scripting.impl.Listener -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -class CoreIPC( - private val scripting: IScripting, - private val moduleInfo: ModuleInfo -) : IPCInterface() { +class CoreIPC : IPCInterface() { override fun onBroadcast(channel: String, eventName: String, listener: Listener) { - scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() { + context.runtime.scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() { override fun onMessage(args: Array<out String?>) { - listener(args) + listener(args.toList()) } }) } override fun on(eventName: String, listener: Listener) { - onBroadcast(moduleInfo.name, eventName, listener) + onBroadcast(context.moduleInfo.name, eventName, listener) } override fun emit(eventName: String, vararg args: String?) { - broadcast(moduleInfo.name, eventName, *args) + broadcast(context.moduleInfo.name, eventName, *args) } override fun broadcast(channel: String, eventName: String, vararg args: String?) { - scripting.sendIPCMessage(channel, eventName, args) + context.runtime.scripting.sendIPCMessage(channel, eventName, args) } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptConfig.kt @@ -1,31 +1,26 @@ package me.rhunk.snapenhance.core.scripting.impl -import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -class CoreScriptConfig( - private val scripting: IScripting, - private val moduleInfo: ModuleInfo -): ConfigInterface() { +class CoreScriptConfig: ConfigInterface() { override fun get(key: String, defaultValue: Any?): String? { - return scripting.configTransaction(moduleInfo.name, ConfigTransactionType.GET.key, key, defaultValue.toString(), false) + return context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.GET.key, key, defaultValue.toString(), false) } override fun set(key: String, value: Any?, save: Boolean) { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.SET.key, key, value.toString(), save) + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.SET.key, key, value.toString(), save) } override fun save() { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.SAVE.key, null, null, false) + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.SAVE.key, null, null, false) } override fun load() { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.LOAD.key, null, null, false) + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.LOAD.key, null, null, false) } - override fun delete() { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.DELETE.key, null, null, false) + override fun deleteConfig() { + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.DELETE.key, null, null, false) } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt @@ -0,0 +1,168 @@ +package me.rhunk.snapenhance.core.scripting.impl + +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject +import me.rhunk.snapenhance.common.scripting.toPrimitiveValue +import me.rhunk.snapenhance.core.util.hook.HookAdapter +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.Hooker +import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.hook.hookConstructor +import org.mozilla.javascript.annotations.JSGetter +import org.mozilla.javascript.annotations.JSSetter +import java.lang.reflect.Constructor +import java.lang.reflect.Member +import java.lang.reflect.Method + + +class ScriptHookCallback( + private val hookAdapter: HookAdapter +) { + var result + @JSGetter("result") get() = hookAdapter.getResult() + @JSSetter("result") set(result) = hookAdapter.setResult(result.toPrimitiveValue(lazy { + when (val member = hookAdapter.method()) { + is Method -> member.returnType.name + else -> "void" + } + })) + + val thisObject + @JSGetter("thisObject") get() = hookAdapter.nullableThisObject<Any>() + + val method + @JSGetter("method") get() = hookAdapter.method() + + val args + @JSGetter("args") get() = hookAdapter.args().toList() + + private val parameterTypes by lazy { + when (val member = hookAdapter.method()) { + is Method -> member.parameterTypes + is Constructor<*> -> member.parameterTypes + else -> emptyArray() + }.toList() + } + + fun cancel() = hookAdapter.setResult(null) + + fun arg(index: Int) = hookAdapter.argNullable<Any>(index) + + fun setArg(index: Int, value: Any?) { + hookAdapter.setArg(index, value.toPrimitiveValue(lazy { parameterTypes[index].name })) + } + + fun invokeOriginal() = hookAdapter.invokeOriginal() + + fun invokeOriginal(args: Array<Any>) = hookAdapter.invokeOriginal(args.map { + it.toPrimitiveValue(lazy { parameterTypes[args.indexOf(it)].name }) ?: it + }.toTypedArray()) + + override fun toString(): String { + return "ScriptHookCallback(\n" + + " thisObject=${ runCatching { thisObject.toString() }.getOrNull() },\n" + + " args=${ runCatching { args.toString() }.getOrNull() }\n" + + " result=${ runCatching { result.toString() }.getOrNull() },\n" + + ")" + } +} + + +typealias HookCallback = (ScriptHookCallback) -> Unit +typealias HookUnhook = () -> Unit + +@Suppress("unused") +class CoreScriptHooker: AbstractBinding("hooker", BindingSide.CORE) { + private val hooks = mutableListOf<HookUnhook>() + + val stage = scriptableObject { + putConst("BEFORE", this, "before") + putConst("AFTER", this, "after") + } + + private fun findClassSafe(className: String): Class<*>? { + return runCatching { + context.runtime.androidContext.classLoader.loadClass(className) + }.onFailure { + context.runtime.logger.warn("Failed to load class $className") + }.getOrNull() + } + + private fun getHookStageFromString(stage: String): HookStage { + return when (stage) { + "before" -> HookStage.BEFORE + "after" -> HookStage.AFTER + else -> throw IllegalArgumentException("Invalid stage: $stage") + } + } + + fun findMethod(clazz: Class<*>, methodName: String): Member? { + return clazz.declaredMethods.find { it.name == methodName } + } + + fun findMethodWithParameters(clazz: Class<*>, methodName: String, vararg types: String): Member? { + return clazz.declaredMethods.find { method -> method.name == methodName && method.parameterTypes.map { it.name }.toTypedArray() contentEquals types } + } + + fun findMethod(className: String, methodName: String): Member? { + return findClassSafe(className)?.let { findMethod(it, methodName) } + } + + fun findMethodWithParameters(className: String, methodName: String, vararg types: String): Member? { + return findClassSafe(className)?.let { findMethodWithParameters(it, methodName, *types) } + } + + fun findConstructor(clazz: Class<*>, vararg types: String): Member? { + return clazz.declaredConstructors.find { constructor -> constructor.parameterTypes.map { it.name }.toTypedArray() contentEquals types } + } + + fun findConstructorParameters(className: String, vararg types: String): Member? { + return findClassSafe(className)?.let { findConstructor(it, *types) } + } + + // -- hooking + + fun hook(method: Member, stage: String, callback: HookCallback): HookUnhook { + val hookAdapter = Hooker.hook(method, getHookStageFromString(stage)) { + callback(ScriptHookCallback(it)) + } + + return { + hookAdapter.unhook() + }.also { hooks.add(it) } + } + + fun hookAllMethods(clazz: Class<*>, methodName: String, stage: String, callback: HookCallback): HookUnhook { + val hookAdapter = clazz.hook(methodName, getHookStageFromString(stage)) { + callback(ScriptHookCallback(it)) + } + + return { + hookAdapter.forEach { it.unhook() } + }.also { hooks.add(it) } + } + + fun hookAllConstructors(clazz: Class<*>, stage: String, callback: HookCallback): HookUnhook { + val hookAdapter = clazz.hookConstructor(getHookStageFromString(stage)) { + callback(ScriptHookCallback(it)) + } + + return { + hookAdapter.forEach { it.unhook() } + }.also { hooks.add(it) } + } + + fun hookAllMethods(className: String, methodName: String, stage: String, callback: HookCallback) + = findClassSafe(className)?.let { hookAllMethods(it, methodName, stage, callback) } + + fun hookAllConstructors(className: String, stage: String, callback: HookCallback) + = findClassSafe(className)?.let { hookAllConstructors(it, stage, callback) } + + override fun onDispose() { + hooks.forEach { it() } + hooks.clear() + } + + override fun getObject() = this +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/ScriptHooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/ScriptHooker.kt @@ -1,161 +0,0 @@ -package me.rhunk.snapenhance.core.scripting.impl - -import me.rhunk.snapenhance.common.logger.AbstractLogger -import me.rhunk.snapenhance.common.scripting.toPrimitiveValue -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -import me.rhunk.snapenhance.core.util.hook.HookAdapter -import me.rhunk.snapenhance.core.util.hook.HookStage -import me.rhunk.snapenhance.core.util.hook.Hooker -import me.rhunk.snapenhance.core.util.hook.hook -import me.rhunk.snapenhance.core.util.hook.hookConstructor -import org.mozilla.javascript.annotations.JSGetter -import org.mozilla.javascript.annotations.JSSetter -import java.lang.reflect.Constructor -import java.lang.reflect.Member -import java.lang.reflect.Method - - -class ScriptHookCallback( - private val hookAdapter: HookAdapter -) { - var result - @JSGetter("result") get() = hookAdapter.getResult() - @JSSetter("result") set(result) = hookAdapter.setResult(result.toPrimitiveValue(lazy { - when (val member = hookAdapter.method()) { - is Method -> member.returnType.name - else -> "void" - } - })) - - val thisObject - @JSGetter("thisObject") get() = hookAdapter.nullableThisObject<Any>() - - val method - @JSGetter("method") get() = hookAdapter.method() - - val args - @JSGetter("args") get() = hookAdapter.args().toList() - - private val parameterTypes by lazy { - when (val member = hookAdapter.method()) { - is Method -> member.parameterTypes - is Constructor<*> -> member.parameterTypes - else -> emptyArray() - }.toList() - } - - fun cancel() = hookAdapter.setResult(null) - - fun arg(index: Int) = hookAdapter.argNullable<Any>(index) - - fun setArg(index: Int, value: Any?) { - hookAdapter.setArg(index, value.toPrimitiveValue(lazy { parameterTypes[index].name })) - } - - fun invokeOriginal() = hookAdapter.invokeOriginal() - - fun invokeOriginal(args: Array<Any>) = hookAdapter.invokeOriginal(args.map { - it.toPrimitiveValue(lazy { parameterTypes[args.indexOf(it)].name }) ?: it - }.toTypedArray()) - - override fun toString(): String { - return "ScriptHookCallback(\n" + - " thisObject=${ runCatching { thisObject.toString() }.getOrNull() },\n" + - " args=${ runCatching { args.toString() }.getOrNull() }\n" + - " result=${ runCatching { result.toString() }.getOrNull() },\n" + - ")" - } -} - - -typealias HookCallback = (ScriptHookCallback) -> Unit -typealias HookUnhook = () -> Unit - -@Suppress("unused", "MemberVisibilityCanBePrivate") -class ScriptHooker( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger, - private val classLoader: ClassLoader -) { - private val hooks = mutableListOf<HookUnhook>() - - // -- search for class members - - private fun findClassSafe(className: String): Class<*>? { - return runCatching { - classLoader.loadClass(className) - }.onFailure { - logger.warn("Failed to load class $className") - }.getOrNull() - } - - private fun getHookStageFromString(stage: String): HookStage { - return when (stage) { - "before" -> HookStage.BEFORE - "after" -> HookStage.AFTER - else -> throw IllegalArgumentException("Invalid stage: $stage") - } - } - - fun findMethod(clazz: Class<*>, methodName: String): Member? { - return clazz.declaredMethods.find { it.name == methodName } - } - - fun findMethodWithParameters(clazz: Class<*>, methodName: String, vararg types: String): Member? { - return clazz.declaredMethods.find { method -> method.name == methodName && method.parameterTypes.map { it.name }.toTypedArray() contentEquals types } - } - - fun findMethod(className: String, methodName: String): Member? { - return findClassSafe(className)?.let { findMethod(it, methodName) } - } - - fun findMethodWithParameters(className: String, methodName: String, vararg types: String): Member? { - return findClassSafe(className)?.let { findMethodWithParameters(it, methodName, *types) } - } - - fun findConstructor(clazz: Class<*>, vararg types: String): Member? { - return clazz.declaredConstructors.find { constructor -> constructor.parameterTypes.map { it.name }.toTypedArray() contentEquals types } - } - - fun findConstructorParameters(className: String, vararg types: String): Member? { - return findClassSafe(className)?.let { findConstructor(it, *types) } - } - - // -- hooking - - fun hook(method: Member, stage: String, callback: HookCallback): HookUnhook { - val hookAdapter = Hooker.hook(method, getHookStageFromString(stage)) { - callback(ScriptHookCallback(it)) - } - - return { - hookAdapter.unhook() - }.also { hooks.add(it) } - } - - fun hookAllMethods(clazz: Class<*>, methodName: String, stage: String, callback: HookCallback): HookUnhook { - val hookAdapter = clazz.hook(methodName, getHookStageFromString(stage)) { - callback(ScriptHookCallback(it)) - } - - return { - hookAdapter.forEach { it.unhook() } - }.also { hooks.add(it) } - } - - fun hookAllConstructors(clazz: Class<*>, stage: String, callback: HookCallback): HookUnhook { - val hookAdapter = clazz.hookConstructor(getHookStageFromString(stage)) { - callback(ScriptHookCallback(it)) - } - - return { - hookAdapter.forEach { it.unhook() } - }.also { hooks.add(it) } - } - - fun hookAllMethods(className: String, methodName: String, stage: String, callback: HookCallback) - = findClassSafe(className)?.let { hookAllMethods(it, methodName, stage, callback) } - - fun hookAllConstructors(className: String, stage: String, callback: HookCallback) - = findClassSafe(className)?.let { hookAllConstructors(it, stage, callback) } -}- \ No newline at end of file