diff --git a/components/ide/jetbrains/backend-plugin/build.gradle.kts b/components/ide/jetbrains/backend-plugin/build.gradle.kts index 83dee90147d839..bc1866b9c828ce 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.8.0" + id("org.jetbrains.intellij") version "1.8.1" // 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/GitpodTerminalService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodTerminalService.kt index 4b1ef9dcfa71d0..c027624127bb7a 100644 --- 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 @@ -7,18 +7,21 @@ package io.gitpod.jetbrains.remote import com.intellij.openapi.client.ClientProjectSession import com.intellij.openapi.diagnostic.thisLogger import com.intellij.util.application +import com.jediterm.terminal.ui.TerminalWidget +import com.jediterm.terminal.ui.TerminalWidgetListener import com.jetbrains.rdserver.terminal.BackendTerminalManager -import io.gitpod.jetbrains.remote.GitpodManager 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.StatusRuntimeException import io.grpc.stub.ClientCallStreamObserver import io.grpc.stub.ClientResponseObserver import org.jetbrains.plugins.terminal.ShellTerminalWidget import org.jetbrains.plugins.terminal.TerminalView import java.util.* import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit @Suppress("UnstableApiUsage") @@ -30,6 +33,7 @@ class GitpodTerminalService(session: ClientProjectSession) { private val terminalView = TerminalView.getInstance(session.project) private val backendTerminalManager = BackendTerminalManager.getInstance(session.project) private val terminalServiceFutureStub = TerminalServiceGrpc.newFutureStub(GitpodManager.supervisorChannel) + private val terminalServiceStub = TerminalServiceGrpc.newStub(GitpodManager.supervisorChannel) private val statusServiceStub = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel) init { start() } @@ -49,7 +53,7 @@ class GitpodTerminalService(session: ClientProjectSession) { } } - private fun createSharedTerminalAndExecuteCommand(title: String, command: String) { + private fun createSharedTerminalAndExecuteCommand(title: String, command: String): ShellTerminalWidget? { val registeredTerminals = terminalView.widgets.toMutableList() backendTerminalManager.createNewSharedTerminal(UUID.randomUUID().toString(), title) @@ -59,8 +63,14 @@ class GitpodTerminalService(session: ClientProjectSession) { widget.terminalTitle.change { applicationTitle = title } - (widget as ShellTerminalWidget).executeCommand(command) + val shellTerminalWidget = widget as ShellTerminalWidget + + shellTerminalWidget.executeCommand(command) + + return shellTerminalWidget } + + return null } private fun createTerminalsAttachedToTasks( @@ -120,8 +130,9 @@ class GitpodTerminalService(session: ClientProjectSession) { } thisLogger().error( - "gitpod: Got an error while trying to get tasks list from Supervisor. Trying again in on second.", - throwable + "gitpod: Got an error while trying to get tasks list from Supervisor. " + + "Trying again in one second.", + throwable ) } @@ -150,8 +161,9 @@ class GitpodTerminalService(session: ClientProjectSession) { } thisLogger().error( - "gitpod: Got an error while trying to get terminals list from Supervisor. Trying again in on second.", - throwable + "gitpod: Got an error while trying to get terminals list from Supervisor. " + + "Trying again in one second.", + throwable ) } @@ -164,9 +176,101 @@ class GitpodTerminalService(session: ClientProjectSession) { } private fun createAttachedSharedTerminal(supervisorTerminal: TerminalOuterClass.Terminal) { - createSharedTerminalAndExecuteCommand( - supervisorTerminal.title, - "gp tasks attach ${supervisorTerminal.alias}" - ) + val shellTerminalWidget = createSharedTerminalAndExecuteCommand( + supervisorTerminal.title, + "gp tasks attach ${supervisorTerminal.alias}" + ) ?: return + + exitTaskWhenTerminalWidgetGetsClosed(supervisorTerminal, shellTerminalWidget) + + listenForTaskTerminationAndTitleChanges(supervisorTerminal, shellTerminalWidget) + } + + private fun listenForTaskTerminationAndTitleChanges( + supervisorTerminal: TerminalOuterClass.Terminal, + shellTerminalWidget: ShellTerminalWidget + ) = application.executeOnPooledThread { + var hasOpenSessions = true + + while (hasOpenSessions) { + val completableFuture = CompletableFuture() + + val listenTerminalRequest = TerminalOuterClass.ListenTerminalRequest.newBuilder() + .setAlias(supervisorTerminal.alias) + .build() + + val listenTerminalResponseObserver = + object : ClientResponseObserver { + override fun beforeStart(request: ClientCallStreamObserver) { + @Suppress("ObjectLiteralToLambda") + shellTerminalWidget.addListener(object : TerminalWidgetListener { + override fun allSessionsClosed(widget: TerminalWidget) { + hasOpenSessions = false + request.cancel("gitpod: Terminal closed on the client.", null) + } + }) + } + + override fun onNext(response: TerminalOuterClass.ListenTerminalResponse) { + when { + response.hasTitle() -> application.invokeLater { + shellTerminalWidget.terminalTitle.change { + applicationTitle = response.title + } + } + + response.hasExitCode() -> application.invokeLater { + shellTerminalWidget.close() + } + } + } + + override fun onCompleted() = Unit + + override fun onError(throwable: Throwable) { + completableFuture.completeExceptionally(throwable) + } + } + + terminalServiceStub.listen(listenTerminalRequest, listenTerminalResponseObserver) + + try { + completableFuture.get() + } catch (throwable: Throwable) { + if ( + throwable is StatusRuntimeException || + throwable is ExecutionException || + throwable is InterruptedException + ) { + shellTerminalWidget.close() + thisLogger() + .info("gitpod: Stopped listening to " + + "'${supervisorTerminal.title}' terminal due to an expected exception.", throwable) + break + } + + thisLogger() + .error("gitpod: Got an error while listening to " + + "'${supervisorTerminal.title}' terminal. Trying again in one second.", throwable) + } + + TimeUnit.SECONDS.sleep(1) + } + } + + private fun exitTaskWhenTerminalWidgetGetsClosed( + supervisorTerminal: TerminalOuterClass.Terminal, + shellTerminalWidget: ShellTerminalWidget + ) { + @Suppress("ObjectLiteralToLambda") + shellTerminalWidget.addListener(object : TerminalWidgetListener { + override fun allSessionsClosed(widget: TerminalWidget) { + terminalServiceFutureStub.shutdown( + TerminalOuterClass.ShutdownTerminalRequest.newBuilder() + .setAlias(supervisorTerminal.alias) + .build() + ) + } + }) } }