-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Auto-forward all workspace open ports when using Latest JetBrains IDEs #11081
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// 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 | ||
|
||
import com.intellij.openapi.diagnostic.thisLogger | ||
import com.jetbrains.rd.util.URI | ||
import org.apache.http.client.utils.URIBuilder | ||
import java.util.Optional | ||
import java.util.regex.Pattern | ||
|
||
class GitpodPortsService { | ||
companion object { | ||
/** Host used by forwarded ports on JetBrains Client. */ | ||
const val FORWARDED_PORT_HOST = "127.0.0.1" | ||
} | ||
private val hostToClientForwardedPortMap: MutableMap<Int, Int> = mutableMapOf() | ||
felladrin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
fun isForwarded(hostPort: Int): Boolean = hostToClientForwardedPortMap.containsKey(hostPort) | ||
|
||
private fun getForwardedPort(hostPort: Int): Optional<Int> = Optional.ofNullable(hostToClientForwardedPortMap[hostPort]) | ||
|
||
fun setForwardedPort(hostPort: Int, clientPort: Int) { | ||
hostToClientForwardedPortMap[hostPort] = clientPort | ||
} | ||
|
||
fun removeForwardedPort(hostPort: Int) { | ||
hostToClientForwardedPortMap.remove(hostPort) | ||
} | ||
|
||
fun getLocalHostUriFromHostPort(hostPort: Int): URI { | ||
val optionalForwardedPort = getForwardedPort(hostPort) | ||
|
||
val port = if (optionalForwardedPort.isPresent) { | ||
optionalForwardedPort.get() | ||
} else { | ||
thisLogger().warn( | ||
"gitpod: Tried to get the forwarded port of $hostPort, which was not forwarded. " + | ||
"Returning $hostPort itself." | ||
) | ||
hostPort | ||
} | ||
|
||
return URIBuilder() | ||
.setScheme("http") | ||
.setHost(FORWARDED_PORT_HOST) | ||
felladrin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.setPort(port) | ||
.build() | ||
} | ||
|
||
interface LocalHostUriMetadata { | ||
val address: String | ||
val port: Int | ||
} | ||
|
||
fun extractLocalHostUriMetaDataForPortMapping(uri: URI): Optional<LocalHostUriMetadata> { | ||
if (uri.scheme != "http" && uri.scheme != "https") return Optional.empty() | ||
|
||
val localhostMatch = Pattern.compile("^(localhost|127(?:\\.[0-9]+){0,2}\\.[0-9]+|0+(?:\\.0+){0,2}\\.0+|\\[(?:0*:)*?:?0*1?])(?::(\\d+))?\$").matcher(uri.authority) | ||
felladrin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (!localhostMatch.find()) return Optional.empty() | ||
|
||
var address = localhostMatch.group(1) | ||
if (address.startsWith('[') && address.endsWith(']')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How could this happen😂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: ipv6 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see https://pl.kotl.in/ERWpCkfaW this case.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch! I've added a unit test for it! They can be run with ℹ️ This function is a translation of this JavaScript function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @akosyakov, that's a good question from @mustard-mh: why is it removing the last 2 characters instead of removing just the last one ( And wouldn't it be better to return the full hostname, like URL returns? Currently the code is returning Should we change it (in Kotlin code from this PR) to return "[::1]"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember no, but looking at your client it does not seem to be matter. You don't use address anyway. What you should understand here though, that the same port can be served on ip4 and ip6 at the same time as far as I remember then on client machine depending on the host there should be different ports too. I think it is worth to create a follow-up issue to consider it, and discuss with JB how to handle such cases. But it should NOT block this PR. |
||
address = address.substring(1, address.length - 2) | ||
} | ||
|
||
var port = 443 | ||
try { | ||
port = localhostMatch.group(2).toInt() | ||
} catch (throwable: Throwable){ | ||
if (uri.scheme == "http") port = 80 | ||
} | ||
|
||
return Optional.of(object: LocalHostUriMetadata { | ||
override val address = address | ||
override val port = port | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,129 @@ | ||||
// 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.components.service | ||||
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.jetbrains.remote.GitpodPortsService | ||||
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" | ||||
} | ||||
|
||||
private val portsService = service<GitpodPortsService>() | ||||
|
||||
init { start() } | ||||
|
||||
private fun start() { | ||||
if (application.isHeadlessEnvironment) return | ||||
|
||||
observePortsListWhileProjectIsOpen() | ||||
} | ||||
|
||||
private fun observePortsListWhileProjectIsOpen() = application.executeOnPooledThread { | ||||
while (project.lifetime.status == LifetimeStatus.Alive) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need See also the implement
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, that Do you think we can conver this case using only PortStatus? |
||||
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. " + | ||||
"Going to try again in a 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) { | ||||
application.invokeLater { updateForwardedPortsList(response) } | ||||
} | ||||
override fun onCompleted() { completableFuture.complete(null) } | ||||
override fun onError(throwable: Throwable) { completableFuture.completeExceptionally(throwable) } | ||||
} | ||||
|
||||
statusServiceStub.portsStatus(portsStatusRequest, portsStatusResponseObserver) | ||||
|
||||
return completableFuture | ||||
} | ||||
|
||||
private fun updateForwardedPortsList(response: Status.PortsStatusResponse) { | ||||
val portForwardingManager = PortForwardingManager.getInstance(project) | ||||
val forwardedPortsList = portForwardingManager.getForwardedPortsWithLabel(FORWARDED_PORT_LABEL) | ||||
|
||||
for (port in response.portsList) { | ||||
val hostPort = port.localPort | ||||
val isServed = port.served | ||||
|
||||
if (isServed && !forwardedPortsList.containsKey(hostPort)) { | ||||
val portEventsProcessor = object : PortEventsProcessor { | ||||
override fun onPortForwarded(hostPort: Int, clientPort: Int) { | ||||
portsService.setForwardedPort(hostPort, clientPort) | ||||
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) | ||||
felladrin marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
|
||||
if (!isServed && forwardedPortsList.containsKey(hostPort)) { | ||||
portForwardingManager.removePort(hostPort) | ||||
portsService.removeForwardedPort(hostPort) | ||||
thisLogger().info("gitpod: Stopped forwarding port $hostPort.") | ||||
} | ||||
} | ||||
} | ||||
} |
Uh oh!
There was an error while loading. Please reload this page.