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

@ -118,7 +118,7 @@ class RemoteSideContext(
}
scriptManager.runtime.eachModule {
callFunction("module.onManagerLoad", androidContext)
callFunction("module.onSnapEnhanceLoad", androidContext)
}
}

View File

@ -268,7 +268,7 @@ class ModDatabase(
version = cursor.getStringOrNull("version")!!,
description = cursor.getStringOrNull("description"),
author = cursor.getStringOrNull("author"),
grantPermissions = null
grantedPermissions = emptyList()
)
)
}

View File

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

View File

@ -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<String, MutableMap<String, MutableSet<IPCListener>>> // 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<out String?>) {
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)
}
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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