Skip to content

Commit

Permalink
[jb] track client project sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
akosyakov authored and roboquat committed Feb 21, 2022
1 parent 3be4e0b commit e111296
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 97 deletions.
9 changes: 0 additions & 9 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 0 additions & 11 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public interface GitpodServer {
@JsonRequest
CompletableFuture<Void> sendHeartBeat(SendHeartBeatOptions options);

@JsonRequest
CompletableFuture<Void> trackEvent(RemoteTrackMessage event);

@JsonRequest
CompletableFuture<List<String>> getGitpodTokenScopes(String tokenHash);

Expand Down
Original file line number Diff line number Diff line change
@@ -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 +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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<GitpodManager>()
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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -127,14 +139,91 @@ class GitpodManager : Disposable {
delay(1000L)
}
}

init {
lifetime.onTerminationOrNow {
notificationsJob.cancel()
}
}

override fun dispose() {
lifetime.terminate()
val pendingInfo = CompletableFuture<SupervisorInfoService.Result>()
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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<GitpodManager>()

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,
Expand All @@ -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)
Expand All @@ -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()
}
Loading

0 comments on commit e111296

Please sign in to comment.