commit 664c6211ca937383730d381eb264850d249db711
parent 47efdf165df5da90f7e66757e7bfbac16ff681ba
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun, 18 Aug 2024 22:33:01 +0200

feat(scripting): support bridge reload
- add module.onBridgeConnected
- add isBridgeAlive in ipc binding

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt | 13++++++++-----
Mapp/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt | 9++++++---
Mcommon/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl | 2+-
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt | 15+++++++++++++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt | 11+++++++++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/IPCInterface.kt | 8++++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt | 7++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt | 53+++++++++++++++++++++++++++++++++--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreIPC.kt | 20+++++++++++---------
10 files changed, 98 insertions(+), 42 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -204,14 +204,17 @@ class RemoteScriptManager( ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(listener) } - override fun sendIPCMessage(channel: String, eventName: String, args: Array<out String>) { - runCatching { - ipcListeners[channel]?.get(eventName)?.toList()?.forEach { + override fun sendIPCMessage(channel: String, eventName: String, args: Array<out String>): Int { + var dispatchCount = 0 + ipcListeners[channel]?.get(eventName)?.toList()?.forEach { + runCatching { it.onMessage(args) + dispatchCount++ + }.onFailure { + context.log.error("Failed to send message for $eventName", it) } - }.onFailure { - context.log.error("Failed to send message for $eventName", it) } + return dispatchCount } override fun configTransaction( 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 @@ -19,8 +19,8 @@ class ManagerIPC( onBroadcast(context.moduleInfo.name, eventName, listener) } - override fun emit(eventName: String, vararg args: String?) { - emit(context.moduleInfo.name, eventName, *args) + override fun emit(eventName: String, vararg args: String?): Int { + return broadcast(context.moduleInfo.name, eventName, *args) } override fun onBroadcast(channel: String, eventName: String, listener: Listener) { @@ -37,15 +37,18 @@ class ManagerIPC( }) } - override fun broadcast(channel: String, eventName: String, vararg args: String?) { + override fun broadcast(channel: String, eventName: String, vararg args: String?): Int { + var dispatchCount = 0 ipcListeners[channel]?.get(eventName)?.toList()?.forEach { try { it.onMessage(args) + dispatchCount++ } 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) } } + return dispatchCount } } \ No newline at end of file diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl @@ -10,7 +10,7 @@ interface IScripting { oneway void registerIPCListener(String channel, String eventName, IPCListener listener); - oneway void sendIPCMessage(String channel, String eventName, in String[] args); + int sendIPCMessage(String channel, String eventName, in String[] args); @nullable String configTransaction(String module, String action, @nullable String key, @nullable String value, boolean save); 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 @@ -230,6 +230,7 @@ class JSModule( } } } + fun registerBindings(vararg bindings: AbstractBinding) { bindings.forEach { moduleBindings[it.name] = it.apply { @@ -238,6 +239,20 @@ class JSModule( } } + fun onBridgeConnected(reloaded: Boolean = false) { + if (reloaded) { + moduleBindings.values.forEach { binding -> + runCatching { + binding.onBridgeReloaded() + }.onFailure { + scriptRuntime.logger.error("Failed to call onBridgeConnected for binding ${binding.name}", it) + } + } + } + + callFunction("module.onBridgeConnected", reloaded) + } + @Suppress("UNCHECKED_CAST") fun <T : Any> getBinding(clazz: KClass<T>): T? { return moduleBindings.values.find { clazz.isInstance(it) } as? T 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 @@ -6,8 +6,19 @@ abstract class AbstractBinding( ) { lateinit var context: BindingsContext + private val bridgeReloadList = mutableListOf<() -> Unit>() + + fun bridgeAutoReload(block: () -> Unit) { + bridgeReloadList += block + block() + } + open fun onInit() {} + open fun onBridgeReloaded() { + bridgeReloadList.forEach { it() } + } + open fun onDispose() {} abstract fun getObject(): Any 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 @@ -2,6 +2,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 typealias Listener = (List<String?>) -> Unit @@ -10,8 +11,8 @@ abstract class IPCInterface : AbstractBinding("ipc", BindingSide.COMMON) { abstract fun onBroadcast(channel: String, eventName: String, listener: Listener) - abstract fun emit(eventName: String, vararg args: String?) - abstract fun broadcast(channel: String, eventName: String, vararg args: String?) + abstract fun emit(eventName: String, vararg args: String?): Int + abstract fun broadcast(channel: String, eventName: String, vararg args: String?): Int @Suppress("unused") fun emit(eventName: String) = emit(eventName, *emptyArray()) @@ -20,4 +21,7 @@ abstract class IPCInterface : AbstractBinding("ipc", BindingSide.COMMON) { broadcast(channel, eventName, *emptyArray()) override fun getObject() = this + + @JSFunction("isBridgeAlive") + fun isBridgeAlive() = context.runtime.scripting.asBinder().pingBinder() } \ 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 @@ -164,7 +164,7 @@ class SnapEnhance { //if mappings aren't loaded, we can't initialize features if (!mappings.isMappingsLoaded) return features.init() - scriptRuntime.connect(bridgeClient.getScriptingInterface()) + scriptRuntime.init() scriptRuntime.eachModule { callFunction("module.onSnapApplicationLoad", androidContext) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -44,10 +44,15 @@ class BridgeClient( private val onConnectedCallbacks = mutableListOf<suspend () -> Unit>() private var cacheSnapEnhanceApkPath: String? = null - fun addOnConnectedCallback(callback: suspend () -> Unit) { + fun addOnConnectedCallback(initNow: Boolean = false, callback: suspend () -> Unit) { synchronized(onConnectedCallbacks) { onConnectedCallbacks.add(callback) } + initNow.takeIf { it && this::service.isInitialized }?.let { + runBlocking { + callback() + } + } } private fun resumeContinuation(state: Boolean) { 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 @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.core.scripting import me.rhunk.snapenhance.bridge.scripting.AutoReloadListener -import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.ScriptRuntime import me.rhunk.snapenhance.common.scripting.bindings.BindingSide @@ -16,33 +15,47 @@ class CoreScriptRuntime( private val modContext: ModContext, logger: AbstractLogger, ): ScriptRuntime(modContext.androidContext, logger) { - fun connect(scriptingInterface: IScripting) { - scripting = scriptingInterface - scriptingInterface.apply { - buildModuleObject = { module -> - putConst("currentSide", this, BindingSide.CORE.key) - module.registerBindings( - CoreScriptConfig(), - CoreIPC(), - CoreScriptHooker(), - CoreMessaging(modContext), - CoreEvents(modContext), - ) - } + // we assume that the bridge is reloaded the next time we connect to it + private var isBridgeReloaded = false + + fun init() { + buildModuleObject = { module -> + putConst("currentSide", this, BindingSide.CORE.key) + module.registerBindings( + CoreScriptConfig(), + CoreIPC(), + CoreScriptHooker(), + CoreMessaging(modContext), + CoreEvents(modContext), + ) + } + + modContext.bridgeClient.addOnConnectedCallback(initNow = true) { + scripting = modContext.bridgeClient.getScriptingInterface() - enabledScripts.forEach { path -> - runCatching { - load(path, scriptingInterface.getScriptContent(path)) - }.onFailure { - logger.error("Failed to load script $path", it) + if (!isBridgeReloaded) { + scripting.enabledScripts.forEach { path -> + runCatching { + load(path, scripting.getScriptContent(path)) + }.onFailure { + logger.error("Failed to load script $path", it) + } } } - registerAutoReloadListener(object : AutoReloadListener.Stub() { + scripting.registerAutoReloadListener(object : AutoReloadListener.Stub() { override fun restartApp() { modContext.softRestartApp() } }) + + eachModule { + onBridgeConnected(reloaded = isBridgeReloaded) + } + + if (!isBridgeReloaded) { + isBridgeReloaded = true + } } } } \ No newline at end of file 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 @@ -6,22 +6,24 @@ import me.rhunk.snapenhance.common.scripting.impl.Listener class CoreIPC : IPCInterface() { override fun onBroadcast(channel: String, eventName: String, listener: Listener) { - context.runtime.scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() { - override fun onMessage(args: Array<out String?>) { - listener(args.toList()) - } - }) + bridgeAutoReload { + context.runtime.scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() { + override fun onMessage(args: Array<out String?>) { + listener(args.toList()) + } + }) + } } override fun on(eventName: String, listener: Listener) { onBroadcast(context.moduleInfo.name, eventName, listener) } - override fun emit(eventName: String, vararg args: String?) { - broadcast(context.moduleInfo.name, eventName, *args) + override fun emit(eventName: String, vararg args: String?): Int { + return broadcast(context.moduleInfo.name, eventName, *args) } - override fun broadcast(channel: String, eventName: String, vararg args: String?) { - context.runtime.scripting.sendIPCMessage(channel, eventName, args) + override fun broadcast(channel: String, eventName: String, vararg args: String?): Int { + return runCatching { context.runtime.scripting.sendIPCMessage(channel, eventName, args) }.getOrNull() ?: 0 } } \ No newline at end of file