Skip to content

[jb] track client project sessions #8339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 21, 2022
Merged
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
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