4
4
5
5
package io.gitpod.jetbrains.remote.latest
6
6
7
+ import com.intellij.icons.AllIcons
7
8
import com.intellij.openapi.Disposable
8
9
import com.intellij.openapi.components.service
9
10
import com.intellij.openapi.diagnostic.thisLogger
10
- import com.intellij.openapi.project.Project
11
11
import com.intellij.openapi.util.Disposer
12
12
import com.intellij.remoteDev.util.onTerminationOrNow
13
+ import com.intellij.ui.RowIcon
13
14
import com.intellij.util.application
14
15
import com.jetbrains.rd.platform.codeWithMe.portForwarding.*
15
- import com.jetbrains.rd.platform.util.lifetime
16
- import com.jetbrains.rd.util.lifetime.LifetimeStatus
16
+ import com.jetbrains.rd.util.lifetime.Lifetime
17
17
import io.gitpod.jetbrains.remote.GitpodIgnoredPortsForNotificationService
18
18
import io.gitpod.jetbrains.remote.GitpodManager
19
19
import io.gitpod.jetbrains.remote.GitpodPortsService
20
20
import io.gitpod.supervisor.api.Status
21
+ import io.gitpod.supervisor.api.Status.PortsStatus
21
22
import io.gitpod.supervisor.api.StatusServiceGrpc
22
23
import io.grpc.stub.ClientCallStreamObserver
23
24
import io.grpc.stub.ClientResponseObserver
24
25
import io.ktor.utils.io.*
25
26
import java.util.concurrent.CompletableFuture
26
27
import java.util.concurrent.TimeUnit
27
- import javax.swing.Icon
28
28
29
29
@Suppress(" UnstableApiUsage" )
30
- class GitpodPortForwardingService ( private val project : Project ) {
30
+ class GitpodPortForwardingService : Disposable {
31
31
companion object {
32
- const val FORWARDED_PORT_LABEL = " gitpod"
32
+ const val FORWARDED_PORT_LABEL = " ForwardedByGitpod"
33
+ const val EXPOSED_PORT_LABEL = " ExposedByGitpod"
33
34
}
34
35
35
36
private val portsService = service<GitpodPortsService >()
36
37
private val perClientPortForwardingManager = service<PerClientPortForwardingManager >()
37
38
private val ignoredPortsForNotificationService = service<GitpodIgnoredPortsForNotificationService >()
38
- private val portToDisposableMap = mutableMapOf< Int , Disposable > ()
39
+ private val lifetime = Lifetime . Eternal .createNested ()
39
40
40
- init { start() }
41
+ init {
42
+ start()
43
+ }
44
+
45
+ override fun dispose () {
46
+ lifetime.terminate()
47
+ }
41
48
42
49
private fun start () {
43
50
if (application.isHeadlessEnvironment) return
@@ -46,12 +53,12 @@ class GitpodPortForwardingService(private val project: Project) {
46
53
}
47
54
48
55
private fun observePortsListWhileProjectIsOpen () = application.executeOnPooledThread {
49
- while (project. lifetime.status == LifetimeStatus . Alive ) {
56
+ lifetime.executeIfAlive {
50
57
try {
51
58
observePortsList().get()
52
59
} catch (throwable: Throwable ) {
53
60
when (throwable) {
54
- is InterruptedException , is CancellationException -> break
61
+ is InterruptedException , is CancellationException -> lifetime.terminate()
55
62
else -> thisLogger().error(
56
63
" gitpod: Got an error while trying to get ports list from Supervisor. " +
57
64
" Going to try again in a second." ,
@@ -74,76 +81,134 @@ class GitpodPortForwardingService(private val project: Project) {
74
81
val portsStatusResponseObserver = object :
75
82
ClientResponseObserver <Status .PortsStatusRequest , Status .PortsStatusResponse > {
76
83
override fun beforeStart (request : ClientCallStreamObserver <Status .PortsStatusRequest >) {
77
- project. lifetime.onTerminationOrNow { request.cancel(" gitpod: Project terminated." , null ) }
84
+ lifetime.onTerminationOrNow { request.cancel(" gitpod: Project terminated." , null ) }
78
85
}
86
+
79
87
override fun onNext (response : Status .PortsStatusResponse ) {
80
- application.invokeLater { updateForwardedPortsList(response) }
88
+ application.invokeLater { syncPortsListWithClient(response) }
89
+ }
90
+
91
+ override fun onCompleted () {
92
+ completableFuture.complete(null )
93
+ }
94
+
95
+ override fun onError (throwable : Throwable ) {
96
+ completableFuture.completeExceptionally(throwable)
81
97
}
82
- override fun onCompleted () { completableFuture.complete(null ) }
83
- override fun onError (throwable : Throwable ) { completableFuture.completeExceptionally(throwable) }
84
98
}
85
99
86
100
statusServiceStub.portsStatus(portsStatusRequest, portsStatusResponseObserver)
87
101
88
102
return completableFuture
89
103
}
90
104
91
- private fun updateForwardedPortsList (response : Status .PortsStatusResponse ) {
105
+ private fun syncPortsListWithClient (response : Status .PortsStatusResponse ) {
92
106
val ignoredPorts = ignoredPortsForNotificationService.getIgnoredPorts()
107
+ val portsList = response.portsList.filter { ! ignoredPorts.contains(it.localPort) }
108
+ val portsNumbersFromPortsList = portsList.map { it.localPort }
109
+ val servedPorts = portsList.filter { it.served }
110
+ val exposedPorts = servedPorts.filter { it.exposed?.url?.isNotBlank() ? : false }
111
+ val portsNumbersFromNonServedPorts = portsList.filter { ! it.served }.map { it.localPort }
112
+ val servedPortsToStartForwarding = servedPorts.filter {
113
+ perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(FORWARDED_PORT_LABEL ) }
114
+ }
115
+ val exposedPortsToStartExposingOnClient = exposedPorts.filter {
116
+ perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(EXPOSED_PORT_LABEL ) }
117
+ }
118
+ val forwardedPortsToStopForwarding = perClientPortForwardingManager.getPorts(FORWARDED_PORT_LABEL )
119
+ .map { it.hostPortNumber }
120
+ .filter { portsNumbersFromNonServedPorts.contains(it) || ! portsNumbersFromPortsList.contains(it) }
121
+ val exposedPortsToStopExposingOnClient = perClientPortForwardingManager.getPorts(EXPOSED_PORT_LABEL )
122
+ .map { it.hostPortNumber }
123
+ .filter { portsNumbersFromNonServedPorts.contains(it) || ! portsNumbersFromPortsList.contains(it) }
93
124
94
- for (port in response.portsList) {
95
- if (ignoredPorts.contains(port.localPort)) continue
125
+ servedPortsToStartForwarding.forEach { startForwarding(it) }
96
126
97
- val hostPort = port.localPort
98
- val isServed = port.served
99
- val isForwarded = perClientPortForwardingManager.getPorts(hostPort).isNotEmpty()
127
+ exposedPortsToStartExposingOnClient.forEach { startExposingOnClient(it) }
100
128
101
- if (isServed && ! isForwarded) {
102
- try {
103
- val forwardedPort = perClientPortForwardingManager.forwardPort(
104
- hostPort,
105
- PortType .TCP ,
106
- setOf (FORWARDED_PORT_LABEL ),
107
- ClientPortAttributes (hostPort, ClientPortPickingStrategy .REASSIGN_WHEN_BUSY ),
108
- )
129
+ forwardedPortsToStopForwarding.forEach { stopForwarding(it) }
130
+
131
+ exposedPortsToStopExposingOnClient.forEach { stopExposingOnClient(it) }
132
+
133
+ portsList.forEach { updatePortsPresentation(it) }
134
+ }
109
135
110
- forwardedPort.presentation.name = port.name
111
- forwardedPort.presentation.description = port.description
112
-
113
- val portListenerDisposable = portToDisposableMap.getOrPut(hostPort, fun () = Disposer .newDisposable())
114
-
115
- forwardedPort.addPortListener(portListenerDisposable, object : ForwardedPortListener {
116
- override fun stateChanged (port : ForwardedPort , newState : ClientPortState ) {
117
- when (newState) {
118
- is ClientPortState .Assigned -> {
119
- thisLogger().warn(" gitpod: Started forwarding host port $hostPort to client port ${newState.clientPort} ." )
120
- portsService.setForwardedPort(hostPort, newState.clientPort)
121
- }
122
- is ClientPortState .FailedToAssign -> {
123
- thisLogger().warn(" gitpod: Detected that host port $hostPort failed to be assigned to a client port." )
124
- }
125
- else -> {
126
- thisLogger().warn(" gitpod: Detected that host port $hostPort is not assigned to any client port." )
127
- }
136
+ private fun startForwarding (portStatus : PortsStatus ) {
137
+ try {
138
+ val forwardedPort = perClientPortForwardingManager.forwardPort(
139
+ portStatus.localPort,
140
+ PortType .TCP ,
141
+ setOf (FORWARDED_PORT_LABEL ),
142
+ ClientPortAttributes (portStatus.localPort, ClientPortPickingStrategy .REASSIGN_WHEN_BUSY ),
143
+ )
144
+
145
+ Disposer .newDisposable().let { listenerDisposable ->
146
+ forwardedPort.addPortListener(listenerDisposable, object : ForwardedPortListener {
147
+ override fun stateChanged (port : ForwardedPort , newState : ClientPortState ) {
148
+ when (newState) {
149
+ is ClientPortState .Assigned -> {
150
+ portsService.setForwardedPort(portStatus.localPort, newState.clientPort)
151
+ }
152
+ is ClientPortState .FailedToAssign -> {
153
+ thisLogger().warn(" gitpod: Detected that host port ${portStatus.localPort} failed " +
154
+ " to be assigned to a client port." )
155
+ }
156
+ else -> {
157
+ thisLogger().warn(" gitpod: Detected that host port ${portStatus.localPort} is not " +
158
+ " assigned to any client port." )
128
159
}
129
160
}
130
- })
131
- } catch (error: Error ) {
132
- thisLogger().warn(" gitpod: ${error.message} " )
133
- }
161
+ listenerDisposable.dispose()
162
+ }
163
+ })
134
164
}
165
+ } catch (exception: Exception ) {
166
+ thisLogger().warn(" gitpod: ${exception.message} " )
167
+ }
168
+ }
135
169
136
- if ( ! isServed && isForwarded ) {
137
- val portListenerDisposable = portToDisposableMap[ hostPort]
138
- if (portListenerDisposable != null ) {
139
- portListenerDisposable.dispose()
140
- portToDisposableMap.remove(hostPort )
170
+ private fun stopForwarding ( hostPort : Int ) {
171
+ perClientPortForwardingManager.getPorts( hostPort)
172
+ .filter { it.labels.contains( FORWARDED_PORT_LABEL ) }
173
+ .forEach { portToRemove ->
174
+ perClientPortForwardingManager.removePort(portToRemove )
141
175
}
142
- perClientPortForwardingManager.getPorts(hostPort).forEach { portToRemove ->
176
+ portsService.removeForwardedPort(hostPort)
177
+ }
178
+
179
+ private fun startExposingOnClient (portStatus : PortsStatus ) {
180
+ perClientPortForwardingManager.exposePort(
181
+ portStatus.localPort,
182
+ portStatus.exposed.url,
183
+ setOf (EXPOSED_PORT_LABEL ),
184
+ )
185
+ }
186
+
187
+ private fun stopExposingOnClient (hostPort : Int ) {
188
+ perClientPortForwardingManager.getPorts(hostPort)
189
+ .filter { it.labels.contains(EXPOSED_PORT_LABEL ) }
190
+ .forEach { portToRemove ->
143
191
perClientPortForwardingManager.removePort(portToRemove)
144
192
}
145
- portsService.removeForwardedPort(hostPort)
146
- thisLogger().info(" gitpod: Stopped forwarding port $hostPort ." )
193
+ }
194
+
195
+ private fun updatePortsPresentation (portStatus : PortsStatus ) {
196
+ perClientPortForwardingManager.getPorts(portStatus.localPort).forEach { portOnClient ->
197
+ if (portOnClient.configuration.isForwardedPort()) {
198
+ portOnClient.presentation.name = portStatus.name
199
+ portOnClient.presentation.description = portStatus.description
200
+ portOnClient.presentation.tooltip = " Forwarded"
201
+ portOnClient.presentation.icon = RowIcon (AllIcons .Nodes .HomeFolder )
202
+ } else if (portOnClient.configuration.isExposedPort()) {
203
+ val isExposedPublicly = (portStatus.exposed.visibility == Status .PortVisibility .public_visibility)
204
+
205
+ portOnClient.presentation.name = portStatus.name
206
+ portOnClient.presentation.description = portStatus.description
207
+ portOnClient.presentation.tooltip = " Exposed (" + (if (isExposedPublicly) " Public" else " Private" ) + " )"
208
+ portOnClient.presentation.icon = RowIcon (
209
+ AllIcons .General .Web ,
210
+ if (isExposedPublicly) AllIcons .Nodes .Public else AllIcons .Nodes .Private
211
+ )
147
212
}
148
213
}
149
214
}
0 commit comments