Skip to content

Commit db680a3

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

File tree

5 files changed

+213
-4
lines changed

5 files changed

+213
-4
lines changed

components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ public interface GitpodServer {
3434

3535
@JsonRequest
3636
CompletableFuture<IDEOptions> getIDEOptions();
37+
38+
@JsonRequest
39+
CompletableFuture<WorkspaceInstancePort> openPort(String workspaceId, WorkspaceInstancePort port);
3740
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.gitpodprotocol.api.entities;
6+
7+
public enum PortVisibility {
8+
PUBLIC("public"), PRIVATE("private");
9+
10+
private final String toString;
11+
12+
private PortVisibility(String toString) {
13+
this.toString = toString;
14+
}
15+
16+
public String toString() {
17+
return toString;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.gitpodprotocol.api.entities;
6+
7+
public class WorkspaceInstancePort {
8+
private Number port;
9+
private String visibility;
10+
private String url;
11+
12+
public void setPort(Number port) {
13+
this.port = port;
14+
}
15+
16+
public void setVisibility(String visibility) {
17+
this.visibility = visibility;
18+
}
19+
20+
public void setUrl(String url) {
21+
this.url = url;
22+
}
23+
24+
public Number getPort() { return this.port; }
25+
26+
public String getVisibility() { return this.visibility; }
27+
28+
public String getUrl() { return this.url; }
29+
}

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

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,51 @@
44

55
package io.gitpod.jetbrains.remote
66

7+
import com.intellij.codeWithMe.ClientId
8+
import com.intellij.ide.BrowserUtil
9+
import com.intellij.idea.StartupUtil
10+
import com.intellij.notification.NotificationAction
11+
import com.intellij.notification.NotificationType
12+
import com.intellij.openapi.Disposable
713
import com.intellij.openapi.application.ApplicationInfo
814
import com.intellij.openapi.client.ClientProjectSession
915
import com.intellij.openapi.components.service
1016
import com.intellij.openapi.diagnostic.thisLogger
1117
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
1218
import com.intellij.openapi.fileEditor.FileEditorManagerListener
1319
import com.intellij.openapi.fileTypes.LanguageFileType
20+
import com.intellij.remoteDev.util.onTerminationOrNow
21+
import com.intellij.util.application
22+
import com.jetbrains.rd.util.lifetime.Lifetime
1423
import io.gitpod.gitpodprotocol.api.entities.RemoteTrackMessage
24+
import io.gitpod.gitpodprotocol.api.entities.WorkspaceInstancePort
1525
import io.gitpod.supervisor.api.Info
16-
import kotlinx.coroutines.GlobalScope
26+
import io.gitpod.supervisor.api.Status
27+
import io.gitpod.supervisor.api.Status.PortVisibility
28+
import io.gitpod.supervisor.api.Status.PortsStatus
29+
import io.gitpod.supervisor.api.StatusServiceGrpc
30+
import io.grpc.stub.ClientCallStreamObserver
31+
import io.grpc.stub.ClientResponseObserver
32+
import kotlinx.coroutines.*
1733
import kotlinx.coroutines.future.await
18-
import kotlinx.coroutines.launch
34+
import org.jetbrains.ide.BuiltInServerManager
35+
import java.util.concurrent.CancellationException
36+
import java.util.concurrent.CompletableFuture
1937

2038
class GitpodClientProjectSessionTracker(
2139
private val session: ClientProjectSession
22-
) {
40+
) : Disposable {
2341

2442
private val manager = service<GitpodManager>()
2543

2644
private lateinit var info: Info.WorkspaceInfoResponse
2745
private val versionName = ApplicationInfo.getInstance().versionName
2846
private val fullVersion = ApplicationInfo.getInstance().fullVersion
47+
private val lifetime = Lifetime.Eternal.createNested()
48+
49+
override fun dispose() {
50+
lifetime.terminate()
51+
}
2952

3053
init {
3154
GlobalScope.launch {
@@ -35,6 +58,140 @@ class GitpodClientProjectSessionTracker(
3558
}
3659
}
3760

61+
private fun isExposedServedPort(port: Status.PortsStatus?) : Boolean {
62+
if (port === null) {
63+
return false
64+
}
65+
return port.served && port.hasExposed()
66+
}
67+
68+
private fun showOpenServiceNotification(port: PortsStatus, offerMakePublic: Boolean = false) {
69+
val message = "A service is available on port ${port.localPort}"
70+
val notification = manager.notificationGroup.createNotification(message, NotificationType.INFORMATION)
71+
72+
val openBrowserAction = NotificationAction.createSimple("Open Browser") {
73+
openBrowser(port.exposed.url)
74+
}
75+
notification.addAction(openBrowserAction)
76+
77+
if (offerMakePublic) {
78+
val makePublicLambda = {
79+
runBlocking {
80+
makePortPublic(info.workspaceId, port)
81+
}
82+
}
83+
val makePublicAction = NotificationAction.createSimple("Make Public", makePublicLambda)
84+
notification.addAction(makePublicAction)
85+
}
86+
87+
ClientId.withClientId(session.clientId) {
88+
notification.notify(session.project)
89+
}
90+
}
91+
92+
private suspend fun makePortPublic(workspaceId: String, port: PortsStatus) {
93+
val p = WorkspaceInstancePort()
94+
p.port = port.localPort
95+
p.visibility = io.gitpod.gitpodprotocol.api.entities.PortVisibility.PUBLIC.toString()
96+
p.url = port.exposed.url
97+
98+
try {
99+
manager.client.server.openPort(workspaceId, p).await()
100+
} catch (e: Exception) {
101+
thisLogger().error("gitpod: failed to open port ${port.localPort}: ", e)
102+
}
103+
}
104+
105+
private fun openBrowser(url: String) {
106+
ClientId.withClientId(session.clientId) {
107+
BrowserUtil.browse(url)
108+
}
109+
}
110+
111+
private val portsObserveJob = GlobalScope.launch {
112+
if (application.isHeadlessEnvironment) {
113+
return@launch
114+
}
115+
116+
// Ignore ports that aren't actually used by the user (e.g. ports used internally by JetBrains IDEs)
117+
val backendPort = BuiltInServerManager.getInstance().waitForStart().port
118+
val serverPort = StartupUtil.getServerFuture().await().port
119+
val ignorePorts = listOf(backendPort, serverPort, 5990)
120+
val portsStatus = hashMapOf<Int, Status.PortsStatus>()
121+
122+
val status = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel)
123+
while (isActive) {
124+
try {
125+
val f = CompletableFuture<Void>()
126+
status.portsStatus(
127+
Status.PortsStatusRequest.newBuilder().setObserve(true).build(),
128+
object : ClientResponseObserver<Status.PortsStatusRequest, Status.PortsStatusResponse> {
129+
130+
override fun beforeStart(requestStream: ClientCallStreamObserver<Status.PortsStatusRequest>) {
131+
lifetime.onTerminationOrNow {
132+
requestStream.cancel(null, null)
133+
}
134+
}
135+
136+
override fun onNext(ps: Status.PortsStatusResponse) {
137+
for (port in ps.portsList) {
138+
// Avoiding undesired notifications
139+
if (ignorePorts.contains(port.localPort)) {
140+
continue
141+
}
142+
143+
val previous = portsStatus[port.localPort]
144+
portsStatus[port.localPort] = port
145+
146+
val shouldSendNotification = !isExposedServedPort(previous) && isExposedServedPort(port)
147+
148+
if (shouldSendNotification) {
149+
if (port.exposed.onExposed.number == Status.OnPortExposedAction.ignore_VALUE) {
150+
continue
151+
}
152+
153+
if (port.exposed.onExposed.number == Status.OnPortExposedAction.open_browser_VALUE || port.exposed.onExposed.number == Status.OnPortExposedAction.open_preview_VALUE) {
154+
openBrowser(port.exposed.url)
155+
continue
156+
}
157+
158+
if (port.exposed.onExposed.number == Status.OnPortExposedAction.notify_VALUE) {
159+
showOpenServiceNotification(port)
160+
continue
161+
}
162+
163+
if (port.exposed.onExposed.number == Status.OnPortExposedAction.notify_private_VALUE) {
164+
showOpenServiceNotification(port, port.exposed.visibilityValue !== PortVisibility.public_visibility_VALUE)
165+
continue
166+
}
167+
}
168+
}
169+
}
170+
171+
override fun onError(t: Throwable) {
172+
f.completeExceptionally(t)
173+
}
174+
175+
override fun onCompleted() {
176+
f.complete(null)
177+
}
178+
})
179+
f.await()
180+
} catch (t: Throwable) {
181+
if (t is CancellationException) {
182+
throw t
183+
}
184+
thisLogger().error("gitpod: failed to stream ports status: ", t)
185+
}
186+
delay(1000L)
187+
}
188+
}
189+
init {
190+
lifetime.onTerminationOrNow {
191+
portsObserveJob.cancel()
192+
}
193+
}
194+
38195
private fun registerActiveLanguageAnalytics() {
39196
val activeLanguages = mutableSetOf<String>()
40197
session.project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class GitpodManager : Disposable {
133133
GitVcsApplicationSettings.getInstance().isUseCredentialHelper = true
134134
}
135135

136-
private val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications")
136+
val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications")
137137
private val notificationsJob = GlobalScope.launch {
138138
if (application.isHeadlessEnvironment) {
139139
return@launch
@@ -234,6 +234,7 @@ class GitpodManager : Disposable {
234234
.setHost(info.gitpodApi.host)
235235
.addScope("function:sendHeartBeat")
236236
.addScope("function:trackEvent")
237+
.addScope("function:openPort")
237238
.setKind("gitpod")
238239
.build()
239240

0 commit comments

Comments
 (0)