From fe7fcb009b194146304f06f96a3f87fd0a8ceb6c Mon Sep 17 00:00:00 2001 From: SerVB Date: Tue, 15 Feb 2022 17:09:28 +0100 Subject: [PATCH] Add status endpoint --- .../protocol/toClient/StatusEndpoint.kt | 41 +++++++++++++++ .../common/protocol/toServer/ClientEvent.kt | 50 +++++++++++-------- .../headless/ProjectorHttpWsServerTest.kt | 17 +++++++ .../server/core/websocket/HttpWsServer.kt | 18 +++++++ .../core/websocket/HttpWsServerBuilder.kt | 2 + 5 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/StatusEndpoint.kt diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/StatusEndpoint.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/StatusEndpoint.kt new file mode 100644 index 000000000..ac006a1f9 --- /dev/null +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/StatusEndpoint.kt @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jetbrains.projector.common.protocol.toClient + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +data class Status( + val mainWindowsCount: Int, + val lastUserActionTimeStampMs: Long, +) + +private val json = Json.Default +private val serializer = Status.serializer() + +fun Status.toJson(): String = json.encodeToString(serializer, this) + +fun String.toStatus(): Status = json.decodeFromString(serializer, this) diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt index 9e45348af..a61102924 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt @@ -56,6 +56,12 @@ enum class KeyModifier { @Serializable sealed class ClientEvent +@Serializable +sealed class ClientUserEvent : ClientEvent() + +@Serializable +sealed class ClientNonUserEvent : ClientEvent() + @Serializable data class ClientMouseEvent( /** From connection opening. */ @@ -67,7 +73,7 @@ data class ClientMouseEvent( val clickCount: Int, val modifiers: Set, val mouseEventType: MouseEventType, -) : ClientEvent() { +) : ClientUserEvent() { enum class MouseEventType { MOVE, @@ -91,7 +97,7 @@ data class ClientWheelEvent( val y: Int, val deltaX: Double, val deltaY: Double, -) : ClientEvent() { +) : ClientUserEvent() { enum class ScrollingMode { PIXEL, @@ -109,7 +115,7 @@ data class ClientKeyEvent( val location: KeyLocation, val modifiers: Set, val keyEventType: KeyEventType, -) : ClientEvent() { +) : ClientUserEvent() { enum class KeyEventType { DOWN, @@ -130,7 +136,7 @@ data class ClientKeyPressEvent( val timeStamp: Int, val char: Char, val modifiers: Set, -) : ClientEvent() +) : ClientUserEvent() @Serializable data class ClientRawKeyEvent( @@ -141,7 +147,7 @@ data class ClientRawKeyEvent( val modifiers: Int, val location: Int, val keyEventType: RawKeyEventType, -) : ClientEvent() { +) : ClientUserEvent() { enum class RawKeyEventType { DOWN, @@ -151,38 +157,38 @@ data class ClientRawKeyEvent( } @Serializable -data class ClientResizeEvent(val size: CommonIntSize) : ClientEvent() +data class ClientResizeEvent(val size: CommonIntSize) : ClientUserEvent() @Serializable -data class ClientRequestImageDataEvent(val imageId: ImageId) : ClientEvent() +data class ClientRequestImageDataEvent(val imageId: ImageId) : ClientNonUserEvent() @Serializable data class ClientClipboardEvent( val stringContent: String, // TODO: support more types -) : ClientEvent() +) : ClientNonUserEvent() @Serializable data class ClientRequestPingEvent( /** From connection opening. */ val clientTimeStamp: Int, -) : ClientEvent() +) : ClientNonUserEvent() @Serializable data class ClientSetKeymapEvent( val keymap: UserKeymap, -) : ClientEvent() +) : ClientUserEvent() @Serializable data class ClientOpenLinkEvent( val link: String, -) : ClientEvent() +) : ClientUserEvent() @Serializable data class ClientWindowMoveEvent( val windowId: Int, val deltaX: Int, val deltaY: Int, -) : ClientEvent() +) : ClientUserEvent() // If delta is negative, we resizing to the left top @Serializable @@ -191,38 +197,38 @@ data class ClientWindowResizeEvent( val deltaX: Int, val deltaY: Int, val direction: ResizeDirection, -) : ClientEvent() +) : ClientUserEvent() @Serializable data class ClientWindowSetBoundsEvent( val windowId: Int, - val bounds: CommonIntRectangle -) : ClientEvent() + val bounds: CommonIntRectangle, +) : ClientUserEvent() @Serializable data class ClientDisplaySetChangeEvent( - val newDisplays: List -) : ClientEvent() + val newDisplays: List, +) : ClientUserEvent() @Serializable data class ClientWindowCloseEvent( val windowId: Int, -) : ClientEvent() +) : ClientUserEvent() @Serializable data class ClientWindowInterestEvent( val windowId: Int, - val isInterested: Boolean -): ClientEvent() + val isInterested: Boolean, +) : ClientNonUserEvent() @Suppress("unused") //used in client-web/org.jetbrains.projector.client.web.window.WindowManager and at server side @Serializable data class ClientWindowsActivationEvent( val windowIds: List, -) : ClientEvent() +) : ClientUserEvent() @Suppress("unused") //used in client-web/org.jetbrains.projector.client.web.window.WindowManager and at server side @Serializable data class ClientWindowsDeactivationEvent( val windowIds: List, -) : ClientEvent() +) : ClientUserEvent() diff --git a/projector-server-core/src/intTest/kotlin/org/jetbrains/projector/intTest/headless/ProjectorHttpWsServerTest.kt b/projector-server-core/src/intTest/kotlin/org/jetbrains/projector/intTest/headless/ProjectorHttpWsServerTest.kt index 2691c288f..75ce2e8e9 100644 --- a/projector-server-core/src/intTest/kotlin/org/jetbrains/projector/intTest/headless/ProjectorHttpWsServerTest.kt +++ b/projector-server-core/src/intTest/kotlin/org/jetbrains/projector/intTest/headless/ProjectorHttpWsServerTest.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.runBlocking import org.java_websocket.WebSocket import org.jetbrains.projector.common.protocol.toClient.MainWindow import org.jetbrains.projector.common.protocol.toClient.toMainWindowList +import org.jetbrains.projector.common.protocol.toClient.toStatus import org.jetbrains.projector.server.core.websocket.HttpWsServer import java.io.File import java.nio.ByteBuffer @@ -75,6 +76,8 @@ class ProjectorHttpWsServerTest { pngBase64Icon = "png base 64", ) ) + + override fun getLastUserActionTimeStampMs(): Long = 123 } } @@ -143,6 +146,20 @@ class ProjectorHttpWsServerTest { server.stop() } + @Test + fun testStatus() { + val server = createServer().also { it.start() } + val client = HttpClient() + + val response = runBlocking { client.get(prj("/status")) }.toStatus() + + assertEquals(1, response.mainWindowsCount) + assertEquals(123, response.lastUserActionTimeStampMs) + + client.close() + server.stop() + } + @Test fun testFiles() { val server = createServer().also { it.start() } diff --git a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServer.kt b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServer.kt index 48fe4017a..94249f57d 100644 --- a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServer.kt +++ b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServer.kt @@ -35,6 +35,7 @@ import org.java_websocket.handshake.* import org.java_websocket.server.WebSocketServer import org.java_websocket.util.Charsetfunctions import org.jetbrains.projector.common.protocol.toClient.MainWindow +import org.jetbrains.projector.common.protocol.toClient.Status import org.jetbrains.projector.common.protocol.toClient.toJson import org.jetbrains.projector.server.core.ClientWrapper import org.jetbrains.projector.server.core.util.getWildcardHostAddress @@ -60,6 +61,7 @@ public abstract class HttpWsServer(host: InetAddress, port: Int) : HttpWsTranspo public constructor(port: Int) : this(getWildcardHostAddress(), port) public abstract fun getMainWindows(): List + public abstract fun getLastUserActionTimeStampMs(): Long public fun onGetRequest(path: String): GetRequestResult { val pathWithoutParams = path.substringBefore('?').substringBefore('#') @@ -75,6 +77,22 @@ public abstract class HttpWsServer(host: InetAddress, port: Int) : HttpWsTranspo ) } + if (pathWithoutParams == "/status") { + val mainWindows = getMainWindows() + val lastUserActionTimeStampMs = getLastUserActionTimeStampMs() + val status = Status( + mainWindowsCount = mainWindows.size, + lastUserActionTimeStampMs = lastUserActionTimeStampMs, + ) + val json = status.toJson().toByteArray() + return GetRequestResult( + statusCode = 200, + statusText = "OK", + contentType = "text/json", + content = json, + ) + } + val resourceFileName = getResourceName(pathWithoutParams) val resourceBytes = getResource(resourceFileName) diff --git a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServerBuilder.kt b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServerBuilder.kt index c23caaa6d..3b23f2073 100644 --- a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServerBuilder.kt +++ b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/websocket/HttpWsServerBuilder.kt @@ -33,10 +33,12 @@ import java.nio.ByteBuffer public class HttpWsServerBuilder(private val host: InetAddress, private val port: Int): WsTransportBuilder() { public lateinit var getMainWindows: () -> List + public lateinit var getLastUserActionTimeStampMs: () -> Long override fun build(): HttpWsServer { val wsServer = object : HttpWsServer(host, port) { override fun getMainWindows(): List = this@HttpWsServerBuilder.getMainWindows() + override fun getLastUserActionTimeStampMs() = this@HttpWsServerBuilder.getLastUserActionTimeStampMs() override fun onError(connection: WebSocket?, e: Exception) = this@HttpWsServerBuilder.onError(connection, e) override fun onWsOpen(connection: WebSocket) = this@HttpWsServerBuilder.onWsOpen(connection) override fun onWsClose(connection: WebSocket) = this@HttpWsServerBuilder.onWsClose(connection)