Explorar el Código

[web] ensure that only a single socket.io transport is connected (#24422)

GitOrigin-RevId: 9397b0c85f0a889385d4761945e976ada7aa537b
Jakob Ackermann hace 1 año
padre
commit
dda94cdfbc

+ 3 - 6
services/web/frontend/js/features/ide-react/connection/connection-manager.ts

@@ -25,8 +25,6 @@ const MAX_RECONNECT_GRACEFULLY_INTERVAL_MS = getMeta(
   'ol-maxReconnectGracefullyIntervalMs'
 )
 
-const BEFORE_RECONNECT = 'beforeReconnect'
-
 const MAX_RETRY_CONNECT = 5
 const RETRY_WEBSOCKET = 3
 
@@ -117,7 +115,7 @@ export class ConnectionManager extends EventTarget {
     }
 
     socket.on('connect', () => this.onConnect())
-    socket.on('disconnect', (reason: string) => this.onDisconnect(reason))
+    socket.on('disconnect', () => this.onDisconnect())
     socket.on('error', err => this.onConnectError(err))
     socket.on('connect_failed', err => this.onConnectError(err))
     socket.on('joinProjectResponse', body => this.onJoinProjectResponse(body))
@@ -276,8 +274,7 @@ export class ConnectionManager extends EventTarget {
     this.websocketFailureCount = 0
   }
 
-  private onDisconnect(reason: string) {
-    if (reason === BEFORE_RECONNECT) return // triggered from reconnect, ignore.
+  private onDisconnect() {
     this.connectionAttempt = null
     if (this.externalHeartbeatInterval) {
       window.clearInterval(this.externalHeartbeatInterval)
@@ -446,7 +443,7 @@ export class ConnectionManager extends EventTarget {
     if (this.socket.socket.connecting || this.socket.socket.connected) {
       // Ensure the old transport has been cleaned up.
       // Socket.disconnect() does not accept a parameter. Go one level deeper.
-      this.socket.socket.onDisconnect(BEFORE_RECONNECT)
+      this.socket.forceDisconnectWithoutEvent()
     }
     this.socket.socket.connect()
   }

+ 1 - 1
services/web/frontend/js/features/ide-react/connection/types/socket.ts

@@ -34,7 +34,6 @@ export type Socket = {
     connected: boolean
     connecting: boolean
     connect(): void
-    onDisconnect(reason: string): void
     disconnect(): void
     sessionid: string
     transport?: {
@@ -43,4 +42,5 @@ export type Socket = {
     transports: string[]
   }
   disconnect(): void
+  forceDisconnectWithoutEvent(): void
 }

+ 58 - 1
services/web/frontend/js/ide/connection/SocketIoShim.js

@@ -11,11 +11,12 @@ class SocketShimBase {
   constructor(socket) {
     this._socket = socket
   }
+
+  forceDisconnectWithoutEvent() {}
 }
 const transparentMethods = [
   'connect',
   'disconnect',
-  'onDisconnect',
   'emit',
   'on',
   'removeListener',
@@ -64,6 +65,62 @@ class SocketShimV0 extends SocketShimBase {
   constructor(socket) {
     super(socket)
     this.socket = this._socket.socket
+    const self = this
+    Object.defineProperty(this.socket, 'transport', {
+      get() {
+        return self._transport
+      },
+      set(v) {
+        self.forceDisconnectWithoutEvent()
+        self._transport = v
+      },
+    })
+  }
+
+  forceDisconnectWithoutEvent() {
+    clearTimeout(this.socket.heartbeatTimeoutTimer)
+    if (this._transport) this.forceCloseTransport(this._transport)
+  }
+
+  forceCloseTransport(transport) {
+    transport.clearTimeouts()
+    if (transport instanceof io.Transport.websocket) {
+      // retry closing
+      transport.websocket.onopen = transport.websocket.onmessage = () =>
+        transport.websocket.close()
+      // mute close/error handler
+      transport.websocket.onclose = transport.websocket.onerror = () => {}
+      // disconnect
+      try {
+        transport.websocket.close()
+      } catch {}
+    } else if (transport instanceof io.Transport['xhr-polling']) {
+      // mute data/close handler and block new polling GET requests
+      transport.onData = transport.onClose = transport.get = () => {}
+      // abort pending long-polling/POST request
+      for (const xhr of [transport.xhr, transport.sendXHR]) {
+        if (!xhr) continue // not pending
+        // mute xhr callbacks
+        xhr.onreadystatechange = xhr.onload = xhr.onerror = () => {}
+        try {
+          xhr.abort()
+        } catch {}
+      }
+      transport.xhr = transport.sendXHR = null
+      // Mark long-polling client as disconnected to avoid "ghost" connected client.
+      fetch(transport.prepareUrl() + '/?disconnect=1', {
+        // Let the request continue after navigating away from or reloading the page.
+        keepalive: true,
+      })
+        // Avoid leaving a dangling response on the wire.
+        .then(res => res.text())
+        .catch(() => {})
+    } else {
+      try {
+        transport.close()
+      } catch {}
+      debugConsole.warn('unexpected socket.io transport', transport)
+    }
   }
 }