feat(scripting): hooker module
This commit is contained in:
@@ -102,6 +102,7 @@ dependencies {
|
||||
implementation(libs.coil.video)
|
||||
implementation(libs.ffmpeg.kit)
|
||||
implementation(libs.osmdroid.android)
|
||||
implementation(libs.rhino)
|
||||
|
||||
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
|
||||
|
||||
@@ -97,7 +97,7 @@ class RemoteSideContext(
|
||||
}
|
||||
|
||||
scriptManager.runtime.eachModule {
|
||||
callOnManagerLoad(androidContext)
|
||||
callFunction("module.onManagerLoad",androidContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,8 @@ import java.io.InputStream
|
||||
class RemoteScriptManager(
|
||||
private val context: RemoteSideContext,
|
||||
) : IScripting.Stub() {
|
||||
val runtime = ScriptRuntime(context.log).apply {
|
||||
ipcManager = IRemoteIPC()
|
||||
}
|
||||
val runtime = ScriptRuntime(context.log)
|
||||
val remoteIpc = IRemoteIPC()
|
||||
|
||||
private fun getScriptFolder()
|
||||
= DocumentFile.fromTreeUri(context.androidContext, Uri.parse(context.config.root.scripting.moduleFolder.get()))
|
||||
@@ -41,6 +40,10 @@ class RemoteScriptManager(
|
||||
}
|
||||
|
||||
fun init() {
|
||||
runtime.buildModuleObject = {
|
||||
putConst("ipc", this, remoteIpc)
|
||||
}
|
||||
|
||||
sync()
|
||||
enabledScripts.forEach { path ->
|
||||
val content = getScriptContent(path) ?: return@forEach
|
||||
@@ -76,12 +79,12 @@ class RemoteScriptManager(
|
||||
}
|
||||
|
||||
override fun registerIPCListener(eventName: String, listener: IPCListener) {
|
||||
runtime.ipcManager.on(eventName, object: Listener {
|
||||
remoteIpc.on(eventName, object: Listener {
|
||||
override fun invoke(args: Array<out String?>) {
|
||||
try {
|
||||
listener.onMessage(args)
|
||||
} catch (e: DeadObjectException) {
|
||||
(runtime.ipcManager as IRemoteIPC).removeListener(eventName, this)
|
||||
remoteIpc.removeListener(eventName, this)
|
||||
} catch (t: Throwable) {
|
||||
context.log.error("Failed to invoke $eventName", t)
|
||||
}
|
||||
@@ -91,7 +94,7 @@ class RemoteScriptManager(
|
||||
|
||||
override fun sendIPCMessage(eventName: String, args: Array<out String>) {
|
||||
runCatching {
|
||||
runtime.ipcManager.emit(eventName, *args)
|
||||
remoteIpc.emit(eventName, args)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to send message for $eventName", it)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import me.rhunk.snapenhance.manager.impl.ActionManager
|
||||
import me.rhunk.snapenhance.manager.impl.FeatureManager
|
||||
import me.rhunk.snapenhance.nativelib.NativeConfig
|
||||
import me.rhunk.snapenhance.nativelib.NativeLib
|
||||
import me.rhunk.snapenhance.scripting.ScriptRuntime
|
||||
import me.rhunk.snapenhance.scripting.core.CoreScriptRuntime
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.reflect.KClass
|
||||
@@ -60,7 +60,7 @@ class ModContext {
|
||||
val messageSender = MessageSender(this)
|
||||
val classCache get() = SnapEnhance.classCache
|
||||
val resources: Resources get() = androidContext.resources
|
||||
val scriptRuntime by lazy { ScriptRuntime(log) }
|
||||
val scriptRuntime by lazy { CoreScriptRuntime(log, androidContext.classLoader) }
|
||||
|
||||
fun <T : Feature> feature(featureClass: KClass<T>): T {
|
||||
return features.get(featureClass)!!
|
||||
|
||||
@@ -117,37 +117,7 @@ class SnapEnhance {
|
||||
if (!mappings.isMappingsLoaded()) return
|
||||
features.init()
|
||||
syncRemote()
|
||||
|
||||
bridgeClient.getScriptingInterface().apply {
|
||||
registerReloadListener(object: ReloadListener.Stub() {
|
||||
override fun reloadScript(path: String, content: String) {
|
||||
scriptRuntime.reload(path, content)
|
||||
}
|
||||
})
|
||||
|
||||
scriptRuntime.ipcManager = object: IPCInterface() {
|
||||
override fun on(eventName: String, listener: Listener) {
|
||||
registerIPCListener(eventName, object: IPCListener.Stub() {
|
||||
override fun onMessage(args: Array<out String?>) {
|
||||
listener(args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun emit(eventName: String, args: Array<out String?>) {
|
||||
sendIPCMessage(eventName, args)
|
||||
}
|
||||
}
|
||||
|
||||
enabledScripts.forEach { path ->
|
||||
runCatching {
|
||||
scriptRuntime.load(path, getScriptContent(path))
|
||||
}.onFailure {
|
||||
log.error("Failed to load script $path", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scriptRuntime.connect(bridgeClient.getScriptingInterface())
|
||||
}
|
||||
}.also { time ->
|
||||
appContext.log.verbose("init took $time")
|
||||
@@ -160,7 +130,7 @@ class SnapEnhance {
|
||||
with(appContext) {
|
||||
features.onActivityCreate()
|
||||
actionManager.init()
|
||||
scriptRuntime.eachModule { callOnSnapActivity(mainActivity!!) }
|
||||
scriptRuntime.eachModule { callFunction("module.onSnapActivity", mainActivity!!) }
|
||||
}
|
||||
}.also { time ->
|
||||
appContext.log.verbose("onActivityCreate took $time")
|
||||
|
||||
@@ -58,9 +58,7 @@ object Hooker {
|
||||
clazz: Class<*>,
|
||||
stage: HookStage,
|
||||
consumer: (HookAdapter) -> Unit
|
||||
) {
|
||||
XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer))
|
||||
}
|
||||
): Set<XC_MethodHook.Unhook> = XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer))
|
||||
|
||||
fun hookConstructor(
|
||||
clazz: Class<*>,
|
||||
|
||||
@@ -6,6 +6,7 @@ abstract class IPCInterface {
|
||||
abstract fun on(eventName: String, listener: Listener)
|
||||
|
||||
abstract fun emit(eventName: String, vararg args: String?)
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
fun emit(eventName: String) = emit(eventName, *emptyArray())
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package me.rhunk.snapenhance.scripting
|
||||
|
||||
import android.app.Activity
|
||||
import me.rhunk.snapenhance.core.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.scripting.ktx.contextScope
|
||||
import me.rhunk.snapenhance.scripting.ktx.putFunction
|
||||
import me.rhunk.snapenhance.scripting.ktx.scriptableObject
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Function
|
||||
import org.mozilla.javascript.NativeJavaObject
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
import org.mozilla.javascript.Undefined
|
||||
|
||||
@@ -14,11 +17,11 @@ class JSModule(
|
||||
lateinit var logger: AbstractLogger
|
||||
private lateinit var moduleObject: ScriptableObject
|
||||
|
||||
fun load(block: Context.(ScriptableObject) -> Unit) {
|
||||
fun load(block: ScriptableObject.() -> Unit) {
|
||||
contextScope {
|
||||
moduleObject = initSafeStandardObjects()
|
||||
moduleObject.putConst("module", moduleObject, buildScriptableObject {
|
||||
putConst("info", this, buildScriptableObject {
|
||||
moduleObject.putConst("module", moduleObject, scriptableObject {
|
||||
putConst("info", this, scriptableObject {
|
||||
putConst("name", this, moduleInfo.name)
|
||||
putConst("version", this, moduleInfo.version)
|
||||
putConst("description", this, moduleInfo.description)
|
||||
@@ -29,46 +32,47 @@ class JSModule(
|
||||
})
|
||||
})
|
||||
|
||||
moduleObject.putFunction("logInfo") { args ->
|
||||
logger.info(args?.getOrNull(0)?.toString() ?: "null")
|
||||
moduleObject.putFunction("setField") { args ->
|
||||
val obj = args?.get(0) as? NativeJavaObject ?: return@putFunction Undefined.instance
|
||||
val name = args[1].toString()
|
||||
val value = args[2]
|
||||
val field = obj.unwrap().javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance
|
||||
field.isAccessible = true
|
||||
field.set(obj.unwrap(), value)
|
||||
Undefined.instance
|
||||
}
|
||||
|
||||
block(this, moduleObject)
|
||||
moduleObject.putFunction("getField") { args ->
|
||||
val obj = args?.get(0) as? NativeJavaObject ?: return@putFunction Undefined.instance
|
||||
val name = args[1].toString()
|
||||
val field = obj.unwrap().javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance
|
||||
field.isAccessible = true
|
||||
field.get(obj.unwrap())
|
||||
}
|
||||
|
||||
moduleObject.putFunction("logInfo") { args ->
|
||||
logger.info(args?.joinToString(" ") ?: "")
|
||||
Undefined.instance
|
||||
}
|
||||
|
||||
block(moduleObject)
|
||||
evaluateString(moduleObject, content, moduleInfo.name, 1, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun unload() {
|
||||
contextScope {
|
||||
moduleObject.scriptable("module")?.function("onUnload")?.call(
|
||||
this,
|
||||
moduleObject,
|
||||
moduleObject,
|
||||
null
|
||||
)
|
||||
}
|
||||
callFunction("module.onUnload")
|
||||
}
|
||||
|
||||
fun callOnSnapActivity(activity: Activity) {
|
||||
fun callFunction(name: String, vararg args: Any?) {
|
||||
contextScope {
|
||||
moduleObject.scriptable("module")?.function("onSnapActivity")?.call(
|
||||
this,
|
||||
moduleObject,
|
||||
moduleObject,
|
||||
arrayOf(activity)
|
||||
)
|
||||
}
|
||||
}
|
||||
name.split(".").also { split ->
|
||||
val function = split.dropLast(1).fold(moduleObject) { obj, key ->
|
||||
obj.get(key, obj) as? ScriptableObject ?: return@contextScope
|
||||
}.get(split.last(), moduleObject) as? Function ?: return@contextScope
|
||||
|
||||
fun callOnManagerLoad(context: android.content.Context) {
|
||||
contextScope {
|
||||
moduleObject.scriptable("module")?.function("onManagerLoad")?.call(
|
||||
this,
|
||||
moduleObject,
|
||||
moduleObject,
|
||||
arrayOf(context)
|
||||
)
|
||||
function.call(this, moduleObject, moduleObject, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,15 @@ package me.rhunk.snapenhance.scripting
|
||||
|
||||
import me.rhunk.snapenhance.core.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class ScriptRuntime(
|
||||
private val logger: AbstractLogger,
|
||||
open class ScriptRuntime(
|
||||
protected val logger: AbstractLogger,
|
||||
) {
|
||||
lateinit var ipcManager: IPCInterface
|
||||
var buildModuleObject: ScriptableObject.(JSModule) -> Unit = {}
|
||||
private val modules = mutableMapOf<String, JSModule>()
|
||||
|
||||
fun eachModule(f: JSModule.() -> Unit) {
|
||||
@@ -76,7 +77,7 @@ class ScriptRuntime(
|
||||
).apply {
|
||||
logger = this@ScriptRuntime.logger
|
||||
load {
|
||||
it.putConst("ipc", it, ipcManager)
|
||||
buildModuleObject(this, this@apply)
|
||||
}
|
||||
modules[path] = this
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package me.rhunk.snapenhance.scripting.core
|
||||
|
||||
import me.rhunk.snapenhance.bridge.scripting.IPCListener
|
||||
import me.rhunk.snapenhance.bridge.scripting.IScripting
|
||||
import me.rhunk.snapenhance.bridge.scripting.ReloadListener
|
||||
import me.rhunk.snapenhance.core.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.scripting.IPCInterface
|
||||
import me.rhunk.snapenhance.scripting.Listener
|
||||
import me.rhunk.snapenhance.scripting.ScriptRuntime
|
||||
import me.rhunk.snapenhance.scripting.core.impl.ScriptHooker
|
||||
import me.rhunk.snapenhance.scripting.ktx.putFunction
|
||||
|
||||
class CoreScriptRuntime(
|
||||
logger: AbstractLogger,
|
||||
private val classLoader: ClassLoader
|
||||
): ScriptRuntime(logger) {
|
||||
private lateinit var ipcInterface: IPCInterface
|
||||
|
||||
private val scriptHookers = mutableListOf<ScriptHooker>()
|
||||
|
||||
|
||||
fun connect(scriptingInterface: IScripting) {
|
||||
scriptingInterface.apply {
|
||||
registerReloadListener(object: ReloadListener.Stub() {
|
||||
override fun reloadScript(path: String, content: String) {
|
||||
reload(path, content)
|
||||
}
|
||||
})
|
||||
|
||||
ipcInterface = object: IPCInterface() {
|
||||
override fun on(eventName: String, listener: Listener) {
|
||||
registerIPCListener(eventName, object: IPCListener.Stub() {
|
||||
override fun onMessage(args: Array<out String?>) {
|
||||
listener(args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun emit(eventName: String, args: Array<out String?>) {
|
||||
sendIPCMessage(eventName, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildModuleObject = { module ->
|
||||
putConst("ipc", this, ipcInterface)
|
||||
putFunction("findClass") {
|
||||
val className = it?.get(0).toString()
|
||||
classLoader.loadClass(className)
|
||||
}
|
||||
putConst("hooker", this, ScriptHooker(module.moduleInfo, logger, classLoader).also {
|
||||
scriptHookers.add(it)
|
||||
})
|
||||
}
|
||||
|
||||
scriptingInterface.enabledScripts.forEach { path ->
|
||||
runCatching {
|
||||
load(path, scriptingInterface.getScriptContent(path))
|
||||
}.onFailure {
|
||||
logger.error("Failed to load script $path", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package me.rhunk.snapenhance.scripting.core.impl
|
||||
|
||||
import me.rhunk.snapenhance.core.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.hook.HookAdapter
|
||||
import me.rhunk.snapenhance.hook.HookStage
|
||||
import me.rhunk.snapenhance.hook.Hooker
|
||||
import me.rhunk.snapenhance.hook.hook
|
||||
import me.rhunk.snapenhance.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import org.mozilla.javascript.annotations.JSGetter
|
||||
import org.mozilla.javascript.annotations.JSSetter
|
||||
import java.lang.reflect.Member
|
||||
|
||||
|
||||
class ScriptHookCallback(
|
||||
private val hookAdapter: HookAdapter
|
||||
) {
|
||||
var result
|
||||
@JSGetter("result") get() = hookAdapter.getResult()
|
||||
@JSSetter("result") set(result) = hookAdapter.setResult(result)
|
||||
|
||||
val thisObject
|
||||
@JSGetter("thisObject") get() = hookAdapter.nullableThisObject<Any>()
|
||||
|
||||
val method
|
||||
@JSGetter("method") get() = hookAdapter.method()
|
||||
|
||||
val args
|
||||
@JSGetter("args") get() = hookAdapter.args().toList()
|
||||
|
||||
fun cancel() = hookAdapter.setResult(null)
|
||||
|
||||
fun arg(index: Int) = hookAdapter.argNullable<Any>(index)
|
||||
|
||||
fun setArg(index: Int, any: Any) = hookAdapter.setArg(index, any)
|
||||
|
||||
fun invokeOriginal() = hookAdapter.invokeOriginal()
|
||||
fun invokeOriginal(args: Array<Any>) = hookAdapter.invokeOriginal(args)
|
||||
}
|
||||
|
||||
|
||||
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) }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.scripting
|
||||
package me.rhunk.snapenhance.scripting.ktx
|
||||
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Function
|
||||
@@ -23,20 +23,20 @@ fun Scriptable.function(name: String): Function? {
|
||||
return this.get(name, this) as? Function
|
||||
}
|
||||
|
||||
fun ScriptableObject.putFunction(name: String, proxy: Scriptable.(Array<out Any>?) -> Any) {
|
||||
fun ScriptableObject.putFunction(name: String, proxy: Scriptable.(Array<out Any>?) -> Any?) {
|
||||
this.putConst(name, this, object: org.mozilla.javascript.BaseFunction() {
|
||||
override fun call(
|
||||
cx: Context?,
|
||||
scope: Scriptable,
|
||||
thisObj: Scriptable,
|
||||
args: Array<out Any>?
|
||||
): Any {
|
||||
): Any? {
|
||||
return thisObj.proxy(args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun buildScriptableObject(name: String? = "ScriptableObject", f: ScriptableObject.() -> Unit): ScriptableObject {
|
||||
fun scriptableObject(name: String? = "ScriptableObject", f: ScriptableObject.() -> Unit): ScriptableObject {
|
||||
return object: ScriptableObject() {
|
||||
override fun getClassName() = name
|
||||
}.apply(f)
|
||||
Reference in New Issue
Block a user