@@ -23,6 +23,8 @@ import kotlinx.coroutines.future.asDeferred
2323import org.apache.http.client.utils.URIBuilder
2424import java.util.*
2525import java.util.concurrent.CompletableFuture
26+ import java.util.concurrent.ConcurrentHashMap
27+ import java.util.concurrent.atomic.AtomicReference
2628
2729@Suppress(" UnstableApiUsage" )
2830abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService {
@@ -35,6 +37,15 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
3537 private val ignoredPortsForNotificationService = service<GitpodIgnoredPortsForNotificationService >()
3638 private val lifetime = Lifetime .Eternal .createNested()
3739
40+ // Store current observed ports and their lifetime references
41+ private val portLifetimes = ConcurrentHashMap <Int , Lifetime >()
42+
43+ // Prevent multiple sync operations from executing simultaneously
44+ private val syncInProgress = AtomicReference (false )
45+
46+ // Last observed ports list, used to prevent duplicate processing
47+ private var lastPortsList = listOf<PortsStatus >()
48+
3849 init { start() }
3950
4051 private fun start () {
@@ -86,7 +97,17 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
8697 }
8798
8899 override fun onNext (response : Status .PortsStatusResponse ) {
89- application.invokeLater { syncPortsListWithClient(response) }
100+ application.invokeLater {
101+ if (syncInProgress.compareAndSet(false , true )) {
102+ try {
103+ syncPortsListWithClient(response)
104+ } finally {
105+ syncInProgress.set(false )
106+ }
107+ } else {
108+ thisLogger().debug(" gitpod: Sync already in progress, skipping update" )
109+ }
110+ }
90111 }
91112
92113 override fun onCompleted () {
@@ -110,44 +131,97 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
110131 private fun syncPortsListWithClient (response : Status .PortsStatusResponse ) {
111132 val ignoredPorts = ignoredPortsForNotificationService.getIgnoredPorts()
112133 val portsList = response.portsList.filter { ! ignoredPorts.contains(it.localPort) }
134+
135+ if (portsList == lastPortsList) {
136+ thisLogger().debug(" gitpod: Port list unchanged, skipping update" )
137+ return
138+ }
139+ lastPortsList = portsList
140+
113141 val portsNumbersFromPortsList = portsList.map { it.localPort }
114142 val servedPorts = portsList.filter { it.served }
115143 val exposedPorts = servedPorts.filter { it.exposed?.url?.isNotBlank() ? : false }
116144 val portsNumbersFromNonServedPorts = portsList.filter { ! it.served }.map { it.localPort }
145+
146+ // Clean up unused port lifetimes
147+ val allCurrentPorts = perClientPortForwardingManager.getPorts()
148+ val allPortsToKeep = mutableSetOf<Int >()
149+
150+ // Find ports that need to start forwarding
117151 val servedPortsToStartForwarding = servedPorts.filter {
118152 perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(FORWARDED_PORT_LABEL ) }
119153 }
154+
155+ // Find ports that need to start exposing on client
120156 val exposedPortsToStartExposingOnClient = exposedPorts.filter {
121157 perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(EXPOSED_PORT_LABEL ) }
122158 }
159+
160+ // Find ports that need to stop forwarding
123161 val forwardedPortsToStopForwarding = perClientPortForwardingManager.getPorts(FORWARDED_PORT_LABEL )
124162 .map { it.hostPortNumber }
125163 .filter { portsNumbersFromNonServedPorts.contains(it) || ! portsNumbersFromPortsList.contains(it) }
164+
165+ // Find ports that need to stop exposing on client
126166 val exposedPortsToStopExposingOnClient = perClientPortForwardingManager.getPorts(EXPOSED_PORT_LABEL )
127167 .map { it.hostPortNumber }
128168 .filter { portsNumbersFromNonServedPorts.contains(it) || ! portsNumbersFromPortsList.contains(it) }
129169
130- servedPortsToStartForwarding.forEach { startForwarding(it) }
170+ forwardedPortsToStopForwarding.forEach { stopForwarding(it) }
171+ exposedPortsToStopExposingOnClient.forEach { stopExposingOnClient(it) }
131172
132- exposedPortsToStartExposingOnClient.forEach { startExposingOnClient(it) }
173+ servedPortsToStartForwarding.forEach {
174+ startForwarding(it)
175+ allPortsToKeep.add(it.localPort)
176+ }
133177
134- forwardedPortsToStopForwarding.forEach { stopForwarding(it) }
178+ exposedPortsToStartExposingOnClient.forEach {
179+ startExposingOnClient(it)
180+ allPortsToKeep.add(it.localPort)
181+ }
135182
136- exposedPortsToStopExposingOnClient.forEach { stopExposingOnClient(it) }
183+ portsList.forEach {
184+ updatePortsPresentation(it)
185+ allPortsToKeep.add(it.localPort)
186+ }
187+
188+ cleanupUnusedLifetimes(allPortsToKeep)
189+ }
137190
138- portsList.forEach { updatePortsPresentation(it) }
191+ private fun cleanupUnusedLifetimes (portsToKeep : Set <Int >) {
192+ val portsToRemove = portLifetimes.keys.filter { ! portsToKeep.contains(it) }
193+ portsToRemove.forEach { port ->
194+ portLifetimes[port]?.let { lifetime ->
195+ thisLogger().debug(" gitpod: Terminating lifetime for port $port " )
196+ lifetime.terminate()
197+ portLifetimes.remove(port)
198+ }
199+ }
139200 }
140201
141202 private fun startForwarding (portStatus : PortsStatus ) {
142203 if (isLocalPortForwardingDisabled()) {
143204 return
144205 }
206+
207+ val portLifetime = getOrCreatePortLifetime(portStatus.localPort)
208+
145209 try {
146- perClientPortForwardingManager.forwardPort(
210+ thisLogger().debug(" gitpod: Starting forwarding for port ${portStatus.localPort} " )
211+ val port = perClientPortForwardingManager.forwardPort(
147212 portStatus.localPort,
148213 PortType .TCP ,
149214 setOf (FORWARDED_PORT_LABEL ),
150215 )
216+
217+ portLifetime.onTerminationOrNow {
218+ thisLogger().debug(" gitpod: Cleaning up port ${portStatus.localPort} due to lifetime termination" )
219+ try {
220+ perClientPortForwardingManager.removePort(port)
221+ } catch (e: Exception ) {
222+ thisLogger().warn(" gitpod: Failed to remove port on lifetime termination" , e)
223+ }
224+ }
151225 } catch (throwable: Throwable ) {
152226 if (throwable !is PortAlreadyForwardedException ) {
153227 thisLogger().warn(" gitpod: Caught an exception while forwarding port: ${throwable.message} " )
@@ -156,23 +230,70 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
156230 }
157231
158232 private fun stopForwarding (hostPort : Int ) {
159- perClientPortForwardingManager.getPorts(hostPort)
233+ thisLogger().debug(" gitpod: Stopping forwarding for port $hostPort " )
234+ val portsToRemove = perClientPortForwardingManager.getPorts(hostPort)
160235 .filter { it.labels.contains(FORWARDED_PORT_LABEL ) }
161- .forEach { perClientPortForwardingManager.removePort(it) }
236+
237+ terminatePortLifetime(hostPort)
238+
239+ portsToRemove.forEach {
240+ try {
241+ perClientPortForwardingManager.removePort(it)
242+ } catch (e: Exception ) {
243+ thisLogger().warn(" gitpod: Failed to remove forwarded port $hostPort " , e)
244+ }
245+ }
162246 }
163247
164248 private fun startExposingOnClient (portStatus : PortsStatus ) {
165- perClientPortForwardingManager.exposePort(
249+ val portLifetime = getOrCreatePortLifetime(portStatus.localPort)
250+
251+ thisLogger().debug(" gitpod: Starting exposing for port ${portStatus.localPort} " )
252+ val port = perClientPortForwardingManager.exposePort(
166253 portStatus.localPort,
167254 portStatus.exposed.url,
168255 setOf (EXPOSED_PORT_LABEL ),
169256 )
257+
258+ portLifetime.onTerminationOrNow {
259+ thisLogger().debug(" gitpod: Cleaning up exposed port ${portStatus.localPort} due to lifetime termination" )
260+ try {
261+ perClientPortForwardingManager.removePort(port)
262+ } catch (e: Exception ) {
263+ thisLogger().warn(" gitpod: Failed to remove exposed port on lifetime termination" , e)
264+ }
265+ }
170266 }
171267
172268 private fun stopExposingOnClient (hostPort : Int ) {
173- perClientPortForwardingManager.getPorts(hostPort)
269+ thisLogger().debug(" gitpod: Stopping exposing for port $hostPort " )
270+ val portsToRemove = perClientPortForwardingManager.getPorts(hostPort)
174271 .filter { it.labels.contains(EXPOSED_PORT_LABEL ) }
175- .forEach { perClientPortForwardingManager.removePort(it) }
272+
273+ terminatePortLifetime(hostPort)
274+
275+ portsToRemove.forEach {
276+ try {
277+ perClientPortForwardingManager.removePort(it)
278+ } catch (e: Exception ) {
279+ thisLogger().warn(" gitpod: Failed to remove exposed port $hostPort " , e)
280+ }
281+ }
282+ }
283+
284+ private fun getOrCreatePortLifetime (port : Int ): Lifetime {
285+ return portLifetimes.computeIfAbsent(port) {
286+ thisLogger().debug(" gitpod: Creating new lifetime for port $port " )
287+ lifetime.createNested()
288+ }
289+ }
290+
291+ private fun terminatePortLifetime (port : Int ) {
292+ portLifetimes[port]?.let { portLifetime ->
293+ thisLogger().debug(" gitpod: Terminating lifetime for port $port " )
294+ portLifetime.terminate()
295+ portLifetimes.remove(port)
296+ }
176297 }
177298
178299 private fun updatePortsPresentation (portStatus : PortsStatus ) {
@@ -212,6 +333,8 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
212333 }
213334
214335 override fun dispose () {
336+ portLifetimes.values.forEach { it.terminate() }
337+ portLifetimes.clear()
215338 lifetime.terminate()
216339 }
217340}
0 commit comments