gioser

consola: no mandar resize antes del open WS (compuerta)

El layout del terminal disparaba resize() antes de que onOpen enviara
el open; OkHttp respeta el orden de envío, así que el resize se colaba
como primer frame y el gateway lo tomaba por apertura → "falta
'session' o 'program'". OpenGate difiere los resize hasta que el open
ha salido y suelta el último pendiente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

e8c6f02247fcf9307f773ba8e7965ba7ebdd4cae · por Sergio · (hace 4 meses) · padre d0390c677 · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

1 archivos · +46 −3

diff --git a/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt b/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
index bfbf7f5..7281646 100644
--- a/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
+++ b/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
@@ -112,10 +112,18 @@ class GatewayClient(@Volatile var config: GatewayConfig) {
             open.put("label", label)
         }
 
+        // El open DEBE ser el primer frame de texto del WS: el gateway lee el
+        // primer texto como spec de apertura. Pero el layout del terminal puede
+        // disparar resize() antes de que llegue onOpen, y OkHttp encola los
+        // envíos en orden de llamada → el resize se colaría delante del open y
+        // el gateway lo tomaría como apertura ("falta 'session' o 'program'").
+        // [gate] difiere cualquier resize hasta haber mandado el open.
+        val gate = OpenGate()
         val ws = client.newWebSocket(req, object : WebSocketListener() {
             override fun onOpen(webSocket: WebSocket, response: Response) {
                 DebugLog.log("WS", "handshake OK (HTTP ${response.code}) → enviando open: ${open.toString().take(120)}")
                 webSocket.send(open.toString())
+                gate.markOpened(webSocket)
                 listener.onOpen()
             }
 
@@ -196,7 +204,7 @@ class GatewayClient(@Volatile var config: GatewayConfig) {
                 listener.onClosed(detail)
             }
         })
-        return PtyConnection(ws)
+        return PtyConnection(ws, gate)
     }
 
     private companion object {
@@ -204,6 +212,39 @@ class GatewayClient(@Volatile var config: GatewayConfig) {
     }
 }
 
+/**
+ * Compuerta que garantiza que el `open` salga antes que cualquier `resize`.
+ * Hasta que [markOpened] confirme que se envió el open, los resize se guardan
+ * (solo el último importa) y se sueltan después. Thread-safe: `markOpened`
+ * corre en el hilo de OkHttp y `gateResize` en el de la UI.
+ */
+class OpenGate {
+    @Volatile private var opened = false
+    private var pending: Pair<Int, Int>? = null
+
+    /** Llamado tras enviar el open: marca abierto y suelta el resize pendiente. */
+    @Synchronized fun markOpened(ws: WebSocket) {
+        opened = true
+        pending?.let { (r, c) -> ws.send(resizeFrame(r, c)) }
+        pending = null
+    }
+
+    /**
+     * Devuelve true si el resize quedó diferido (aún no hay open); false si ya
+     * se puede mandar directo.
+     */
+    @Synchronized fun gateResize(rows: Int, cols: Int): Boolean {
+        if (opened) return false
+        pending = rows to cols
+        return true
+    }
+
+    companion object {
+        fun resizeFrame(rows: Int, cols: Int): String =
+            JSONObject().put("t", "resize").put("rows", rows).put("cols", cols).toString()
+    }
+}
+
 /** Eventos de una conexión PTY (callbacks desde el hilo de OkHttp). */
 interface PtyListener {
     fun onOpen() {}
@@ -215,7 +256,7 @@ interface PtyListener {
 }
 
 /** Handle de un PTY adjunto: manda teclas/resize y cierra (DETACH). */
-class PtyConnection(private val ws: WebSocket) {
+class PtyConnection(private val ws: WebSocket, private val gate: OpenGate) {
     fun sendInput(bytes: ByteArray) {
         ws.send(bytes.toByteString())
     }
@@ -225,7 +266,9 @@ class PtyConnection(private val ws: WebSocket) {
     }
 
     fun resize(rows: Int, cols: Int) {
-        ws.send(JSONObject().put("t", "resize").put("rows", rows).put("cols", cols).toString())
+        // Si el open todavía no salió, se difiere (lo manda la compuerta luego).
+        if (gate.gateResize(rows, cols)) return
+        ws.send(OpenGate.resizeFrame(rows, cols))
     }
 
     /** Cierra el WS → DETACH en el daemon (la sesión sigue viva). */