使用Ktor替代NanoHTTPD后端
This commit is contained in:
@@ -29,8 +29,7 @@ import io.legado.app.utils.startForegroundServiceCompat
|
||||
import io.legado.app.utils.startService
|
||||
import io.legado.app.utils.stopService
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.web.HttpServer
|
||||
import io.legado.app.web.WebSocketServer
|
||||
import io.legado.app.web.KtorServer
|
||||
import splitties.init.appCtx
|
||||
import splitties.systemservices.powerManager
|
||||
import splitties.systemservices.wifiManager
|
||||
@@ -76,8 +75,7 @@ class WebService : BaseService() {
|
||||
setReferenceCounted(false)
|
||||
}
|
||||
}
|
||||
private var httpServer: HttpServer? = null
|
||||
private var webSocketServer: WebSocketServer? = null
|
||||
private var ktorServer: KtorServer? = null
|
||||
private var notificationList = mutableListOf(appCtx.getString(R.string.service_starting))
|
||||
private val networkChangedListener by lazy {
|
||||
NetworkChangedListener(this)
|
||||
@@ -143,32 +141,21 @@ class WebService : BaseService() {
|
||||
}
|
||||
networkChangedListener.unRegister()
|
||||
isRun = false
|
||||
if (httpServer?.isAlive == true) {
|
||||
httpServer?.stop()
|
||||
}
|
||||
if (webSocketServer?.isAlive == true) {
|
||||
webSocketServer?.stop()
|
||||
}
|
||||
ktorServer?.stop()
|
||||
postEvent(EventBus.WEB_SERVICE, "")
|
||||
FlowEventBus.post(EventBus.WEB_SERVICE, "")
|
||||
upTile(false)
|
||||
}
|
||||
|
||||
private fun upWebServer() {
|
||||
if (httpServer?.isAlive == true) {
|
||||
httpServer?.stop()
|
||||
}
|
||||
if (webSocketServer?.isAlive == true) {
|
||||
webSocketServer?.stop()
|
||||
}
|
||||
ktorServer?.stop()
|
||||
val addressList = NetworkUtils.getLocalIPAddress()
|
||||
if (addressList.any()) {
|
||||
val port = getPort()
|
||||
httpServer = HttpServer(port)
|
||||
webSocketServer = WebSocketServer(port + 1)
|
||||
ktorServer = KtorServer(port)
|
||||
try {
|
||||
httpServer?.start()
|
||||
webSocketServer?.start(1000 * 30) // 通信超时设置
|
||||
ktorServer?.start()
|
||||
ktorServer?.startWebSocket(port + 1)
|
||||
notificationList.clear()
|
||||
notificationList.addAll(addressList.map { address ->
|
||||
getString(
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
package io.legado.app.web
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.api.controller.BookController
|
||||
import io.legado.app.api.controller.BookSourceController
|
||||
import io.legado.app.api.controller.ReplaceRuleController
|
||||
import io.legado.app.api.controller.RssSourceController
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.service.WebService
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.LogUtils
|
||||
import io.legado.app.utils.stackTraceStr
|
||||
import io.legado.app.web.utils.AssetsWeb
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.Pipe
|
||||
import okio.buffer
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class HttpServer(port: Int) : NanoHTTPD(port) {
|
||||
private val assetsWeb = AssetsWeb("web")
|
||||
|
||||
override fun serve(session: IHTTPSession): Response {
|
||||
WebService.serve()
|
||||
var returnData: ReturnData? = null
|
||||
val ct = ContentType(session.headers["content-type"]).tryUTF8()
|
||||
session.headers["content-type"] = ct.contentTypeHeader
|
||||
var uri = session.uri
|
||||
|
||||
val startAt = System.currentTimeMillis()
|
||||
LogUtils.d(TAG) {
|
||||
"${session.method.name} - $uri - ${session.queryParameterString} - Start($startAt)"
|
||||
}
|
||||
|
||||
try {
|
||||
when (session.method) {
|
||||
Method.OPTIONS -> {
|
||||
val response = newFixedLengthResponse("")
|
||||
response.addHeader("Access-Control-Allow-Methods", "POST")
|
||||
response.addHeader("Access-Control-Allow-Headers", "content-type")
|
||||
response.addHeader("Access-Control-Allow-Origin", session.headers["origin"])
|
||||
//response.addHeader("Access-Control-Max-Age", "3600");
|
||||
return response
|
||||
}
|
||||
|
||||
Method.POST -> {
|
||||
val files = HashMap<String, String>()
|
||||
session.parseBody(files)
|
||||
val postData = files["postData"]
|
||||
|
||||
returnData = runBlocking {
|
||||
when (uri) {
|
||||
"/saveBookSource" -> BookSourceController.saveSource(postData)
|
||||
"/saveBookSources" -> BookSourceController.saveSources(postData)
|
||||
"/deleteBookSources" -> BookSourceController.deleteSources(postData)
|
||||
"/saveBook" -> BookController.saveBook(postData)
|
||||
"/deleteBook" -> BookController.deleteBook(postData)
|
||||
"/saveBookProgress" -> BookController.saveBookProgress(postData)
|
||||
"/addLocalBook" -> BookController.addLocalBook(session.parameters, files)
|
||||
"/saveReadConfig" -> BookController.saveWebReadConfig(postData)
|
||||
"/saveRssSource" -> RssSourceController.saveSource(postData)
|
||||
"/saveRssSources" -> RssSourceController.saveSources(postData)
|
||||
"/deleteRssSources" -> RssSourceController.deleteSources(postData)
|
||||
"/saveReplaceRule" -> ReplaceRuleController.saveRule(postData)
|
||||
"/deleteReplaceRule" -> ReplaceRuleController.delete(postData)
|
||||
"/testReplaceRule" -> ReplaceRuleController.testRule(postData)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Method.GET -> {
|
||||
val parameters = session.parameters
|
||||
|
||||
returnData = when (uri) {
|
||||
"/getBookSource" -> BookSourceController.getSource(parameters)
|
||||
"/getBookSources" -> BookSourceController.sources
|
||||
"/getBookshelf" -> BookController.bookshelf
|
||||
"/getChapterList" -> BookController.getChapterList(parameters)
|
||||
"/refreshToc" -> BookController.refreshToc(parameters)
|
||||
"/getBookContent" -> BookController.getBookContent(parameters)
|
||||
"/cover" -> BookController.getCover(parameters)
|
||||
"/image" -> BookController.getImg(parameters)
|
||||
"/getReadConfig" -> BookController.getWebReadConfig()
|
||||
"/getRssSource" -> RssSourceController.getSource(parameters)
|
||||
"/getRssSources" -> RssSourceController.sources
|
||||
"/getReplaceRules" -> ReplaceRuleController.allRules
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
if (returnData == null) {
|
||||
if (uri.endsWith("/"))
|
||||
uri += "index.html"
|
||||
return assetsWeb.getResponse(uri)
|
||||
}
|
||||
|
||||
val response = if (returnData.data is Bitmap) {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
(returnData.data as Bitmap).compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
val byteArray = outputStream.toByteArray()
|
||||
outputStream.close()
|
||||
val inputStream = ByteArrayInputStream(byteArray)
|
||||
newFixedLengthResponse(
|
||||
Response.Status.OK,
|
||||
"image/png",
|
||||
inputStream,
|
||||
byteArray.size.toLong()
|
||||
)
|
||||
} else {
|
||||
val data = returnData.data
|
||||
if (data is List<*> && data.size > 3000) {
|
||||
val pipe = Pipe(16 * 1024)
|
||||
Coroutine.async {
|
||||
pipe.sink.buffer().outputStream().bufferedWriter(Charsets.UTF_8).use {
|
||||
GSON.toJson(returnData, it)
|
||||
}
|
||||
}
|
||||
newChunkedResponse(
|
||||
Response.Status.OK,
|
||||
"application/json",
|
||||
pipe.source.buffer().inputStream()
|
||||
)
|
||||
} else {
|
||||
newFixedLengthResponse(GSON.toJson(returnData))
|
||||
}
|
||||
}
|
||||
response.addHeader("Access-Control-Allow-Methods", "GET, POST")
|
||||
response.addHeader("Access-Control-Allow-Origin", session.headers["origin"])
|
||||
LogUtils.d(TAG) {
|
||||
"${session.method.name} - $uri - ${session.queryParameterString} - End($startAt)"
|
||||
}
|
||||
return response
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(TAG) {
|
||||
"${session.method.name} - $uri - ${session.queryParameterString} - Error End($startAt)\n$e\n${e.stackTraceStr}"
|
||||
}
|
||||
return newFixedLengthResponse(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HttpServer"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package io.legado.app.web
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.serialization.gson.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.cio.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.util.pipeline.*
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.api.controller.BookController
|
||||
import io.legado.app.api.controller.BookSourceController
|
||||
import io.legado.app.api.controller.ReplaceRuleController
|
||||
import io.legado.app.api.controller.RssSourceController
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.service.WebService
|
||||
import io.legado.app.utils.LogUtils
|
||||
import io.legado.app.utils.stackTraceStr
|
||||
import io.legado.app.web.socket.BookSearchWebSocket
|
||||
import io.legado.app.web.socket.BookSourceDebugWebSocket
|
||||
import io.legado.app.web.socket.RssSourceDebugWebSocket
|
||||
import io.legado.app.web.utils.AssetsWeb
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class KtorServer(private val port: Int) {
|
||||
private var server: ApplicationEngine? = null
|
||||
private var wsServer: ApplicationEngine? = null
|
||||
private val assetsWeb = AssetsWeb("web")
|
||||
|
||||
fun start() {
|
||||
server = embeddedServer(CIO, port = port) {
|
||||
install(ContentNegotiation) {
|
||||
gson {
|
||||
setLenient()
|
||||
}
|
||||
}
|
||||
install(CORS) {
|
||||
anyHost()
|
||||
allowHeader(HttpHeaders.ContentType)
|
||||
allowMethod(HttpMethod.Options)
|
||||
allowMethod(HttpMethod.Post)
|
||||
allowMethod(HttpMethod.Get)
|
||||
}
|
||||
|
||||
routing {
|
||||
post("/saveBookSource") { handlePost { BookSourceController.saveSource(it) } }
|
||||
post("/saveBookSources") { handlePost { BookSourceController.saveSources(it) } }
|
||||
post("/deleteBookSources") { handlePost { BookSourceController.deleteSources(it) } }
|
||||
post("/saveBook") { handlePost { BookController.saveBook(it) } }
|
||||
post("/deleteBook") { handlePost { BookController.deleteBook(it) } }
|
||||
post("/saveBookProgress") { handlePost { BookController.saveBookProgress(it) } }
|
||||
post("/addLocalBook") {
|
||||
WebService.serve()
|
||||
val multipart = call.receiveMultipart()
|
||||
var fileName: String? = null
|
||||
var fileBytes: ByteArray? = null
|
||||
multipart.forEachPart { part ->
|
||||
when (part) {
|
||||
is PartData.FormItem -> {
|
||||
if (part.name == "fileName") fileName = part.value
|
||||
}
|
||||
is PartData.FileItem -> {
|
||||
fileBytes = part.streamProvider().readBytes()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
part.dispose()
|
||||
}
|
||||
if (fileName != null && fileBytes != null) {
|
||||
val returnData = withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
val uri = LocalBook.saveBookFile(fileBytes!!.inputStream(), fileName!!)
|
||||
LocalBook.importFile(uri)
|
||||
ReturnData().setData(true)
|
||||
}.getOrElse {
|
||||
ReturnData().setErrorMsg(it.localizedMessage ?: "Save book error")
|
||||
}
|
||||
}
|
||||
respondReturnData(returnData)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, "Missing fileName or fileData")
|
||||
}
|
||||
}
|
||||
post("/saveReadConfig") { handlePost { BookController.saveWebReadConfig(it) } }
|
||||
post("/saveRssSource") { handlePost { RssSourceController.saveSource(it) } }
|
||||
post("/saveRssSources") { handlePost { RssSourceController.saveSources(it) } }
|
||||
post("/deleteRssSources") { handlePost { RssSourceController.deleteSources(it) } }
|
||||
post("/saveReplaceRule") { handlePost { ReplaceRuleController.saveRule(it) } }
|
||||
post("/deleteReplaceRule") { handlePost { ReplaceRuleController.delete(it) } }
|
||||
post("/testReplaceRule") { handlePost { ReplaceRuleController.testRule(it) } }
|
||||
|
||||
get("/getBookSource") { handleGet { BookSourceController.getSource(it) } }
|
||||
get("/getBookSources") { handleGet { BookSourceController.sources } }
|
||||
get("/getBookshelf") { handleGet { BookController.bookshelf } }
|
||||
get("/getChapterList") { handleGet { BookController.getChapterList(it) } }
|
||||
get("/refreshToc") { handleGet { BookController.refreshToc(it) } }
|
||||
get("/getBookContent") { handleGet { BookController.getBookContent(it) } }
|
||||
get("/cover") { handleGet { BookController.getCover(it) } }
|
||||
get("/image") { handleGet { BookController.getImg(it) } }
|
||||
get("/getReadConfig") { handleGet { BookController.getWebReadConfig() } }
|
||||
get("/getRssSource") { handleGet { RssSourceController.getSource(it) } }
|
||||
get("/getRssSources") { handleGet { RssSourceController.sources } }
|
||||
get("/getReplaceRules") { handleGet { ReplaceRuleController.allRules } }
|
||||
|
||||
get("{...}") {
|
||||
WebService.serve()
|
||||
var uri = call.request.uri.substringBefore("?")
|
||||
if (uri.endsWith("/")) uri += "index.html"
|
||||
val inputStream = assetsWeb.getInputStream(uri)
|
||||
if (inputStream != null) {
|
||||
call.respondOutputStream(ContentType.parse(assetsWeb.getMimeType(uri))) {
|
||||
inputStream.copyTo(this)
|
||||
}
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start(wait = false)
|
||||
}
|
||||
|
||||
fun startWebSocket(wsPort: Int) {
|
||||
wsServer = embeddedServer(CIO, port = wsPort) {
|
||||
install(WebSockets)
|
||||
routing {
|
||||
webSocket("/bookSourceDebug") {
|
||||
BookSourceDebugWebSocket(this).handle()
|
||||
}
|
||||
webSocket("/rssSourceDebug") {
|
||||
RssSourceDebugWebSocket(this).handle()
|
||||
}
|
||||
webSocket("/searchBook") {
|
||||
BookSearchWebSocket(this).handle()
|
||||
}
|
||||
}
|
||||
}.start(wait = false)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
server?.stop(1000, 1000)
|
||||
wsServer?.stop(1000, 1000)
|
||||
}
|
||||
|
||||
private suspend fun PipelineContext<Unit, ApplicationCall>.handlePost(
|
||||
block: suspend (String?) -> ReturnData
|
||||
) {
|
||||
WebService.serve()
|
||||
try {
|
||||
val postData = call.receiveText()
|
||||
val returnData = block(postData)
|
||||
respondReturnData(returnData)
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG, e.stackTraceStr)
|
||||
call.respondText(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun PipelineContext<Unit, ApplicationCall>.handleGet(
|
||||
block: (Map<String, List<String>>) -> ReturnData?
|
||||
) {
|
||||
WebService.serve()
|
||||
try {
|
||||
val parameters = call.request.queryParameters.toMap()
|
||||
val returnData = block(parameters)
|
||||
if (returnData != null) {
|
||||
respondReturnData(returnData)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG, e.stackTraceStr)
|
||||
call.respondText(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun PipelineContext<Unit, ApplicationCall>.respondReturnData(returnData: ReturnData) {
|
||||
if (returnData.data is Bitmap) {
|
||||
val bitmap = returnData.data as Bitmap
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
withContext(Dispatchers.IO) {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
}
|
||||
call.respondBytes(outputStream.toByteArray(), ContentType.Image.PNG)
|
||||
} else {
|
||||
call.respond(returnData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parameters.toMap(): Map<String, List<String>> {
|
||||
val map = mutableMapOf<String, List<String>>()
|
||||
this.forEach { s, list -> map[s] = list }
|
||||
return map
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "KtorServer"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package io.legado.app.web
|
||||
|
||||
import fi.iki.elonen.NanoWSD
|
||||
import io.legado.app.service.WebService
|
||||
import io.legado.app.web.socket.*
|
||||
|
||||
class WebSocketServer(port: Int) : NanoWSD(port) {
|
||||
|
||||
override fun openWebSocket(handshake: IHTTPSession): WebSocket? {
|
||||
WebService.serve()
|
||||
return when (handshake.uri) {
|
||||
"/bookSourceDebug" -> {
|
||||
BookSourceDebugWebSocket(handshake)
|
||||
}
|
||||
"/rssSourceDebug" -> {
|
||||
RssSourceDebugWebSocket(handshake)
|
||||
}
|
||||
"/searchBook" -> {
|
||||
BookSearchWebSocket(handshake)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.legado.app.web.socket
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoWSD
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.domain.model.BookSearchScope
|
||||
@@ -11,27 +11,14 @@ import io.legado.app.domain.usecase.SearchBooksUseCase
|
||||
import io.legado.app.domain.usecase.SearchRunEvent
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.isJson
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import org.koin.core.context.GlobalContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.IOException
|
||||
|
||||
class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
||||
NanoWSD.WebSocket(handshakeRequest),
|
||||
CoroutineScope by MainScope() {
|
||||
class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) : CoroutineScope by session {
|
||||
|
||||
private val normalClosure = NanoWSD.WebSocketFrame.CloseCode.NormalClosure
|
||||
private val searchBooksUseCase: SearchBooksUseCase by lazy { GlobalContext.get().get() }
|
||||
private val searchControl = BookSearchControl()
|
||||
private val sentBookUrls = linkedSetOf<String>()
|
||||
@@ -39,62 +26,40 @@ class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
||||
|
||||
private val SEARCH_FINISH = "Search finish"
|
||||
|
||||
override fun onOpen() {
|
||||
launch(IO) {
|
||||
kotlin.runCatching {
|
||||
while (isOpen) {
|
||||
ping("ping".toByteArray())
|
||||
delay(30000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClose(
|
||||
code: NanoWSD.WebSocketFrame.CloseCode,
|
||||
reason: String,
|
||||
initiatedByRemote: Boolean
|
||||
) {
|
||||
searchJob?.cancel()
|
||||
cancel()
|
||||
}
|
||||
|
||||
override fun onMessage(message: NanoWSD.WebSocketFrame) {
|
||||
launch(IO) {
|
||||
kotlin.runCatching {
|
||||
if (!message.textPayload.isJson()) {
|
||||
send("数据必须为Json格式")
|
||||
close(normalClosure, SEARCH_FINISH, false)
|
||||
return@launch
|
||||
}
|
||||
val searchMap =
|
||||
GSON.fromJsonObject<Map<String, String>>(message.textPayload).getOrNull()
|
||||
if (searchMap != null) {
|
||||
val key = searchMap["key"]?.trim()
|
||||
if (key.isNullOrBlank()) {
|
||||
send(appCtx.getString(R.string.cannot_empty))
|
||||
close(normalClosure, SEARCH_FINISH, false)
|
||||
return@launch
|
||||
suspend fun handle() {
|
||||
try {
|
||||
session.incoming.consumeEach { frame ->
|
||||
if (frame is Frame.Text) {
|
||||
val text = frame.readText()
|
||||
if (!text.isJson()) {
|
||||
session.send("数据必须为Json格式")
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, SEARCH_FINISH))
|
||||
return@consumeEach
|
||||
}
|
||||
val searchMap = GSON.fromJsonObject<Map<String, String>>(text).getOrNull()
|
||||
if (searchMap != null) {
|
||||
val key = searchMap["key"]?.trim()
|
||||
if (key.isNullOrBlank()) {
|
||||
session.send(appCtx.getString(R.string.cannot_empty))
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, SEARCH_FINISH))
|
||||
return@consumeEach
|
||||
}
|
||||
startSearch(key)
|
||||
}
|
||||
startSearch(key)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
searchJob?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPong(pong: NanoWSD.WebSocketFrame) {
|
||||
|
||||
}
|
||||
|
||||
override fun onException(exception: IOException) {
|
||||
|
||||
}
|
||||
|
||||
private fun startSearch(key: String) {
|
||||
searchJob?.cancel()
|
||||
sentBookUrls.clear()
|
||||
searchControl.resume()
|
||||
searchJob = launch(IO) {
|
||||
searchJob = launch(Dispatchers.IO) {
|
||||
try {
|
||||
searchBooksUseCase
|
||||
.execute(
|
||||
@@ -113,17 +78,17 @@ class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
||||
is SearchRunEvent.Progress -> {
|
||||
val newBooks = event.upsertBooks.filter { sentBookUrls.add(it.bookUrl) }
|
||||
if (newBooks.isNotEmpty()) {
|
||||
send(GSON.toJson(newBooks))
|
||||
session.send(GSON.toJson(newBooks))
|
||||
}
|
||||
}
|
||||
|
||||
is SearchRunEvent.Finished -> close(normalClosure, SEARCH_FINISH, false)
|
||||
is SearchRunEvent.Finished -> session.close(CloseReason(CloseReason.Codes.NORMAL, SEARCH_FINISH))
|
||||
}
|
||||
}
|
||||
} catch (exception: CancellationException) {
|
||||
throw exception
|
||||
} catch (exception: Throwable) {
|
||||
close(normalClosure, exception.toString(), false)
|
||||
session.close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, exception.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,71 @@
|
||||
package io.legado.app.web.socket
|
||||
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoWSD
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.model.Debug
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import splitties.init.appCtx
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* web端书源调试
|
||||
*/
|
||||
class BookSourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
||||
NanoWSD.WebSocket(handshakeRequest),
|
||||
CoroutineScope by MainScope(),
|
||||
class BookSourceDebugWebSocket(private val session: DefaultWebSocketServerSession) :
|
||||
CoroutineScope by session,
|
||||
Debug.Callback {
|
||||
|
||||
private val notPrintState = arrayOf(10, 20, 30, 40)
|
||||
|
||||
override fun onOpen() {
|
||||
launch(IO) {
|
||||
kotlin.runCatching {
|
||||
while (isOpen) {
|
||||
ping("ping".toByteArray())
|
||||
delay(30000)
|
||||
suspend fun handle() {
|
||||
try {
|
||||
session.incoming.consumeEach { frame ->
|
||||
if (frame is Frame.Text) {
|
||||
val text = frame.readText()
|
||||
if (!text.isJson()) {
|
||||
session.send("数据必须为Json格式")
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||
return@consumeEach
|
||||
}
|
||||
val debugBean = GSON.fromJsonObject<Map<String, String>>(text).getOrNull()
|
||||
if (debugBean != null) {
|
||||
val tag = debugBean["tag"]
|
||||
val key = debugBean["key"]
|
||||
if (tag.isNullOrBlank() || key.isNullOrBlank()) {
|
||||
session.send(appCtx.getString(R.string.cannot_empty))
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||
return@consumeEach
|
||||
}
|
||||
appDb.bookSourceDao.getBookSource(tag)?.let {
|
||||
Debug.callback = this@BookSourceDebugWebSocket
|
||||
Debug.startDebug(this, it, key)
|
||||
}
|
||||
} else {
|
||||
session.send("数据必须为Json格式")
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||
return@consumeEach
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
Debug.cancelDebug(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClose(
|
||||
code: NanoWSD.WebSocketFrame.CloseCode,
|
||||
reason: String,
|
||||
initiatedByRemote: Boolean
|
||||
) {
|
||||
cancel()
|
||||
Debug.cancelDebug(true)
|
||||
}
|
||||
|
||||
override fun onMessage(message: NanoWSD.WebSocketFrame) {
|
||||
launch(IO) {
|
||||
kotlin.runCatching {
|
||||
if (!message.textPayload.isJson()) {
|
||||
send("数据必须为Json格式")
|
||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
||||
return@launch
|
||||
}
|
||||
val debugBean =
|
||||
GSON.fromJsonObject<Map<String, String>>(message.textPayload).getOrNull()
|
||||
if (debugBean != null) {
|
||||
val tag = debugBean["tag"]
|
||||
val key = debugBean["key"]
|
||||
if (tag.isNullOrBlank() || key.isNullOrBlank()) {
|
||||
send(appCtx.getString(R.string.cannot_empty))
|
||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
||||
return@launch
|
||||
}
|
||||
appDb.bookSourceDao.getBookSource(tag)?.let {
|
||||
Debug.callback = this@BookSourceDebugWebSocket
|
||||
Debug.startDebug(this, it, key)
|
||||
}
|
||||
} else {
|
||||
send("数据必须为Json格式")
|
||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPong(pong: NanoWSD.WebSocketFrame) {
|
||||
|
||||
}
|
||||
|
||||
override fun onException(exception: IOException) {
|
||||
Debug.cancelDebug(true)
|
||||
}
|
||||
|
||||
override fun printLog(state: Int, msg: String) {
|
||||
if (state in notPrintState) {
|
||||
return
|
||||
}
|
||||
runOnIO {
|
||||
launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
send(msg)
|
||||
session.send(msg)
|
||||
if (state == -1 || state == 1000) {
|
||||
Debug.cancelDebug(true)
|
||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||
}
|
||||
}.onFailure {
|
||||
it.printOnDebug()
|
||||
|
||||
@@ -1,97 +1,70 @@
|
||||
package io.legado.app.web.socket
|
||||
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoWSD
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.model.Debug
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import splitties.init.appCtx
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* web端订阅源调试
|
||||
*/
|
||||
class RssSourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
||||
NanoWSD.WebSocket(handshakeRequest),
|
||||
CoroutineScope by MainScope(),
|
||||
class RssSourceDebugWebSocket(private val session: DefaultWebSocketServerSession) :
|
||||
CoroutineScope by session,
|
||||
Debug.Callback {
|
||||
|
||||
private val notPrintState = arrayOf(10, 20, 30, 40)
|
||||
|
||||
override fun onOpen() {
|
||||
launch(IO) {
|
||||
kotlin.runCatching {
|
||||
while (isOpen) {
|
||||
ping("ping".toByteArray())
|
||||
delay(30000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClose(
|
||||
code: NanoWSD.WebSocketFrame.CloseCode,
|
||||
reason: String,
|
||||
initiatedByRemote: Boolean
|
||||
) {
|
||||
cancel()
|
||||
Debug.cancelDebug(true)
|
||||
}
|
||||
|
||||
override fun onMessage(message: NanoWSD.WebSocketFrame) {
|
||||
launch(IO) {
|
||||
kotlin.runCatching {
|
||||
if (!message.textPayload.isJson()) {
|
||||
send("数据必须为Json格式")
|
||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
||||
return@launch
|
||||
}
|
||||
val debugBean =
|
||||
GSON.fromJsonObject<Map<String, String>>(message.textPayload).getOrNull()
|
||||
if (debugBean != null) {
|
||||
val tag = debugBean["tag"]
|
||||
if (tag.isNullOrBlank()) {
|
||||
send(appCtx.getString(R.string.cannot_empty))
|
||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
||||
return@launch
|
||||
suspend fun handle() {
|
||||
try {
|
||||
session.incoming.consumeEach { frame ->
|
||||
if (frame is Frame.Text) {
|
||||
val text = frame.readText()
|
||||
if (!text.isJson()) {
|
||||
session.send("数据必须为Json格式")
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||
return@consumeEach
|
||||
}
|
||||
appDb.rssSourceDao.getByKey(tag)?.let {
|
||||
Debug.callback = this@RssSourceDebugWebSocket
|
||||
Debug.startDebug(this, it)
|
||||
val debugBean = GSON.fromJsonObject<Map<String, String>>(text).getOrNull()
|
||||
if (debugBean != null) {
|
||||
val tag = debugBean["tag"]
|
||||
if (tag.isNullOrBlank()) {
|
||||
session.send(appCtx.getString(R.string.cannot_empty))
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||
return@consumeEach
|
||||
}
|
||||
appDb.rssSourceDao.getByKey(tag)?.let {
|
||||
Debug.callback = this@RssSourceDebugWebSocket
|
||||
Debug.startDebug(this, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
Debug.cancelDebug(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPong(pong: NanoWSD.WebSocketFrame) {
|
||||
|
||||
}
|
||||
|
||||
override fun onException(exception: IOException) {
|
||||
Debug.cancelDebug(true)
|
||||
}
|
||||
|
||||
override fun printLog(state: Int, msg: String) {
|
||||
if (state in notPrintState) {
|
||||
return
|
||||
}
|
||||
runOnIO {
|
||||
launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
send(msg)
|
||||
session.send(msg)
|
||||
if (state == -1 || state == 1000) {
|
||||
Debug.cancelDebug(true)
|
||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
||||
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||
}
|
||||
}.onFailure {
|
||||
it.printOnDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ package io.legado.app.web.utils
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.text.TextUtils
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
class AssetsWeb(rootPath: String) {
|
||||
@@ -18,20 +17,19 @@ class AssetsWeb(rootPath: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getResponse(path: String): NanoHTTPD.Response {
|
||||
var path1 = path
|
||||
path1 = (rootPath + path1).replace("/+".toRegex(), File.separator)
|
||||
val inputStream = assetManager.open(path1)
|
||||
return NanoHTTPD.newChunkedResponse(
|
||||
NanoHTTPD.Response.Status.OK,
|
||||
getMimeType(path1),
|
||||
inputStream
|
||||
)
|
||||
fun getInputStream(path: String): InputStream? {
|
||||
val path1 = (rootPath + path).replace("/+".toRegex(), File.separator)
|
||||
return try {
|
||||
assetManager.open(path1)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMimeType(path: String): String {
|
||||
val suffix = path.substring(path.lastIndexOf("."))
|
||||
fun getMimeType(path: String): String {
|
||||
val lastDot = path.lastIndexOf(".")
|
||||
if (lastDot == -1) return "text/html"
|
||||
val suffix = path.substring(lastDot)
|
||||
return when {
|
||||
suffix.equals(".html", ignoreCase = true)
|
||||
|| suffix.equals(".htm", ignoreCase = true) -> "text/html"
|
||||
@@ -39,6 +37,8 @@ class AssetsWeb(rootPath: String) {
|
||||
suffix.equals(".css", ignoreCase = true) -> "text/css"
|
||||
suffix.equals(".ico", ignoreCase = true) -> "image/x-icon"
|
||||
suffix.equals(".jpg", ignoreCase = true) -> "image/jpg"
|
||||
suffix.equals(".png", ignoreCase = true) -> "image/png"
|
||||
suffix.equals(".svg", ignoreCase = true) -> "image/svg+xml"
|
||||
else -> "text/html"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user