feat(scripting): config interface

- change onLaunched to onDispose
- refactor JSModule extras
This commit is contained in:
rhunk
2023-10-14 12:14:30 +02:00
parent baf8727912
commit b92589fa07
15 changed files with 303 additions and 71 deletions

View File

@ -14,6 +14,8 @@ import coil.ImageLoader
import coil.decode.VideoFrameDecoder
import coil.disk.DiskCache
import coil.memory.MemoryCache
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import me.rhunk.snapenhance.bridge.BridgeService
@ -81,6 +83,8 @@ class RemoteSideContext(
.components { add(VideoFrameDecoder.Factory()) }.build()
}
val gson: Gson by lazy { GsonBuilder().setPrettyPrinting().create() }
fun reload() {
log.verbose("Loading RemoteSideContext")
runCatching {

View File

@ -6,18 +6,20 @@ import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.bridge.scripting.IPCListener
import me.rhunk.snapenhance.bridge.scripting.IScripting
import me.rhunk.snapenhance.common.scripting.ScriptRuntime
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.ui.InterfaceBuilder
import me.rhunk.snapenhance.scripting.impl.RemoteScriptConfig
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager
import java.io.File
import java.io.InputStream
class RemoteScriptManager(
private val context: RemoteSideContext,
val context: RemoteSideContext,
) : IScripting.Stub() {
val runtime = ScriptRuntime(context.androidContext, context.log)
private val userInterfaces = mutableMapOf<String, MutableMap<String, InterfaceBuilder>>()
private val cachedModuleInfo = mutableMapOf<String, ModuleInfo>()
private val ipcListeners = IPCListeners()
@ -40,19 +42,15 @@ class RemoteScriptManager(
fun init() {
runtime.buildModuleObject = { module ->
putConst("ipc", this, RemoteManagerIPC(module.moduleInfo, context.log, ipcListeners))
putConst("im", this, InterfaceManager(module.moduleInfo, context.log) { name, interfaceBuilder ->
userInterfaces.getOrPut(module.moduleInfo.name) {
mutableMapOf()
}[name] = interfaceBuilder
})
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()
}
}
sync()
enabledScripts.forEach { name ->
if (getModuleDataFolder(name) == null) {
context.log.warn("Module data folder not found for $name")
}
loadScript(name)
}
}
@ -62,19 +60,17 @@ class RemoteScriptManager(
runtime.load(name, content)
}
fun getScriptInterface(scriptName: String, interfaceName: String)
= userInterfaces[scriptName]?.get(interfaceName)
private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R {
val file = getScriptsFolder()?.findFile(name) ?: return callback(null)
return context.androidContext.contentResolver.openInputStream(file.uri)?.use(callback) ?: callback(null)
}
private fun getModuleDataFolder(moduleFileName: String): DocumentFile? {
val folderName = moduleFileName.substringBeforeLast(".js")
val folder = getScriptsFolder() ?: return null
return folder.findFile(folderName) ?: folder.createDirectory(folderName)
fun getModuleDataFolder(moduleFileName: String): File {
return context.androidContext.filesDir.resolve("modules").resolve(moduleFileName).also {
if (!it.exists()) {
it.mkdirs()
}
}
}
private fun getScriptsFolder() = runCatching {
@ -114,4 +110,35 @@ class RemoteScriptManager(
context.log.error("Failed to send message for $eventName", it)
}
}
override fun configTransaction(
module: String?,
action: String,
key: String?,
value: String?,
save: Boolean
): String? {
val scriptConfig = runtime.getModuleByName(module ?: return null)?.extras?.get("config") as? ConfigInterface ?: return null.also {
context.log.warn("Failed to get config interface for $module")
}
val transactionType = ConfigTransactionType.fromKey(action)
return runCatching {
scriptConfig.run {
if (transactionType == ConfigTransactionType.GET) {
return get(key ?: return@runCatching null, value)
}
when (transactionType) {
ConfigTransactionType.SET -> set(key ?: return@runCatching null, value, save)
ConfigTransactionType.SAVE -> save()
ConfigTransactionType.LOAD -> load()
ConfigTransactionType.DELETE -> delete()
else -> {}
}
null
}
}.onFailure {
context.log.error("Failed to perform config transaction", it)
}.getOrDefault("")
}
}

View File

@ -3,8 +3,8 @@ 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.IPCInterface
import me.rhunk.snapenhance.common.scripting.Listener
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

View File

@ -0,0 +1,57 @@
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,
) : ConfigInterface() {
private val configFile = File(remoteScriptManager.getModuleDataFolder(moduleInfo.name), "config.json")
private var config = JsonObject()
override fun get(key: String, defaultValue: Any?): String? {
return config[key]?.asString ?: defaultValue?.toString()
}
override fun set(key: String, value: Any?, save: Boolean) {
when (value) {
is Int -> config.addProperty(key, value)
is Double -> config.addProperty(key, value)
is Boolean -> config.addProperty(key, value)
is Long -> config.addProperty(key, value)
is Float -> config.addProperty(key, value)
is Byte -> config.addProperty(key, value)
is Short -> config.addProperty(key, value)
else -> config.addProperty(key, value?.toString())
}
if (save) save()
}
override fun save() {
configFile.writeText(config.toString())
}
override fun load() {
runCatching {
if (!configFile.exists()) {
save()
return@runCatching
}
config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java)
}.onFailure {
logger.error("Failed to load config file", it)
save()
}
}
override fun delete() {
configFile.delete()
}
}

View File

@ -12,15 +12,15 @@ import org.mozilla.javascript.annotations.JSFunction
class InterfaceBuilder {
val nodes = mutableListOf<Node>()
var onLaunchedCallback: (() -> Unit)? = null
var onDisposeCallback: (() -> Unit)? = null
private fun createNode(type: NodeType, block: Node.() -> Unit): Node {
return Node(type).apply(block).also { nodes.add(it) }
}
fun onLaunched(block: () -> Unit) {
onLaunchedCallback = block
fun onDispose(block: () -> Unit) {
onDisposeCallback = block
}
fun row(block: (InterfaceBuilder) -> Unit) = RowColumnNode(NodeType.ROW).apply {
@ -66,14 +66,25 @@ class InterfaceBuilder {
class InterfaceManager(
private val moduleInfo: ModuleInfo,
private val logger: AbstractLogger,
private val registerInterface: (String, InterfaceBuilder) -> Unit,
private val logger: AbstractLogger
) {
@JSFunction
fun create(name: String, callback: Function) {
logger.info("Creating interface $name for ${moduleInfo.name}")
private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>()
fun buildInterface(name: String): InterfaceBuilder? {
return interfaces[name]?.invoke()
}
@JSFunction fun create(name: String, callback: Function) {
interfaces[name] = {
val interfaceBuilder = InterfaceBuilder()
runCatching {
Context.enter()
callback.call(Context.getCurrentContext(), callback, callback, arrayOf(interfaceBuilder))
registerInterface(name, interfaceBuilder)
Context.exit()
interfaceBuilder
}.onFailure {
logger.error("Failed to create interface $name for ${moduleInfo.name}", it)
}.getOrNull()
}
}
}

View File

@ -147,8 +147,14 @@ fun ScriptInterface(interfaceBuilder: InterfaceBuilder) {
DrawNode(node)
}
LaunchedEffect(interfaceBuilder) {
interfaceBuilder.onLaunchedCallback?.invoke()
DisposableEffect(Unit) {
onDispose {
runCatching {
interfaceBuilder.onDisposeCallback?.invoke()
}.onFailure {
AbstractLogger.directError("Error running onDisposed callback", it)
}
}
}
}
}

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager
import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator
import me.rhunk.snapenhance.ui.util.pullrefresh.pullRefresh
@ -88,7 +89,8 @@ class ScriptsSection : Section() {
@Composable
fun ScriptSettings(script: ModuleInfo) {
val settingsInterface = remember {
context.scriptManager.getScriptInterface(script.name, "settings")
val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null
(module.extras["im"] as? InterfaceManager)?.buildInterface("settings")
} ?: run {
Text(
text = "This module does not have any settings",
@ -101,7 +103,6 @@ class ScriptsSection : Section() {
ScriptInterface(interfaceBuilder = settingsInterface)
}
@Composable
override fun Content() {
var scriptModules by remember {

View File

@ -10,4 +10,6 @@ interface IScripting {
void registerIPCListener(String channel, String eventName, IPCListener listener);
void sendIPCMessage(String channel, String eventName, in String[] args);
@nullable String configTransaction(String module, String action, @nullable String key, @nullable String value, boolean save);
}

View File

@ -18,6 +18,7 @@ class JSModule(
val moduleInfo: ModuleInfo,
val content: String,
) {
val extras = mutableMapOf<String, Any>()
private lateinit var moduleObject: ScriptableObject
fun load(block: ScriptableObject.() -> Unit) {
@ -115,8 +116,10 @@ class JSModule(
Undefined.instance
}
}
block(moduleObject)
extras.forEach { (key, value) ->
moduleObject.putConst(key, moduleObject, value)
}
evaluateString(moduleObject, content, moduleInfo.name, 1, null)
}
}

View File

@ -25,6 +25,10 @@ open class ScriptRuntime(
}
}
fun getModuleByName(name: String): JSModule? {
return modules.values.find { it.moduleInfo.name == name }
}
private fun readModuleInfo(reader: BufferedReader): ModuleInfo {
val header = reader.readLine()
if (!header.startsWith("// ==SE_module==")) {

View File

@ -0,0 +1,74 @@
package me.rhunk.snapenhance.common.scripting.impl
import org.mozilla.javascript.annotations.JSFunction
enum class ConfigTransactionType(
val key: String
) {
GET("get"),
SET("set"),
SAVE("save"),
LOAD("load"),
DELETE("delete");
companion object {
fun fromKey(key: String) = entries.find { it.key == key }
}
}
abstract class ConfigInterface {
@JSFunction fun get(key: String): String? = get(key, null)
@JSFunction abstract fun get(key: String, defaultValue: Any?): String?
@JSFunction fun getInteger(key: String): Int? = getInteger(key, null)
@JSFunction fun getInteger(key: String, defaultValue: Int?): Int? = get(key, defaultValue.toString())?.toIntOrNull() ?: defaultValue
@JSFunction fun getDouble(key: String): Double? = getDouble(key, null)
@JSFunction fun getDouble(key: String, defaultValue: Double?): Double? = get(key, defaultValue.toString())?.toDoubleOrNull() ?: defaultValue
@JSFunction fun getBoolean(key: String): Boolean? = getBoolean(key, null)
@JSFunction fun getBoolean(key: String, defaultValue: Boolean?): Boolean? = get(key, defaultValue.toString())?.toBoolean() ?: defaultValue
@JSFunction fun getLong(key: String): Long? = getLong(key, null)
@JSFunction fun getLong(key: String, defaultValue: Long?): Long? = get(key, defaultValue.toString())?.toLongOrNull() ?: defaultValue
@JSFunction fun getFloat(key: String): Float? = getFloat(key, null)
@JSFunction fun getFloat(key: String, defaultValue: Float?): Float? = get(key, defaultValue.toString())?.toFloatOrNull() ?: defaultValue
@JSFunction fun getByte(key: String): Byte? = getByte(key, null)
@JSFunction fun getByte(key: String, defaultValue: Byte?): Byte? = get(key, defaultValue.toString())?.toByteOrNull() ?: defaultValue
@JSFunction fun getShort(key: String): Short? = getShort(key, null)
@JSFunction fun getShort(key: String, defaultValue: Short?): Short? = get(key, defaultValue.toString())?.toShortOrNull() ?: defaultValue
@JSFunction fun set(key: String, value: Any?) = set(key, value, false)
@JSFunction abstract fun set(key: String, value: Any?, save: Boolean)
@JSFunction fun setInteger(key: String, value: Int?) = setInteger(key, value, false)
@JSFunction fun setInteger(key: String, value: Int?, save: Boolean) = set(key, value, save)
@JSFunction fun setDouble(key: String, value: Double?) = setDouble(key, value, false)
@JSFunction fun setDouble(key: String, value: Double?, save: Boolean) = set(key, value, save)
@JSFunction fun setBoolean(key: String, value: Boolean?) = setBoolean(key, value, false)
@JSFunction fun setBoolean(key: String, value: Boolean?, save: Boolean) = set(key, value, save)
@JSFunction fun setLong(key: String, value: Long?) = setLong(key, value, false)
@JSFunction fun setLong(key: String, value: Long?, save: Boolean) = set(key, value, save)
@JSFunction fun setFloat(key: String, value: Float?) = setFloat(key, value, false)
@JSFunction fun setFloat(key: String, value: Float?, save: Boolean) = set(key, value, save)
@JSFunction fun setByte(key: String, value: Byte?) = setByte(key, value, false)
@JSFunction fun setByte(key: String, value: Byte?, save: Boolean) = set(key, value, save)
@JSFunction fun setShort(key: String, value: Short?) = setShort(key, value, false)
@JSFunction fun setShort(key: String, value: Short?, save: Boolean) = set(key, value, save)
@JSFunction abstract fun save()
@JSFunction abstract fun load()
@JSFunction abstract fun delete()
}

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.common.scripting
package me.rhunk.snapenhance.common.scripting.impl
typealias Listener = (Array<out String?>) -> Unit

View File

@ -1,12 +1,11 @@
package me.rhunk.snapenhance.core.scripting
import android.content.Context
import me.rhunk.snapenhance.bridge.scripting.IPCListener
import me.rhunk.snapenhance.bridge.scripting.IScripting
import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.common.scripting.IPCInterface
import me.rhunk.snapenhance.common.scripting.Listener
import me.rhunk.snapenhance.common.scripting.ScriptRuntime
import me.rhunk.snapenhance.core.scripting.impl.CoreIPC
import me.rhunk.snapenhance.core.scripting.impl.CoreScriptConfig
import me.rhunk.snapenhance.core.scripting.impl.ScriptHooker
class CoreScriptRuntime(
@ -18,34 +17,14 @@ class CoreScriptRuntime(
fun connect(scriptingInterface: IScripting) {
scriptingInterface.apply {
buildModuleObject = { module ->
putConst("ipc", this, object: IPCInterface() {
override fun onBroadcast(channel: String, eventName: String, listener: Listener) {
registerIPCListener(channel, eventName, object: IPCListener.Stub() {
override fun onMessage(args: Array<out String?>) {
listener(args)
}
})
}
override fun on(eventName: String, listener: Listener) {
onBroadcast(module.moduleInfo.name, eventName, listener)
}
override fun emit(eventName: String, vararg args: String?) {
broadcast(module.moduleInfo.name, eventName, *args)
}
override fun broadcast(channel: String, eventName: String, vararg args: String?) {
sendIPCMessage(channel, eventName, args)
}
})
putConst("hooker", this, ScriptHooker(module.moduleInfo, logger, androidContext.classLoader).also {
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)
}
scriptingInterface.enabledScripts.forEach { path ->
enabledScripts.forEach { path ->
runCatching {
load(path, scriptingInterface.getScriptContent(path))
}.onFailure {
@ -54,3 +33,4 @@ class CoreScriptRuntime(
}
}
}
}

View File

@ -0,0 +1,32 @@
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() {
override fun onBroadcast(channel: String, eventName: String, listener: Listener) {
scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() {
override fun onMessage(args: Array<out String?>) {
listener(args)
}
})
}
override fun on(eventName: String, listener: Listener) {
onBroadcast(moduleInfo.name, eventName, listener)
}
override fun emit(eventName: String, vararg args: String?) {
broadcast(moduleInfo.name, eventName, *args)
}
override fun broadcast(channel: String, eventName: String, vararg args: String?) {
scripting.sendIPCMessage(channel, eventName, args)
}
}

View File

@ -0,0 +1,31 @@
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() {
override fun get(key: String, defaultValue: Any?): String? {
return scripting.configTransaction(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)
}
override fun save() {
scripting.configTransaction(moduleInfo.name, ConfigTransactionType.SAVE.key, null, null, false)
}
override fun load() {
scripting.configTransaction(moduleInfo.name, ConfigTransactionType.LOAD.key, null, null, false)
}
override fun delete() {
scripting.configTransaction(moduleInfo.name, ConfigTransactionType.DELETE.key, null, null, false)
}
}