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 @@
+