Skip to content

Commit

Permalink
Auto-forward all ports served from Gitpod when using JetBrains IDEs
Browse files Browse the repository at this point in the history
  • Loading branch information
felladrin committed Jul 8, 2022
1 parent 48aff21 commit c15b665
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 51 deletions.
2 changes: 1 addition & 1 deletion components/ide/jetbrains/backend-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {
// Kotlin support - check the latest version at https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm
id("org.jetbrains.kotlin.jvm") version "1.7.0"
// gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin
id("org.jetbrains.intellij") version "1.6.0"
id("org.jetbrains.intellij") version "1.7.0"
// detekt linter - read more: https://detekt.github.io/detekt/gradle.html
id("io.gitlab.arturbosch.detekt") version "1.17.1"
// ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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.latest

import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.remoteDev.util.onTerminationOrNow
import com.intellij.util.application
import com.jetbrains.codeWithMe.model.RdPortType
import com.jetbrains.rd.platform.util.lifetime
import com.jetbrains.rd.util.lifetime.LifetimeStatus
import com.jetbrains.rdserver.portForwarding.ForwardedPortInfo
import com.jetbrains.rdserver.portForwarding.PortForwardingManager
import com.jetbrains.rdserver.portForwarding.remoteDev.PortEventsProcessor
import io.gitpod.jetbrains.remote.GitpodManager
import io.gitpod.supervisor.api.Status
import io.gitpod.supervisor.api.StatusServiceGrpc
import io.grpc.stub.ClientCallStreamObserver
import io.grpc.stub.ClientResponseObserver
import io.ktor.utils.io.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit

@Suppress("UnstableApiUsage")
class GitpodPortForwardingService(private val project: Project) {
companion object {
const val FORWARDED_PORT_LABEL = "gitpod"
}

init { start() }

private fun start() {
if (application.isHeadlessEnvironment) return

observePortsListWhileProjectIsOpen()
}

private fun observePortsListWhileProjectIsOpen() = application.executeOnPooledThread {
while (project.lifetime.status == LifetimeStatus.Alive) {
try {
observePortsList().get()
} catch (throwable: Throwable) {
when (throwable) {
is InterruptedException, is CancellationException -> break
else -> thisLogger().error(
"gitpod: Got an error while trying to get ports list from Supervisor. " +
"Will try again in 1 second.",
throwable
)
}
}

TimeUnit.SECONDS.sleep(1)
}
}

private fun observePortsList(): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()

val statusServiceStub = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel)

val portsStatusRequest = Status.PortsStatusRequest.newBuilder().setObserve(true).build()

val portsStatusResponseObserver = object :
ClientResponseObserver<Status.PortsStatusRequest, Status.PortsStatusResponse> {
override fun beforeStart(request: ClientCallStreamObserver<Status.PortsStatusRequest>) {
project.lifetime.onTerminationOrNow { request.cancel("gitpod: Project terminated.", null) }
}
override fun onNext(response: Status.PortsStatusResponse) { handlePortStatusResponse(response) }
override fun onCompleted() { completableFuture.complete(null) }
override fun onError(throwable: Throwable) { completableFuture.completeExceptionally(throwable) }
}

statusServiceStub.portsStatus(portsStatusRequest, portsStatusResponseObserver)

return completableFuture
}

private fun handlePortStatusResponse(response: Status.PortsStatusResponse) {
val portNumberToServedStatusMap = mutableMapOf<Int, Boolean>()

for (port in response.portsList) {
val isServed = (port.exposed != null || port.tunneled != null) && port.served

portNumberToServedStatusMap[port.localPort] = isServed
}

updateForwardedPortsList(portNumberToServedStatusMap)
}

private fun updateForwardedPortsList(portNumberToServedStatusMap: Map<Int, Boolean>) {
val portForwardingManager = PortForwardingManager.getInstance(project)
val forwardedPortsList = portForwardingManager.getForwardedPortsWithLabel(FORWARDED_PORT_LABEL)

portNumberToServedStatusMap.forEach { (hostPort, isServed) ->
if (isServed && !forwardedPortsList.containsKey(hostPort)) {
val portEventsProcessor = object : PortEventsProcessor {
override fun onPortForwarded(hostPort: Int, clientPort: Int) {
thisLogger().info("gitpod: Forwarded port $hostPort to client's port $clientPort.")
}

override fun onPortForwardingFailed(hostPort: Int, reason: String) {
thisLogger().error("gitpod: Failed to forward port $hostPort: $reason")
}
}

val portInfo = ForwardedPortInfo(
hostPort,
RdPortType.HTTP,
FORWARDED_PORT_LABEL,
emptyList(),
portEventsProcessor
)

portForwardingManager.forwardPort(portInfo)
}

if (!isServed && forwardedPortsList.containsKey(hostPort)) {
portForwardingManager.removePort(hostPort)
thisLogger().info("gitpod: Stopped forwarding port $hostPort.")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ package io.gitpod.jetbrains.remote.latest

import com.intellij.openapi.client.ClientProjectSession
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.rd.createLifetime
import com.intellij.remoteDev.util.onTerminationOrNow
import com.intellij.util.application
import com.jetbrains.rdserver.portForwarding.PortForwardingDiscovery
import com.jetbrains.rdserver.portForwarding.PortForwardingManager
import com.jetbrains.rdserver.portForwarding.remoteDev.PortEventsProcessor
import com.jetbrains.rdserver.terminal.BackendTerminalManager
import io.gitpod.jetbrains.remote.GitpodManager
import io.gitpod.supervisor.api.Status
Expand All @@ -27,15 +22,13 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit

@Suppress("UnstableApiUsage")
class GitpodTerminalService(private val session: ClientProjectSession) {
class GitpodTerminalService(session: ClientProjectSession) {
private companion object {
var hasStarted = false
val forwardedPortsList: MutableSet<Int> = mutableSetOf()
}

private val terminalView = TerminalView.getInstance(session.project)
private val backendTerminalManager = BackendTerminalManager.getInstance(session.project)
private val portForwardingManager = PortForwardingManager.getInstance(session.project)
private val terminalServiceFutureStub = TerminalServiceGrpc.newFutureStub(GitpodManager.supervisorChannel)
private val statusServiceStub = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel)

Expand Down Expand Up @@ -88,7 +81,6 @@ class GitpodTerminalService(private val session: ClientProjectSession) {
val terminal = aliasToTerminalMap[terminalAlias] ?: continue

createAttachedSharedTerminal(terminal)
autoForwardAllPortsFromTerminal(terminal)
}
}

Expand Down Expand Up @@ -177,45 +169,4 @@ class GitpodTerminalService(private val session: ClientProjectSession) {
"gp tasks attach ${supervisorTerminal.alias}"
)
}

private fun autoForwardAllPortsFromTerminal(supervisorTerminal: TerminalOuterClass.Terminal) {
val projectLifetime = session.project.createLifetime()

val discoveryCallback = object : PortForwardingDiscovery {
/**
* @return Whether port should be forwarded or not.
* We shouldn't try to forward ports that are already forwarded.
*/
override fun onPortDiscovered(hostPort: Int): Boolean = !forwardedPortsList.contains(hostPort)

override fun getEventsProcessor(hostPort: Int) = object : PortEventsProcessor {
override fun onPortForwarded(hostPort: Int, clientPort: Int) {
forwardedPortsList.add(hostPort)
thisLogger().info("gitpod: Forwarded port $hostPort from Supervisor's Terminal " +
"${supervisorTerminal.pid} to client's port $clientPort.")

projectLifetime.onTerminationOrNow {
if (forwardedPortsList.contains(hostPort)) {
forwardedPortsList.remove(hostPort)
portForwardingManager.removePort(hostPort)
thisLogger().info("gitpod: Removing forwarded port $hostPort from Supervisor's Terminal " +
"${supervisorTerminal.pid}")
}
}
}

override fun onPortForwardingFailed(hostPort: Int, reason: String) {
thisLogger().error("gitpod: Failed to forward port $hostPort from Supervisor's Terminal " +
"${supervisorTerminal.pid}: $reason")
}

override fun onPortForwardingEnded(hostPort: Int) {
thisLogger().info("gitpod: Port $hostPort from Supervisor's Terminal " +
"${supervisorTerminal.pid} is not being forwarded anymore.")
}
}
}

portForwardingManager.forwardPortsOfPid(projectLifetime, supervisorTerminal.pid, discoveryCallback, true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<projectService serviceImplementation="io.gitpod.jetbrains.remote.latest.GitpodTerminalService" client="guest" preload="true"/>
<projectService serviceImplementation="io.gitpod.jetbrains.remote.latest.GitpodPortForwardingService" preload="true"/>
</extensions>
</idea-plugin>

0 comments on commit c15b665

Please sign in to comment.