@@ -20,14 +20,13 @@ import io.gitpod.supervisor.api.TerminalServiceGrpc
20
20
import io.grpc.StatusRuntimeException
21
21
import io.grpc.stub.ClientCallStreamObserver
22
22
import io.grpc.stub.ClientResponseObserver
23
- import kotlinx.coroutines.CoroutineScope
24
- import kotlinx.coroutines.Job
25
- import kotlinx.coroutines.delay
23
+ import kotlinx.coroutines.*
26
24
import kotlinx.coroutines.future.await
27
25
import kotlinx.coroutines.guava.await
28
26
import org.jetbrains.plugins.terminal.ShellTerminalWidget
29
27
import java.util.concurrent.CompletableFuture
30
28
import java.util.concurrent.ExecutionException
29
+ import kotlin.coroutines.coroutineContext
31
30
32
31
abstract class AbstractGitpodTerminalService (project : Project ) : Disposable {
33
32
private val lifetime = defineNestedLifetime()
@@ -39,27 +38,38 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
39
38
start()
40
39
}
41
40
42
- protected abstract fun runJob (lifetime : Lifetime , block : suspend CoroutineScope .() -> Unit ): Job ;
41
+ protected abstract fun runJob (lifetime : Lifetime , block : suspend CoroutineScope .() -> Unit ): Job
43
42
44
43
override fun dispose () = Unit
45
44
protected fun start () {
46
45
if (application.isHeadlessEnvironment) return
47
46
48
47
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)
54
64
}
55
65
}
56
66
}
57
67
58
- protected abstract fun createSharedTerminal (title : String ): ShellTerminalWidget
68
+ protected abstract fun createSharedTerminal (id : String , title : String ): ShellTerminalWidget
59
69
60
70
private fun createTerminalsAttachedToTasks (
61
- terminals : List <TerminalOuterClass .Terminal >,
62
- tasks : List <Status .TaskStatus >
71
+ terminals : List <TerminalOuterClass .Terminal >,
72
+ tasks : List <Status .TaskStatus >
63
73
) {
64
74
if (tasks.isEmpty()) return
65
75
@@ -70,24 +80,31 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
70
80
aliasToTerminalMap[terminalAlias] = terminal
71
81
}
72
82
73
- for (task in tasks) {
83
+ tasks.forEachIndexed { index, task ->
74
84
val terminalAlias = task.terminal
75
- val terminal = aliasToTerminalMap[terminalAlias] ? : continue
85
+ val terminal = aliasToTerminalMap[terminalAlias]
76
86
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} )" )
78
95
}
79
96
}
80
97
81
98
private tailrec suspend fun getSupervisorTasksList (): List <Status .TaskStatus > {
82
99
var tasksList: List <Status .TaskStatus >? = null
83
-
100
+ coroutineContext.ensureActive()
84
101
try {
85
102
val completableFuture = CompletableFuture <List <Status .TaskStatus >>()
86
103
87
104
val taskStatusRequest = Status .TasksStatusRequest .newBuilder().setObserve(true ).build()
88
105
89
106
val taskStatusResponseObserver = object :
90
- ClientResponseObserver <Status .TasksStatusRequest , Status .TasksStatusResponse > {
107
+ ClientResponseObserver <Status .TasksStatusRequest , Status .TasksStatusResponse > {
91
108
override fun beforeStart (request : ClientCallStreamObserver <Status .TasksStatusRequest >) = Unit
92
109
93
110
override fun onNext (response : Status .TasksStatusResponse ) {
@@ -114,23 +131,20 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
114
131
}
115
132
116
133
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
120
136
)
121
137
}
122
138
123
- return if (tasksList != null ) {
124
- tasksList
125
- } else {
139
+ return tasksList ? : run {
126
140
delay(1000 )
127
141
getSupervisorTasksList()
128
142
}
129
143
}
130
144
131
145
private tailrec suspend fun getSupervisorTerminalsList (): List <TerminalOuterClass .Terminal > {
132
146
var terminalsList: List <TerminalOuterClass .Terminal >? = null
133
-
147
+ coroutineContext.ensureActive()
134
148
try {
135
149
val listTerminalsRequest = TerminalOuterClass .ListTerminalsRequest .newBuilder().build()
136
150
@@ -145,129 +159,129 @@ abstract class AbstractGitpodTerminalService(project: Project) : Disposable {
145
159
}
146
160
147
161
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
151
164
)
152
165
}
153
166
154
- return if (terminalsList != null ) {
155
- terminalsList
156
- } else {
167
+ return terminalsList ? : run {
157
168
delay(1000 )
158
169
getSupervisorTerminalsList()
159
170
}
160
171
}
161
172
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)
164
175
shellTerminalWidget.executeCommand(" gp tasks attach ${supervisorTerminal.alias} " )
165
- closeTerminalWidgetWhenClientGetsClosed(shellTerminalWidget)
176
+ closeTerminalWidgetWhenClientGetsClosed(supervisorTerminal, shellTerminalWidget)
166
177
exitTaskWhenTerminalWidgetGetsClosed(supervisorTerminal, shellTerminalWidget)
167
178
listenForTaskTerminationAndTitleChanges(supervisorTerminal, shellTerminalWidget)
168
179
}
169
180
170
181
private fun listenForTaskTerminationAndTitleChanges (
171
- supervisorTerminal : TerminalOuterClass .Terminal ,
172
- shellTerminalWidget : ShellTerminalWidget
182
+ supervisorTerminal : TerminalOuterClass .Terminal ,
183
+ shellTerminalWidget : ShellTerminalWidget
173
184
) = runJob(lifetime) {
174
185
var hasOpenSessions = true
175
186
176
187
while (hasOpenSessions) {
177
188
val completableFuture = CompletableFuture <Void >()
178
189
179
190
val listenTerminalRequest = TerminalOuterClass .ListenTerminalRequest .newBuilder()
180
- .setAlias(supervisorTerminal.alias)
181
- .build()
191
+ .setAlias(supervisorTerminal.alias)
192
+ .build()
182
193
183
194
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
+ }
196
206
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
203
212
}
213
+ }
204
214
205
- response.hasExitCode() -> application.invokeLater {
206
- shellTerminalWidget.close()
207
- }
215
+ response.hasExitCode() -> application.invokeLater {
216
+ shellTerminalWidget.close()
208
217
}
209
218
}
219
+ }
210
220
211
- override fun onCompleted () = Unit
221
+ override fun onCompleted () = Unit
212
222
213
- override fun onError (throwable : Throwable ) {
214
- completableFuture.completeExceptionally(throwable)
215
- }
223
+ override fun onError (throwable : Throwable ) {
224
+ completableFuture.completeExceptionally(throwable)
216
225
}
226
+ }
217
227
218
228
terminalServiceStub.listen(listenTerminalRequest, listenTerminalResponseObserver)
219
229
220
230
try {
221
231
completableFuture.await()
222
232
} catch (throwable: Throwable ) {
223
233
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
227
237
) {
228
238
shellTerminalWidget.close()
229
- thisLogger().info(" gitpod: Stopped listening to " +
230
- " '${supervisorTerminal.title} ' terminal due to an expected exception." )
231
239
break
232
240
}
233
241
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
+ )
237
246
}
238
247
239
248
delay(1000 )
240
249
}
241
250
}
242
251
243
252
private fun exitTaskWhenTerminalWidgetGetsClosed (
244
- supervisorTerminal : TerminalOuterClass .Terminal ,
245
- shellTerminalWidget : ShellTerminalWidget
253
+ supervisorTerminal : TerminalOuterClass .Terminal ,
254
+ shellTerminalWidget : ShellTerminalWidget
246
255
) {
247
- @Suppress(" ObjectLiteralToLambda" )
248
256
shellTerminalWidget.addListener(object : TerminalWidgetListener {
249
257
override fun allSessionsClosed (widget : TerminalWidget ) {
250
258
runJob(lifetime) {
251
259
delay(5000 )
252
260
try {
261
+ thisLogger().info(" gitpod: shutdown task ${supervisorTerminal.title} (${supervisorTerminal.alias} )" )
253
262
terminalServiceFutureStub.shutdown(
254
263
TerminalOuterClass .ShutdownTerminalRequest .newBuilder()
255
264
.setAlias(supervisorTerminal.alias)
256
265
.build()
257
- )
266
+ ).await()
258
267
} 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
+ )
261
272
}
262
273
}
263
274
}
264
275
})
265
276
}
266
277
267
278
private fun closeTerminalWidgetWhenClientGetsClosed (
268
- shellTerminalWidget : ShellTerminalWidget
279
+ supervisorTerminal : TerminalOuterClass .Terminal ,
280
+ shellTerminalWidget : ShellTerminalWidget
269
281
) {
282
+ @Suppress(" UnstableApiUsage" )
270
283
lifetime.onTerminationOrNow {
284
+ thisLogger().debug(" gitpod: closing task terminal service ${supervisorTerminal.title} (${supervisorTerminal.alias} )" )
271
285
shellTerminalWidget.close()
272
286
}
273
287
}
0 commit comments