Skip to content

Commit

Permalink
[JetBrains] improve tasks' terminals (#19772)
Browse files Browse the repository at this point in the history
* 1

* Add more logs and timeout

* remove delay

* Make terminal service stable

* Avoid empty terminal name + task id

* 💄

* format

* skip if all empty

* remove blocking
  • Loading branch information
mustard-mh authored May 27, 2024
1 parent f079d8d commit 984fbea
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 127 deletions.
22 changes: 12 additions & 10 deletions components/ide/jetbrains/backend-plugin/hot-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,26 @@ leeway build -DnoVerifyJBPlugin=true -Dversion="$version" -DimageRepoBase=eu.gcr
dev_image="$(tar xfO "$bldfn" ./imgnames.txt | head -n1)"
echo "Dev Image: $dev_image"

ide_list=("intellij" "goland" "pycharm" "phpstorm" "rubymine" "webstorm" "rider" "clion")

if [ "$qualifier" == "stable" ]; then
prop="pluginImage"
prop_list=("pluginImage" "imageLayers[0]")
else
prop="pluginLatestImage"
prop_list=("pluginLatestImage" "latestImageLayers[0]")
fi

cf_patch=$(kubectl get cm ide-config -o=json | jq '.data."config.json"' |jq -r)
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.intellij.$prop = \"$dev_image\"")
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.goland.$prop = \"$dev_image\"")
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.pycharm.$prop = \"$dev_image\"")
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.phpstorm.$prop = \"$dev_image\"")
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.rubymine.$prop = \"$dev_image\"")
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.webstorm.$prop = \"$dev_image\"")
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.rider.$prop = \"$dev_image\"")
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.clion.$prop = \"$dev_image\"")

for ide in "${ide_list[@]}"; do
for prop in "${prop_list[@]}"; do
cf_patch=$(echo "$cf_patch" | jq ".ideOptions.options.$ide.$prop = \"$dev_image\"")
done
done

cf_patch=$(echo "$cf_patch" |jq tostring)
cf_patch="{\"data\": {\"config.json\": $cf_patch}}"

kubectl patch cm ide-config --type=merge -p "$cf_patch"

kubectl rollout restart deployment ide-service
kubectl rollout restart deployment server
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ import io.gitpod.supervisor.api.TerminalServiceGrpc
import io.grpc.StatusRuntimeException
import io.grpc.stub.ClientCallStreamObserver
import io.grpc.stub.ClientResponseObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.future.await
import kotlinx.coroutines.guava.await
import org.jetbrains.plugins.terminal.ShellTerminalWidget
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import kotlin.coroutines.coroutineContext

abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
private val lifetime = defineNestedLifetime()
Expand All @@ -39,27 +38,38 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
start()
}

protected abstract fun runJob(lifetime: Lifetime, block: suspend CoroutineScope.() -> Unit): Job;
protected abstract fun runJob(lifetime: Lifetime, block: suspend CoroutineScope.() -> Unit): Job

override fun dispose() = Unit
protected fun start() {
if (application.isHeadlessEnvironment) return

runJob(lifetime) {
val terminals = getSupervisorTerminalsList()
val tasks = getSupervisorTasksList()

application.invokeLater {
createTerminalsAttachedToTasks(terminals, tasks)
try {
val terminals = withTimeout(20000L) { getSupervisorTerminalsList() }
val tasks = withTimeout(20000L) { getSupervisorTasksList() }
thisLogger().info("gitpod: attaching tasks ${tasks.size}, terminals ${terminals.size}")
if (tasks.isEmpty() && terminals.isEmpty()) {
return@runJob
}
// see internal chat https://gitpod.slack.com/archives/C02BRJLGPGF/p1716540080028119
delay(5000L)
application.invokeLater {
createTerminalsAttachedToTasks(terminals, tasks)
}
} catch (e: TimeoutCancellationException) {
thisLogger().error("gitpod: timeout while fetching tasks or terminals", e)
} catch (e: Exception) {
thisLogger().error("gitpod: error while attaching tasks", e)
}
}
}

protected abstract fun createSharedTerminal(title: String): ShellTerminalWidget
protected abstract fun createSharedTerminal(id: String, title: String): ShellTerminalWidget

private fun createTerminalsAttachedToTasks(
terminals: List<TerminalOuterClass.Terminal>,
tasks: List<Status.TaskStatus>
terminals: List<TerminalOuterClass.Terminal>,
tasks: List<Status.TaskStatus>
) {
if (tasks.isEmpty()) return

Expand All @@ -70,24 +80,31 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
aliasToTerminalMap[terminalAlias] = terminal
}

for (task in tasks) {
tasks.forEachIndexed { index, task ->
val terminalAlias = task.terminal
val terminal = aliasToTerminalMap[terminalAlias] ?: continue
val terminal = aliasToTerminalMap[terminalAlias]

createAttachedSharedTerminal(terminal)
if (terminal == null) {
thisLogger().warn("gitpod: found no terminal for task ${task.id}, expecting ${task.terminal}")
return
}
val title = terminal.title.takeIf { !it.isNullOrBlank() } ?: "Gitpod Task ${index + 1}"
thisLogger().info("gitpod: attaching task ${terminal.title} (${task.terminal}) with title $title")
createAttachedSharedTerminal(title, terminal)
thisLogger().info("gitpod: attached task ${terminal.title} (${task.terminal})")
}
}

private tailrec suspend fun getSupervisorTasksList(): List<Status.TaskStatus> {
var tasksList: List<Status.TaskStatus>? = null

coroutineContext.ensureActive()
try {
val completableFuture = CompletableFuture<List<Status.TaskStatus>>()

val taskStatusRequest = Status.TasksStatusRequest.newBuilder().setObserve(true).build()

val taskStatusResponseObserver = object :
ClientResponseObserver<Status.TasksStatusRequest, Status.TasksStatusResponse> {
ClientResponseObserver<Status.TasksStatusRequest, Status.TasksStatusResponse> {
override fun beforeStart(request: ClientCallStreamObserver<Status.TasksStatusRequest>) = Unit

override fun onNext(response: Status.TasksStatusResponse) {
Expand All @@ -114,23 +131,20 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
}

thisLogger().error(
"gitpod: Got an error while trying to get tasks list from Supervisor. " +
"Trying again in one second.",
throwable
"gitpod: Got an error while trying to get tasks list from Supervisor. Trying again in one second.",
throwable
)
}

return if (tasksList != null) {
tasksList
} else {
return tasksList ?: run {
delay(1000)
getSupervisorTasksList()
}
}

private tailrec suspend fun getSupervisorTerminalsList(): List<TerminalOuterClass.Terminal> {
var terminalsList: List<TerminalOuterClass.Terminal>? = null

coroutineContext.ensureActive()
try {
val listTerminalsRequest = TerminalOuterClass.ListTerminalsRequest.newBuilder().build()

Expand All @@ -145,129 +159,129 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
}

thisLogger().error(
"gitpod: Got an error while trying to get terminals list from Supervisor. " +
"Trying again in one second.",
throwable
"gitpod: Got an error while trying to get terminals list from Supervisor. Trying again in one second.",
throwable
)
}

return if (terminalsList != null) {
terminalsList
} else {
return terminalsList ?: run {
delay(1000)
getSupervisorTerminalsList()
}
}

private fun createAttachedSharedTerminal(supervisorTerminal: TerminalOuterClass.Terminal) {
val shellTerminalWidget = createSharedTerminal(supervisorTerminal.title)
private fun createAttachedSharedTerminal(title: String, supervisorTerminal: TerminalOuterClass.Terminal) {
val shellTerminalWidget = createSharedTerminal(supervisorTerminal.alias, title)
shellTerminalWidget.executeCommand("gp tasks attach ${supervisorTerminal.alias}")
closeTerminalWidgetWhenClientGetsClosed(shellTerminalWidget)
closeTerminalWidgetWhenClientGetsClosed(supervisorTerminal, shellTerminalWidget)
exitTaskWhenTerminalWidgetGetsClosed(supervisorTerminal, shellTerminalWidget)
listenForTaskTerminationAndTitleChanges(supervisorTerminal, shellTerminalWidget)
}

private fun listenForTaskTerminationAndTitleChanges(
supervisorTerminal: TerminalOuterClass.Terminal,
shellTerminalWidget: ShellTerminalWidget
supervisorTerminal: TerminalOuterClass.Terminal,
shellTerminalWidget: ShellTerminalWidget
) = runJob(lifetime) {
var hasOpenSessions = true

while (hasOpenSessions) {
val completableFuture = CompletableFuture<Void>()

val listenTerminalRequest = TerminalOuterClass.ListenTerminalRequest.newBuilder()
.setAlias(supervisorTerminal.alias)
.build()
.setAlias(supervisorTerminal.alias)
.build()

val listenTerminalResponseObserver =
object : ClientResponseObserver<TerminalOuterClass.ListenTerminalRequest, TerminalOuterClass.ListenTerminalResponse> {
override fun beforeStart(
request: ClientCallStreamObserver<TerminalOuterClass.ListenTerminalRequest>
) {
@Suppress("ObjectLiteralToLambda")
shellTerminalWidget.addListener(object : TerminalWidgetListener {
override fun allSessionsClosed(widget: TerminalWidget) {
hasOpenSessions = false
request.cancel("gitpod: Terminal closed on the client.", null)
}
})
}
object :
ClientResponseObserver<TerminalOuterClass.ListenTerminalRequest, TerminalOuterClass.ListenTerminalResponse> {
override fun beforeStart(request: ClientCallStreamObserver<TerminalOuterClass.ListenTerminalRequest>) {
@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
}
override fun onNext(response: TerminalOuterClass.ListenTerminalResponse) {
when {
response.hasTitle() -> application.invokeLater {
shellTerminalWidget.terminalTitle.change {
applicationTitle = response.title
}
}

response.hasExitCode() -> application.invokeLater {
shellTerminalWidget.close()
}
response.hasExitCode() -> application.invokeLater {
shellTerminalWidget.close()
}
}
}

override fun onCompleted() = Unit
override fun onCompleted() = Unit

override fun onError(throwable: Throwable) {
completableFuture.completeExceptionally(throwable)
}
override fun onError(throwable: Throwable) {
completableFuture.completeExceptionally(throwable)
}
}

terminalServiceStub.listen(listenTerminalRequest, listenTerminalResponseObserver)

try {
completableFuture.await()
} catch (throwable: Throwable) {
if (
throwable is StatusRuntimeException ||
throwable is ExecutionException ||
throwable is InterruptedException
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.")
break
}

thisLogger()
.error("gitpod: Got an error while listening to " +
"'${supervisorTerminal.title}' terminal. Trying again in one second.", throwable)
thisLogger().error(
"gitpod: got an error while listening to '${supervisorTerminal.title}' terminal. Trying again in one second.",
throwable
)
}

delay(1000)
}
}

private fun exitTaskWhenTerminalWidgetGetsClosed(
supervisorTerminal: TerminalOuterClass.Terminal,
shellTerminalWidget: ShellTerminalWidget
supervisorTerminal: TerminalOuterClass.Terminal,
shellTerminalWidget: ShellTerminalWidget
) {
@Suppress("ObjectLiteralToLambda")
shellTerminalWidget.addListener(object : TerminalWidgetListener {
override fun allSessionsClosed(widget: TerminalWidget) {
runJob(lifetime) {
delay(5000)
try {
thisLogger().info("gitpod: shutdown task ${supervisorTerminal.title} (${supervisorTerminal.alias})")
terminalServiceFutureStub.shutdown(
TerminalOuterClass.ShutdownTerminalRequest.newBuilder()
.setAlias(supervisorTerminal.alias)
.build()
)
).await()
} catch (throwable: Throwable) {
thisLogger().error("gitpod: Got an error while shutting down " +
"'${supervisorTerminal.title}' terminal.", throwable)
thisLogger().error(
"gitpod: got an error while shutting down '${supervisorTerminal.title}' terminal.",
throwable
)
}
}
}
})
}

private fun closeTerminalWidgetWhenClientGetsClosed(
shellTerminalWidget: ShellTerminalWidget
supervisorTerminal: TerminalOuterClass.Terminal,
shellTerminalWidget: ShellTerminalWidget
) {
@Suppress("UnstableApiUsage")
lifetime.onTerminationOrNow {
thisLogger().debug("gitpod: closing task terminal service ${supervisorTerminal.title} (${supervisorTerminal.alias})")
shellTerminalWidget.close()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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
package io.gitpod.jetbrains.remote.internal

import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
Expand All @@ -11,21 +11,28 @@ import com.jetbrains.rd.util.threading.coroutines.launch
import com.jetbrains.rdserver.terminal.BackendTerminalManager
import io.gitpod.jetbrains.remote.AbstractGitpodTerminalService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import org.jetbrains.plugins.terminal.ShellTerminalWidget
import org.jetbrains.plugins.terminal.TerminalToolWindowManager
import java.util.*

@Suppress("UnstableApiUsage")
class GitpodTerminalService(val project: Project): AbstractGitpodTerminalService(project) {
class GitpodTerminalServiceImpl(val project: Project) : AbstractGitpodTerminalService(project) {

private val terminalToolWindowManager = TerminalToolWindowManager.getInstance(project)
private val backendTerminalManager = BackendTerminalManager.getInstance(project)

override fun runJob(lifetime: Lifetime, block: suspend CoroutineScope.() -> Unit) = lifetime.launch { block() }

override fun createSharedTerminal(title: String): ShellTerminalWidget {
val shellTerminalWidget = ShellTerminalWidget.toShellJediTermWidgetOrThrow(terminalToolWindowManager.createShellWidget(null, title, true, false))
backendTerminalManager.shareTerminal(shellTerminalWidget.asNewWidget(), UUID.randomUUID().toString())
override fun createSharedTerminal(id: String, title: String): ShellTerminalWidget {
val shellTerminalWidget = ShellTerminalWidget.toShellJediTermWidgetOrThrow(
terminalToolWindowManager.createShellWidget(
null,
title,
true,
false
)
)
backendTerminalManager.shareTerminal(shellTerminalWidget.asNewWidget(), id)
return shellTerminalWidget
}
}
Loading

0 comments on commit 984fbea

Please sign in to comment.