diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index 76fec8f83..5ae9d10e3 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext import splitties.init.appCtx -import java.io.BufferedOutputStream import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -206,10 +205,8 @@ object Backup { withContext(IO) { if (list.isNotEmpty()) { val file = FileUtils.createFileIfNotExist(path + File.separator + fileName) - FileOutputStream(file).use { fos -> - BufferedOutputStream(fos, 64 * 1024).use { - GSON.writeToOutputStream(it, list) - } + file.outputStream().buffered().use { + GSON.writeToOutputStream(it, list) } } } diff --git a/app/src/main/java/io/legado/app/service/ExportBookService.kt b/app/src/main/java/io/legado/app/service/ExportBookService.kt index 7cc091246..5b117aa16 100644 --- a/app/src/main/java/io/legado/app/service/ExportBookService.kt +++ b/app/src/main/java/io/legado/app/service/ExportBookService.kt @@ -75,10 +75,8 @@ import me.ag2s.epublib.epub.EpubWriterProcessor import me.ag2s.epublib.util.ResourceUtil import splitties.init.appCtx import splitties.systemservices.notificationManager -import java.io.BufferedOutputStream import java.io.ByteArrayOutputStream import java.io.File -import java.io.FileOutputStream import java.nio.charset.Charset import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.coroutineContext @@ -257,23 +255,21 @@ class ExportBookService : BaseService() { val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename) ?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹") val charset = Charset.forName(AppConfig.exportCharset) - contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> - BufferedOutputStream(bookOs, 64 * 1024).use { bos -> - getAllContents(book) { text, srcList -> - bos.write(text.toByteArray(charset)) - srcList?.forEach { - val vFile = BookHelp.getImage(book, it.src) - if (vFile.exists()) { - DocumentUtils.createFileIfNotExist( - doc, - "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg", - subDirs = arrayOf( - "${book.name}_${book.author}", - "images", - it.chapterTitle - ) - )?.writeBytes(this, vFile.readBytes()) - } + contentResolver.openOutputStream(bookDoc.uri, "wa")?.bufferedWriter(charset)?.use { bw -> + getAllContents(book) { text, srcList -> + bw.write(text) + srcList?.forEach { + val vFile = BookHelp.getImage(book, it.src) + if (vFile.exists()) { + DocumentUtils.createFileIfNotExist( + doc, + "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg", + subDirs = arrayOf( + "${book.name}_${book.author}", + "images", + it.chapterTitle + ) + )?.writeBytes(this, vFile.readBytes()) } } } @@ -289,10 +285,10 @@ class ExportBookService : BaseService() { val bookPath = FileUtils.getPath(file, filename) val bookFile = FileUtils.createFileWithReplace(bookPath) val charset = Charset.forName(AppConfig.exportCharset) - val bos = BufferedOutputStream(bookFile.outputStream(true), 64 * 1024) - bos.use { + val bw = bookFile.outputStream(true).bufferedWriter(charset) + bw.use { getAllContents(book) { text, srcList -> - bos.write(text.toByteArray(charset)) + it.write(text) srcList?.forEach { val vFile = BookHelp.getImage(book, it.src) if (vFile.exists()) { @@ -315,7 +311,7 @@ class ExportBookService : BaseService() { private suspend fun getAllContents( book: Book, append: (text: String, srcList: ArrayList?) -> Unit - ) { + ) = coroutineScope { val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule() val contentProcessor = ContentProcessor.get(book.name, book.origin) val qy = "${book.name}\n${ @@ -327,34 +323,27 @@ class ExportBookService : BaseService() { ) }" append(qy, null) - if (AppConfig.parallelExportBook) { - coroutineScope { - flow { - appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> - val task = async(Default, start = CoroutineStart.LAZY) { - getExportData(book, chapter, contentProcessor, useReplace) - } - emit(task) - } - }.onEach { it.start() } - .buffer(AppConfig.threadCount) - .map { it.await() } - .withIndex() - .collect { (index, result) -> - postEvent(EventBus.EXPORT_BOOK, book.bookUrl) - exportProgress[book.bookUrl] = index - append.invoke(result.first, result.second) - } - } + val threads = if (AppConfig.parallelExportBook) { + AppConst.MAX_THREAD } else { - appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> - coroutineContext.ensureActive() + 0 + } + flow { + appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> + val task = async(Default, start = CoroutineStart.LAZY) { + getExportData(book, chapter, contentProcessor, useReplace) + } + emit(task) + } + }.onEach { it.start() } + .buffer(threads) + .map { it.await() } + .withIndex() + .collect { (index, result) -> postEvent(EventBus.EXPORT_BOOK, book.bookUrl) exportProgress[book.bookUrl] = index - val result = getExportData(book, chapter, contentProcessor, useReplace) append.invoke(result.first, result.second) } - } } @@ -364,34 +353,33 @@ class ExportBookService : BaseService() { contentProcessor: ContentProcessor, useReplace: Boolean ): Pair?> { - BookHelp.getContent(book, chapter).let { content -> - val content1 = contentProcessor - .getContent( - book, - // 不导出vip标识 - chapter.apply { isVip = false }, - content ?: if (chapter.isVolume) "" else "null", - includeTitle = !AppConfig.exportNoChapterName, - useReplace = useReplace, - chineseConvert = false, - reSegment = false - ).toString() - if (AppConfig.exportPictureFile) { - //txt导出图片文件 - val srcList = arrayListOf() - content?.split("\n")?.forEachIndexed { index, text -> - val matcher = AppPattern.imgPattern.matcher(text) - while (matcher.find()) { - matcher.group(1)?.let { - val src = NetworkUtils.getAbsoluteURL(chapter.url, it) - srcList.add(SrcData(chapter.title, index, src)) - } + val content = BookHelp.getContent(book, chapter) + val content1 = contentProcessor + .getContent( + book, + // 不导出vip标识 + chapter.apply { isVip = false }, + content ?: if (chapter.isVolume) "" else "null", + includeTitle = !AppConfig.exportNoChapterName, + useReplace = useReplace, + chineseConvert = false, + reSegment = false + ).toString() + if (AppConfig.exportPictureFile) { + //txt导出图片文件 + val srcList = arrayListOf() + content?.split("\n")?.forEachIndexed { index, text -> + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { + val src = NetworkUtils.getAbsoluteURL(chapter.url, it) + srcList.add(SrcData(chapter.title, index, src)) } } - return Pair("\n\n$content1", srcList) - } else { - return Pair("\n\n$content1", null) } + return Pair("\n\n$content1", srcList) + } else { + return Pair("\n\n$content1", null) } } @@ -463,8 +451,8 @@ class ExportBookService : BaseService() { //设置正文 setEpubContent(contentModel, book, epubBook) DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc -> - contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> - EpubWriter().write(epubBook, BufferedOutputStream(bookOs)) + contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs -> + EpubWriter().write(epubBook, bookOs) } if (AppConfig.exportToWebDav) { // 导出到webdav @@ -489,8 +477,7 @@ class ExportBookService : BaseService() { val bookFile = FileUtils.createFileWithReplace(bookPath) //设置正文 setEpubContent(contentModel, book, epubBook) - @Suppress("BlockingMethodInNonBlockingContext") - EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile))) + EpubWriter().write(epubBook, bookFile.outputStream().buffered()) if (AppConfig.exportToWebDav) { // 导出到webdav AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) @@ -647,59 +634,79 @@ class ExportBookService : BaseService() { contentModel: String, book: Book, epubBook: EpubBook - ) { + ) = coroutineScope { //正文 val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule() val contentProcessor = ContentProcessor.get(book.name, book.origin) - appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> - coroutineContext.ensureActive() - postEvent(EventBus.EXPORT_BOOK, book.bookUrl) - exportProgress[book.bookUrl] = index - BookHelp.getContent(book, chapter).let { content -> - var content1 = fixPic( - epubBook, - book, - content ?: if (chapter.isVolume) "" else "null", - chapter - ) - content1 = contentProcessor - .getContent( + val threads = if (AppConfig.parallelExportBook) { + AppConst.MAX_THREAD + } else { + 0 + } + flow { + appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> + val task = async(Default, start = CoroutineStart.LAZY) { + val content = BookHelp.getContent(book, chapter) + val (contentFix, resources) = fixPic( book, - chapter, - content1, - includeTitle = false, - useReplace = useReplace, - chineseConvert = false, - reSegment = false - ).toString() - val title = chapter.run { - // 不导出vip标识 - isVip = false - getDisplayTitle( - contentProcessor.getTitleReplaceRules(), - useReplace = useReplace + content ?: if (chapter.isVolume) "" else "null", + chapter ) - } - epubBook.addSection( - title, - ResourceUtil.createChapterResource( + // 不导出vip标识 + chapter.isVip = false + val content1 = contentProcessor + .getContent( + book, + chapter, + contentFix, + includeTitle = false, + useReplace = useReplace, + chineseConvert = false, + reSegment = false + ).toString() + val title = chapter.run { + // 不导出vip标识 + isVip = false + getDisplayTitle( + contentProcessor.getTitleReplaceRules(), + useReplace = useReplace + ) + } + val chapterResource = ResourceUtil.createChapterResource( title.replace("\uD83D\uDD12", ""), content1, contentModel, "Text/chapter_${index}.html" ) - ) + ExportChapter(title, chapterResource, resources) + } + emit(task) + } + }.onEach { it.start() } + .buffer(threads) + .map { it.await() } + .withIndex() + .collect { (index, exportChapter) -> + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + exportProgress[book.bookUrl] = index + epubBook.resources.addAll(exportChapter.resources) + epubBook.addSection(exportChapter.title, exportChapter.chapterResource) } - } } + data class ExportChapter( + val title: String, + val chapterResource: Resource, + val resources: ArrayList + ) + private fun fixPic( - epubBook: EpubBook, book: Book, content: String, chapter: BookChapter - ): String { + ): Pair> { val data = StringBuilder("") + val resources = arrayListOf() content.split("\n").forEach { text -> var text1 = text val matcher = AppPattern.imgPattern.matcher(text) @@ -714,14 +721,14 @@ class ExportBookService : BaseService() { val fp = FileResourceProvider(vFile.parent) if (vFile.exists()) { val img = LazyResource(fp, href, originalHref) - epubBook.resources.add(img) + resources.add(img) } text1 = text1.replace(src, "../${href}") } } data.append(text1).append("\n") } - return data.toString() + return data.toString() to resources } private fun setEpubMetadata(book: Book, epubBook: EpubBook) { @@ -875,17 +882,17 @@ class ExportBookService : BaseService() { coroutineContext.ensureActive() updateProgress(chapterList, index) BookHelp.getContent(book, chapter).let { content -> - var content1 = fixPic( - epubBook, + val (contentFix, resources) = fixPic( book, content ?: if (chapter.isVolume) "" else "null", chapter ) - content1 = contentProcessor + epubBook.resources.addAll(resources) + val content1 = contentProcessor .getContent( book, chapter, - content1, + contentFix, includeTitle = false, useReplace = useReplace, chineseConvert = false, @@ -960,14 +967,14 @@ class ExportBookService : BaseService() { callback: (total: Int, progress: Int) -> Unit ) { DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc -> - contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> + contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs -> EpubWriter() .setCallback(object : EpubWriterProcessor.Callback { override fun onProgressing(total: Int, progress: Int) { callback(total, progress) } }) - .write(epubBook, BufferedOutputStream(bookOs)) + .write(epubBook, bookOs) } if (AppConfig.exportToWebDav) { // 导出到webdav @@ -987,14 +994,13 @@ class ExportBookService : BaseService() { ) { val bookPath = FileUtils.getPath(file, filename) val bookFile = FileUtils.createFileWithReplace(bookPath) - @Suppress("BlockingMethodInNonBlockingContext") EpubWriter() .setCallback(object : EpubWriterProcessor.Callback { override fun onProgressing(total: Int, progress: Int) { callback(total, progress) } }) - .write(epubBook, BufferedOutputStream(FileOutputStream(bookFile))) + .write(epubBook, bookFile.outputStream().buffered()) if (AppConfig.exportToWebDav) { // 导出到webdav AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) diff --git a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt index 775d48e16..0bb1754bf 100644 --- a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt @@ -22,9 +22,7 @@ import io.legado.app.utils.stackTraceStr import io.legado.app.utils.toastOnUi import io.legado.app.utils.writeToOutputStream import kotlinx.coroutines.delay -import java.io.BufferedOutputStream import java.io.File -import java.io.FileOutputStream class BookshelfManageViewModel(application: Application) : BaseViewModel(application) { @@ -64,17 +62,14 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica } } - @Suppress("BlockingMethodInNonBlockingContext") fun saveAllUseBookSourceToFile(success: (file: File) -> Unit) { execute { val path = "${context.filesDir}/shareBookSource.json" FileUtils.delete(path) val file = FileUtils.createFileWithReplace(path) val sources = appDb.bookDao.getAllUseBookSource() - FileOutputStream(file).use { out -> - BufferedOutputStream(out, 64 * 1024).use { - GSON.writeToOutputStream(it, sources) - } + file.outputStream().buffered().use { + GSON.writeToOutputStream(it, sources) } file }.onSuccess { diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index 3179e9371..fdde64ff5 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -9,11 +9,16 @@ import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.toBookSource import io.legado.app.help.config.SourceConfig -import io.legado.app.utils.* +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.cnCompare +import io.legado.app.utils.outputStream +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.writeToOutputStream import splitties.init.appCtx -import java.io.BufferedOutputStream import java.io.File -import java.io.FileOutputStream /** * 书源管理数据修改 @@ -121,16 +126,13 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) } } - @Suppress("BlockingMethodInNonBlockingContext") private fun saveToFile(sources: List, success: (file: File) -> Unit) { execute { val path = "${context.filesDir}/shareBookSource.json" FileUtils.delete(path) val file = FileUtils.createFileWithReplace(path) - FileOutputStream(file).use { out -> - BufferedOutputStream(out, 64 * 1024).use { - GSON.writeToOutputStream(it, sources) - } + file.outputStream().buffered().use { + GSON.writeToOutputStream(it, sources) } file }.onSuccess { diff --git a/app/src/main/java/io/legado/app/utils/RhinoExtensions.kt b/app/src/main/java/io/legado/app/utils/RhinoExtensions.kt new file mode 100644 index 000000000..b2f2c7a38 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RhinoExtensions.kt @@ -0,0 +1,30 @@ +package io.legado.app.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope +import org.mozilla.javascript.Context +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +inline fun suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + val cx = Context.enter() + try { + val pending = cx.captureContinuation() + pending.applicationState = suspend { + supervisorScope { + block() + } + } + throw pending + } catch (e: IllegalStateException) { + return runBlocking { block() } + } finally { + Context.exit() + } +} diff --git a/app/src/main/res/menu/book_cache.xml b/app/src/main/res/menu/book_cache.xml index a943d2669..7ed22ae77 100644 --- a/app/src/main/res/menu/book_cache.xml +++ b/app/src/main/res/menu/book_cache.xml @@ -1,19 +1,19 @@ - @@ -27,39 +27,39 @@ Decode Cover Js(coverDecodeJs) 网络未分组 本地未分组 - 多线程导出TXT + 多线程导出 进度条行为 源编辑框最大行数 %s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑 diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 9339d75e3..2dddeca34 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -1041,7 +1041,7 @@ Decode Cover Js(coverDecodeJs) 网络未分组 本地未分组 - 多线程导出TXT + 多线程导出 进度条行为 源编辑框最大行数 %s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑 diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 66d3cad9f..6b0835166 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1041,7 +1041,7 @@ Decode Cover Js(coverDecodeJs) 网络未分组 本地未分组 - 多线程导出TXT + 多线程导出 进度条行为 源编辑框最大行数 %s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑 diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index aa3cc07ff..8a3773de4 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1037,7 +1037,7 @@ Còn Giải mã Bìa Js(coverDecodeJs) Đã tách nhóm mạng Đã tách nhóm cục bộ - XT xuất đa luồng + Xuất đa luồng Hành vi của thanh tiến trình Số hàng tối đa trong hộp chỉnh sửa nguồn %s,Đặt số hàng ít hơn số hàng tối đa có thể hiển thị trên màn hình giúp trượt sang các trường khác để chỉnh sửa dễ dàng hơn. diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 42043da26..c0f5d9aac 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1038,7 +1038,7 @@ 封面解密(coverDecodeJs) 网络未分组 本地未分组 - 多线程导出TXT + 多线程导出 进度条行为 源编辑框最大行数 %s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 5f9aa4371..2b2283475 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1040,7 +1040,7 @@ 封面解密(coverDecodeJs) 網路未分組 本機未分組 - 多執行緒匯出TXT + 多執行緒匯出 進度條行為 源編輯框最大行數 %s,設定行數小於螢幕可顯示的最大行數可以更方便的滑動到其他的欄位進行編輯 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index e5ba5b2b3..46e92a07a 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1041,7 +1041,7 @@ 封面解密(coverDecodeJs) 网络未分组 本地未分组 - 多线程导出TXT + 多线程导出 进度条行为 源编辑框最大行数 %s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf1dffdee..ab111d668 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1041,7 +1041,7 @@ Decode Cover Js(coverDecodeJs) Network ungrouped Local ungrouped - Multithreaded export TXT + Multithreaded export Progress bar behavior Maximum number of rows in the source edit box %s,Setting the number of rows less than the maximum number of rows that can be displayed on the screen makes it easier to slide to other fields for editing. diff --git a/modules/rhino1.7.3/build.gradle b/modules/rhino1.7.3/build.gradle index 075ba3d06..3f48b6547 100644 --- a/modules/rhino1.7.3/build.gradle +++ b/modules/rhino1.7.3/build.gradle @@ -32,4 +32,7 @@ android { dependencies { api(fileTree(dir: 'lib', include: ['rhino-1.7.13-2.jar'])) + + def coroutines_version = '1.7.3' + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") } \ No newline at end of file diff --git a/modules/rhino1.7.3/src/main/java/com/script/AbstractScriptEngine.kt b/modules/rhino1.7.3/src/main/java/com/script/AbstractScriptEngine.kt index fcd0938d2..dedb0dae9 100644 --- a/modules/rhino1.7.3/src/main/java/com/script/AbstractScriptEngine.kt +++ b/modules/rhino1.7.3/src/main/java/com/script/AbstractScriptEngine.kt @@ -51,6 +51,10 @@ abstract class AbstractScriptEngine(val bindings: Bindings? = null) : ScriptEngi return getBindings(100)?.get(key) } + override suspend fun evalSuspend(script: String, scope: Scriptable): Any? { + return this.evalSuspend(StringReader(script), scope) + } + override fun eval(script: String, scope: Scriptable): Any? { return this.eval(StringReader(script), scope) } diff --git a/modules/rhino1.7.3/src/main/java/com/script/CompiledScript.kt b/modules/rhino1.7.3/src/main/java/com/script/CompiledScript.kt index 30c206621..e4c752e09 100644 --- a/modules/rhino1.7.3/src/main/java/com/script/CompiledScript.kt +++ b/modules/rhino1.7.3/src/main/java/com/script/CompiledScript.kt @@ -15,6 +15,9 @@ abstract class CompiledScript { @Throws(ScriptException::class) abstract fun eval(scope: Scriptable): Any? + @Throws(ScriptException::class) + abstract suspend fun evalSuspend(scope: Scriptable): Any? + @Throws(ScriptException::class) fun eval(bindings: Bindings?): Any? { var ctxt = getEngine().context diff --git a/modules/rhino1.7.3/src/main/java/com/script/ScriptEngine.kt b/modules/rhino1.7.3/src/main/java/com/script/ScriptEngine.kt index 7648ef6b4..aa57213e6 100644 --- a/modules/rhino1.7.3/src/main/java/com/script/ScriptEngine.kt +++ b/modules/rhino1.7.3/src/main/java/com/script/ScriptEngine.kt @@ -14,6 +14,12 @@ interface ScriptEngine { @Throws(ScriptException::class) fun eval(reader: Reader, scope: Scriptable): Any? + @Throws(ScriptException::class) + suspend fun evalSuspend(reader: Reader, scope: Scriptable): Any? + + @Throws(ScriptException::class) + suspend fun evalSuspend(script: String, scope: Scriptable): Any? + @Throws(ScriptException::class) fun eval(script: String, scope: Scriptable): Any? diff --git a/modules/rhino1.7.3/src/main/java/com/script/rhino/ContextElement.kt b/modules/rhino1.7.3/src/main/java/com/script/rhino/ContextElement.kt new file mode 100644 index 000000000..e4806155f --- /dev/null +++ b/modules/rhino1.7.3/src/main/java/com/script/rhino/ContextElement.kt @@ -0,0 +1,25 @@ +package com.script.rhino + +import kotlinx.coroutines.ThreadContextElement +import kotlin.coroutines.CoroutineContext + +class ContextElement : ThreadContextElement { + + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key + get() = Key + + private val contextHelper: Any? = VMBridgeReflect.contextLocal.get() + + override fun updateThreadContext(context: CoroutineContext): Any? { + val oldState = VMBridgeReflect.contextLocal.get() + VMBridgeReflect.contextLocal.set(contextHelper) + return oldState + } + + override fun restoreThreadContext(context: CoroutineContext, oldState: Any?) { + VMBridgeReflect.contextLocal.set(oldState) + } + +} diff --git a/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoCompiledScript.kt b/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoCompiledScript.kt index a6c8ae740..073fe6dd5 100644 --- a/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoCompiledScript.kt +++ b/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoCompiledScript.kt @@ -28,7 +28,11 @@ import com.script.CompiledScript import com.script.ScriptContext import com.script.ScriptEngine import com.script.ScriptException +import kotlinx.coroutines.withContext import org.mozilla.javascript.* +import java.io.IOException +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn /** * Represents compiled JavaScript code. @@ -91,4 +95,48 @@ internal class RhinoCompiledScript( return result } + override suspend fun evalSuspend(scope: Scriptable): Any? { + val cx = Context.enter() + var ret: Any? + withContext(ContextElement()) { + try { + try { + ret = cx.executeScriptWithContinuations(script, scope) + } catch (e: ContinuationPending) { + var pending = e + while (true) { + try { + @Suppress("UNCHECKED_CAST") + val suspendFunction = + pending.applicationState as Function1, Any?> + val functionResult = suspendCoroutineUninterceptedOrReturn { cout -> + suspendFunction.invoke(cout) + } + val continuation = pending.continuation + ret = cx.resumeContinuation(continuation, scope, functionResult) + break + } catch (e: ContinuationPending) { + pending = e + } + } + } + } catch (re: RhinoException) { + val line = if (re.lineNumber() == 0) -1 else re.lineNumber() + val msg: String = if (re is JavaScriptException) { + re.value.toString() + } else { + re.toString() + } + val se = ScriptException(msg, re.sourceName(), line) + se.initCause(re) + throw se + } catch (var14: IOException) { + throw ScriptException(var14) + } finally { + Context.exit() + } + } + return engine.unwrapReturnValue(ret) + } + } \ No newline at end of file diff --git a/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoScriptEngine.kt b/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoScriptEngine.kt index 8d0f487bb..1c436565c 100644 --- a/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoScriptEngine.kt +++ b/modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoScriptEngine.kt @@ -25,6 +25,7 @@ package com.script.rhino import com.script.* +import kotlinx.coroutines.withContext import org.mozilla.javascript.* import org.mozilla.javascript.Function import java.io.IOException @@ -32,6 +33,8 @@ import java.io.Reader import java.io.StringReader import java.lang.reflect.Method import java.security.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn /** * Implementation of `ScriptEngine` using the Mozilla Rhino @@ -80,6 +83,54 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable { return unwrapReturnValue(ret) } + @Throws(ContinuationPending::class) + override suspend fun evalSuspend(reader: Reader, scope: Scriptable): Any? { + val cx = Context.enter() + var ret: Any? + withContext(ContextElement()) { + try { + var filename = this@RhinoScriptEngine["javax.script.filename"] as? String + filename = filename ?: "" + val script = cx.compileReader(reader, filename, 1, null) + try { + ret = cx.executeScriptWithContinuations(script, scope) + } catch (e: ContinuationPending) { + var pending = e + while (true) { + try { + @Suppress("UNCHECKED_CAST") + val suspendFunction = + pending.applicationState as Function1, Any?> + val functionResult = suspendCoroutineUninterceptedOrReturn { cout -> + suspendFunction.invoke(cout) + } + val continuation = pending.continuation + ret = cx.resumeContinuation(continuation, scope, functionResult) + break + } catch (e: ContinuationPending) { + pending = e + } + } + } + } catch (re: RhinoException) { + val line = if (re.lineNumber() == 0) -1 else re.lineNumber() + val msg: String = if (re is JavaScriptException) { + re.value.toString() + } else { + re.toString() + } + val se = ScriptException(msg, re.sourceName(), line) + se.initCause(re) + throw se + } catch (var14: IOException) { + throw ScriptException(var14) + } finally { + Context.exit() + } + } + return unwrapReturnValue(ret) + } + override fun createBindings(): Bindings { return SimpleBindings() } @@ -227,7 +278,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable { callable: Callable, cx: Context, scope: Scriptable, - thisObj: Scriptable, + thisObj: Scriptable?, args: Array ): Any? { var accContext: AccessControlContext? = null @@ -253,7 +304,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable { callable: Callable, cx: Context, scope: Scriptable, - thisObj: Scriptable, + thisObj: Scriptable?, args: Array ): Any? { return super.doTopCall(callable, cx, scope, thisObj, args) diff --git a/modules/rhino1.7.3/src/main/java/com/script/rhino/VMBridgeReflect.kt b/modules/rhino1.7.3/src/main/java/com/script/rhino/VMBridgeReflect.kt new file mode 100644 index 000000000..eac5e209b --- /dev/null +++ b/modules/rhino1.7.3/src/main/java/com/script/rhino/VMBridgeReflect.kt @@ -0,0 +1,20 @@ +package com.script.rhino + +import org.mozilla.javascript.VMBridge + +object VMBridgeReflect { + + val instance: VMBridge by lazy { + VMBridge::class.java.getDeclaredField("instance").apply { + isAccessible = true + }.get(null) as VMBridge + } + + val contextLocal: ThreadLocal by lazy { + @Suppress("UNCHECKED_CAST") + instance::class.java.getDeclaredField("contextLocal").apply { + isAccessible = true + }.get(null) as ThreadLocal + } + +}