feat(scripting): module system

This commit is contained in:
rhunk
2023-12-24 17:29:19 +01:00
parent 7d6978f961
commit 392cd95dac
22 changed files with 315 additions and 129 deletions

View File

@ -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");

View File

@ -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"
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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>,
)