Skip to content
This repository has been archived by the owner on Mar 26, 2024. It is now read-only.

Add status endpoint #133

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ val commonVersionList = listOf(
1980644253,
1328208692,
-1867356666,
-1218764012,
)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -67,7 +73,7 @@ data class ClientMouseEvent(
val clickCount: Int,
val modifiers: Set<MouseModifier>,
val mouseEventType: MouseEventType,
) : ClientEvent() {
) : ClientUserEvent() {

enum class MouseEventType {
MOVE,
Expand All @@ -91,7 +97,7 @@ data class ClientWheelEvent(
val y: Int,
val deltaX: Double,
val deltaY: Double,
) : ClientEvent() {
) : ClientUserEvent() {

enum class ScrollingMode {
PIXEL,
Expand All @@ -109,7 +115,7 @@ data class ClientKeyEvent(
val location: KeyLocation,
val modifiers: Set<KeyModifier>,
val keyEventType: KeyEventType,
) : ClientEvent() {
) : ClientUserEvent() {

enum class KeyEventType {
DOWN,
Expand All @@ -130,7 +136,7 @@ data class ClientKeyPressEvent(
val timeStamp: Int,
val char: Char,
val modifiers: Set<KeyModifier>,
) : ClientEvent()
) : ClientUserEvent()

@Serializable
data class ClientRawKeyEvent(
Expand All @@ -141,7 +147,7 @@ data class ClientRawKeyEvent(
val modifiers: Int,
val location: Int,
val keyEventType: RawKeyEventType,
) : ClientEvent() {
) : ClientUserEvent() {

enum class RawKeyEventType {
DOWN,
Expand All @@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why clipboard is not a user event? It's intentionally initiated by user

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider this one as a non-user because it happens when clipboard content on the client side is changed. Well, in Web Client, it's currently generated just before a user presses Ctrl+V, but it's a limitation of the browser that we can't easily listen to clipboard updates system-wide (I have some noted thoughts how we can support listening in the future; and there shouldn't be any problems when implementing listeners for native apps).

So this is low-level clipboard sync event, it's not user-generated.

I agree it's unclear.

Do you think adding a comment for ClientClipboardEvent like the following is enough here?

/** A change of the clipboard content of the client-side has happened. */

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think comment should mention that change happened because of 'low-level clipboard sync event'


@Serializable
data class ClientRequestPingEvent(
/** From connection opening. */
val clientTimeStamp: Int,
) : ClientEvent()
) : ClientNonUserEvent()

@Serializable
data class ClientSetKeymapEvent(
val keymap: UserKeymap,
) : ClientEvent()
) : ClientUserEvent()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inverse situation of clipboard: isn't keymap updated automatically? Or please specify what you understand saying user and non-user event

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, in the Web Client, indeed this event is sent only during the start of the connection. However, it can be implemented that a user can send this event manually. That's why I've selected to make it ClientUserEvent.

I agree that it's a bit unclear how to divide events. Initial though for this is to allow the server to understand if this action is counted as user activity or not. Maybe the naming user and non-user isn't good here. Maybe we can have a call and brainstorm it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to schedule a call on Tuesday or Wednesday. Also I plan to work about a half of Monday, so it's also an option

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is PR is not in priority so let me add this topic to the weekly meeting


@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
Expand All @@ -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<DisplayDescription>
) : ClientEvent()
val newDisplays: List<DisplayDescription>,
) : 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<Int>,
) : 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<Int>,
) : ClientEvent()
) : ClientUserEvent()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,6 +76,8 @@ class ProjectorHttpWsServerTest {
pngBase64Icon = "png base 64",
)
)

override fun getLastUserActionTimeStampMs(): Long = 123
}
}

Expand Down Expand Up @@ -143,6 +146,20 @@ class ProjectorHttpWsServerTest {
server.stop()
}

@Test
fun testStatus() {
val server = createServer().also { it.start() }
val client = HttpClient()
ARTI1208 marked this conversation as resolved.
Show resolved Hide resolved

val response = runBlocking { client.get<String>(prj("/status")) }.toStatus()

assertEquals(1, response.mainWindowsCount)
assertEquals(123, response.lastUserActionTimeStampMs)

client.close()
server.stop()
}

@Test
fun testFiles() {
val server = createServer().also { it.start() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MainWindow>
public abstract fun getLastUserActionTimeStampMs(): Long

public fun onGetRequest(path: String): GetRequestResult {
val pathWithoutParams = path.substringBefore('?').substringBefore('#')
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MainWindow>
public lateinit var getLastUserActionTimeStampMs: () -> Long
ARTI1208 marked this conversation as resolved.
Show resolved Hide resolved

override fun build(): HttpWsServer {
val wsServer = object : HttpWsServer(host, port) {
override fun getMainWindows(): List<MainWindow> = [email protected]()
override fun getLastUserActionTimeStampMs() = [email protected]()
override fun onError(connection: WebSocket?, e: Exception) = [email protected](connection, e)
override fun onWsOpen(connection: WebSocket) = [email protected](connection)
override fun onWsClose(connection: WebSocket) = [email protected](connection)
Expand Down