diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt index 2478ccd4334eac..035e48097a30c9 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodManager.kt @@ -4,11 +4,17 @@ package io.gitpod.jetbrains.remote +import com.intellij.openapi.client.ClientSessionsManager +import org.jetbrains.ide.RestService +import com.intellij.codeWithMe.ClientId +import com.intellij.ide.BrowserUtil import com.intellij.ide.plugins.PluginManagerCore import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable +import com.intellij.openapi.client.ClientProjectSession +import com.intellij.openapi.client.ClientSession import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.extensions.PluginId @@ -23,6 +29,10 @@ import io.gitpod.jetbrains.remote.utils.Retrier.retry import io.gitpod.supervisor.api.* import io.gitpod.supervisor.api.Info.WorkspaceInfoResponse import io.gitpod.supervisor.api.Notification.* +import io.gitpod.supervisor.api.Status.OnPortExposedAction +import io.gitpod.supervisor.api.Status.PortsStatus +import io.gitpod.supervisor.api.Status.PortsStatusRequest +import io.gitpod.supervisor.api.Status.PortsStatusResponse import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.stub.ClientCallStreamObserver @@ -46,6 +56,7 @@ import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture import javax.websocket.DeploymentException + @Service class GitpodManager : Disposable { @@ -199,6 +210,127 @@ class GitpodManager : Disposable { } } + private val portsObserveJob = GlobalScope.launch { + if (application.isHeadlessEnvironment) { + return@launch + } + + // Ignore ports that aren't actually used by the user (e.g. ports used internally by JetBrains IDEs) + val ignorePorts = listOf(5990, 5991, 6942, 6943) + val portsStatus = hashMapOf() + + val status = StatusServiceGrpc.newStub(supervisorChannel) + while (isActive) { + try { + val project = RestService.getLastFocusedOrOpenedProject() + + var session: ClientSession? = null + if (project != null) { + session = ClientSessionsManager.getProjectSessions(project, false).first() + } + if (session == null) { + session = ClientSessionsManager.getAppSessions(false).first() + } + + val gitpodClientProjectSessionTracker = GitpodClientProjectSessionTracker(session as ClientProjectSession) + + val f = CompletableFuture() + status.portsStatus( + PortsStatusRequest.newBuilder().setObserve(true).build(), + object : ClientResponseObserver { + + override fun beforeStart(requestStream: ClientCallStreamObserver) { + lifetime.onTerminationOrNow { + requestStream.cancel(null, null) + } + } + + override fun onNext(ps: PortsStatusResponse) { + for (port in ps.portsList) { + // Avoiding undesired notifications + if (ignorePorts.contains(port.localPort)) { + continue + } + + val hasPreviousStatus = portsStatus.containsKey(port.localPort) + + if (!hasPreviousStatus) { + portsStatus[port.localPort] = port + } + + val wasServed = portsStatus[port.localPort]?.served!! + val wasExposed = portsStatus[port.localPort]?.hasExposed()!! + val wasServedExposed = wasServed && wasExposed + val isServedExposed = port.served && port.hasExposed() + + // If the initial update received shows that the port is served and exposed, then notify + val isFirstUpdate = !hasPreviousStatus && wasServedExposed && isServedExposed + + // If the port changes its status to served and exposed, notify the user + val shouldSendNotification = isFirstUpdate || !wasServedExposed && isServedExposed + + portsStatus[port.localPort] = port + + if (shouldSendNotification) { + if (port.exposed.onExposed.number == OnPortExposedAction.ignore_VALUE) { + continue + } + + if (port.exposed.onExposed.number == OnPortExposedAction.open_browser_VALUE) { + ClientId.withClientId(session.clientId) { + BrowserUtil.browse(port.exposed.url) + gitpodClientProjectSessionTracker.trackEvent("jb_execute_command_gitpod_ports", mapOf("action" to "openBrowser")) + } + continue + } + + if (port.exposed.onExposed.number == OnPortExposedAction.open_preview_VALUE) { + ClientId.withClientId(session.clientId) { + BrowserUtil.browse(port.exposed.url) + gitpodClientProjectSessionTracker.trackEvent("jb_execute_command_gitpod_ports", mapOf("action" to "openBrowser")) + } + continue + } + + val message = "A service is available on port ${port.localPort}" + val notification = notificationGroup.createNotification(message, NotificationType.INFORMATION) + + val lambda = { + BrowserUtil.browse(port.exposed.url) + gitpodClientProjectSessionTracker.trackEvent("jb_execute_command_gitpod_ports", mapOf("action" to "openBrowser")) + } + + val action = NotificationAction.createSimpleExpiring("Open Browser", lambda) + notification.addAction(action) + notification.notify(null) + } + } + } + + override fun onError(t: Throwable) { + f.completeExceptionally(t) + } + + override fun onCompleted() { + f.complete(null) + } + }) + f.await() + } catch (t: Throwable) { + if (t is CancellationException) { + throw t + } + thisLogger().error("gitpod: failed to stream ports status: ", t) + } + delay(1000L) + } + } + init { + lifetime.onTerminationOrNow { + portsObserveJob.cancel() + } + } + val pendingInfo = CompletableFuture() private val infoJob = GlobalScope.launch { if (application.isHeadlessEnvironment) {