Files
Suwayomi-Server/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/webkit/KcefHelper.kt
Constantin Piber 00861d7750 Switch to JCEF (#2038)
* Switch to JCEF

This is a full implementation, but it does not yet include downloading
CEF as KCEF did

* Download CEF automatically

* Handle and propagate CEF init errors

* Lint

* Simplify jcef version extract

* CEF: Download async

* Copy StartupAsync to support handling errors

Startup failures are simply swallowed, since they are recorded in the
future, but there is no way to get that exception

* CEF: Search for release file recursively

On Mac, the file is buried a bit deeper than first level, like on Win
and Linux

* KcefWebViewProvider: Suppress deprecation

We need to send those events, even if they are deprecated

* Update readme

* Optimize imports

* Suggestion

Co-authored-by: Mitchell Syer <syer10@users.noreply.github.com>

* Refactor: stick to `Path` instead of `File`

Also extracts the downloading of CEF to a separate method

* Lint

* Support disabling CEF

Co-authored-by: Kolby Moroz Liebl <31669092+kolbyml@users.noreply.github.com>

* Move JBR version to build constants

Allows embedding into Manifest so docker can later extract the proper version

* Create test to verify JCEF dependency matches downloaded JBR

* Update server/src/main/kotlin/suwayomi/tachidesk/server/util/CEFManager.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Fix compile, apply Path suggestions

* Download progress

* Lint

* Fix exception on non-posix

* Delete recursively

Others can be non-empty

* Support disabling CEF at will

Not really functional, but nice

* Fix test

* Exclude masstest unless explicitly requested

* PR-CI: Run tests

* Add Changelog entry

---------

Co-authored-by: Mitchell Syer <syer10@users.noreply.github.com>
Co-authored-by: Kolby Moroz Liebl <31669092+kolbyml@users.noreply.github.com>
2026-05-19 17:05:59 -04:00

137 lines
4.1 KiB
Kotlin

package xyz.nulldev.androidcompat.webkit
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.cef.CefClient
import org.cef.browser.CefBrowser
import org.cef.browser.CefFrame
import org.cef.browser.CefMessageRouter
import org.cef.callback.CefQueryCallback
import org.cef.handler.CefMessageRouterHandlerAdapter
import kotlin.random.Random
private val logger = KotlinLogging.logger {}
private val jsHandler: MutableMap<CefClient, JsHandler> = mutableMapOf()
fun CefBrowser.evaluateJavaScript(
expression: String,
cb: (String?) -> Unit,
) = jsHandler[this.client]!!.eval(this, expression, cb)
fun CefBrowser.dispose() {
stopLoad()
setCloseAllowed()
close(true)
}
class JsHandler : CefMessageRouterHandlerAdapter {
private val handler: MutableMap<String, (String?) -> Unit> = mutableMapOf()
constructor(client: CefClient) {
val config = CefMessageRouter.CefMessageRouterConfig()
config.jsQueryFunction = QUERY_FN
config.jsCancelFunction = QUERY_CANCEL_FN
client.addMessageRouter(CefMessageRouter.create(config, this))
jsHandler[client] = this
}
fun eval(
frame: CefFrame,
expression: String,
cb: (String?) -> Unit,
) {
val id = Random.nextBytes(48).toHexString()
handler[id] = cb
frame.executeJavaScript(expression.toCode(id), "about:cef", 0)
}
fun eval(
browser: CefBrowser,
expression: String,
cb: (String?) -> Unit,
) {
val id = Random.nextBytes(48).toHexString()
handler[id] = cb
browser.executeJavaScript(expression.toCode(id), "about:cef", 0)
}
override fun onQuery(
browser: CefBrowser?,
frame: CefFrame?,
queryId: Long,
request: String?,
persistent: Boolean,
callback: CefQueryCallback?,
): Boolean {
super.onQuery(browser, frame, queryId, request, persistent, callback)
if (request != null) {
val invoke =
try {
Json.decodeFromString<FunctionCall>(request)
} catch (e: Exception) {
logger.warn(e) { "Invalid request received" }
return false
}
val handler = handler.remove(invoke.id) ?: return false
handler(invoke.result)
callback?.success("")
return true
}
return false
}
@Serializable
private data class FunctionCall(
val id: String,
val result: String? = null,
)
companion object {
const val QUERY_FN = "__\$_evalQuery"
const val QUERY_CANCEL_FN = "__\$_evalQueryCancel"
private fun Char.isLineBreak(): Boolean = this == '\n' || this == '\r'
private fun String.containsLineBreak(): Boolean =
this.any {
it.isLineBreak()
}
private fun String.asFunctionBody(): String =
let { expression ->
when {
expression.containsLineBreak() -> expression
expression.trim().startsWith("return", false) -> expression
else -> "return $expression"
}
}
private fun String.toCode(id: String): String =
"""
function payload() {
${this.asFunctionBody()}
}
try {
var result = payload();
window.${QUERY_FN}({
request: JSON.stringify({ id: "$id", result }),
onSuccess: function (response) {},
onFailure: function (error_code, error_message) {}
});
} catch (e) {
console.error("Failed to eval $id", e)
window.${QUERY_CANCEL_FN}({
request: JSON.stringify({ id: "$id", error: ""+e }),
onSuccess: function (response) {},
onFailure: function (error_code, error_message) {}
});
}
""".trimIndent()
}
}