Skip to content

Commit 984fbea

Browse files
authored
[JetBrains] improve tasks' terminals (#19772)
* 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
1 parent f079d8d commit 984fbea

File tree

7 files changed

+119
-127
lines changed

7 files changed

+119
-127
lines changed

components/ide/jetbrains/backend-plugin/hot-deploy.sh

+12-10
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,26 @@ leeway build -DnoVerifyJBPlugin=true -Dversion="$version" -DimageRepoBase=eu.gcr
2121
dev_image="$(tar xfO "$bldfn" ./imgnames.txt | head -n1)"
2222
echo "Dev Image: $dev_image"
2323

24+
ide_list=("intellij" "goland" "pycharm" "phpstorm" "rubymine" "webstorm" "rider" "clion")
25+
2426
if [ "$qualifier" == "stable" ]; then
25-
prop="pluginImage"
27+
prop_list=("pluginImage" "imageLayers[0]")
2628
else
27-
prop="pluginLatestImage"
29+
prop_list=("pluginLatestImage" "latestImageLayers[0]")
2830
fi
2931

3032
cf_patch=$(kubectl get cm ide-config -o=json | jq '.data."config.json"' |jq -r)
31-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.intellij.$prop = \"$dev_image\"")
32-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.goland.$prop = \"$dev_image\"")
33-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.pycharm.$prop = \"$dev_image\"")
34-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.phpstorm.$prop = \"$dev_image\"")
35-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.rubymine.$prop = \"$dev_image\"")
36-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.webstorm.$prop = \"$dev_image\"")
37-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.rider.$prop = \"$dev_image\"")
38-
cf_patch=$(echo "$cf_patch" |jq ".ideOptions.options.clion.$prop = \"$dev_image\"")
33+
34+
for ide in "${ide_list[@]}"; do
35+
for prop in "${prop_list[@]}"; do
36+
cf_patch=$(echo "$cf_patch" | jq ".ideOptions.options.$ide.$prop = \"$dev_image\"")
37+
done
38+
done
39+
3940
cf_patch=$(echo "$cf_patch" |jq tostring)
4041
cf_patch="{\"data\": {\"config.json\": $cf_patch}}"
4142

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

4445
kubectl rollout restart deployment ide-service
46+
kubectl rollout restart deployment server

components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/AbstractGitpodTerminalService.kt

+91-77
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ import io.gitpod.supervisor.api.TerminalServiceGrpc
2020
import io.grpc.StatusRuntimeException
2121
import io.grpc.stub.ClientCallStreamObserver
2222
import io.grpc.stub.ClientResponseObserver
23-
import kotlinx.coroutines.CoroutineScope
24-
import kotlinx.coroutines.Job
25-
import kotlinx.coroutines.delay
23+
import kotlinx.coroutines.*
2624
import kotlinx.coroutines.future.await
2725
import kotlinx.coroutines.guava.await
2826
import org.jetbrains.plugins.terminal.ShellTerminalWidget
2927
import java.util.concurrent.CompletableFuture
3028
import java.util.concurrent.ExecutionException
29+
import kotlin.coroutines.coroutineContext
3130

3231
abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
3332
private val lifetime = defineNestedLifetime()
@@ -39,27 +38,38 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
3938
start()
4039
}
4140

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

4443
override fun dispose() = Unit
4544
protected fun start() {
4645
if (application.isHeadlessEnvironment) return
4746

4847
runJob(lifetime) {
49-
val terminals = getSupervisorTerminalsList()
50-
val tasks = getSupervisorTasksList()
51-
52-
application.invokeLater {
53-
createTerminalsAttachedToTasks(terminals, tasks)
48+
try {
49+
val terminals = withTimeout(20000L) { getSupervisorTerminalsList() }
50+
val tasks = withTimeout(20000L) { getSupervisorTasksList() }
51+
thisLogger().info("gitpod: attaching tasks ${tasks.size}, terminals ${terminals.size}")
52+
if (tasks.isEmpty() && terminals.isEmpty()) {
53+
return@runJob
54+
}
55+
// see internal chat https://gitpod.slack.com/archives/C02BRJLGPGF/p1716540080028119
56+
delay(5000L)
57+
application.invokeLater {
58+
createTerminalsAttachedToTasks(terminals, tasks)
59+
}
60+
} catch (e: TimeoutCancellationException) {
61+
thisLogger().error("gitpod: timeout while fetching tasks or terminals", e)
62+
} catch (e: Exception) {
63+
thisLogger().error("gitpod: error while attaching tasks", e)
5464
}
5565
}
5666
}
5767

58-
protected abstract fun createSharedTerminal(title: String): ShellTerminalWidget
68+
protected abstract fun createSharedTerminal(id: String, title: String): ShellTerminalWidget
5969

6070
private fun createTerminalsAttachedToTasks(
61-
terminals: List<TerminalOuterClass.Terminal>,
62-
tasks: List<Status.TaskStatus>
71+
terminals: List<TerminalOuterClass.Terminal>,
72+
tasks: List<Status.TaskStatus>
6373
) {
6474
if (tasks.isEmpty()) return
6575

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

73-
for (task in tasks) {
83+
tasks.forEachIndexed { index, task ->
7484
val terminalAlias = task.terminal
75-
val terminal = aliasToTerminalMap[terminalAlias] ?: continue
85+
val terminal = aliasToTerminalMap[terminalAlias]
7686

77-
createAttachedSharedTerminal(terminal)
87+
if (terminal == null) {
88+
thisLogger().warn("gitpod: found no terminal for task ${task.id}, expecting ${task.terminal}")
89+
return
90+
}
91+
val title = terminal.title.takeIf { !it.isNullOrBlank() } ?: "Gitpod Task ${index + 1}"
92+
thisLogger().info("gitpod: attaching task ${terminal.title} (${task.terminal}) with title $title")
93+
createAttachedSharedTerminal(title, terminal)
94+
thisLogger().info("gitpod: attached task ${terminal.title} (${task.terminal})")
7895
}
7996
}
8097

8198
private tailrec suspend fun getSupervisorTasksList(): List<Status.TaskStatus> {
8299
var tasksList: List<Status.TaskStatus>? = null
83-
100+
coroutineContext.ensureActive()
84101
try {
85102
val completableFuture = CompletableFuture<List<Status.TaskStatus>>()
86103

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

89106
val taskStatusResponseObserver = object :
90-
ClientResponseObserver<Status.TasksStatusRequest, Status.TasksStatusResponse> {
107+
ClientResponseObserver<Status.TasksStatusRequest, Status.TasksStatusResponse> {
91108
override fun beforeStart(request: ClientCallStreamObserver<Status.TasksStatusRequest>) = Unit
92109

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

116133
thisLogger().error(
117-
"gitpod: Got an error while trying to get tasks list from Supervisor. " +
118-
"Trying again in one second.",
119-
throwable
134+
"gitpod: Got an error while trying to get tasks list from Supervisor. Trying again in one second.",
135+
throwable
120136
)
121137
}
122138

123-
return if (tasksList != null) {
124-
tasksList
125-
} else {
139+
return tasksList ?: run {
126140
delay(1000)
127141
getSupervisorTasksList()
128142
}
129143
}
130144

131145
private tailrec suspend fun getSupervisorTerminalsList(): List<TerminalOuterClass.Terminal> {
132146
var terminalsList: List<TerminalOuterClass.Terminal>? = null
133-
147+
coroutineContext.ensureActive()
134148
try {
135149
val listTerminalsRequest = TerminalOuterClass.ListTerminalsRequest.newBuilder().build()
136150

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

147161
thisLogger().error(
148-
"gitpod: Got an error while trying to get terminals list from Supervisor. " +
149-
"Trying again in one second.",
150-
throwable
162+
"gitpod: Got an error while trying to get terminals list from Supervisor. Trying again in one second.",
163+
throwable
151164
)
152165
}
153166

154-
return if (terminalsList != null) {
155-
terminalsList
156-
} else {
167+
return terminalsList ?: run {
157168
delay(1000)
158169
getSupervisorTerminalsList()
159170
}
160171
}
161172

162-
private fun createAttachedSharedTerminal(supervisorTerminal: TerminalOuterClass.Terminal) {
163-
val shellTerminalWidget = createSharedTerminal(supervisorTerminal.title)
173+
private fun createAttachedSharedTerminal(title: String, supervisorTerminal: TerminalOuterClass.Terminal) {
174+
val shellTerminalWidget = createSharedTerminal(supervisorTerminal.alias, title)
164175
shellTerminalWidget.executeCommand("gp tasks attach ${supervisorTerminal.alias}")
165-
closeTerminalWidgetWhenClientGetsClosed(shellTerminalWidget)
176+
closeTerminalWidgetWhenClientGetsClosed(supervisorTerminal, shellTerminalWidget)
166177
exitTaskWhenTerminalWidgetGetsClosed(supervisorTerminal, shellTerminalWidget)
167178
listenForTaskTerminationAndTitleChanges(supervisorTerminal, shellTerminalWidget)
168179
}
169180

170181
private fun listenForTaskTerminationAndTitleChanges(
171-
supervisorTerminal: TerminalOuterClass.Terminal,
172-
shellTerminalWidget: ShellTerminalWidget
182+
supervisorTerminal: TerminalOuterClass.Terminal,
183+
shellTerminalWidget: ShellTerminalWidget
173184
) = runJob(lifetime) {
174185
var hasOpenSessions = true
175186

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

179190
val listenTerminalRequest = TerminalOuterClass.ListenTerminalRequest.newBuilder()
180-
.setAlias(supervisorTerminal.alias)
181-
.build()
191+
.setAlias(supervisorTerminal.alias)
192+
.build()
182193

183194
val listenTerminalResponseObserver =
184-
object : ClientResponseObserver<TerminalOuterClass.ListenTerminalRequest, TerminalOuterClass.ListenTerminalResponse> {
185-
override fun beforeStart(
186-
request: ClientCallStreamObserver<TerminalOuterClass.ListenTerminalRequest>
187-
) {
188-
@Suppress("ObjectLiteralToLambda")
189-
shellTerminalWidget.addListener(object : TerminalWidgetListener {
190-
override fun allSessionsClosed(widget: TerminalWidget) {
191-
hasOpenSessions = false
192-
request.cancel("gitpod: Terminal closed on the client.", null)
193-
}
194-
})
195-
}
195+
object :
196+
ClientResponseObserver<TerminalOuterClass.ListenTerminalRequest, TerminalOuterClass.ListenTerminalResponse> {
197+
override fun beforeStart(request: ClientCallStreamObserver<TerminalOuterClass.ListenTerminalRequest>) {
198+
@Suppress("ObjectLiteralToLambda")
199+
shellTerminalWidget.addListener(object : TerminalWidgetListener {
200+
override fun allSessionsClosed(widget: TerminalWidget) {
201+
hasOpenSessions = false
202+
request.cancel("gitpod: Terminal closed on the client.", null)
203+
}
204+
})
205+
}
196206

197-
override fun onNext(response: TerminalOuterClass.ListenTerminalResponse) {
198-
when {
199-
response.hasTitle() -> application.invokeLater {
200-
shellTerminalWidget.terminalTitle.change {
201-
applicationTitle = response.title
202-
}
207+
override fun onNext(response: TerminalOuterClass.ListenTerminalResponse) {
208+
when {
209+
response.hasTitle() -> application.invokeLater {
210+
shellTerminalWidget.terminalTitle.change {
211+
applicationTitle = response.title
203212
}
213+
}
204214

205-
response.hasExitCode() -> application.invokeLater {
206-
shellTerminalWidget.close()
207-
}
215+
response.hasExitCode() -> application.invokeLater {
216+
shellTerminalWidget.close()
208217
}
209218
}
219+
}
210220

211-
override fun onCompleted() = Unit
221+
override fun onCompleted() = Unit
212222

213-
override fun onError(throwable: Throwable) {
214-
completableFuture.completeExceptionally(throwable)
215-
}
223+
override fun onError(throwable: Throwable) {
224+
completableFuture.completeExceptionally(throwable)
216225
}
226+
}
217227

218228
terminalServiceStub.listen(listenTerminalRequest, listenTerminalResponseObserver)
219229

220230
try {
221231
completableFuture.await()
222232
} catch (throwable: Throwable) {
223233
if (
224-
throwable is StatusRuntimeException ||
225-
throwable is ExecutionException ||
226-
throwable is InterruptedException
234+
throwable is StatusRuntimeException ||
235+
throwable is ExecutionException ||
236+
throwable is InterruptedException
227237
) {
228238
shellTerminalWidget.close()
229-
thisLogger().info("gitpod: Stopped listening to " +
230-
"'${supervisorTerminal.title}' terminal due to an expected exception.")
231239
break
232240
}
233241

234-
thisLogger()
235-
.error("gitpod: Got an error while listening to " +
236-
"'${supervisorTerminal.title}' terminal. Trying again in one second.", throwable)
242+
thisLogger().error(
243+
"gitpod: got an error while listening to '${supervisorTerminal.title}' terminal. Trying again in one second.",
244+
throwable
245+
)
237246
}
238247

239248
delay(1000)
240249
}
241250
}
242251

243252
private fun exitTaskWhenTerminalWidgetGetsClosed(
244-
supervisorTerminal: TerminalOuterClass.Terminal,
245-
shellTerminalWidget: ShellTerminalWidget
253+
supervisorTerminal: TerminalOuterClass.Terminal,
254+
shellTerminalWidget: ShellTerminalWidget
246255
) {
247-
@Suppress("ObjectLiteralToLambda")
248256
shellTerminalWidget.addListener(object : TerminalWidgetListener {
249257
override fun allSessionsClosed(widget: TerminalWidget) {
250258
runJob(lifetime) {
251259
delay(5000)
252260
try {
261+
thisLogger().info("gitpod: shutdown task ${supervisorTerminal.title} (${supervisorTerminal.alias})")
253262
terminalServiceFutureStub.shutdown(
254263
TerminalOuterClass.ShutdownTerminalRequest.newBuilder()
255264
.setAlias(supervisorTerminal.alias)
256265
.build()
257-
)
266+
).await()
258267
} catch (throwable: Throwable) {
259-
thisLogger().error("gitpod: Got an error while shutting down " +
260-
"'${supervisorTerminal.title}' terminal.", throwable)
268+
thisLogger().error(
269+
"gitpod: got an error while shutting down '${supervisorTerminal.title}' terminal.",
270+
throwable
271+
)
261272
}
262273
}
263274
}
264275
})
265276
}
266277

267278
private fun closeTerminalWidgetWhenClientGetsClosed(
268-
shellTerminalWidget: ShellTerminalWidget
279+
supervisorTerminal: TerminalOuterClass.Terminal,
280+
shellTerminalWidget: ShellTerminalWidget
269281
) {
282+
@Suppress("UnstableApiUsage")
270283
lifetime.onTerminationOrNow {
284+
thisLogger().debug("gitpod: closing task terminal service ${supervisorTerminal.title} (${supervisorTerminal.alias})")
271285
shellTerminalWidget.close()
272286
}
273287
}

components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/internal/GitpodTerminalServiceImpl.kt

+13-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the GNU Affero General Public License (AGPL).
33
// See License.AGPL.txt in the project root for license information.
44

5-
package io.gitpod.jetbrains.remote.latest
5+
package io.gitpod.jetbrains.remote.internal
66

77
import com.intellij.openapi.diagnostic.thisLogger
88
import com.intellij.openapi.project.Project
@@ -11,21 +11,28 @@ import com.jetbrains.rd.util.threading.coroutines.launch
1111
import com.jetbrains.rdserver.terminal.BackendTerminalManager
1212
import io.gitpod.jetbrains.remote.AbstractGitpodTerminalService
1313
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.delay
1415
import org.jetbrains.plugins.terminal.ShellTerminalWidget
1516
import org.jetbrains.plugins.terminal.TerminalToolWindowManager
1617
import java.util.*
1718

18-
@Suppress("UnstableApiUsage")
19-
class GitpodTerminalService(val project: Project): AbstractGitpodTerminalService(project) {
19+
class GitpodTerminalServiceImpl(val project: Project) : AbstractGitpodTerminalService(project) {
2020

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

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

26-
override fun createSharedTerminal(title: String): ShellTerminalWidget {
27-
val shellTerminalWidget = ShellTerminalWidget.toShellJediTermWidgetOrThrow(terminalToolWindowManager.createShellWidget(null, title, true, false))
28-
backendTerminalManager.shareTerminal(shellTerminalWidget.asNewWidget(), UUID.randomUUID().toString())
26+
override fun createSharedTerminal(id: String, title: String): ShellTerminalWidget {
27+
val shellTerminalWidget = ShellTerminalWidget.toShellJediTermWidgetOrThrow(
28+
terminalToolWindowManager.createShellWidget(
29+
null,
30+
title,
31+
true,
32+
false
33+
)
34+
)
35+
backendTerminalManager.shareTerminal(shellTerminalWidget.asNewWidget(), id)
2936
return shellTerminalWidget
3037
}
3138
}

0 commit comments

Comments
 (0)