tawasuyu

Consola: arreglar build roto + token URL-encoded en WS + diagnóstico a Logcat

ConsolaService.consume no compilaba con Kotlin 2.2.0 (when no exhaustivo:
el var ev no recibía smart-cast a no-nulo). Por eso el diagnóstico de WS
nunca llegaba al APK instalado. Se fija con `val e = ev ?: break`.

GatewayClient: el token iba crudo en el query del WS; un '+', '/' o '='
(base64/JWT) rompía el handshake (401/400) aunque funcionara por header
en /rpc. Ahora va con URLEncoder.encode. Además, fallo de WS con stack
completo a Logcat (tag ConsolaWS) + cuerpo de respuesta HTTP, y detalle
en pantalla que nunca queda vacío (incluye la URL si no hay respuesta).

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

dcab575433c88246da30c19988a29265f262f653 · por sergio · (hace 4 meses) · padre 803a42987 · ver el árbol en este commit

Delta semántico

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

2 archivos · +36 −7

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 4eba9da..8d67968 100644
--- a/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
+++ b/consola/src/main/java/com/rizoma/consola/data/GatewayClient.kt
@@ -1,5 +1,6 @@
 package com.rizoma.consola.data
 
+import android.util.Log
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import okhttp3.MediaType.Companion.toMediaType
@@ -12,6 +13,7 @@ import okhttp3.WebSocketListener
 import okio.ByteString
 import okio.ByteString.Companion.toByteString
 import org.json.JSONObject
+import java.net.URLEncoder
 import java.util.concurrent.TimeUnit
 
 /**
@@ -73,9 +75,16 @@ class GatewayClient(@Volatile var config: GatewayConfig) {
             url.startsWith("http://") -> "ws://" + url.removePrefix("http://")
             else -> url
         }
+        // El token también va en el query: muchos gateways no pueden leer la
+        // cabecera Authorization en el handshake WS (los WebSocket de navegador
+        // no la permiten). DEBE ir URL-encoded: un token con '+', '/' o '='
+        // (base64/JWT) crudo rompe el query y el server contesta 401/400 al
+        // upgrade aunque el mismo token funcione por header en /rpc.
         if (config.token.isNotBlank()) {
-            url += "?token=" + config.token
+            url += "?token=" + URLEncoder.encode(config.token, "UTF-8")
         }
+        // URL sin query: para logs y para el mensaje de error sin filtrar el token.
+        val safeUrl = url.substringBefore('?')
         val req = Request.Builder()
             .url(url)
             .apply { if (config.token.isNotBlank()) header("Authorization", "Bearer ${config.token}") }
@@ -120,21 +129,40 @@ class GatewayClient(@Volatile var config: GatewayConfig) {
             }
 
             override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
-                // Detalle diagnóstico: el handshake suele fallar ANTES del
-                // primer mensaje. Sin esto, una caída de conexión llega como
-                // "fallo del WebSocket" a secas (t.message null) y no se sabe
-                // si es 401, conexión rechazada, proxy sin upgrade, o TLS.
+                // El cuerpo de la respuesta de error (si el server lo mandó) suele
+                // explicar el rechazo del upgrade; léelo ANTES de armar el detalle.
+                val httpBody = response?.let { runCatching { it.body?.string() }.getOrNull() }
+
+                // Stack completo a Logcat: la única forma fiable de ver la causa
+                // raíz cuando en pantalla solo cabe una línea. Captúralo con:
+                //   adb logcat -s ConsolaWS
+                Log.w(TAG, "WS handshake/stream falló contra $safeUrl (HTTP ${response?.code ?: "—"})", t)
+                if (!httpBody.isNullOrBlank()) Log.w(TAG, "cuerpo de respuesta: ${httpBody.take(500)}")
+
+                // Detalle diagnóstico para la UI: el handshake suele fallar ANTES
+                // del primer mensaje. Sin esto, una caída llega como "fallo del
+                // WebSocket" a secas (t.message null, response null) y no se sabe
+                // si es 401, conexión rechazada, proxy sin upgrade, o TLS. Por eso
+                // siempre incluimos algo: clase, HTTP, cuerpo, o al menos la URL.
                 val detail = buildString {
                     append(t.javaClass.simpleName)
                     t.message?.let { append(": "); append(it) }
                     response?.let { append(" · HTTP "); append(it.code) }
+                    if (!httpBody.isNullOrBlank()) { append(" · "); append(httpBody.trim().take(120)) }
+                    if (t.message == null && response == null) {
+                        append(" · sin respuesta al abrir "); append(safeUrl)
+                    }
                 }
                 listener.onError(detail)
-                listener.onClosed(t.message)
+                listener.onClosed(detail)
             }
         })
         return PtyConnection(ws)
     }
+
+    private companion object {
+        const val TAG = "ConsolaWS"
+    }
 }
 
 /** Eventos de una conexión PTY (callbacks desde el hilo de OkHttp). */
diff --git a/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt b/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt
index 693901b..6b04955 100644
--- a/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt
+++ b/consola/src/main/java/com/rizoma/consola/service/ConsolaService.kt
@@ -160,7 +160,8 @@ class ConsolaService : Service() {
             var ev: TermEvent? = first
             var changed = false
             while (ev != null) {
-                when (val e = ev) {
+                val e = ev ?: break
+                when (e) {
                     is TermEvent.Bytes -> { emulator.feed(e.data); changed = true }
                     is TermEvent.Resize -> {
                         emulator.resize(e.rows, e.cols)