Skip to content

Commit cf6cf73

Browse files
author
Andrea Falzetti
committed
feat(jb): observe ports status and send notification
1 parent 6972cd5 commit cf6cf73

File tree

1 file changed

+105
-0
lines changed
  • components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote

1 file changed

+105
-0
lines changed

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package io.gitpod.jetbrains.remote
66

7+
import com.intellij.ide.BrowserUtil
78
import com.intellij.ide.plugins.PluginManagerCore
89
import com.intellij.notification.NotificationAction
910
import com.intellij.notification.NotificationGroupManager
@@ -23,6 +24,10 @@ import io.gitpod.jetbrains.remote.utils.Retrier.retry
2324
import io.gitpod.supervisor.api.*
2425
import io.gitpod.supervisor.api.Info.WorkspaceInfoResponse
2526
import io.gitpod.supervisor.api.Notification.*
27+
import io.gitpod.supervisor.api.Status.OnPortExposedAction
28+
import io.gitpod.supervisor.api.Status.PortsStatus
29+
import io.gitpod.supervisor.api.Status.PortsStatusRequest
30+
import io.gitpod.supervisor.api.Status.PortsStatusResponse
2631
import io.grpc.ManagedChannel
2732
import io.grpc.ManagedChannelBuilder
2833
import io.grpc.stub.ClientCallStreamObserver
@@ -46,6 +51,7 @@ import java.util.concurrent.CancellationException
4651
import java.util.concurrent.CompletableFuture
4752
import javax.websocket.DeploymentException
4853

54+
4955
@Service
5056
class GitpodManager : Disposable {
5157

@@ -199,6 +205,105 @@ class GitpodManager : Disposable {
199205
}
200206
}
201207

208+
private val portsObserveJob = GlobalScope.launch {
209+
if (application.isHeadlessEnvironment) {
210+
return@launch
211+
}
212+
213+
// Ignore ports that aren't actually used by the user (e.g. ports used internally by JetBrains IDEs)
214+
val ignorePorts = listOf(5990, 5991, 6679, 6942, 6943)
215+
val portsStatus = hashMapOf<Int, PortsStatus>()
216+
217+
val status = StatusServiceGrpc.newStub(supervisorChannel)
218+
while (isActive) {
219+
try {
220+
val f = CompletableFuture<Void>()
221+
status.portsStatus(
222+
PortsStatusRequest.newBuilder().setObserve(true).build(),
223+
object : ClientResponseObserver<PortsStatusRequest, PortsStatusResponse> {
224+
225+
override fun beforeStart(requestStream: ClientCallStreamObserver<PortsStatusRequest>) {
226+
lifetime.onTerminationOrNow {
227+
requestStream.cancel(null, null)
228+
}
229+
}
230+
231+
override fun onNext(ps: PortsStatusResponse) {
232+
for (port in ps.portsList) {
233+
// Avoiding undesired notifications
234+
if (ignorePorts.contains(port.localPort)) {
235+
continue
236+
}
237+
238+
val hasPreviousStatus = portsStatus.containsKey(port.localPort)
239+
240+
if (!hasPreviousStatus) {
241+
portsStatus[port.localPort] = port
242+
}
243+
244+
val wasServed = portsStatus[port.localPort]?.served!!
245+
val wasExposed = portsStatus[port.localPort]?.hasExposed()!!
246+
val wasServedExposed = wasServed && wasExposed
247+
val isServedExposed = port.served && port.hasExposed()
248+
249+
// If the initial update received shows that the port is served and exposed, then notify
250+
val isFirstUpdate = !hasPreviousStatus && wasServedExposed && isServedExposed
251+
252+
// If the port changes its status to served and exposed, notify the user
253+
val shouldSendNotification = isFirstUpdate || !wasServedExposed && isServedExposed
254+
255+
portsStatus[port.localPort] = port
256+
257+
if (shouldSendNotification) {
258+
if (port.exposed.onExposed.number == OnPortExposedAction.ignore_VALUE) {
259+
continue
260+
}
261+
262+
if (port.exposed.onExposed.number == OnPortExposedAction.open_browser_VALUE) {
263+
BrowserUtil.browse(port.exposed.url)
264+
continue
265+
}
266+
267+
if (port.exposed.onExposed.number == OnPortExposedAction.open_preview_VALUE) {
268+
BrowserUtil.browse(port.exposed.url)
269+
continue
270+
}
271+
272+
val message = "A service is available on port ${port.localPort}"
273+
val notification = notificationGroup.createNotification(message, NotificationType.INFORMATION)
274+
// TODO(andreafalzetti): add analytics event similar to https://github.com/gitpod-io/openvscode-server/blob/7c912399c70799e4e4452a63cf3c0f7cc8f5832c/extensions/gitpod-web/src/extension.ts#L582
275+
val lambda = { BrowserUtil.browse(port.exposed.url) }
276+
val action = NotificationAction.createSimpleExpiring("Open Browser", lambda)
277+
notification.addAction(action)
278+
notification.notify(null)
279+
}
280+
}
281+
}
282+
283+
override fun onError(t: Throwable) {
284+
f.completeExceptionally(t)
285+
}
286+
287+
override fun onCompleted() {
288+
f.complete(null)
289+
}
290+
})
291+
f.await()
292+
} catch (t: Throwable) {
293+
if (t is CancellationException) {
294+
throw t
295+
}
296+
thisLogger().error("gitpod: failed to stream ports status: ", t)
297+
}
298+
delay(1000L)
299+
}
300+
}
301+
init {
302+
lifetime.onTerminationOrNow {
303+
portsObserveJob.cancel()
304+
}
305+
}
306+
202307
val pendingInfo = CompletableFuture<WorkspaceInfoResponse>()
203308
private val infoJob = GlobalScope.launch {
204309
if (application.isHeadlessEnvironment) {

0 commit comments

Comments
 (0)