4
4
5
5
package io.gitpod.jetbrains.remote
6
6
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
7
13
import com.intellij.openapi.application.ApplicationInfo
8
14
import com.intellij.openapi.client.ClientProjectSession
9
15
import com.intellij.openapi.components.service
10
16
import com.intellij.openapi.diagnostic.thisLogger
11
17
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
12
18
import com.intellij.openapi.fileEditor.FileEditorManagerListener
13
19
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
14
23
import io.gitpod.gitpodprotocol.api.entities.RemoteTrackMessage
24
+ import io.gitpod.gitpodprotocol.api.entities.WorkspaceInstancePort
15
25
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.*
17
33
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
19
37
20
38
class GitpodClientProjectSessionTracker (
21
39
private val session : ClientProjectSession
22
- ) {
40
+ ) : Disposable {
23
41
24
42
private val manager = service<GitpodManager >()
25
43
26
44
private lateinit var info: Info .WorkspaceInfoResponse
27
45
private val versionName = ApplicationInfo .getInstance().versionName
28
46
private val fullVersion = ApplicationInfo .getInstance().fullVersion
47
+ private val lifetime = Lifetime .Eternal .createNested()
48
+
49
+ override fun dispose () {
50
+ lifetime.terminate()
51
+ }
29
52
30
53
init {
31
54
GlobalScope .launch {
@@ -35,6 +58,140 @@ class GitpodClientProjectSessionTracker(
35
58
}
36
59
}
37
60
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
+
38
195
private fun registerActiveLanguageAnalytics () {
39
196
val activeLanguages = mutableSetOf<String >()
40
197
session.project.messageBus.connect().subscribe(FileEditorManagerListener .FILE_EDITOR_MANAGER , object : FileEditorManagerListener {
0 commit comments