Skip to content

Commit 66bafa0

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

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

.gitpod.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,21 @@ ports:
3030
onOpen: ignore
3131
# Dev Theia
3232
- port: 13444
33+
- port: 8080
34+
onOpen: open-browser
35+
visibility: public
36+
- port: 8081
37+
onOpen: notify
38+
visibility: public
39+
- port: 8082
40+
onOpen: ignore
41+
- port: 8083
42+
onOpen: notify
3343
tasks:
44+
- command: npx --yes live-server .
45+
- command: npx --yes live-server --port 8081 .
46+
- command: npx --yes live-server --port 8082 .
47+
- command: npx --yes live-server --port 8083 .
3448
- name: Add Harvester kubeconfig
3549
command: |
3650
./dev/preview/util/download-and-merge-harvester-kubeconfig.sh

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

Lines changed: 117 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,117 @@ 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+
// TODO: remove following IF, used for debugging/dev
234+
if (port.localPort < 8080 || port.localPort > 8085) {
235+
continue
236+
}
237+
238+
// Avoiding undesired notifications
239+
if (ignorePorts.contains(port.localPort)) {
240+
continue
241+
}
242+
243+
val hasPreviousStatus = portsStatus.containsKey(port.localPort)
244+
245+
if (!hasPreviousStatus) {
246+
portsStatus[port.localPort] = port
247+
}
248+
249+
val wasServed = portsStatus[port.localPort]?.served!!
250+
val wasExposed = portsStatus[port.localPort]?.hasExposed()!!
251+
val wasServedExposed = wasServed && wasExposed
252+
val isServedExposed = port.served && port.hasExposed()
253+
254+
// If the initial update received shows that the port is served and exposed, then notify
255+
val isFirstUpdate = !hasPreviousStatus && wasServedExposed && isServedExposed
256+
257+
// If the port changes its status to served and exposed, notify the user
258+
val shouldSendNotification = isFirstUpdate || !wasServedExposed && isServedExposed
259+
260+
println("______")
261+
println("DEBUG[${port.localPort}] -> wasServed(${wasServed.toString()}); wasExposed(${wasExposed.toString()}); wasServedExposed(${wasServedExposed.toString()}); isServedExposed(${isServedExposed.toString()}); shouldSendNotification(${shouldSendNotification.toString()})")
262+
println(" META: ${port.exposed.onExposed.number} (IGNORE=${OnPortExposedAction.ignore_VALUE})")
263+
println("______")
264+
265+
// TODO: add check for !port.exposed.url.isNullOrEmpty()
266+
267+
portsStatus[port.localPort] = port
268+
269+
if (shouldSendNotification) {
270+
if (port.exposed.onExposed.number == OnPortExposedAction.ignore_VALUE) {
271+
continue
272+
}
273+
274+
if (port.exposed.onExposed.number == OnPortExposedAction.open_browser_VALUE) {
275+
BrowserUtil.browse(port.exposed.url)
276+
continue
277+
}
278+
279+
if (port.exposed.onExposed.number == OnPortExposedAction.open_preview_VALUE) {
280+
BrowserUtil.browse(port.exposed.url)
281+
continue
282+
}
283+
284+
val message = "A service is available on port ${port.localPort}"
285+
val notification = notificationGroup.createNotification(message, NotificationType.INFORMATION)
286+
// TODO(andreafalzetti): add analytics event similar to https://github.com/gitpod-io/openvscode-server/blob/7c912399c70799e4e4452a63cf3c0f7cc8f5832c/extensions/gitpod-web/src/extension.ts#L582
287+
val lambda = { BrowserUtil.browse(port.exposed.url) }
288+
val action = NotificationAction.createSimpleExpiring("Open in browser", lambda)
289+
notification.addAction(action)
290+
notification.notify(null)
291+
}
292+
}
293+
}
294+
295+
override fun onError(t: Throwable) {
296+
f.completeExceptionally(t)
297+
}
298+
299+
override fun onCompleted() {
300+
f.complete(null)
301+
}
302+
})
303+
f.await()
304+
} catch (t: Throwable) {
305+
if (t is CancellationException) {
306+
throw t
307+
}
308+
thisLogger().error("gitpod: failed to stream ports status: ", t)
309+
}
310+
delay(1000L)
311+
}
312+
}
313+
init {
314+
lifetime.onTerminationOrNow {
315+
portsObserveJob.cancel()
316+
}
317+
}
318+
202319
val pendingInfo = CompletableFuture<WorkspaceInfoResponse>()
203320
private val infoJob = GlobalScope.launch {
204321
if (application.isHeadlessEnvironment) {

0 commit comments

Comments
 (0)