RemoteScriptManager.kt (9732B) - raw
1 package me.rhunk.snapenhance.scripting 2 3 import android.annotation.SuppressLint 4 import android.net.Uri 5 import android.os.ParcelFileDescriptor 6 import androidx.documentfile.provider.DocumentFile 7 import me.rhunk.snapenhance.RemoteSideContext 8 import me.rhunk.snapenhance.bridge.scripting.AutoReloadListener 9 import me.rhunk.snapenhance.bridge.scripting.IPCListener 10 import me.rhunk.snapenhance.bridge.scripting.IScripting 11 import me.rhunk.snapenhance.common.scripting.ScriptRuntime 12 import me.rhunk.snapenhance.common.scripting.bindings.BindingSide 13 import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface 14 import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType 15 import me.rhunk.snapenhance.common.scripting.type.ModuleInfo 16 import me.rhunk.snapenhance.common.scripting.type.readModuleInfo 17 import me.rhunk.snapenhance.common.util.ktx.await 18 import me.rhunk.snapenhance.common.util.ktx.toParcelFileDescriptor 19 import me.rhunk.snapenhance.scripting.impl.IPCListeners 20 import me.rhunk.snapenhance.scripting.impl.ManagerIPC 21 import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig 22 import me.rhunk.snapenhance.storage.isScriptEnabled 23 import okhttp3.OkHttpClient 24 import okhttp3.Request 25 import java.io.File 26 import java.io.InputStream 27 import kotlin.system.exitProcess 28 29 class RemoteScriptManager( 30 val context: RemoteSideContext, 31 ) : IScripting.Stub() { 32 val runtime = ScriptRuntime(context.androidContext, context.log).apply { 33 scripting = this@RemoteScriptManager 34 } 35 36 private val okHttpClient by lazy { 37 OkHttpClient.Builder().build() 38 } 39 40 private var autoReloadListener: AutoReloadListener? = null 41 private val autoReloadHandler by lazy { 42 AutoReloadHandler(context.coroutineScope) { 43 runCatching { 44 autoReloadListener?.restartApp() 45 if (context.config.root.scripting.autoReload.getNullable() == "all") { 46 exitProcess(1) 47 } 48 }.onFailure { 49 context.log.warn("Failed to restart app") 50 autoReloadListener = null 51 } 52 }.apply { 53 start() 54 } 55 } 56 57 private val cachedModuleInfo = mutableMapOf<String, ModuleInfo>() 58 private val ipcListeners = IPCListeners() 59 60 fun getSyncedModules(): List<ModuleInfo> { 61 return cachedModuleInfo.values.toList() 62 } 63 64 fun sync() { 65 cachedModuleInfo.clear() 66 getScriptFileNames().forEach { name -> 67 runCatching { 68 getScriptInputStream(name) { stream -> 69 stream?.use { 70 cachedModuleInfo[name] = it.bufferedReader().readModuleInfo() 71 } 72 } 73 }.onFailure { 74 context.log.error("Failed to load module info for $name", it) 75 } 76 } 77 } 78 79 fun init() { 80 runtime.buildModuleObject = { module -> 81 putConst("currentSide", this, BindingSide.MANAGER.key) 82 module.registerBindings( 83 ManagerIPC(ipcListeners), 84 ManagerScriptConfig(this@RemoteScriptManager) 85 ) 86 } 87 88 sync() 89 getEnabledScripts(listOf(BindingSide.MANAGER.key)).forEach { name -> 90 runCatching { 91 loadScript(name) 92 }.onFailure { 93 context.log.error("Failed to load script $name", it) 94 } 95 } 96 } 97 98 fun getModulePath(name: String): String? { 99 return cachedModuleInfo.entries.find { it.value.name == name }?.key 100 } 101 102 fun loadScript(path: String) { 103 val content = getScriptContent(path) ?: return 104 runtime.load(path, content) 105 if (context.config.root.scripting.autoReload.getNullable() != null) { 106 autoReloadHandler.addFile(getScriptsFolder()?.findFile(path) ?: return) 107 } 108 } 109 110 fun unloadScript(scriptPath: String) { 111 runtime.unload(scriptPath) 112 } 113 114 @SuppressLint("Recycle") 115 private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R { 116 val file = getScriptsFolder()?.findFile(name) ?: return callback(null) 117 return context.androidContext.contentResolver.openInputStream(file.uri)?.let(callback) ?: callback(null) 118 } 119 120 fun getModuleDataFolder(moduleFileName: String): File { 121 return context.androidContext.filesDir.resolve("modules").resolve(moduleFileName).also { 122 if (!it.exists()) { 123 it.mkdirs() 124 } 125 } 126 } 127 128 fun getScriptsFolder() = runCatching { 129 DocumentFile.fromTreeUri(context.androidContext, Uri.parse(context.config.root.scripting.moduleFolder.get())) 130 }.getOrNull() 131 132 private fun getScriptFileNames(): List<String> { 133 return (getScriptsFolder() ?: return emptyList()).listFiles().filter { it.name?.endsWith(".js") ?: false }.map { it.name!! } 134 } 135 136 fun importFromUrl( 137 url: String, 138 filepath: String? = null 139 ): ModuleInfo { 140 val response = okHttpClient.newCall(Request.Builder().url(url).build()).execute() 141 if (!response.isSuccessful) { 142 throw Exception("Failed to fetch script. Code: ${response.code}") 143 } 144 response.body.byteStream().use { inputStream -> 145 val bufferedInputStream = inputStream.buffered() 146 bufferedInputStream.mark(0) 147 val moduleInfo = bufferedInputStream.bufferedReader().readModuleInfo() 148 bufferedInputStream.reset() 149 150 val scriptPath = filepath ?: (moduleInfo.name + ".js") 151 val scriptFile = getScriptsFolder()?.findFile(scriptPath) ?: getScriptsFolder()?.createFile("text/javascript", scriptPath) 152 ?: throw Exception("Failed to create script file") 153 154 context.androidContext.contentResolver.openOutputStream(scriptFile.uri, "wt")?.use { output -> 155 bufferedInputStream.copyTo(output) 156 } 157 158 sync() 159 loadScript(scriptPath) 160 runtime.removeModule(scriptPath) 161 return moduleInfo 162 } 163 } 164 165 suspend fun checkForUpdate(inputModuleInfo: ModuleInfo): ModuleInfo? { 166 return runCatching { 167 context.log.verbose("checking for updates for ${inputModuleInfo.name} ${inputModuleInfo.updateUrl}") 168 val response = okHttpClient.newCall(Request.Builder().url(inputModuleInfo.updateUrl ?: return@runCatching null).build()).await() 169 if (!response.isSuccessful) { 170 return@runCatching null 171 } 172 response.body.byteStream().use { inputStream -> 173 val reader = inputStream.buffered().bufferedReader() 174 val moduleInfo = reader.readModuleInfo() 175 moduleInfo.takeIf { 176 it.version != inputModuleInfo.version 177 } 178 } 179 }.onFailure { 180 context.log.error("Failed to check for updates", it) 181 }.getOrNull() 182 } 183 184 private fun getEnabledScripts(sides: List<String>): List<String> { 185 return runCatching { 186 getScriptFileNames().filter { name -> 187 cachedModuleInfo[name]?.executionSides?.any { it in sides } ?: true && 188 context.database.isScriptEnabled(cachedModuleInfo[name]?.name ?: return@filter false) 189 } 190 }.onFailure { 191 context.log.error("Failed to get enabled scripts", it) 192 }.getOrDefault(emptyList()) 193 } 194 195 override fun getEnabledScripts(): List<String> { 196 return getEnabledScripts(listOf(BindingSide.CORE.key)) 197 } 198 199 override fun getScriptContent(moduleName: String): ParcelFileDescriptor? { 200 return getScriptInputStream(moduleName) { it?.toParcelFileDescriptor(context.coroutineScope) } 201 } 202 203 override fun registerIPCListener(channel: String, eventName: String, listener: IPCListener) { 204 ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(listener) 205 } 206 207 override fun sendIPCMessage(channel: String, eventName: String, args: Array<out String>): Int { 208 var dispatchCount = 0 209 ipcListeners[channel]?.get(eventName)?.toList()?.forEach { 210 runCatching { 211 it.onMessage(args) 212 dispatchCount++ 213 }.onFailure { 214 context.log.error("Failed to send message for $eventName", it) 215 } 216 } 217 return dispatchCount 218 } 219 220 override fun configTransaction( 221 module: String?, 222 action: String, 223 key: String?, 224 value: String?, 225 save: Boolean 226 ): String? { 227 val scriptConfig = runtime.getModuleByName(module ?: return null)?.getBinding(ConfigInterface::class) ?: return null.also { 228 context.log.warn("Failed to get config interface for $module") 229 } 230 val transactionType = ConfigTransactionType.fromKey(action) 231 232 return runCatching { 233 scriptConfig.run { 234 if (transactionType == ConfigTransactionType.GET) { 235 return get(key ?: return@runCatching null, value) 236 } 237 when (transactionType) { 238 ConfigTransactionType.SET -> set(key ?: return@runCatching null, value, save) 239 ConfigTransactionType.SAVE -> save() 240 ConfigTransactionType.LOAD -> load() 241 ConfigTransactionType.DELETE -> deleteConfig() 242 else -> {} 243 } 244 null 245 } 246 }.onFailure { 247 context.log.error("Failed to perform config transaction", it) 248 }.getOrDefault("") 249 } 250 251 override fun registerAutoReloadListener(listener: AutoReloadListener?) { 252 autoReloadListener = listener 253 } 254 }