From 392cd95dacaced5aa1c297fb06d34545f6c8c4c6 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 24 Dec 2023 17:29:19 +0100 Subject: [PATCH] feat(scripting): module system --- .../me/rhunk/snapenhance/RemoteSideContext.kt | 2 +- .../snapenhance/messaging/ModDatabase.kt | 2 +- .../scripting/RemoteScriptManager.kt | 38 +++++---- .../{RemoteManagerIPC.kt => ManagerIPC.kt} | 16 ++-- ...ScriptConfig.kt => ManagerScriptConfig.kt} | 18 ++--- .../scripting/impl/ui/InterfaceManager.kt | 26 +++--- .../sections/scripting/ScriptsSection.kt | 43 ++++++++-- .../snapenhance/common/logger/LogChannel.kt | 2 + .../snapenhance/common/scripting/JSModule.kt | 81 ++++++++++++++++--- .../common/scripting/ScriptRuntime.kt | 29 +++---- .../common/scripting/ScriptingLogger.kt | 40 +++++++++ .../scripting/bindings/AbstractBinding.kt | 14 ++++ .../common/scripting/bindings/BindingSide.kt | 15 ++++ .../scripting/bindings/BindingsContext.kt | 9 +++ .../common/scripting/impl/ConfigInterface.kt | 9 ++- .../common/scripting/impl/IPCInterface.kt | 12 ++- .../common/scripting/type/ModuleInfo.kt | 2 +- .../me/rhunk/snapenhance/core/SnapEnhance.kt | 6 +- .../core/scripting/CoreScriptRuntime.kt | 15 ++-- .../core/scripting/impl/CoreIPC.kt | 17 ++-- .../core/scripting/impl/CoreScriptConfig.kt | 19 ++--- .../{ScriptHooker.kt => CoreScriptHooker.kt} | 29 ++++--- 22 files changed, 315 insertions(+), 129 deletions(-) rename app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/{RemoteManagerIPC.kt => ManagerIPC.kt} (74%) rename app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/{RemoteScriptConfig.kt => ManagerScriptConfig.kt} (75%) create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptingLogger.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingSide.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingsContext.kt rename core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/{ScriptHooker.kt => CoreScriptHooker.kt} (88%) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt index 931dfac1..1f379631 100644 --- 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 index 85ed56cc..2a04a68f 100644 --- 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 index ac3a027d..83a592d9 100644 --- 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 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/RemoteManagerIPC.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt similarity index 74% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteManagerIPC.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt index 32860f1b..57f5902c 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteManagerIPC.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt @@ -2,17 +2,13 @@ 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>> // channel, eventName -> listeners -class RemoteManagerIPC( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger, +class ManagerIPC( private val ipcListeners: IPCListeners = ConcurrentHashMap(), ) : IPCInterface() { companion object { @@ -20,22 +16,22 @@ class RemoteManagerIPC( } 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?) { - emit(moduleInfo.name, eventName, *args) + 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) { try { - listener(args) + listener(args.toList()) } 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) + context.runtime.logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG) } } }) @@ -48,7 +44,7 @@ class RemoteManagerIPC( } 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) + context.runtime.logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt similarity index 75% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt index a9776585..552f6709 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt @@ -1,18 +1,14 @@ 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, +class ManagerScriptConfig( + private val remoteScriptManager: RemoteScriptManager ) : ConfigInterface() { - private val configFile = File(remoteScriptManager.getModuleDataFolder(moduleInfo.name), "config.json") + 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? { @@ -46,12 +42,16 @@ class RemoteScriptConfig( } config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java) }.onFailure { - logger.error("Failed to load config file", it) + context.runtime.logger.error("Failed to load config file", it) save() } } - override fun delete() { + 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/ui/InterfaceManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt index e4c2e4fa..fbfc9c60 100644 --- 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 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 index d8ae0602..28f99f0c 100644 --- 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 index 1d3024bd..0d9696e5 100644 --- 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 index 161da228..e61cb3c3 100644 --- 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() + private val moduleBindings = mutableMapOf() 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 getBinding(clazz: KClass): T? { + return moduleBindings.values.find { clazz.isInstance(it) } as? T + } + + private fun argsToString(args: Array?): 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 index 9c2678ba..b9bed87d 100644 --- 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() 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 new file mode 100644 index 00000000..f9406095 --- /dev/null +++ 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 new file mode 100644 index 00000000..0baf42fe --- /dev/null +++ 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 new file mode 100644 index 00000000..2ab8c971 --- /dev/null +++ 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 new file mode 100644 index 00000000..8b250bb7 --- /dev/null +++ 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 index bc4f7a1d..acc54acd 100644 --- 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 index 8eb42762..ca2a0115 100644 --- 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) -> Unit +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide -abstract class IPCInterface { +typealias Listener = (List) -> 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 index 1bdeb2ea..047536a1 100644 --- 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? = null, + val grantedPermissions: List, ) \ 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 index 8e222f5a..a2199184 100644 --- 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 index 32362ed6..bd39c99f 100644 --- 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() - 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 index 8725e021..18ae8bb5 100644 --- 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) { - 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 index 81177482..4baba70e 100644 --- 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/ScriptHooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt similarity index 88% rename from core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/ScriptHooker.kt rename to core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt index b53c80e3..ca127a95 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/ScriptHooker.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt @@ -1,8 +1,9 @@ package me.rhunk.snapenhance.core.scripting.impl -import me.rhunk.snapenhance.common.logger.AbstractLogger +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.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 @@ -71,21 +72,20 @@ class ScriptHookCallback( typealias HookCallback = (ScriptHookCallback) -> Unit typealias HookUnhook = () -> Unit -@Suppress("unused", "MemberVisibilityCanBePrivate") -class ScriptHooker( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger, - private val classLoader: ClassLoader -) { +@Suppress("unused") +class CoreScriptHooker: AbstractBinding("hooker", BindingSide.CORE) { private val hooks = mutableListOf() - // -- search for class members + val stage = scriptableObject { + putConst("BEFORE", this, "before") + putConst("AFTER", this, "after") + } private fun findClassSafe(className: String): Class<*>? { return runCatching { - classLoader.loadClass(className) + context.runtime.androidContext.classLoader.loadClass(className) }.onFailure { - logger.warn("Failed to load class $className") + context.runtime.logger.warn("Failed to load class $className") }.getOrNull() } @@ -158,4 +158,11 @@ class ScriptHooker( 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