From e1112966cc5ad74f6b6386a58d06fcb0b27964ed Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 17 Feb 2022 14:07:11 +0000 Subject: [PATCH] [jb] track client project sessions --- .idea/compiler.xml | 9 -- .idea/gradle.xml | 11 --- .idea/misc.xml | 1 + .../gitpodprotocol/api/GitpodServer.java | 3 + .../api/entities/RemoteTrackMessage.java | 34 +++++++ .../backend-plugin/launch-dev-server.sh | 1 + .../GitpodClientProjectSessionTracker.kt | 42 ++++++++ .../gitpod/jetbrains/remote/GitpodManager.kt | 99 ++++++++++++++++++- .../remote/services/HeartbeatService.kt | 78 ++------------- .../remote/services/SupervisorInfoService.kt | 1 + .../src/main/resources/META-INF/plugin.xml | 1 + 11 files changed, 183 insertions(+), 97 deletions(-) create mode 100644 components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/RemoteTrackMessage.java create mode 100644 components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 836a569c033401..763be33f18cff8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,15 +2,6 @@ - - - - - - - - - diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 24e6200b768ab1..66d956e011c983 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -15,17 +15,6 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 6ed36dd36d45a7..efc86773015516 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,5 @@ + \ No newline at end of file diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java index 0af229018d7550..0a95ad325e5984 100644 --- a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java @@ -17,6 +17,9 @@ public interface GitpodServer { @JsonRequest CompletableFuture sendHeartBeat(SendHeartBeatOptions options); + @JsonRequest + CompletableFuture trackEvent(RemoteTrackMessage event); + @JsonRequest CompletableFuture> getGitpodTokenScopes(String tokenHash); diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/RemoteTrackMessage.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/RemoteTrackMessage.java new file mode 100644 index 00000000000000..b20f0806b53acb --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/RemoteTrackMessage.java @@ -0,0 +1,34 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class RemoteTrackMessage { + private String event; + private Object properties; + + public String getEvent() { + return event; + } + + public void setEvent(String event) { + this.event = event; + } + + public Object getProperties() { + return properties; + } + + public void setProperties(Object properties) { + this.properties = properties; + } + + @Override + public String toString() { + return "RemoteTrackMessage{" + + "event='" + event + '\'' + + ", properties=" + properties + + '}'; + } +} diff --git a/components/ide/jetbrains/backend-plugin/launch-dev-server.sh b/components/ide/jetbrains/backend-plugin/launch-dev-server.sh index 966dbd2eb40f00..82d62e6ad3f25a 100755 --- a/components/ide/jetbrains/backend-plugin/launch-dev-server.sh +++ b/components/ide/jetbrains/backend-plugin/launch-dev-server.sh @@ -26,6 +26,7 @@ if [ ! -d "$TEST_DIR" ]; then git clone $TEST_REPO $TEST_DIR fi +export JB_DEV=true export JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:0" $TEST_BACKEND_DIR/bin/remote-dev-server.sh run $TEST_DIR \ No newline at end of file diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt new file mode 100644 index 00000000000000..0d52ae13a4f4e0 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt @@ -0,0 +1,42 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote + +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.client.ClientProjectSession +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import io.gitpod.gitpodprotocol.api.entities.RemoteTrackMessage +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch + +class GitpodClientProjectSessionTracker( + private val session: ClientProjectSession +) { + + private val manager = service() + init { + GlobalScope.launch { + val info = manager.pendingInfo.await() + val event = RemoteTrackMessage().apply { + event = "jb_session" + properties = mapOf( + "sessionId" to session.clientId.value, + "instanceId" to info.infoResponse.instanceId, + "workspaceId" to info.infoResponse.workspaceId, + "appName" to ApplicationInfo.getInstance().versionName, + "appVersion" to ApplicationInfo.getInstance().fullVersion, + "timestamp" to System.currentTimeMillis() + ) + } + if (manager.devMode) { + thisLogger().warn("gitpod: $event") + } else { + manager.client.server.trackEvent(event) + } + } + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt index 49b97ae4ee09f6..143c0ceb891ad3 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt @@ -4,15 +4,20 @@ package io.gitpod.jetbrains.remote +import com.intellij.ide.plugins.PluginManagerCore import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.PluginId import com.intellij.remoteDev.util.onTerminationOrNow import com.jetbrains.rd.util.lifetime.Lifetime import git4idea.config.GitVcsApplicationSettings +import io.gitpod.gitpodprotocol.api.GitpodClient +import io.gitpod.gitpodprotocol.api.GitpodServerLauncher +import io.gitpod.jetbrains.remote.services.HeartbeatService import io.gitpod.jetbrains.remote.services.SupervisorInfoService import io.gitpod.supervisor.api.Notification.* import io.gitpod.supervisor.api.NotificationServiceGrpc @@ -31,10 +36,19 @@ import java.net.http.HttpResponse import java.time.Duration import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture +import javax.websocket.DeploymentException @Service class GitpodManager : Disposable { + val devMode = System.getenv("JB_DEV").toBoolean() + + private val lifetime = Lifetime.Eternal.createNested() + + override fun dispose() { + lifetime.terminate() + } + init { GlobalScope.launch { try { @@ -68,8 +82,6 @@ class GitpodManager : Disposable { GitVcsApplicationSettings.getInstance().isUseCredentialHelper = true } - private val lifetime = Lifetime.Eternal.createNested() - private val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications") private val notificationsJob = GlobalScope.launch { val notifications = NotificationServiceGrpc.newStub(SupervisorInfoService.channel) @@ -127,14 +139,91 @@ class GitpodManager : Disposable { delay(1000L) } } - init { lifetime.onTerminationOrNow { notificationsJob.cancel() } } - override fun dispose() { - lifetime.terminate() + val pendingInfo = CompletableFuture() + private val infoJob = GlobalScope.launch { + try { + // TOO(ak) inline SupervisorInfoService + pendingInfo.complete(SupervisorInfoService.fetch()) + } catch (t: Throwable) { + pendingInfo.completeExceptionally(t) + } + } + init { + lifetime.onTerminationOrNow { + infoJob.cancel() + } + } + + val client = GitpodClient() + private val serverJob = GlobalScope.launch { + val info = pendingInfo.await() + val launcher = GitpodServerLauncher.create(client) + val plugin = PluginManagerCore.getPlugin(PluginId.getId("io.gitpod.jetbrains.remote"))!! + val connect = { + val originalClassLoader = Thread.currentThread().contextClassLoader + try { + // see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003146180/comments/360000376240 + Thread.currentThread().contextClassLoader = HeartbeatService::class.java.classLoader + launcher.listen( + info.infoResponse.gitpodApi.endpoint, + info.infoResponse.gitpodHost, + plugin.pluginId.idString, + plugin.version, + info.tokenResponse.token + ) + } finally { + Thread.currentThread().contextClassLoader = originalClassLoader; + } + } + + val minReconnectionDelay = 2 * 1000L + val maxReconnectionDelay = 30 * 1000L + val reconnectionDelayGrowFactor = 1.5; + var reconnectionDelay = minReconnectionDelay; + val gitpodHost = info.infoResponse.gitpodApi.host + var closeReason: Any = "cancelled" + try { + while (kotlin.coroutines.coroutineContext.isActive) { + try { + val connection = connect() + thisLogger().info("$gitpodHost: connected") + reconnectionDelay = minReconnectionDelay + closeReason = connection.await() + thisLogger().warn("$gitpodHost: connection closed, reconnecting after $reconnectionDelay milliseconds: $closeReason") + } catch (t: Throwable) { + if (t is DeploymentException) { + // connection is alright, but server does not want to handshake, there is no point to try with the same token again + throw t + } + closeReason = t + thisLogger().warn( + "$gitpodHost: failed to connect, trying again after $reconnectionDelay milliseconds:", + closeReason + ) + } + delay(reconnectionDelay) + closeReason = "cancelled" + reconnectionDelay = (reconnectionDelay * reconnectionDelayGrowFactor).toLong() + if (reconnectionDelay > maxReconnectionDelay) { + reconnectionDelay = maxReconnectionDelay + } + } + } catch (t: Throwable) { + if (t !is CancellationException) { + closeReason = t + } + } + thisLogger().warn("$gitpodHost: connection permanently closed: $closeReason") + } + init { + lifetime.onTerminationOrNow { + serverJob.cancel() + } } } \ No newline at end of file diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt index 4bd59f294aa833..43532aea82b417 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt @@ -4,31 +4,24 @@ package io.gitpod.jetbrains.remote.services -import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.extensions.PluginId -import io.gitpod.gitpodprotocol.api.GitpodClient -import io.gitpod.gitpodprotocol.api.GitpodServerLauncher import io.gitpod.gitpodprotocol.api.entities.SendHeartBeatOptions +import io.gitpod.jetbrains.remote.GitpodManager import io.gitpod.jetbrains.remote.services.ControllerStatusService.ControllerStatus import kotlinx.coroutines.* import kotlinx.coroutines.future.await -import javax.websocket.DeploymentException -import kotlin.coroutines.coroutineContext import kotlin.random.Random.Default.nextInt @Service class HeartbeatService : Disposable { + private val manager = service() + private val job = GlobalScope.launch { - val info = SupervisorInfoService.fetch() - val client = GitpodClient() - val launcher = GitpodServerLauncher.create(client) - launch { - connectToServer(info, launcher) - } + val info = manager.pendingInfo.await() val intervalInSeconds = 30 var current = ControllerStatus( connected = false, @@ -47,7 +40,7 @@ class HeartbeatService : Disposable { } if (wasClosed != null) { - client.server.sendHeartBeat(SendHeartBeatOptions(info.infoResponse.instanceId, wasClosed)).await() + manager.client.server.sendHeartBeat(SendHeartBeatOptions(info.infoResponse.instanceId, wasClosed)).await() } } catch (t: Throwable) { thisLogger().error("gitpod: failed to check activity:", t) @@ -56,64 +49,5 @@ class HeartbeatService : Disposable { } } - private suspend fun connectToServer(info: SupervisorInfoService.Result, launcher: GitpodServerLauncher) { - val plugin = PluginManagerCore.getPlugin(PluginId.getId("io.gitpod.jetbrains.remote"))!! - val connect = { - val originalClassLoader = Thread.currentThread().contextClassLoader - try { - // see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003146180/comments/360000376240 - Thread.currentThread().contextClassLoader = HeartbeatService::class.java.classLoader - launcher.listen( - info.infoResponse.gitpodApi.endpoint, - info.infoResponse.gitpodHost, - plugin.pluginId.idString, - plugin.version, - info.tokenResponse.token - ) - } finally { - Thread.currentThread().contextClassLoader = originalClassLoader; - } - } - - val minReconnectionDelay = 2 * 1000L - val maxReconnectionDelay = 30 * 1000L - val reconnectionDelayGrowFactor = 1.5; - var reconnectionDelay = minReconnectionDelay; - val gitpodHost = info.infoResponse.gitpodApi.host - var closeReason: Any = "cancelled" - try { - while (coroutineContext.isActive) { - try { - val connection = connect() - thisLogger().info("$gitpodHost: connected") - reconnectionDelay = minReconnectionDelay - closeReason = connection.await() - thisLogger().warn("$gitpodHost: connection closed, reconnecting after $reconnectionDelay milliseconds: $closeReason") - } catch (t: Throwable) { - if (t is DeploymentException) { - // connection is alright, but server does not want to handshake, there is no point to try with the same token again - throw t - } - closeReason = t - thisLogger().warn( - "$gitpodHost: failed to connect, trying again after $reconnectionDelay milliseconds:", - closeReason - ) - } - delay(reconnectionDelay) - closeReason = "cancelled" - reconnectionDelay = (reconnectionDelay * reconnectionDelayGrowFactor).toLong() - if (reconnectionDelay > maxReconnectionDelay) { - reconnectionDelay = maxReconnectionDelay - } - } - } catch (t: Throwable) { - if (t !is CancellationException) { - closeReason = t - } - } - thisLogger().warn("$gitpodHost: connection permanently closed: $closeReason") - } - override fun dispose() = job.cancel() } diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/SupervisorInfoService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/SupervisorInfoService.kt index 66a0a86318fb61..5edee438b769ae 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/SupervisorInfoService.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/SupervisorInfoService.kt @@ -37,6 +37,7 @@ object SupervisorInfoService { val request = GetTokenRequest.newBuilder() .setHost(infoResponse.gitpodApi.host) .addScope("function:sendHeartBeat") + .addScope("function:trackEvent") .setKind("gitpod") .build() diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml index 5c073f8200ba73..74c203ae7bf5e5 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml @@ -21,6 +21,7 @@ +