Skip to content

Commit 0018b88

Browse files
committed
Reuse the same tunnel for an environment
1 parent d82d38b commit 0018b88

11 files changed

+160
-171
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
44
networkTimeout=10000
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

Diff for: components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodAuthManager.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import java.net.URI
2222
import java.util.*
2323
import java.util.concurrent.Future
2424

25-
// TODO: Validate Scopes
25+
// TODO(hw): Validate Scopes
2626
val authScopesJetBrainsToolbox = listOf(
2727
"function:getGitpodTokenScopes",
2828
"function:getLoggedInUser",

Diff for: components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/components/AbstractUiPage.kt

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ package io.gitpod.toolbox.components
66

77
import com.jetbrains.toolbox.gateway.ui.UiField
88
import com.jetbrains.toolbox.gateway.ui.UiPage
9-
import java.util.function.BiConsumer
10-
import java.util.function.Function
119

1210
abstract class AbstractUiPage : UiPage {
1311
private var stateAccessor: UiPage.UiFieldStateAccessor? = null

Diff for: components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProviderEnvironment.kt renamed to components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteEnvironment.kt

+11-18
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ package io.gitpod.toolbox.gateway
66

77
import com.jetbrains.toolbox.gateway.AbstractRemoteProviderEnvironment
88
import com.jetbrains.toolbox.gateway.EnvironmentVisibilityState
9-
import com.jetbrains.toolbox.gateway.deploy.CommandExecution
10-
import com.jetbrains.toolbox.gateway.deploy.HostAccessInterfaces
11-
import com.jetbrains.toolbox.gateway.deploy.HostCommandExecutor
12-
import com.jetbrains.toolbox.gateway.deploy.HostFileAccessor
139
import com.jetbrains.toolbox.gateway.environments.EnvironmentContentsView
14-
import com.jetbrains.toolbox.gateway.environments.HostAccessCapableEnvironmentContentsView
1510
import com.jetbrains.toolbox.gateway.states.EnvironmentStateConsumer
1611
import com.jetbrains.toolbox.gateway.states.StandardRemoteEnvironmentState
1712
import com.jetbrains.toolbox.gateway.ui.ActionDescription
@@ -30,19 +25,15 @@ import kotlinx.coroutines.flow.MutableSharedFlow
3025
import kotlinx.coroutines.launch
3126
import java.util.concurrent.CompletableFuture
3227

33-
class GitpodRemoteProviderEnvironment(
28+
class GitpodRemoteEnvironment(
3429
private val authManager: GitpodAuthManager,
3530
private val connectParams: ConnectParams,
3631
private val publicApi: GitpodPublicApiManager, observablePropertiesFactory: ObservablePropertiesFactory?,
3732
) : AbstractRemoteProviderEnvironment(observablePropertiesFactory), DisposableHandle {
3833
private val actionList = Utils.observablePropertiesFactory.emptyObservableList<ActionDescription>();
39-
private val contentsViewFuture: CompletableFuture<EnvironmentContentsView> = CompletableFuture.completedFuture(
40-
GitpodSSHEnvironmentContentsView(
41-
authManager,
42-
connectParams,
43-
publicApi,
44-
)
45-
)
34+
private val envContentsView = GitpodRemoteEnvironmentContentsView(connectParams, publicApi)
35+
private val contentsViewFuture: CompletableFuture<EnvironmentContentsView> =
36+
CompletableFuture.completedFuture(envContentsView)
4637
private var watchWorkspaceJob: Job? = null
4738

4839
private val lastWSEnvState = MutableSharedFlow<WorkspaceEnvState>(1, 0, BufferOverflow.DROP_OLDEST)
@@ -65,6 +56,9 @@ class GitpodRemoteProviderEnvironment(
6556
lastPhase = status.phase
6657
GitpodLogger.debug("${connectParams.workspaceId} status updated: $lastPhase")
6758
lastWSEnvState.tryEmit(WorkspaceEnvState(status.phase))
59+
Utils.coroutineScope.launch {
60+
envContentsView.updateEnvironmentMeta(status)
61+
}
6862
}
6963
}
7064
}
@@ -78,19 +72,17 @@ class GitpodRemoteProviderEnvironment(
7872
}
7973

8074
override fun getId(): String = connectParams.uniqueID
81-
override fun getName(): String = connectParams.resolvedWorkspaceId
75+
override fun getName(): String = connectParams.uniqueID
8276

83-
override fun getContentsView(): CompletableFuture<EnvironmentContentsView> {
84-
GitpodLogger.info("=============test.getContentView id: $id")
85-
return contentsViewFuture
86-
}
77+
override fun getContentsView(): CompletableFuture<EnvironmentContentsView> = contentsViewFuture
8778

8879
override fun setVisible(visibilityState: EnvironmentVisibilityState) {
8980
}
9081

9182
override fun getActionList(): ObservableList<ActionDescription> = actionList
9283

9384
override fun onDelete() {
85+
// TODO: delete workspace?
9486
watchWorkspaceJob?.cancel()
9587
}
9688

@@ -120,4 +112,5 @@ private class WorkspaceEnvState(val phase: WorkspaceInstanceStatus.Phase) {
120112
WorkspaceInstanceStatus.Phase.PHASE_STOPPED to StandardRemoteEnvironmentState.Hibernated,
121113
)
122114
}
115+
// TODO(hw): add customized state
123116
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2024 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.toolbox.gateway
6+
7+
import com.jetbrains.toolbox.gateway.environments.CachedIdeStub
8+
import com.jetbrains.toolbox.gateway.environments.CachedProjectStub
9+
import com.jetbrains.toolbox.gateway.environments.ManualEnvironmentContentsView
10+
import com.jetbrains.toolbox.gateway.environments.SshEnvironmentContentsView
11+
import com.jetbrains.toolbox.gateway.ssh.SshConnectionInfo
12+
import io.gitpod.publicapi.experimental.v1.Workspaces.WorkspaceInstanceStatus
13+
import io.gitpod.toolbox.service.*
14+
import io.gitpod.toolbox.utils.GitpodLogger
15+
import java.util.concurrent.CompletableFuture
16+
17+
class GitpodRemoteEnvironmentContentsView(
18+
private val connectParams: ConnectParams,
19+
private val publicApi: GitpodPublicApiManager,
20+
) : SshEnvironmentContentsView, ManualEnvironmentContentsView {
21+
private var cancel = {}
22+
private val stateListeners = mutableSetOf<ManualEnvironmentContentsView.Listener>()
23+
private val provider = GitpodConnectionProvider(object : ConnectionInfoProvider {
24+
override fun getUniqueID() = connectParams.uniqueID
25+
26+
override suspend fun getWebsocketTunnelUrl(): String {
27+
val workspace = publicApi.getWorkspace(connectParams.workspaceId)
28+
return workspace.getTunnelUrl()
29+
}
30+
31+
override suspend fun getOwnerToken(): String {
32+
return publicApi.getWorkspaceOwnerToken(connectParams.workspaceId)
33+
}
34+
})
35+
36+
private val connectionInfo = CompletableFuture.supplyAsync {
37+
GitpodLogger.info("===============connectionInfo ${connectParams.uniqueID}")
38+
val (connInfo, cancel) = provider.connect()
39+
this.cancel = cancel
40+
connInfo
41+
}
42+
43+
override fun getConnectionInfo(): CompletableFuture<SshConnectionInfo> = connectionInfo
44+
45+
var metadata: GitpodPublicApiManager.JoinLink2Response? = null
46+
suspend fun updateEnvironmentMeta(status: WorkspaceInstanceStatus) {
47+
if (metadata == null && status.phase == WorkspaceInstanceStatus.Phase.PHASE_RUNNING) {
48+
metadata = publicApi.fetchJoinLink2Info(connectParams.workspaceId, status.getIDEUrl())
49+
}
50+
if (metadata == null) {
51+
// TODO(hw): restore from cache?
52+
return
53+
}
54+
stateListeners.forEach {
55+
it.onProjectListUpdated(listOf(object : CachedProjectStub {
56+
override fun getPath() = metadata!!.projectPath
57+
override fun getName() = metadata!!.projectPath.split("/").last()
58+
override fun getIdeHint() = metadata!!.ideVersion
59+
}))
60+
it.onIdeListUpdated(listOf(object : CachedIdeStub {
61+
override fun getProductCode() = metadata!!.ideVersion
62+
override fun isRunning() = status.phase == WorkspaceInstanceStatus.Phase.PHASE_RUNNING
63+
}))
64+
}
65+
}
66+
67+
override fun addEnvironmentContentsListener(p0: ManualEnvironmentContentsView.Listener) {
68+
stateListeners += p0
69+
}
70+
71+
override fun removeEnvironmentContentsListener(p0: ManualEnvironmentContentsView.Listener) {
72+
stateListeners -= p0
73+
}
74+
75+
override fun close() {
76+
cancel()
77+
}
78+
}

Diff for: components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProvider.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class GitpodRemoteProvider(
3030
private val loginPage = GitpodLoginPage(authManger)
3131

3232
// cache consumed environments map locally
33-
private val environmentMap = mutableMapOf<String, Pair<Workspaces.Workspace, GitpodRemoteProviderEnvironment>>()
33+
private val environmentMap = mutableMapOf<String, Pair<Workspaces.Workspace, GitpodRemoteEnvironment>>()
3434

3535
private var pendingConnectParams: Pair<String, ConnectParams>? = null
3636
private val openInToolboxUriHandler = GitpodOpenInToolboxUriHandler { (gitpodHost, connectParams) ->
@@ -54,7 +54,7 @@ class GitpodRemoteProvider(
5454
var (workspace) = obj ?: Pair(null, null)
5555
if (obj == null) {
5656
workspace = publicApi.getWorkspace(workspaceId)
57-
val env = GitpodRemoteProviderEnvironment(
57+
val env = GitpodRemoteEnvironment(
5858
authManger,
5959
connectParams,
6060
publicApi,
@@ -63,9 +63,10 @@ class GitpodRemoteProvider(
6363
environmentMap[connectParams.uniqueID] = Pair(workspace, env)
6464
consumer.consumeEnvironments(environmentMap.values.map { it.second })
6565
}
66-
val joinLinkInfo = workspace!!.fetchJoinLink2Info(publicApi.getWorkspaceOwnerToken(workspaceId))
66+
val joinLinkInfo = publicApi.fetchJoinLink2Info(workspaceId, workspace!!.getIDEUrl())
67+
// TODO(hw): verify if it's working
6768
Utils.clientHelper.prepareClient(joinLinkInfo.ideVersion)
68-
Utils.clientHelper.setAutoConnectOnEnvironmentReady(workspaceId, joinLinkInfo.ideVersion, joinLinkInfo.projectPath)
69+
Utils.clientHelper.setAutoConnectOnEnvironmentReady(connectParams.uniqueID, joinLinkInfo.ideVersion, joinLinkInfo.projectPath)
6970
}
7071

7172
private fun showWorkspacesList() {
@@ -77,7 +78,7 @@ class GitpodRemoteProvider(
7778
}
7879
consumer.consumeEnvironments(workspaces.map {
7980
val connectParams = it.getConnectParams()
80-
val env = environmentMap[connectParams.uniqueID]?.second ?: GitpodRemoteProviderEnvironment(
81+
val env = environmentMap[connectParams.uniqueID]?.second ?: GitpodRemoteEnvironment(
8182
authManger,
8283
connectParams,
8384
publicApi,

Diff for: components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSSHEnvironmentContentsView.kt

-75
This file was deleted.

Diff for: components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/DataManager.kt

+11-20
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
package io.gitpod.toolbox.service
66

77
import io.gitpod.publicapi.experimental.v1.Workspaces.Workspace
8-
import io.gitpod.toolbox.utils.await
9-
import kotlinx.serialization.Serializable
10-
import kotlinx.serialization.decodeFromString
11-
import kotlinx.serialization.json.Json
12-
import okhttp3.Request
8+
import io.gitpod.publicapi.experimental.v1.Workspaces.WorkspaceInstanceStatus
9+
import java.net.URI
1310
import java.net.URL
1411

1512
fun Workspace.getConnectParams(): ConnectParams {
@@ -26,20 +23,14 @@ fun Workspace.getGitpodHost(): String {
2623
return hostSegments.takeLast(2).joinToString(".")
2724
}
2825

29-
@Serializable
30-
class JoinLink2Response(val appPid: Int, val joinLink: String, val ideVersion: String, val projectPath: String)
31-
32-
suspend fun Workspace.fetchJoinLink2Info(ownerToken: String): JoinLink2Response {
33-
val backendUrl = "https://24000-${URL(getIDEUrl()).host}/joinLink2"
34-
val client = Utils.httpClient
35-
val req = Request.Builder().url(backendUrl).header("x-gitpod-owner-token", ownerToken)
36-
val response = client.newCall(req.build()).await()
37-
if (!response.isSuccessful) {
38-
throw IllegalStateException("Failed to get join link $backendUrl info: ${response.code} ${response.message}")
39-
}
40-
if (response.body == null) {
41-
throw IllegalStateException("Failed to get join link $backendUrl info: no body")
42-
}
43-
return Json.decodeFromString<JoinLink2Response>(response.body!!.string())
26+
fun WorkspaceInstanceStatus.usingToolbox() = editor.preferToolbox
27+
28+
fun Workspace.getTunnelUrl(): String {
29+
val workspaceHost = URI.create(status.instance.status.url).host
30+
return "wss://${workspaceHost}/_supervisor/tunnel/ssh"
31+
}
32+
33+
fun WorkspaceInstanceStatus.getIDEUrl(): String {
34+
return url
4435
}
4536

0 commit comments

Comments
 (0)