From c8141c633842b1aa16fffbcbede53e5a1aa5f7f1 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 3 Dec 2022 13:53:34 +0300 Subject: [PATCH] Refactor server API --- .../src/jvmMain/kotlin/formServer.kt | 80 +++--- .../main/kotlin/ru/mipt/npm/sat/satServer.kt | 20 +- jupyter/src/jvmMain/kotlin/VFForNotebook.kt | 24 +- .../visionforge/server/VisionServer.kt | 269 +++++++++--------- .../server/applicationExtensions.kt | 38 +++ 5 files changed, 240 insertions(+), 191 deletions(-) create mode 100644 visionforge-server/src/main/kotlin/space/kscience/visionforge/server/applicationExtensions.kt diff --git a/demo/playground/src/jvmMain/kotlin/formServer.kt b/demo/playground/src/jvmMain/kotlin/formServer.kt index 4b63b569..9e635dc9 100644 --- a/demo/playground/src/jvmMain/kotlin/formServer.kt +++ b/demo/playground/src/jvmMain/kotlin/formServer.kt @@ -2,6 +2,7 @@ package space.kscience.visionforge.examples import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer +import io.ktor.server.http.content.resources import io.ktor.server.routing.routing import kotlinx.html.* import space.kscience.dataforge.context.Global @@ -10,6 +11,7 @@ import space.kscience.visionforge.VisionManager import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.html.formFragment import space.kscience.visionforge.onPropertyChange +import space.kscience.visionforge.server.EngineConnectorConfig import space.kscience.visionforge.server.close import space.kscience.visionforge.server.openInBrowser import space.kscience.visionforge.server.visionPage @@ -17,48 +19,54 @@ import space.kscience.visionforge.server.visionPage fun main() { val visionManager = Global.fetch(VisionManager) - val server = embeddedServer(CIO, 7777, "localhost") { + + val connector = EngineConnectorConfig("localhost", 7777) + + val server = embeddedServer(CIO, connector.port, connector.host) { routing { - visionPage(visionManager, VisionPage.scriptHeader("js/visionforge-playground.js")) { - val form = formFragment("form") { - label { - htmlFor = "fname" - +"First name:" - } - br() - input { - type = InputType.text - id = "fname" - name = "fname" - value = "John" - } - br() - label { - htmlFor = "lname" - +"Last name:" - } - br() - input { - type = InputType.text - id = "lname" - name = "lname" - value = "Doe" - } - br() - br() - input { - type = InputType.submit - value = "Submit" - } - } + resources() + } - vision("form") { form } - form.onPropertyChange { - println(this) + visionPage(connector, visionManager, VisionPage.scriptHeader("js/visionforge-playground.js")) { + val form = formFragment("form") { + label { + htmlFor = "fname" + +"First name:" + } + br() + input { + type = InputType.text + id = "fname" + name = "fname" + value = "John" + } + br() + label { + htmlFor = "lname" + +"Last name:" + } + br() + input { + type = InputType.text + id = "lname" + name = "lname" + value = "Doe" + } + br() + br() + input { + type = InputType.submit + value = "Submit" } } + + vision("form") { form } + form.onPropertyChange { + println(this) + } } + }.start(false) server.openInBrowser() diff --git a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt index a55020a2..ed8ae38f 100644 --- a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt +++ b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt @@ -15,6 +15,7 @@ import space.kscience.dataforge.meta.Null import space.kscience.dataforge.names.Name import space.kscience.visionforge.Colors import space.kscience.visionforge.html.VisionPage +import space.kscience.visionforge.server.EngineConnectorConfig import space.kscience.visionforge.server.close import space.kscience.visionforge.server.openInBrowser import space.kscience.visionforge.server.visionPage @@ -36,21 +37,26 @@ fun main() { color.set(Colors.white) } } + val connector = EngineConnectorConfig("localhost", 7777) - val server = embeddedServer(CIO,7777, "localhost") { + val server = embeddedServer(CIO, connector.port, connector.host) { routing { - static { resources() } + } - visionPage(solids.visionManager, VisionPage.threeJsHeader, VisionPage.styleSheetHeader("css/styles.css")) { - div("flex-column") { - h1 { +"Satellite detector demo" } - vision { sat } - } + visionPage( + connector, + solids.visionManager, VisionPage.threeJsHeader, + VisionPage.styleSheetHeader("css/styles.css") + ) { + div("flex-column") { + h1 { +"Satellite detector demo" } + vision { sat } } } + }.start(false) server.openInBrowser() diff --git a/jupyter/src/jvmMain/kotlin/VFForNotebook.kt b/jupyter/src/jvmMain/kotlin/VFForNotebook.kt index c3b79734..1d6a430c 100644 --- a/jupyter/src/jvmMain/kotlin/VFForNotebook.kt +++ b/jupyter/src/jvmMain/kotlin/VFForNotebook.kt @@ -4,9 +4,8 @@ import io.ktor.http.URLProtocol import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.ApplicationEngine +import io.ktor.server.engine.EngineConnectorConfig import io.ktor.server.engine.embeddedServer -import io.ktor.server.routing.Routing -import io.ktor.server.routing.route import io.ktor.server.util.url import io.ktor.server.websocket.WebSockets import kotlinx.coroutines.CoroutineScope @@ -26,8 +25,8 @@ import space.kscience.visionforge.html.HtmlFormFragment import space.kscience.visionforge.html.HtmlVisionFragment import space.kscience.visionforge.html.VisionCollector import space.kscience.visionforge.html.visionFragment -import space.kscience.visionforge.server.VisionRouteConfiguration -import space.kscience.visionforge.server.require +import space.kscience.visionforge.server.EngineConnectorConfig +import space.kscience.visionforge.server.VisionRoute import space.kscience.visionforge.server.serveVisionData import space.kscience.visionforge.visionManager import kotlin.coroutines.CoroutineContext @@ -48,8 +47,6 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout public val visionManager: VisionManager = context.visionManager - private val configuration = VisionRouteConfiguration(visionManager) - private var counter = 0 private var engine: ApplicationEngine? = null @@ -68,7 +65,7 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout public fun startServer( host: String = context.properties["visionforge.host"].string ?: "localhost", - port: Int = context.properties["visionforge.port"].int ?: VisionRouteConfiguration.DEFAULT_PORT, + port: Int = context.properties["visionforge.port"].int ?: VisionRoute.DEFAULT_PORT, ): MimeTypedResult = html { if (engine != null) { p { @@ -77,6 +74,8 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout } } + val connector: EngineConnectorConfig = EngineConnectorConfig(host, port) + engine?.stop(1000, 2000) engine = context.embeddedServer(CIO, port, host) { install(WebSockets) @@ -111,7 +110,7 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout val collector: VisionCollector = mutableMapOf() val url = engine.environment.connectors.first().let { - url{ + url { protocol = URLProtocol.WS host = it.host port = it.port @@ -119,6 +118,8 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout } } + engine.application.serveVisionData(VisionRoute(cellRoute, visionManager), collector) + visionFragment( context, embedData = true, @@ -126,13 +127,6 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout collector = collector, fragment = fragment ) - - engine.application.require(Routing) { - route(cellRoute) { - serveVisionData(TODO(), collector) - } - } - } else { //if not, use static rendering visionFragment(context, fragment = fragment) diff --git a/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt index 2fe84660..957936a3 100644 --- a/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.html.* +import kotlinx.html.stream.createHTML import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.meta.* @@ -30,33 +31,32 @@ import space.kscience.visionforge.VisionChange import space.kscience.visionforge.VisionManager import space.kscience.visionforge.flowChanges import space.kscience.visionforge.html.* -import java.awt.Desktop -import java.net.URI import kotlin.time.Duration.Companion.milliseconds -public enum class DataServeMode { - /** - * Embed the initial state of the vision inside its html tag. - */ - EMBED, - - /** - * Fetch data on vision load. Do not embed data. - */ - FETCH, - - /** - * Connect to server to get pushes. The address of the server is embedded in the tag. - */ - UPDATE -} - -public class VisionRouteConfiguration( +public class VisionRoute( + public val route: String, public val visionManager: VisionManager, override val meta: ObservableMutableMeta = MutableMeta(), ) : Configurable, ContextAware { + public enum class Mode { + /** + * Embed the initial state of the vision inside its html tag. + */ + EMBED, + + /** + * Fetch data on vision load. Do not embed data. + */ + FETCH, + + /** + * Connect to server to get pushes. The address of the server is embedded in the tag. + */ + UPDATE + } + override val context: Context get() = visionManager.context /** @@ -64,7 +64,7 @@ public class VisionRouteConfiguration( */ public var updateInterval: Long by meta.long(300, key = UPDATE_INTERVAL_KEY) - public var dataMode: DataServeMode by meta.enum(DataServeMode.UPDATE) + public var dataMode: Mode by meta.enum(Mode.UPDATE) public companion object { public const val DEFAULT_PORT: Int = 7777 @@ -78,65 +78,74 @@ public class VisionRouteConfiguration( * Serve visions in a given [route] without providing a page template. * [visions] could be changed during the service. */ -public fun Route.serveVisionData( - configuration: VisionRouteConfiguration, +public fun Application.serveVisionData( + configuration: VisionRoute, resolveVision: (Name) -> Vision?, ) { - application.log.info("Serving visions at ${this@serveVisionData}") - - //Update websocket - webSocket("ws") { - val name: String = call.request.queryParameters.getOrFail("name") - application.log.debug("Opened server socket for $name") - val vision: Vision = resolveVision(Name.parse(name)) ?: error("Plot with id='$name' not registered") - - launch { - incoming.consumeEach { - val data = it.data.decodeToString() - application.log.debug("Received update: \n$data") - val change = configuration.visionManager.jsonFormat.decodeFromString( - VisionChange.serializer(), data - ) - vision.update(change) + require(WebSockets) + routing { + route(configuration.route) { + install(CORS) { + anyHost() } - } + application.log.info("Serving visions at ${configuration.route}") - try { - withContext(configuration.context.coroutineContext) { - vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update -> - val json = configuration.visionManager.jsonFormat.encodeToString( - VisionChange.serializer(), - update + //Update websocket + webSocket("ws") { + val name: String = call.request.queryParameters.getOrFail("name") + application.log.debug("Opened server socket for $name") + val vision: Vision = resolveVision(Name.parse(name)) ?: error("Plot with id='$name' not registered") + + launch { + incoming.consumeEach { + val data = it.data.decodeToString() + application.log.debug("Received update: \n$data") + val change = configuration.visionManager.jsonFormat.decodeFromString( + VisionChange.serializer(), data + ) + vision.update(change) + } + } + + try { + withContext(configuration.context.coroutineContext) { + vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update -> + val json = configuration.visionManager.jsonFormat.encodeToString( + VisionChange.serializer(), + update + ) + application.log.debug("Sending update: \n$json") + outgoing.send(Frame.Text(json)) + }.collect() + } + } catch (t: Throwable) { + this.application.log.info("WebSocket update channel for $name is closed with exception: $t") + } + } + //Plots in their json representation + get("data") { + val name: String = call.request.queryParameters.getOrFail("name") + + val vision: Vision? = resolveVision(Name.parse(name)) + if (vision == null) { + call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found") + } else { + call.respondText( + configuration.visionManager.encodeToString(vision), + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK ) - application.log.debug("Sending update: \n$json") - outgoing.send(Frame.Text(json)) - }.collect() + } } - } catch (t: Throwable) { - this.application.log.info("WebSocket update channel for $name is closed with exception: $t") - } - } - //Plots in their json representation - get("data") { - val name: String = call.request.queryParameters.getOrFail("name") - - val vision: Vision? = resolveVision(Name.parse(name)) - if (vision == null) { - call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found") - } else { - call.respondText( - configuration.visionManager.encodeToString(vision), - contentType = ContentType.Application.Json, - status = HttpStatusCode.OK - ) } } } -public fun Route.serveVisionData( - configuration: VisionRouteConfiguration, +public fun Application.serveVisionData( + configuration: VisionRoute, cache: VisionCollector, ): Unit = serveVisionData(configuration) { cache[it]?.second } + // ///** // * Compile a fragment to string and serve visions from it @@ -161,91 +170,85 @@ public fun Route.serveVisionData( /** * Serve a page, potentially containing any number of visions at a given [route] with given [header]. */ -public fun Route.visionPage( - configuration: VisionRouteConfiguration, +public fun Application.visionPage( + route: String, + configuration: VisionRoute, + connector: EngineConnectorConfig, headers: Collection, visionFragment: HtmlVisionFragment, ) { - application.require(WebSockets) - require(CORS) { - anyHost() - } + require(WebSockets) - val visionCache: VisionCollector = mutableMapOf() - serveVisionData(configuration, visionCache) + val collector: VisionCollector = mutableMapOf() - //filled pages - get { - //re-create html and vision list on each call - call.respondHtml { - val callbackUrl = call.url() - head { - meta { - charset = "utf-8" - } - headers.forEach { header -> - consumer.header() - } + val html = createHTML().apply { + head { + meta { + charset = "utf-8" } - body { - //Load the fragment and remember all loaded visions - visionFragment( - context = configuration.context, - embedData = configuration.dataMode == DataServeMode.EMBED, - fetchDataUrl = if (configuration.dataMode != DataServeMode.EMBED) { - URLBuilder(callbackUrl).apply { - pathSegments = pathSegments + "data" - }.buildString() - } else null, - updatesUrl = if (configuration.dataMode == DataServeMode.UPDATE) { - URLBuilder(callbackUrl).apply { - protocol = URLProtocol.WS - pathSegments = pathSegments + "ws" - }.buildString() - } else null, - visionCache = visionCache, - fragment = visionFragment - ) + headers.forEach { header -> + consumer.header() } } + body { + //Load the fragment and remember all loaded visions + visionFragment( + context = configuration.context, + embedData = configuration.dataMode == VisionRoute.Mode.EMBED, + fetchDataUrl = if (configuration.dataMode != VisionRoute.Mode.EMBED) { + url { + host = connector.host + port = connector.port + path(route, "data") + } + } else null, + updatesUrl = if (configuration.dataMode == VisionRoute.Mode.UPDATE) { + url { + protocol = URLProtocol.WS + host = connector.host + port = connector.port + path(route, "ws") + } + } else null, + visionCache = collector, + fragment = visionFragment + ) + } + }.finalize() + //serve data + serveVisionData(configuration, collector) + + //filled pages + routing { + get(route) { + call.respondText(html, ContentType.Text.Html) + } } } -public fun Route.visionPage( +public fun Application.visionPage( + connector: EngineConnectorConfig, visionManager: VisionManager, vararg headers: HtmlFragment, - configurationBuilder: VisionRouteConfiguration.() -> Unit = {}, + route: String = "/", + configurationBuilder: VisionRoute.() -> Unit = {}, visionFragment: HtmlVisionFragment, ) { - val configuration = VisionRouteConfiguration(visionManager).apply(configurationBuilder) - visionPage(configuration, listOf(*headers), visionFragment) + val configuration = VisionRoute(route, visionManager).apply(configurationBuilder) + visionPage(route, configuration, connector, listOf(*headers), visionFragment) } /** * Render given [VisionPage] at server */ -public fun Route.visionPage(page: VisionPage, configurationBuilder: VisionRouteConfiguration.() -> Unit = {}) { - val configuration = VisionRouteConfiguration(page.visionManager).apply(configurationBuilder) - visionPage(configuration, page.pageHeaders.values, visionFragment = page.content) +public fun Application.visionPage( + connector: EngineConnectorConfig, + page: VisionPage, + route: String = "/", + configurationBuilder: VisionRoute.() -> Unit = {}, +) { + val configuration = VisionRoute(route, page.visionManager).apply(configurationBuilder) + visionPage(route, configuration, connector, page.pageHeaders.values, visionFragment = page.content) } -public fun

, B : Any, F : Any> P.require( - plugin: Plugin, - configure: B.() -> Unit = {}, -): F = pluginOrNull(plugin) ?: install(plugin, configure) - - -/** - * Connect to a given Ktor server using browser - */ -public fun ApplicationEngine.openInBrowser() { - val connector = environment.connectors.first() - val uri = URI("http", null, connector.host, connector.port, null, null, null) - Desktop.getDesktop().browse(uri) -} - -/** - * Stop the server with default timeouts - */ -public fun ApplicationEngine.close(): Unit = stop(1000, 5000) \ No newline at end of file diff --git a/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/applicationExtensions.kt b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/applicationExtensions.kt new file mode 100644 index 00000000..f33b59b2 --- /dev/null +++ b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/applicationExtensions.kt @@ -0,0 +1,38 @@ +package space.kscience.visionforge.server + +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.Plugin +import io.ktor.server.application.install +import io.ktor.server.application.pluginOrNull +import io.ktor.server.engine.ApplicationEngine +import io.ktor.server.engine.EngineConnectorBuilder +import io.ktor.server.engine.EngineConnectorConfig +import io.ktor.util.pipeline.Pipeline +import java.awt.Desktop +import java.net.URI + + +public fun

, B : Any, F : Any> P.require( + plugin: Plugin, +): F = pluginOrNull(plugin) ?: install(plugin) + + +/** + * Connect to a given Ktor server using browser + */ +public fun ApplicationEngine.openInBrowser() { + val connector = environment.connectors.first() + val uri = URI("http", null, connector.host, connector.port, null, null, null) + Desktop.getDesktop().browse(uri) +} + +/** + * Stop the server with default timeouts + */ +public fun ApplicationEngine.close(): Unit = stop(1000, 5000) + + +public fun EngineConnectorConfig(host: String, port: Int): EngineConnectorConfig = EngineConnectorBuilder().apply { + this.host = host + this.port = port +} \ No newline at end of file