Skip to content

Commit e111296

Browse files
akosyakovroboquat
authored andcommitted
[jb] track client project sessions
1 parent 3be4e0b commit e111296

File tree

11 files changed

+183
-97
lines changed

11 files changed

+183
-97
lines changed

.idea/compiler.xml

-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/gradle.xml

-11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public interface GitpodServer {
1717
@JsonRequest
1818
CompletableFuture<Void> sendHeartBeat(SendHeartBeatOptions options);
1919

20+
@JsonRequest
21+
CompletableFuture<Void> trackEvent(RemoteTrackMessage event);
22+
2023
@JsonRequest
2124
CompletableFuture<List<String>> getGitpodTokenScopes(String tokenHash);
2225

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.gitpodprotocol.api.entities;
6+
7+
public class RemoteTrackMessage {
8+
private String event;
9+
private Object properties;
10+
11+
public String getEvent() {
12+
return event;
13+
}
14+
15+
public void setEvent(String event) {
16+
this.event = event;
17+
}
18+
19+
public Object getProperties() {
20+
return properties;
21+
}
22+
23+
public void setProperties(Object properties) {
24+
this.properties = properties;
25+
}
26+
27+
@Override
28+
public String toString() {
29+
return "RemoteTrackMessage{" +
30+
"event='" + event + '\'' +
31+
", properties=" + properties +
32+
'}';
33+
}
34+
}

components/ide/jetbrains/backend-plugin/launch-dev-server.sh

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ if [ ! -d "$TEST_DIR" ]; then
2626
git clone $TEST_REPO $TEST_DIR
2727
fi
2828

29+
export JB_DEV=true
2930
export JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:0"
3031

3132
$TEST_BACKEND_DIR/bin/remote-dev-server.sh run $TEST_DIR
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.jetbrains.remote
6+
7+
import com.intellij.openapi.application.ApplicationInfo
8+
import com.intellij.openapi.client.ClientProjectSession
9+
import com.intellij.openapi.components.service
10+
import com.intellij.openapi.diagnostic.thisLogger
11+
import io.gitpod.gitpodprotocol.api.entities.RemoteTrackMessage
12+
import kotlinx.coroutines.GlobalScope
13+
import kotlinx.coroutines.future.await
14+
import kotlinx.coroutines.launch
15+
16+
class GitpodClientProjectSessionTracker(
17+
private val session: ClientProjectSession
18+
) {
19+
20+
private val manager = service<GitpodManager>()
21+
init {
22+
GlobalScope.launch {
23+
val info = manager.pendingInfo.await()
24+
val event = RemoteTrackMessage().apply {
25+
event = "jb_session"
26+
properties = mapOf(
27+
"sessionId" to session.clientId.value,
28+
"instanceId" to info.infoResponse.instanceId,
29+
"workspaceId" to info.infoResponse.workspaceId,
30+
"appName" to ApplicationInfo.getInstance().versionName,
31+
"appVersion" to ApplicationInfo.getInstance().fullVersion,
32+
"timestamp" to System.currentTimeMillis()
33+
)
34+
}
35+
if (manager.devMode) {
36+
thisLogger().warn("gitpod: $event")
37+
} else {
38+
manager.client.server.trackEvent(event)
39+
}
40+
}
41+
}
42+
}

components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt

+94-5
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@
44

55
package io.gitpod.jetbrains.remote
66

7+
import com.intellij.ide.plugins.PluginManagerCore
78
import com.intellij.notification.NotificationAction
89
import com.intellij.notification.NotificationGroupManager
910
import com.intellij.notification.NotificationType
1011
import com.intellij.openapi.Disposable
1112
import com.intellij.openapi.components.Service
1213
import com.intellij.openapi.diagnostic.thisLogger
14+
import com.intellij.openapi.extensions.PluginId
1315
import com.intellij.remoteDev.util.onTerminationOrNow
1416
import com.jetbrains.rd.util.lifetime.Lifetime
1517
import git4idea.config.GitVcsApplicationSettings
18+
import io.gitpod.gitpodprotocol.api.GitpodClient
19+
import io.gitpod.gitpodprotocol.api.GitpodServerLauncher
20+
import io.gitpod.jetbrains.remote.services.HeartbeatService
1621
import io.gitpod.jetbrains.remote.services.SupervisorInfoService
1722
import io.gitpod.supervisor.api.Notification.*
1823
import io.gitpod.supervisor.api.NotificationServiceGrpc
@@ -31,10 +36,19 @@ import java.net.http.HttpResponse
3136
import java.time.Duration
3237
import java.util.concurrent.CancellationException
3338
import java.util.concurrent.CompletableFuture
39+
import javax.websocket.DeploymentException
3440

3541
@Service
3642
class GitpodManager : Disposable {
3743

44+
val devMode = System.getenv("JB_DEV").toBoolean()
45+
46+
private val lifetime = Lifetime.Eternal.createNested()
47+
48+
override fun dispose() {
49+
lifetime.terminate()
50+
}
51+
3852
init {
3953
GlobalScope.launch {
4054
try {
@@ -68,8 +82,6 @@ class GitpodManager : Disposable {
6882
GitVcsApplicationSettings.getInstance().isUseCredentialHelper = true
6983
}
7084

71-
private val lifetime = Lifetime.Eternal.createNested()
72-
7385
private val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications")
7486
private val notificationsJob = GlobalScope.launch {
7587
val notifications = NotificationServiceGrpc.newStub(SupervisorInfoService.channel)
@@ -127,14 +139,91 @@ class GitpodManager : Disposable {
127139
delay(1000L)
128140
}
129141
}
130-
131142
init {
132143
lifetime.onTerminationOrNow {
133144
notificationsJob.cancel()
134145
}
135146
}
136147

137-
override fun dispose() {
138-
lifetime.terminate()
148+
val pendingInfo = CompletableFuture<SupervisorInfoService.Result>()
149+
private val infoJob = GlobalScope.launch {
150+
try {
151+
// TOO(ak) inline SupervisorInfoService
152+
pendingInfo.complete(SupervisorInfoService.fetch())
153+
} catch (t: Throwable) {
154+
pendingInfo.completeExceptionally(t)
155+
}
156+
}
157+
init {
158+
lifetime.onTerminationOrNow {
159+
infoJob.cancel()
160+
}
161+
}
162+
163+
val client = GitpodClient()
164+
private val serverJob = GlobalScope.launch {
165+
val info = pendingInfo.await()
166+
val launcher = GitpodServerLauncher.create(client)
167+
val plugin = PluginManagerCore.getPlugin(PluginId.getId("io.gitpod.jetbrains.remote"))!!
168+
val connect = {
169+
val originalClassLoader = Thread.currentThread().contextClassLoader
170+
try {
171+
// see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003146180/comments/360000376240
172+
Thread.currentThread().contextClassLoader = HeartbeatService::class.java.classLoader
173+
launcher.listen(
174+
info.infoResponse.gitpodApi.endpoint,
175+
info.infoResponse.gitpodHost,
176+
plugin.pluginId.idString,
177+
plugin.version,
178+
info.tokenResponse.token
179+
)
180+
} finally {
181+
Thread.currentThread().contextClassLoader = originalClassLoader;
182+
}
183+
}
184+
185+
val minReconnectionDelay = 2 * 1000L
186+
val maxReconnectionDelay = 30 * 1000L
187+
val reconnectionDelayGrowFactor = 1.5;
188+
var reconnectionDelay = minReconnectionDelay;
189+
val gitpodHost = info.infoResponse.gitpodApi.host
190+
var closeReason: Any = "cancelled"
191+
try {
192+
while (kotlin.coroutines.coroutineContext.isActive) {
193+
try {
194+
val connection = connect()
195+
thisLogger().info("$gitpodHost: connected")
196+
reconnectionDelay = minReconnectionDelay
197+
closeReason = connection.await()
198+
thisLogger().warn("$gitpodHost: connection closed, reconnecting after $reconnectionDelay milliseconds: $closeReason")
199+
} catch (t: Throwable) {
200+
if (t is DeploymentException) {
201+
// connection is alright, but server does not want to handshake, there is no point to try with the same token again
202+
throw t
203+
}
204+
closeReason = t
205+
thisLogger().warn(
206+
"$gitpodHost: failed to connect, trying again after $reconnectionDelay milliseconds:",
207+
closeReason
208+
)
209+
}
210+
delay(reconnectionDelay)
211+
closeReason = "cancelled"
212+
reconnectionDelay = (reconnectionDelay * reconnectionDelayGrowFactor).toLong()
213+
if (reconnectionDelay > maxReconnectionDelay) {
214+
reconnectionDelay = maxReconnectionDelay
215+
}
216+
}
217+
} catch (t: Throwable) {
218+
if (t !is CancellationException) {
219+
closeReason = t
220+
}
221+
}
222+
thisLogger().warn("$gitpodHost: connection permanently closed: $closeReason")
223+
}
224+
init {
225+
lifetime.onTerminationOrNow {
226+
serverJob.cancel()
227+
}
139228
}
140229
}

components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt

+6-72
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,24 @@
44

55
package io.gitpod.jetbrains.remote.services
66

7-
import com.intellij.ide.plugins.PluginManagerCore
87
import com.intellij.openapi.Disposable
98
import com.intellij.openapi.components.Service
9+
import com.intellij.openapi.components.service
1010
import com.intellij.openapi.diagnostic.thisLogger
11-
import com.intellij.openapi.extensions.PluginId
12-
import io.gitpod.gitpodprotocol.api.GitpodClient
13-
import io.gitpod.gitpodprotocol.api.GitpodServerLauncher
1411
import io.gitpod.gitpodprotocol.api.entities.SendHeartBeatOptions
12+
import io.gitpod.jetbrains.remote.GitpodManager
1513
import io.gitpod.jetbrains.remote.services.ControllerStatusService.ControllerStatus
1614
import kotlinx.coroutines.*
1715
import kotlinx.coroutines.future.await
18-
import javax.websocket.DeploymentException
19-
import kotlin.coroutines.coroutineContext
2016
import kotlin.random.Random.Default.nextInt
2117

2218
@Service
2319
class HeartbeatService : Disposable {
2420

21+
private val manager = service<GitpodManager>()
22+
2523
private val job = GlobalScope.launch {
26-
val info = SupervisorInfoService.fetch()
27-
val client = GitpodClient()
28-
val launcher = GitpodServerLauncher.create(client)
29-
launch {
30-
connectToServer(info, launcher)
31-
}
24+
val info = manager.pendingInfo.await()
3225
val intervalInSeconds = 30
3326
var current = ControllerStatus(
3427
connected = false,
@@ -47,7 +40,7 @@ class HeartbeatService : Disposable {
4740
}
4841

4942
if (wasClosed != null) {
50-
client.server.sendHeartBeat(SendHeartBeatOptions(info.infoResponse.instanceId, wasClosed)).await()
43+
manager.client.server.sendHeartBeat(SendHeartBeatOptions(info.infoResponse.instanceId, wasClosed)).await()
5144
}
5245
} catch (t: Throwable) {
5346
thisLogger().error("gitpod: failed to check activity:", t)
@@ -56,64 +49,5 @@ class HeartbeatService : Disposable {
5649
}
5750
}
5851

59-
private suspend fun connectToServer(info: SupervisorInfoService.Result, launcher: GitpodServerLauncher) {
60-
val plugin = PluginManagerCore.getPlugin(PluginId.getId("io.gitpod.jetbrains.remote"))!!
61-
val connect = {
62-
val originalClassLoader = Thread.currentThread().contextClassLoader
63-
try {
64-
// see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003146180/comments/360000376240
65-
Thread.currentThread().contextClassLoader = HeartbeatService::class.java.classLoader
66-
launcher.listen(
67-
info.infoResponse.gitpodApi.endpoint,
68-
info.infoResponse.gitpodHost,
69-
plugin.pluginId.idString,
70-
plugin.version,
71-
info.tokenResponse.token
72-
)
73-
} finally {
74-
Thread.currentThread().contextClassLoader = originalClassLoader;
75-
}
76-
}
77-
78-
val minReconnectionDelay = 2 * 1000L
79-
val maxReconnectionDelay = 30 * 1000L
80-
val reconnectionDelayGrowFactor = 1.5;
81-
var reconnectionDelay = minReconnectionDelay;
82-
val gitpodHost = info.infoResponse.gitpodApi.host
83-
var closeReason: Any = "cancelled"
84-
try {
85-
while (coroutineContext.isActive) {
86-
try {
87-
val connection = connect()
88-
thisLogger().info("$gitpodHost: connected")
89-
reconnectionDelay = minReconnectionDelay
90-
closeReason = connection.await()
91-
thisLogger().warn("$gitpodHost: connection closed, reconnecting after $reconnectionDelay milliseconds: $closeReason")
92-
} catch (t: Throwable) {
93-
if (t is DeploymentException) {
94-
// connection is alright, but server does not want to handshake, there is no point to try with the same token again
95-
throw t
96-
}
97-
closeReason = t
98-
thisLogger().warn(
99-
"$gitpodHost: failed to connect, trying again after $reconnectionDelay milliseconds:",
100-
closeReason
101-
)
102-
}
103-
delay(reconnectionDelay)
104-
closeReason = "cancelled"
105-
reconnectionDelay = (reconnectionDelay * reconnectionDelayGrowFactor).toLong()
106-
if (reconnectionDelay > maxReconnectionDelay) {
107-
reconnectionDelay = maxReconnectionDelay
108-
}
109-
}
110-
} catch (t: Throwable) {
111-
if (t !is CancellationException) {
112-
closeReason = t
113-
}
114-
}
115-
thisLogger().warn("$gitpodHost: connection permanently closed: $closeReason")
116-
}
117-
11852
override fun dispose() = job.cancel()
11953
}

0 commit comments

Comments
 (0)