feat(scripting): hooker module

This commit is contained in:
rhunk
2023-09-17 12:43:57 +02:00
parent e79aba8165
commit 98b3f2bfc9
12 changed files with 260 additions and 86 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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