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>
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). */