Skip to content

Commit e4e753a

Browse files
committed
Improve port forwarding in JetBrains EAP IDEs
1 parent 9cf6060 commit e4e753a

File tree

3 files changed

+148
-87
lines changed

3 files changed

+148
-87
lines changed

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,31 @@ import com.intellij.openapi.actionSystem.ActionPlaces
88
import com.intellij.openapi.actionSystem.ActionUpdateThread
99
import com.intellij.openapi.actionSystem.AnAction
1010
import com.intellij.openapi.actionSystem.AnActionEvent
11-
import com.intellij.openapi.components.service
1211
import com.intellij.openapi.ide.CopyPasteManager
12+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.ClientPortState
13+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PortConfiguration
1314
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PortForwardingDataKeys
14-
import io.gitpod.jetbrains.remote.GitpodPortsService
15+
import org.apache.http.client.utils.URIBuilder
1516
import java.awt.datatransfer.StringSelection
1617

1718
@Suppress("ComponentNotRegistered", "UnstableApiUsage")
1819
class GitpodCopyUrlAction : AnAction() {
1920
override fun actionPerformed(e: AnActionEvent) {
20-
e.dataContext.getData(PortForwardingDataKeys.SUGGESTION)?.getSuggestedHostPort()?.let { hostPort ->
21-
service<GitpodPortsService>().getLocalHostUriFromHostPort(hostPort).let { localHostUri ->
22-
CopyPasteManager.getInstance().setContents(StringSelection(localHostUri.toString()))
23-
}
24-
}
21+
(e.dataContext.getData(PortForwardingDataKeys.PORT)?.configuration as PortConfiguration.PerClientTcpForwarding?)
22+
?.clientPortState?.let {
23+
if (it is ClientPortState.Assigned) {
24+
CopyPasteManager.getInstance().setContents(
25+
StringSelection(
26+
URIBuilder()
27+
.setScheme("http")
28+
.setHost(it.clientInterface)
29+
.setPort(it.clientPort)
30+
.build()
31+
.toString()
32+
)
33+
)
34+
}
35+
}
2536
}
2637

2738
override fun update(e: AnActionEvent) {

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

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,21 @@ import com.intellij.openapi.actionSystem.ActionPlaces
88
import com.intellij.openapi.actionSystem.ActionUpdateThread
99
import com.intellij.openapi.actionSystem.AnAction
1010
import com.intellij.openapi.actionSystem.AnActionEvent
11+
import com.intellij.openapi.components.service
1112
import com.intellij.openapi.ide.CopyPasteManager
12-
import com.intellij.util.application
13+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PerClientPortForwardingManager
14+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PortConfiguration
1315
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PortForwardingDataKeys
14-
import io.gitpod.jetbrains.remote.GitpodManager
15-
import io.gitpod.supervisor.api.Status.PortsStatusRequest
16-
import io.gitpod.supervisor.api.StatusServiceGrpc
17-
import kotlinx.coroutines.launch
1816
import java.awt.datatransfer.StringSelection
1917

2018
@Suppress("ComponentNotRegistered", "UnstableApiUsage")
2119
class GitpodCopyWebUrlAction : AnAction() {
2220
override fun actionPerformed(e: AnActionEvent) {
2321
e.dataContext.getData(PortForwardingDataKeys.SUGGESTION)?.getSuggestedHostPort()?.let { hostPort ->
24-
application.coroutineScope.launch {
25-
getUrlFromPort(hostPort)?.let {
26-
CopyPasteManager.getInstance().setContents(StringSelection(it))
27-
}
22+
(service<PerClientPortForwardingManager>().getPorts(hostPort).firstOrNull {
23+
it.labels.contains(GitpodPortForwardingService.EXPOSED_PORT_LABEL)
24+
}?.configuration as PortConfiguration.UrlExposure?)?.exposedUrl?.let {
25+
CopyPasteManager.getInstance().setContents(StringSelection(it))
2826
}
2927
}
3028
}
@@ -34,17 +32,4 @@ class GitpodCopyWebUrlAction : AnAction() {
3432
}
3533

3634
override fun getActionUpdateThread() = ActionUpdateThread.BGT
37-
38-
private fun getUrlFromPort(port: Number): String? {
39-
val blockingStub = StatusServiceGrpc.newBlockingStub(GitpodManager.supervisorChannel)
40-
val request = PortsStatusRequest.newBuilder().setObserve(false).build()
41-
val response = blockingStub.portsStatus(request)
42-
while (response.hasNext()) {
43-
val portStatusResponse = response.next()
44-
for (portStatus in portStatusResponse.portsList) {
45-
if (portStatus.localPort == port) return portStatus.exposed.url
46-
}
47-
}
48-
return null
49-
}
5035
}

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

Lines changed: 123 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,47 @@
44

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

7+
import com.intellij.icons.AllIcons
78
import com.intellij.openapi.Disposable
89
import com.intellij.openapi.components.service
910
import com.intellij.openapi.diagnostic.thisLogger
10-
import com.intellij.openapi.project.Project
1111
import com.intellij.openapi.util.Disposer
1212
import com.intellij.remoteDev.util.onTerminationOrNow
13+
import com.intellij.ui.RowIcon
1314
import com.intellij.util.application
1415
import com.jetbrains.rd.platform.codeWithMe.portForwarding.*
15-
import com.jetbrains.rd.platform.util.lifetime
16-
import com.jetbrains.rd.util.lifetime.LifetimeStatus
16+
import com.jetbrains.rd.util.lifetime.Lifetime
1717
import io.gitpod.jetbrains.remote.GitpodIgnoredPortsForNotificationService
1818
import io.gitpod.jetbrains.remote.GitpodManager
1919
import io.gitpod.jetbrains.remote.GitpodPortsService
2020
import io.gitpod.supervisor.api.Status
21+
import io.gitpod.supervisor.api.Status.PortsStatus
2122
import io.gitpod.supervisor.api.StatusServiceGrpc
2223
import io.grpc.stub.ClientCallStreamObserver
2324
import io.grpc.stub.ClientResponseObserver
2425
import io.ktor.utils.io.*
2526
import java.util.concurrent.CompletableFuture
2627
import java.util.concurrent.TimeUnit
27-
import javax.swing.Icon
2828

2929
@Suppress("UnstableApiUsage")
30-
class GitpodPortForwardingService(private val project: Project) {
30+
class GitpodPortForwardingService : Disposable {
3131
companion object {
32-
const val FORWARDED_PORT_LABEL = "gitpod"
32+
const val FORWARDED_PORT_LABEL = "ForwardedByGitpod"
33+
const val EXPOSED_PORT_LABEL = "ExposedByGitpod"
3334
}
3435

3536
private val portsService = service<GitpodPortsService>()
3637
private val perClientPortForwardingManager = service<PerClientPortForwardingManager>()
3738
private val ignoredPortsForNotificationService = service<GitpodIgnoredPortsForNotificationService>()
38-
private val portToDisposableMap = mutableMapOf<Int,Disposable>()
39+
private val lifetime = Lifetime.Eternal.createNested()
3940

40-
init { start() }
41+
init {
42+
start()
43+
}
44+
45+
override fun dispose() {
46+
lifetime.terminate()
47+
}
4148

4249
private fun start() {
4350
if (application.isHeadlessEnvironment) return
@@ -46,12 +53,12 @@ class GitpodPortForwardingService(private val project: Project) {
4653
}
4754

4855
private fun observePortsListWhileProjectIsOpen() = application.executeOnPooledThread {
49-
while (project.lifetime.status == LifetimeStatus.Alive) {
56+
lifetime.executeIfAlive {
5057
try {
5158
observePortsList().get()
5259
} catch (throwable: Throwable) {
5360
when (throwable) {
54-
is InterruptedException, is CancellationException -> break
61+
is InterruptedException, is CancellationException -> lifetime.terminate()
5562
else -> thisLogger().error(
5663
"gitpod: Got an error while trying to get ports list from Supervisor. " +
5764
"Going to try again in a second.",
@@ -74,76 +81,134 @@ class GitpodPortForwardingService(private val project: Project) {
7481
val portsStatusResponseObserver = object :
7582
ClientResponseObserver<Status.PortsStatusRequest, Status.PortsStatusResponse> {
7683
override fun beforeStart(request: ClientCallStreamObserver<Status.PortsStatusRequest>) {
77-
project.lifetime.onTerminationOrNow { request.cancel("gitpod: Project terminated.", null) }
84+
lifetime.onTerminationOrNow { request.cancel("gitpod: Project terminated.", null) }
7885
}
86+
7987
override fun onNext(response: Status.PortsStatusResponse) {
80-
application.invokeLater { updateForwardedPortsList(response) }
88+
application.invokeLater { syncPortsListWithClient(response) }
89+
}
90+
91+
override fun onCompleted() {
92+
completableFuture.complete(null)
93+
}
94+
95+
override fun onError(throwable: Throwable) {
96+
completableFuture.completeExceptionally(throwable)
8197
}
82-
override fun onCompleted() { completableFuture.complete(null) }
83-
override fun onError(throwable: Throwable) { completableFuture.completeExceptionally(throwable) }
8498
}
8599

86100
statusServiceStub.portsStatus(portsStatusRequest, portsStatusResponseObserver)
87101

88102
return completableFuture
89103
}
90104

91-
private fun updateForwardedPortsList(response: Status.PortsStatusResponse) {
105+
private fun syncPortsListWithClient(response: Status.PortsStatusResponse) {
92106
val ignoredPorts = ignoredPortsForNotificationService.getIgnoredPorts()
107+
val portsList = response.portsList.filter { !ignoredPorts.contains(it.localPort) }
108+
val portsNumbersFromPortsList = portsList.map { it.localPort }
109+
val servedPorts = portsList.filter { it.served }
110+
val exposedPorts = servedPorts.filter { it.exposed?.url?.isNotBlank() ?: false }
111+
val portsNumbersFromNonServedPorts = portsList.filter { !it.served }.map { it.localPort }
112+
val servedPortsToStartForwarding = servedPorts.filter {
113+
perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(FORWARDED_PORT_LABEL) }
114+
}
115+
val exposedPortsToStartExposingOnClient = exposedPorts.filter {
116+
perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(EXPOSED_PORT_LABEL) }
117+
}
118+
val forwardedPortsToStopForwarding = perClientPortForwardingManager.getPorts(FORWARDED_PORT_LABEL)
119+
.map { it.hostPortNumber }
120+
.filter { portsNumbersFromNonServedPorts.contains(it) || !portsNumbersFromPortsList.contains(it) }
121+
val exposedPortsToStopExposingOnClient = perClientPortForwardingManager.getPorts(EXPOSED_PORT_LABEL)
122+
.map { it.hostPortNumber }
123+
.filter { portsNumbersFromNonServedPorts.contains(it) || !portsNumbersFromPortsList.contains(it) }
93124

94-
for (port in response.portsList) {
95-
if (ignoredPorts.contains(port.localPort)) continue
125+
servedPortsToStartForwarding.forEach { startForwarding(it) }
96126

97-
val hostPort = port.localPort
98-
val isServed = port.served
99-
val isForwarded = perClientPortForwardingManager.getPorts(hostPort).isNotEmpty()
127+
exposedPortsToStartExposingOnClient.forEach { startExposingOnClient(it) }
100128

101-
if (isServed && !isForwarded) {
102-
try {
103-
val forwardedPort = perClientPortForwardingManager.forwardPort(
104-
hostPort,
105-
PortType.TCP,
106-
setOf(FORWARDED_PORT_LABEL),
107-
ClientPortAttributes(hostPort, ClientPortPickingStrategy.REASSIGN_WHEN_BUSY),
108-
)
129+
forwardedPortsToStopForwarding.forEach { stopForwarding(it) }
130+
131+
exposedPortsToStopExposingOnClient.forEach { stopExposingOnClient(it) }
132+
133+
portsList.forEach { updatePortsPresentation(it) }
134+
}
109135

110-
forwardedPort.presentation.name = port.name
111-
forwardedPort.presentation.description = port.description
112-
113-
val portListenerDisposable = portToDisposableMap.getOrPut(hostPort, fun() = Disposer.newDisposable())
114-
115-
forwardedPort.addPortListener(portListenerDisposable, object: ForwardedPortListener {
116-
override fun stateChanged(port: ForwardedPort, newState: ClientPortState) {
117-
when (newState) {
118-
is ClientPortState.Assigned -> {
119-
thisLogger().warn("gitpod: Started forwarding host port $hostPort to client port ${newState.clientPort}.")
120-
portsService.setForwardedPort(hostPort, newState.clientPort)
121-
}
122-
is ClientPortState.FailedToAssign -> {
123-
thisLogger().warn("gitpod: Detected that host port $hostPort failed to be assigned to a client port.")
124-
}
125-
else -> {
126-
thisLogger().warn("gitpod: Detected that host port $hostPort is not assigned to any client port.")
127-
}
136+
private fun startForwarding(portStatus: PortsStatus) {
137+
try {
138+
val forwardedPort = perClientPortForwardingManager.forwardPort(
139+
portStatus.localPort,
140+
PortType.TCP,
141+
setOf(FORWARDED_PORT_LABEL),
142+
ClientPortAttributes(portStatus.localPort, ClientPortPickingStrategy.REASSIGN_WHEN_BUSY),
143+
)
144+
145+
Disposer.newDisposable().let { listenerDisposable ->
146+
forwardedPort.addPortListener(listenerDisposable, object : ForwardedPortListener {
147+
override fun stateChanged(port: ForwardedPort, newState: ClientPortState) {
148+
when (newState) {
149+
is ClientPortState.Assigned -> {
150+
portsService.setForwardedPort(portStatus.localPort, newState.clientPort)
151+
}
152+
is ClientPortState.FailedToAssign -> {
153+
thisLogger().warn("gitpod: Detected that host port ${portStatus.localPort} failed " +
154+
"to be assigned to a client port.")
155+
}
156+
else -> {
157+
thisLogger().warn("gitpod: Detected that host port ${portStatus.localPort} is not " +
158+
"assigned to any client port.")
128159
}
129160
}
130-
})
131-
} catch (error: Error) {
132-
thisLogger().warn("gitpod: ${error.message}")
133-
}
161+
listenerDisposable.dispose()
162+
}
163+
})
134164
}
165+
} catch (exception: Exception) {
166+
thisLogger().warn("gitpod: ${exception.message}")
167+
}
168+
}
135169

136-
if (!isServed && isForwarded) {
137-
val portListenerDisposable = portToDisposableMap[hostPort]
138-
if (portListenerDisposable != null) {
139-
portListenerDisposable.dispose()
140-
portToDisposableMap.remove(hostPort)
170+
private fun stopForwarding(hostPort: Int) {
171+
perClientPortForwardingManager.getPorts(hostPort)
172+
.filter { it.labels.contains(FORWARDED_PORT_LABEL) }
173+
.forEach { portToRemove ->
174+
perClientPortForwardingManager.removePort(portToRemove)
141175
}
142-
perClientPortForwardingManager.getPorts(hostPort).forEach { portToRemove ->
176+
portsService.removeForwardedPort(hostPort)
177+
}
178+
179+
private fun startExposingOnClient(portStatus: PortsStatus) {
180+
perClientPortForwardingManager.exposePort(
181+
portStatus.localPort,
182+
portStatus.exposed.url,
183+
setOf(EXPOSED_PORT_LABEL),
184+
)
185+
}
186+
187+
private fun stopExposingOnClient(hostPort: Int) {
188+
perClientPortForwardingManager.getPorts(hostPort)
189+
.filter { it.labels.contains(EXPOSED_PORT_LABEL) }
190+
.forEach { portToRemove ->
143191
perClientPortForwardingManager.removePort(portToRemove)
144192
}
145-
portsService.removeForwardedPort(hostPort)
146-
thisLogger().info("gitpod: Stopped forwarding port $hostPort.")
193+
}
194+
195+
private fun updatePortsPresentation(portStatus: PortsStatus) {
196+
perClientPortForwardingManager.getPorts(portStatus.localPort).forEach { portOnClient ->
197+
if (portOnClient.configuration.isForwardedPort()) {
198+
portOnClient.presentation.name = portStatus.name
199+
portOnClient.presentation.description = portStatus.description
200+
portOnClient.presentation.tooltip = "Forwarded"
201+
portOnClient.presentation.icon = RowIcon(AllIcons.Nodes.HomeFolder)
202+
} else if (portOnClient.configuration.isExposedPort()) {
203+
val isExposedPublicly = (portStatus.exposed.visibility == Status.PortVisibility.public_visibility)
204+
205+
portOnClient.presentation.name = portStatus.name
206+
portOnClient.presentation.description = portStatus.description
207+
portOnClient.presentation.tooltip = "Exposed (" + (if (isExposedPublicly) "Public" else "Private") + ")"
208+
portOnClient.presentation.icon = RowIcon(
209+
AllIcons.General.Web,
210+
if (isExposedPublicly) AllIcons.Nodes.Public else AllIcons.Nodes.Private
211+
)
147212
}
148213
}
149214
}

0 commit comments

Comments
 (0)