4
4
5
5
package io.gitpod.jetbrains.remote
6
6
7
+ import com.intellij.openapi.client.ClientSessionsManager
8
+ import org.jetbrains.ide.RestService
9
+ import com.intellij.codeWithMe.ClientId
10
+ import com.intellij.ide.BrowserUtil
7
11
import com.intellij.ide.plugins.PluginManagerCore
8
12
import com.intellij.notification.NotificationAction
9
13
import com.intellij.notification.NotificationGroupManager
10
14
import com.intellij.notification.NotificationType
11
15
import com.intellij.openapi.Disposable
16
+ import com.intellij.openapi.client.ClientProjectSession
17
+ import com.intellij.openapi.client.ClientSession
12
18
import com.intellij.openapi.components.Service
13
19
import com.intellij.openapi.diagnostic.thisLogger
14
20
import com.intellij.openapi.extensions.PluginId
@@ -23,6 +29,10 @@ import io.gitpod.jetbrains.remote.utils.Retrier.retry
23
29
import io.gitpod.supervisor.api.*
24
30
import io.gitpod.supervisor.api.Info.WorkspaceInfoResponse
25
31
import io.gitpod.supervisor.api.Notification.*
32
+ import io.gitpod.supervisor.api.Status.OnPortExposedAction
33
+ import io.gitpod.supervisor.api.Status.PortsStatus
34
+ import io.gitpod.supervisor.api.Status.PortsStatusRequest
35
+ import io.gitpod.supervisor.api.Status.PortsStatusResponse
26
36
import io.grpc.ManagedChannel
27
37
import io.grpc.ManagedChannelBuilder
28
38
import io.grpc.stub.ClientCallStreamObserver
@@ -46,6 +56,7 @@ import java.util.concurrent.CancellationException
46
56
import java.util.concurrent.CompletableFuture
47
57
import javax.websocket.DeploymentException
48
58
59
+
49
60
@Service
50
61
class GitpodManager : Disposable {
51
62
@@ -199,6 +210,128 @@ class GitpodManager : Disposable {
199
210
}
200
211
}
201
212
213
+ private val portsObserveJob = GlobalScope .launch {
214
+ if (application.isHeadlessEnvironment) {
215
+ return @launch
216
+ }
217
+
218
+ // Ignore ports that aren't actually used by the user (e.g. ports used internally by JetBrains IDEs)
219
+ val backendPort = BuiltInServerManager .getInstance().waitForStart().port
220
+ val ignorePorts = listOf (backendPort)
221
+ val portsStatus = hashMapOf<Int , PortsStatus >()
222
+
223
+ val status = StatusServiceGrpc .newStub(supervisorChannel)
224
+ while (isActive) {
225
+ try {
226
+ val project = RestService .getLastFocusedOrOpenedProject()
227
+
228
+ var session: ClientSession ? = null
229
+ if (project != null ) {
230
+ session = ClientSessionsManager .getProjectSessions(project, false ).first()
231
+ }
232
+ if (session == null ) {
233
+ session = ClientSessionsManager .getAppSessions(false ).first()
234
+ }
235
+
236
+ val gitpodClientProjectSessionTracker = GitpodClientProjectSessionTracker (session as ClientProjectSession )
237
+
238
+ val f = CompletableFuture <Void >()
239
+ status.portsStatus(
240
+ PortsStatusRequest .newBuilder().setObserve(true ).build(),
241
+ object : ClientResponseObserver <PortsStatusRequest , PortsStatusResponse > {
242
+
243
+ override fun beforeStart (requestStream : ClientCallStreamObserver <PortsStatusRequest >) {
244
+ lifetime.onTerminationOrNow {
245
+ requestStream.cancel(null , null )
246
+ }
247
+ }
248
+
249
+ override fun onNext (ps : PortsStatusResponse ) {
250
+ for (port in ps.portsList) {
251
+ // Avoiding undesired notifications
252
+ if (ignorePorts.contains(port.localPort)) {
253
+ continue
254
+ }
255
+
256
+ val hasPreviousStatus = portsStatus.containsKey(port.localPort)
257
+
258
+ if (! hasPreviousStatus) {
259
+ portsStatus[port.localPort] = port
260
+ }
261
+
262
+ val wasServed = portsStatus[port.localPort]?.served!!
263
+ val wasExposed = portsStatus[port.localPort]?.hasExposed()!!
264
+ val wasServedExposed = wasServed && wasExposed
265
+ val isServedExposed = port.served && port.hasExposed()
266
+
267
+ // If the initial update received shows that the port is served and exposed, then notify
268
+ val isFirstUpdate = ! hasPreviousStatus && wasServedExposed && isServedExposed
269
+
270
+ // If the port changes its status to served and exposed, notify the user
271
+ val shouldSendNotification = isFirstUpdate || ! wasServedExposed && isServedExposed
272
+
273
+ portsStatus[port.localPort] = port
274
+
275
+ if (shouldSendNotification) {
276
+ if (port.exposed.onExposed.number == OnPortExposedAction .ignore_VALUE) {
277
+ continue
278
+ }
279
+
280
+ if (port.exposed.onExposed.number == OnPortExposedAction .open_browser_VALUE) {
281
+ ClientId .withClientId(session.clientId) {
282
+ BrowserUtil .browse(port.exposed.url)
283
+ gitpodClientProjectSessionTracker.trackEvent(" jb_execute_command_gitpod_ports" , mapOf (" action" to " openBrowser" ))
284
+ }
285
+ continue
286
+ }
287
+
288
+ if (port.exposed.onExposed.number == OnPortExposedAction .open_preview_VALUE) {
289
+ ClientId .withClientId(session.clientId) {
290
+ BrowserUtil .browse(port.exposed.url)
291
+ gitpodClientProjectSessionTracker.trackEvent(" jb_execute_command_gitpod_ports" , mapOf (" action" to " openBrowser" ))
292
+ }
293
+ continue
294
+ }
295
+
296
+ val message = " A service is available on port ${port.localPort} "
297
+ val notification = notificationGroup.createNotification(message, NotificationType .INFORMATION )
298
+
299
+ val lambda = {
300
+ BrowserUtil .browse(port.exposed.url)
301
+ gitpodClientProjectSessionTracker.trackEvent(" jb_execute_command_gitpod_ports" , mapOf (" action" to " openBrowser" ))
302
+ }
303
+
304
+ val action = NotificationAction .createSimpleExpiring(" Open Browser" , lambda)
305
+ notification.addAction(action)
306
+ notification.notify(null )
307
+ }
308
+ }
309
+ }
310
+
311
+ override fun onError (t : Throwable ) {
312
+ f.completeExceptionally(t)
313
+ }
314
+
315
+ override fun onCompleted () {
316
+ f.complete(null )
317
+ }
318
+ })
319
+ f.await()
320
+ } catch (t: Throwable ) {
321
+ if (t is CancellationException ) {
322
+ throw t
323
+ }
324
+ thisLogger().error(" gitpod: failed to stream ports status: " , t)
325
+ }
326
+ delay(1000L )
327
+ }
328
+ }
329
+ init {
330
+ lifetime.onTerminationOrNow {
331
+ portsObserveJob.cancel()
332
+ }
333
+ }
334
+
202
335
val pendingInfo = CompletableFuture <WorkspaceInfoResponse >()
203
336
private val infoJob = GlobalScope .launch {
204
337
if (application.isHeadlessEnvironment) {
0 commit comments