Skip to content

Commit 063d845

Browse files
committed
WIP: Reuse the same tunnel for an environment
1 parent 13e4a51 commit 063d845

File tree

9 files changed

+112
-129
lines changed

9 files changed

+112
-129
lines changed

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

Lines changed: 1 addition & 1 deletion
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",

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

Lines changed: 0 additions & 2 deletions
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

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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() {

components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProviderEnvironment.kt

Lines changed: 10 additions & 17 deletions
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
@@ -36,13 +31,9 @@ class GitpodRemoteProviderEnvironment(
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 = GitpodSSHEnvironmentContentsView(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
}

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

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,62 +9,63 @@ import com.jetbrains.toolbox.gateway.environments.CachedProjectStub
99
import com.jetbrains.toolbox.gateway.environments.ManualEnvironmentContentsView
1010
import com.jetbrains.toolbox.gateway.environments.SshEnvironmentContentsView
1111
import com.jetbrains.toolbox.gateway.ssh.SshConnectionInfo
12-
import io.gitpod.toolbox.auth.GitpodAuthManager
13-
import io.gitpod.toolbox.service.ConnectParams
14-
import io.gitpod.toolbox.service.GitpodConnectionProvider
15-
import io.gitpod.toolbox.service.GitpodPublicApiManager
16-
import io.gitpod.toolbox.service.Utils
17-
import io.gitpod.toolbox.utils.GitpodLogger
18-
import kotlinx.coroutines.future.future
12+
import io.gitpod.publicapi.experimental.v1.Workspaces.WorkspaceInstanceStatus
13+
import io.gitpod.toolbox.service.*
1914
import java.util.concurrent.CompletableFuture
2015

2116
class GitpodSSHEnvironmentContentsView(
22-
private val authManager: GitpodAuthManager,
2317
private val connectParams: ConnectParams,
2418
private val publicApi: GitpodPublicApiManager,
2519
) : SshEnvironmentContentsView, ManualEnvironmentContentsView {
2620
private var cancel = {}
2721
private val stateListeners = mutableSetOf<ManualEnvironmentContentsView.Listener>()
22+
private val provider = GitpodConnectionProvider(object : ConnectionInfoProvider {
23+
override fun getUniqueID() = connectParams.uniqueID
2824

29-
override fun getConnectionInfo(): CompletableFuture<SshConnectionInfo> {
30-
return Utils.coroutineScope.future {
31-
val provider = GitpodConnectionProvider(authManager, connectParams, publicApi)
32-
val (connInfo, cancel) = provider.connect()
33-
this@GitpodSSHEnvironmentContentsView.cancel = cancel
34-
GitpodLogger.info("=============test.getConnectionInfo port: ${connInfo.port}")
35-
return@future connInfo
25+
override suspend fun getWebsocketTunnelUrl(): String {
26+
val workspace = publicApi.getWorkspace(connectParams.workspaceId)
27+
return workspace.getTunnelUrl()
3628
}
37-
}
3829

39-
override fun addEnvironmentContentsListener(p0: ManualEnvironmentContentsView.Listener) {
40-
stateListeners += p0
41-
stateListeners.forEach{
42-
// TODO: get from fetchJoinLink2Info
43-
it.onProjectListUpdated(listOf(object : CachedProjectStub {
44-
override fun getPath(): String {
45-
return "/workspace/template-golang-cli"
46-
}
30+
override suspend fun getOwnerToken(): String {
31+
return publicApi.getWorkspaceOwnerToken(connectParams.workspaceId)
32+
}
33+
})
4734

48-
override fun getName(): String? {
49-
return "template-golang-cli"
50-
}
35+
private val connectionInfo = CompletableFuture.supplyAsync {
36+
val (connInfo, cancel) = provider.connect()
37+
this.cancel = cancel
38+
connInfo
39+
}
5140

52-
override fun getIdeHint(): String? {
53-
return "GO-243.21565.208"
54-
}
55-
}))
56-
it.onIdeListUpdated(listOf(object: CachedIdeStub {
57-
override fun getProductCode(): String {
58-
return "GO-243.21565.208"
59-
}
41+
override fun getConnectionInfo(): CompletableFuture<SshConnectionInfo> = connectionInfo
6042

61-
override fun isRunning(): Boolean? {
62-
return true
63-
}
43+
var metadata: GitpodPublicApiManager.JoinLink2Response? = null
44+
suspend fun updateEnvironmentMeta(status: WorkspaceInstanceStatus) {
45+
if (metadata == null && status.phase == WorkspaceInstanceStatus.Phase.PHASE_RUNNING) {
46+
metadata = publicApi.fetchJoinLink2Info(connectParams.workspaceId, status.getIDEUrl())
47+
}
48+
if (metadata == null) {
49+
// TODO(hw): restore from cache?
50+
return
51+
}
52+
stateListeners.forEach {
53+
it.onProjectListUpdated(listOf(object : CachedProjectStub {
54+
override fun getPath() = metadata!!.projectPath
55+
override fun getName() = metadata!!.projectPath.split("/").last()
56+
override fun getIdeHint() = metadata!!.ideVersion
57+
}))
58+
it.onIdeListUpdated(listOf(object : CachedIdeStub {
59+
override fun getProductCode() = metadata!!.ideVersion
60+
override fun isRunning() = status.phase == WorkspaceInstanceStatus.Phase.PHASE_RUNNING
6461
}))
6562
}
6663
}
6764

65+
override fun addEnvironmentContentsListener(p0: ManualEnvironmentContentsView.Listener) {
66+
stateListeners += p0
67+
}
68+
6869
override fun removeEnvironmentContentsListener(p0: ManualEnvironmentContentsView.Listener) {
6970
stateListeners -= p0
7071
}

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

Lines changed: 9 additions & 20 deletions
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,12 @@ 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 Workspace.getTunnelUrl(): String {
27+
val workspaceHost = URI.create(status.instance.status.url).host
28+
return "wss://${workspaceHost}/_supervisor/tunnel/ssh"
29+
}
30+
31+
fun WorkspaceInstanceStatus.getIDEUrl(): String {
32+
return url
4433
}
4534

components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodConnectionProvider.kt

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,19 @@
55
package io.gitpod.toolbox.service
66

77
import com.jetbrains.rd.util.ConcurrentHashMap
8-
import com.jetbrains.rd.util.URI
98
import com.jetbrains.toolbox.gateway.ssh.SshConnectionInfo
10-
import io.gitpod.publicapi.experimental.v1.Workspaces
11-
import io.gitpod.toolbox.auth.GitpodAuthManager
12-
import io.gitpod.toolbox.utils.GitpodLogger
13-
import kotlinx.serialization.Serializable
149

15-
class GitpodConnectionProvider(
16-
private val authManager: GitpodAuthManager,
17-
private val connectParams: ConnectParams,
18-
private val publicApi: GitpodPublicApiManager,
19-
) {
20-
private val activeConnections = ConcurrentHashMap<String, Boolean>()
10+
interface ConnectionInfoProvider {
11+
fun getUniqueID(): String
12+
suspend fun getWebsocketTunnelUrl(): String
13+
suspend fun getOwnerToken(): String
14+
}
2115

22-
suspend fun connect(): Pair<SshConnectionInfo, () -> Unit> {
23-
val workspaceId = connectParams.workspaceId
24-
val workspace = publicApi.getWorkspace(workspaceId)
25-
val ownerToken = publicApi.getWorkspaceOwnerToken(workspaceId)
16+
class GitpodConnectionProvider(private val provider: ConnectionInfoProvider) {
17+
private val activeConnections = ConcurrentHashMap<String, Boolean>()
2618

27-
val (serverPort, cancel) = tunnelWithWebSocket(workspace, connectParams, ownerToken)
19+
fun connect(): Pair<SshConnectionInfo, () -> Unit> {
20+
val (serverPort, cancel) = tunnelWithWebSocket(provider)
2821

2922
val connInfo = GitpodWebSocketSshConnectionInfo(
3023
"gitpod",
@@ -34,12 +27,8 @@ class GitpodConnectionProvider(
3427
return (connInfo to cancel)
3528
}
3629

37-
private fun tunnelWithWebSocket(
38-
workspace: Workspaces.Workspace,
39-
connectParams: ConnectParams,
40-
ownerToken: String,
41-
): Pair<Int, () -> Unit> {
42-
val connectionKeyId = connectParams.uniqueID
30+
private fun tunnelWithWebSocket(provider: ConnectionInfoProvider): Pair<Int, () -> Unit> {
31+
val connectionKeyId = provider.getUniqueID()
4332

4433
var found = true
4534
activeConnections.computeIfAbsent(connectionKeyId) {
@@ -52,9 +41,7 @@ class GitpodConnectionProvider(
5241
throw IllegalStateException(errMessage)
5342
}
5443

55-
val workspaceHost = URI.create(workspace.status.instance.status.url).host
56-
val server =
57-
GitpodWebSocketTunnelServer("wss://${workspaceHost}/_supervisor/tunnel/ssh", ownerToken)
44+
val server = GitpodWebSocketTunnelServer(provider)
5845

5946
val cancelServer = server.start()
6047

@@ -82,13 +69,5 @@ data class ConnectParams(
8269
val host: String,
8370
val debugWorkspace: Boolean = false,
8471
) {
85-
val resolvedWorkspaceId = "${if (debugWorkspace) "debug-" else ""}$workspaceId"
86-
val title = "$resolvedWorkspaceId"
87-
val uniqueID = "$workspaceId-$debugWorkspace"
72+
val uniqueID = if (debugWorkspace) "debug-$workspaceId" else workspaceId
8873
}
89-
90-
@Serializable
91-
private data class SSHPublicKey(
92-
val type: String,
93-
val value: String
94-
)

components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodPublicApiManager.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ import io.gitpod.publicapi.experimental.v1.*
1414
import io.gitpod.toolbox.auth.GitpodAccount
1515
import io.gitpod.toolbox.auth.GitpodAuthManager
1616
import io.gitpod.toolbox.utils.GitpodLogger
17+
import io.gitpod.toolbox.utils.await
1718
import kotlinx.coroutines.Job
1819
import kotlinx.coroutines.launch
20+
import kotlinx.serialization.Serializable
21+
import kotlinx.serialization.decodeFromString
22+
import kotlinx.serialization.json.Json
23+
import okhttp3.Request
24+
import java.net.URL
1925
import java.time.Duration
2026

2127
class GitpodPublicApiManager(private val authManger: GitpodAuthManager) {
@@ -81,6 +87,24 @@ class GitpodPublicApiManager(private val authManger: GitpodAuthManager) {
8187
return this.handleResp("getOwnerToken", resp).token
8288
}
8389

90+
@Serializable
91+
class JoinLink2Response(val appPid: Int, val joinLink: String, val ideVersion: String, val projectPath: String)
92+
93+
suspend fun fetchJoinLink2Info(workspaceId: String, ideURL: String): JoinLink2Response {
94+
val token = getWorkspaceOwnerToken(workspaceId)
95+
val backendUrl = "https://24000-${URL(ideURL).host}/joinLink2"
96+
val client = Utils.httpClient
97+
val req = Request.Builder().url(backendUrl).header("x-gitpod-owner-token", token)
98+
val response = client.newCall(req.build()).await()
99+
if (!response.isSuccessful) {
100+
throw IllegalStateException("Failed to get join link $backendUrl info: ${response.code} ${response.message}")
101+
}
102+
if (response.body == null) {
103+
throw IllegalStateException("Failed to get join link $backendUrl info: no body")
104+
}
105+
return Json.decodeFromString<JoinLink2Response>(response.body!!.string())
106+
}
107+
84108
suspend fun getAuthenticatedUser(): UserOuterClass.User {
85109
return tryGetAuthenticatedUser(userApi)
86110
}

0 commit comments

Comments
 (0)