From 1535f542c417d797501c70f3278d71d2025d3bf0 Mon Sep 17 00:00:00 2001 From: Victor Nogueira Date: Fri, 1 Jul 2022 13:21:29 +0000 Subject: [PATCH] Auto-forward all ports served from Gitpod when using JetBrains IDEs --- .../jetbrains/backend-plugin/build.gradle.kts | 2 +- .../latest/GitpodPortForwardingService.kt | 130 ++++++++++++++++++ .../remote/latest/GitpodTerminalService.kt | 51 +------ .../resources-latest/META-INF/extensions.xml | 1 + 4 files changed, 133 insertions(+), 51 deletions(-) create mode 100644 components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt diff --git a/components/ide/jetbrains/backend-plugin/build.gradle.kts b/components/ide/jetbrains/backend-plugin/build.gradle.kts index 3769309c2c9ba2..75fd1af92e3a8e 100644 --- a/components/ide/jetbrains/backend-plugin/build.gradle.kts +++ b/components/ide/jetbrains/backend-plugin/build.gradle.kts @@ -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 diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt new file mode 100644 index 00000000000000..123062014a7fec --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt @@ -0,0 +1,130 @@ +// 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 { + val completableFuture = CompletableFuture() + + val statusServiceStub = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel) + + val portsStatusRequest = Status.PortsStatusRequest.newBuilder().setObserve(true).build() + + val portsStatusResponseObserver = object : + ClientResponseObserver { + override fun beforeStart(request: ClientCallStreamObserver) { + 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() + + 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) { + 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 onPortForwardingEnded(hostPort: Int) { + thisLogger().info("gitpod: Finished forwarding port $hostPort.") + } + + 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.") + } + } + } +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt index 7367897a47d7ec..40c37d22994407 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt @@ -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 @@ -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 = 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) @@ -88,7 +81,6 @@ class GitpodTerminalService(private val session: ClientProjectSession) { val terminal = aliasToTerminalMap[terminalAlias] ?: continue createAttachedSharedTerminal(terminal) - autoForwardAllPortsFromTerminal(terminal) } } @@ -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) - } } diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml b/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml index d705f363926944..c98241d241d8c9 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml @@ -6,5 +6,6 @@ +