From 3e6f395d4547185d4b944cc9c50e1c1368dfcc87 Mon Sep 17 00:00:00 2001 From: Victor Nogueira Date: Wed, 13 Apr 2022 09:27:06 +0000 Subject: [PATCH] [ide] Integrate .gitpod.yml tasks into JetBrains IDEs --- .../backend-plugin/gradle.properties | 2 +- .../jetbrains/remote/GitpodTerminalService.kt | 240 ++++++++++++++++++ .../src/main/resources/META-INF/plugin.xml | 3 + 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodTerminalService.kt diff --git a/components/ide/jetbrains/backend-plugin/gradle.properties b/components/ide/jetbrains/backend-plugin/gradle.properties index 9eddc33c33a1a4..42fbf8c61900a2 100644 --- a/components/ide/jetbrains/backend-plugin/gradle.properties +++ b/components/ide/jetbrains/backend-plugin/gradle.properties @@ -16,7 +16,7 @@ platformVersion=221.4994-EAP-CANDIDATE-SNAPSHOT platformDownloadSources=true # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -platformPlugins=Git4Idea +platformPlugins=Git4Idea, org.jetbrains.plugins.terminal, com.jetbrains.codeWithMe # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. kotlin.stdlib.default.dependency=false diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodTerminalService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodTerminalService.kt new file mode 100644 index 00000000000000..9828544cb9718e --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodTerminalService.kt @@ -0,0 +1,240 @@ +// 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.Disposable +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.openapi.wm.ex.ToolWindowManagerListener +import com.intellij.remoteDev.util.onTerminationOrNow +import com.intellij.terminal.JBTerminalWidget +import com.intellij.util.application +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rdserver.terminal.BackendTerminalManager +import com.jetbrains.rdserver.unattendedHost.UnattendedHostManager +import io.gitpod.supervisor.api.Status.* +import io.gitpod.supervisor.api.StatusServiceGrpc +import io.gitpod.supervisor.api.TerminalOuterClass +import io.gitpod.supervisor.api.TerminalServiceGrpc +import io.grpc.stub.ClientCallStreamObserver +import io.grpc.stub.ClientResponseObserver +import kotlinx.coroutines.* +import kotlinx.coroutines.future.await +import kotlinx.coroutines.guava.asDeferred +import org.jetbrains.plugins.terminal.ShellTerminalWidget +import org.jetbrains.plugins.terminal.TerminalToolWindowFactory +import org.jetbrains.plugins.terminal.TerminalView +import java.util.concurrent.CancellationException +import java.util.concurrent.CompletableFuture + +@Suppress("UnstableApiUsage", "EXPERIMENTAL_IS_NOT_ENABLED", "OPT_IN_IS_NOT_ENABLED") +@OptIn(DelicateCoroutinesApi::class) +class GitpodTerminalService(private val project: Project) : Disposable { + private val lifetime = Lifetime.Eternal.createNested() + private val terminalView = TerminalView.getInstance(project) + private val terminalServiceFutureStub = TerminalServiceGrpc.newFutureStub(GitpodManager.supervisorChannel) + private val statusServiceStub = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel) + private val backendTerminalManager = BackendTerminalManager.getInstance(project) + + override fun dispose() { + lifetime.terminate() + } + + init { + if (!application.isHeadlessEnvironment) { + launch() + } + } + + private fun launch() = GlobalScope.launch { + getTerminalToolWindowRegisteredEvent().await() + + delayUntilControllerClientConnects() + + val tasks = getSupervisorTasksList().await() + + val terminals = getSupervisorTerminalsListAsync().await() + + connectTasksToTerminals(tasks, terminals) + } + + private tailrec suspend fun delayUntilControllerClientConnects() { + if (UnattendedHostManager.getInstance().controllerClientId == null) { + delay(1000L) + return delayUntilControllerClientConnects() + } + } + + private fun connectTasksToTerminals( + tasks: List, + terminals: List + ) = runInEdt { + if (tasks.isEmpty()) { + backendTerminalManager.createNewSharedTerminal("Gitpod", "Terminal") + } else { + val aliasToTerminalMap: MutableMap = mutableMapOf() + + for (terminal in terminals) { + val terminalAlias = terminal.alias + aliasToTerminalMap[terminalAlias] = terminal + } + + val registeredTerminals = terminalView.widgets.toMutableList() + + for (task in tasks) { + val terminalAlias = task.terminal + val terminal = aliasToTerminalMap[terminalAlias] + + if (terminal != null) { + createSharedTerminal(terminal, registeredTerminals) + } + } + } + } + + private fun getTerminalToolWindowRegisteredEvent(): CompletableFuture { + debug("Waiting for TerminalToolWindow to be registered...") + + val completableFuture = CompletableFuture() + + val messageBusConnection = project.messageBus.connect() + + val toolWindowManagerListener = object : ToolWindowManagerListener { + override fun toolWindowsRegistered(ids: MutableList, toolWindowManager: ToolWindowManager) { + if (ids.contains(TerminalToolWindowFactory.TOOL_WINDOW_ID)) { + debug("TerminalToolWindow got registered!") + completableFuture.complete(null) + messageBusConnection.disconnect() + } + } + } + + messageBusConnection.subscribe(ToolWindowManagerListener.TOPIC, toolWindowManagerListener) + + return completableFuture + } + + private suspend fun getSupervisorTasksList(): CompletableFuture> { + val externalCompletableFuture = CompletableFuture>() + + GlobalScope.launch { + val coroutineScope = this + + while(coroutineScope.isActive) { + try { + val internalCompletableFuture = CompletableFuture>() + + val taskStatusRequest = TasksStatusRequest.newBuilder().setObserve(true).build() + + val taskStatusResponseObserver = object : ClientResponseObserver { + override fun beforeStart(request: ClientCallStreamObserver) { + lifetime.onTerminationOrNow { + request.cancel(null, null) + } + } + + override fun onNext(response: TasksStatusResponse) { + for (task in response.tasksList) { + if (task.state === TaskState.opening) { + return + } + } + internalCompletableFuture.complete(response.tasksList) + } + + override fun onCompleted() { } + + override fun onError(throwable: Throwable) { + internalCompletableFuture.completeExceptionally(throwable) + } + } + + statusServiceStub.tasksStatus(taskStatusRequest, taskStatusResponseObserver) + + val tasksList = internalCompletableFuture.await() + + debug("Successfully got tasks from Supervisor:\n$tasksList") + + coroutineScope.cancel() + externalCompletableFuture.complete(tasksList) + } catch (throwable: Throwable) { + if (throwable is CancellationException) { + coroutineScope.cancel() + externalCompletableFuture.completeExceptionally(throwable) + } else { + thisLogger().error("Got an error while trying to get tasks from Supervisor.", throwable) + } + } + + delay(1000L) + } + } + + return externalCompletableFuture + } + + private fun getSupervisorTerminalsListAsync(): CompletableFuture> { + val completableFuture = CompletableFuture>() + + GlobalScope.launch { + val coroutineScope = this + + while(coroutineScope.isActive) { + try { + val listTerminalsRequest = TerminalOuterClass.ListTerminalsRequest.newBuilder().build() + + val deferredListTerminalsRequest = terminalServiceFutureStub.list(listTerminalsRequest).asDeferred() + + lifetime.onTerminationOrNow { + deferredListTerminalsRequest.cancel() + } + + val listTerminalsResponse = deferredListTerminalsRequest.await() + + val terminalsList = listTerminalsResponse.terminalsList + + debug("Successfully got the list of Supervisor terminals:\n${terminalsList}") + + coroutineScope.cancel() + completableFuture.complete(terminalsList) + } catch (throwable: Throwable) { + if (throwable is CancellationException) { + coroutineScope.cancel() + completableFuture.completeExceptionally(throwable) + } else { + thisLogger().error("Got an error while trying to get terminals list from Supervisor.", throwable) + } + } + + delay(1000L) + } + } + + return completableFuture + } + + private fun createSharedTerminal(supervisorTerminal: TerminalOuterClass.Terminal, registeredTerminals: MutableList) { + debug("Creating shared terminal '${supervisorTerminal.title}' on Backend IDE") + + backendTerminalManager.createNewSharedTerminal(supervisorTerminal.alias, supervisorTerminal.title) + + for (widget in terminalView.widgets) { + if (!registeredTerminals.contains(widget)) { + registeredTerminals.add(widget) + (widget as ShellTerminalWidget).executeCommand("gp tasks attach ${supervisorTerminal.alias}") + } + } + } + + private fun debug(message: String) = runInEdt { + if (System.getenv("JB_DEV").toBoolean()) { + thisLogger().warn(message) + } else { + thisLogger().info(message) + } + } +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml index 4f4a2bcca020f9..e30a75b80f350e 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml @@ -16,6 +16,8 @@ com.intellij.modules.platform + + @@ -24,6 +26,7 @@ +