feat: Enhance packet handling logging to notify number of waiters and catch errors
This commit is contained in:
@@ -104,6 +104,7 @@ class Protocol(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start heartbeat to keep connection alive
|
* Start heartbeat to keep connection alive
|
||||||
|
* Как в Архиве - отправляем text "heartbeat"
|
||||||
*/
|
*/
|
||||||
private fun startHeartbeat(intervalSeconds: Int) {
|
private fun startHeartbeat(intervalSeconds: Int) {
|
||||||
heartbeatJob?.cancel()
|
heartbeatJob?.cancel()
|
||||||
@@ -114,10 +115,14 @@ class Protocol(
|
|||||||
heartbeatJob = scope.launch {
|
heartbeatJob = scope.launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(intervalMs)
|
delay(intervalMs)
|
||||||
if (webSocket?.send("heartbeat") == true) {
|
try {
|
||||||
log("💓 Heartbeat sent")
|
if (webSocket?.send("heartbeat") == true) {
|
||||||
} else {
|
log("💓 Heartbeat sent")
|
||||||
log("💔 Heartbeat failed to send")
|
} else {
|
||||||
|
log("💔 Heartbeat failed - socket closed or null")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log("💔 Heartbeat error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,14 +137,18 @@ class Protocol(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow reconnection even if connected (for manual reconnect)
|
// Закрываем старый сокет если есть (как в Архиве)
|
||||||
if (_state.value == ProtocolState.CONNECTED || _state.value == ProtocolState.AUTHENTICATED) {
|
webSocket?.let { oldSocket ->
|
||||||
log("Already connected/authenticated, skipping...")
|
try {
|
||||||
return
|
oldSocket.close(1000, "Reconnecting")
|
||||||
|
log("🔄 Closed old WebSocket before reconnect")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log("⚠️ Error closing old socket: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
webSocket = null
|
||||||
|
|
||||||
isManuallyClosed = false
|
isManuallyClosed = false
|
||||||
// Don't reset reconnectAttempts here - it's reset on successful connection in onOpen
|
|
||||||
_state.value = ProtocolState.CONNECTING
|
_state.value = ProtocolState.CONNECTING
|
||||||
_lastError.value = null
|
_lastError.value = null
|
||||||
|
|
||||||
@@ -164,6 +173,7 @@ class Protocol(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
|
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
|
||||||
|
log("📥 onMessage called - ${bytes.size} bytes")
|
||||||
handleMessage(bytes.toByteArray())
|
handleMessage(bytes.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,16 +182,23 @@ class Protocol(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
log("⚠️ WebSocket closing: $code - $reason")
|
log("⚠️ WebSocket closing: code=$code reason='$reason'")
|
||||||
|
log("⚠️ Stack trace at closing:")
|
||||||
|
Thread.currentThread().stackTrace.take(10).forEach {
|
||||||
|
log(" at $it")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
log("❌ WebSocket closed: $code - $reason")
|
log("❌ WebSocket closed: code=$code reason='$reason'")
|
||||||
|
log("❌ State was: ${_state.value}")
|
||||||
handleDisconnect()
|
handleDisconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
log("❌ WebSocket error: ${t.message}")
|
log("❌ WebSocket failure: ${t.message}")
|
||||||
|
log("❌ Response: ${response?.code} ${response?.message}")
|
||||||
|
t.printStackTrace()
|
||||||
_lastError.value = t.message
|
_lastError.value = t.message
|
||||||
handleDisconnect()
|
handleDisconnect()
|
||||||
}
|
}
|
||||||
@@ -251,7 +268,23 @@ class Protocol(
|
|||||||
val hexDump = data.take(50).joinToString(" ") { String.format("%02X", it) }
|
val hexDump = data.take(50).joinToString(" ") { String.format("%02X", it) }
|
||||||
log(" Hex: $hexDump${if (data.size > 50) "..." else ""}")
|
log(" Hex: $hexDump${if (data.size > 50) "..." else ""}")
|
||||||
|
|
||||||
webSocket?.send(ByteString.of(*data))
|
val socket = webSocket
|
||||||
|
if (socket == null) {
|
||||||
|
log("❌ WebSocket is null, cannot send packet ${packet.getPacketId()}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val sent = socket.send(ByteString.of(*data))
|
||||||
|
if (sent) {
|
||||||
|
log("✅ Packet ${packet.getPacketId()} sent successfully")
|
||||||
|
} else {
|
||||||
|
log("❌ Failed to send packet ${packet.getPacketId()} - send() returned false")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flushPacketQueue() {
|
private fun flushPacketQueue() {
|
||||||
@@ -282,11 +315,15 @@ class Protocol(
|
|||||||
packet.receive(stream)
|
packet.receive(stream)
|
||||||
|
|
||||||
// Notify waiters
|
// Notify waiters
|
||||||
|
val waitersCount = packetWaiters[packetId]?.size ?: 0
|
||||||
|
log("📥 Notifying $waitersCount waiter(s) for packet $packetId")
|
||||||
|
|
||||||
packetWaiters[packetId]?.forEach { callback ->
|
packetWaiters[packetId]?.forEach { callback ->
|
||||||
try {
|
try {
|
||||||
callback(packet)
|
callback(packet)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log("❌ Error in packet handler: ${e.message}")
|
log("❌ Error in packet handler: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -301,37 +338,26 @@ class Protocol(
|
|||||||
handshakeComplete = false
|
handshakeComplete = false
|
||||||
handshakeJob?.cancel()
|
handshakeJob?.cancel()
|
||||||
heartbeatJob?.cancel()
|
heartbeatJob?.cancel()
|
||||||
webSocket = null // Обнуляем сокет чтобы connect() мог создать новый
|
|
||||||
|
|
||||||
// Автоматический reconnect (как в Архиве)
|
// Автоматический reconnect (упрощённая логика как в Архиве)
|
||||||
if (!isManuallyClosed) {
|
if (!isManuallyClosed) {
|
||||||
// Логируем потерю соединения
|
log("🔄 Connection lost from $previousState, reconnecting...")
|
||||||
if (previousState == ProtocolState.AUTHENTICATED || previousState == ProtocolState.CONNECTED) {
|
reconnectAttempts++
|
||||||
log("🔄 Connection lost from $previousState, will reconnect...")
|
|
||||||
reconnectAttempts = 0 // Сбрасываем счётчик при неожиданной потере
|
val delay = if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
RECONNECT_INTERVAL
|
||||||
|
} else {
|
||||||
|
// После максимума попыток - ждём дольше
|
||||||
|
reconnectAttempts = 0
|
||||||
|
30000L
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
log("🔄 Reconnecting in ${delay}ms (attempt $reconnectAttempts)")
|
||||||
reconnectAttempts++
|
|
||||||
val delay = RECONNECT_INTERVAL * minOf(reconnectAttempts, 3) // Exponential backoff до 15 секунд
|
|
||||||
log("🔄 Reconnecting in ${delay}ms (attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS)")
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(delay)
|
delay(delay)
|
||||||
if (!isManuallyClosed) {
|
if (!isManuallyClosed) {
|
||||||
connect()
|
connect()
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log("❌ Max reconnect attempts reached, resetting counter...")
|
|
||||||
// Сбрасываем счетчик и пробуем снова через большой интервал
|
|
||||||
reconnectAttempts = 0
|
|
||||||
scope.launch {
|
|
||||||
delay(30000) // 30 секунд перед следующей серией попыток
|
|
||||||
if (!isManuallyClosed) {
|
|
||||||
log("🔄 Restarting reconnection attempts...")
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,9 @@ fun ChatsListScreen(
|
|||||||
var titleClickCount by remember { mutableStateOf(0) }
|
var titleClickCount by remember { mutableStateOf(0) }
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
|
// Status dialog state
|
||||||
|
var showStatusDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Search state - используем ViewModel для поиска пользователей
|
// Search state - используем ViewModel для поиска пользователей
|
||||||
val searchViewModel = remember { SearchUsersViewModel() }
|
val searchViewModel = remember { SearchUsersViewModel() }
|
||||||
val searchQuery by searchViewModel.searchQuery.collectAsState()
|
val searchQuery by searchViewModel.searchQuery.collectAsState()
|
||||||
@@ -226,12 +229,58 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Status dialog
|
||||||
|
if (showStatusDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showStatusDialog = false },
|
||||||
|
title = { Text("Connection Status", fontWeight = FontWeight.Bold) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
when (protocolState) {
|
||||||
|
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50)
|
||||||
|
ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107)
|
||||||
|
ProtocolState.DISCONNECTED -> Color(0xFFF44336)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = when (protocolState) {
|
||||||
|
ProtocolState.DISCONNECTED -> "Disconnected"
|
||||||
|
ProtocolState.CONNECTING -> "Connecting..."
|
||||||
|
ProtocolState.CONNECTED -> "Connected"
|
||||||
|
ProtocolState.HANDSHAKING -> "Authenticating..."
|
||||||
|
ProtocolState.AUTHENTICATED -> "Authenticated"
|
||||||
|
},
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = { showStatusDialog = false }) {
|
||||||
|
Text("OK")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Simple background
|
// Simple background
|
||||||
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
|
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
ModalDrawerSheet(drawerContainerColor = drawerBackgroundColor) {
|
ModalDrawerSheet(
|
||||||
|
drawerContainerColor = drawerBackgroundColor,
|
||||||
|
modifier = Modifier.width(280.dp)
|
||||||
|
) {
|
||||||
// Drawer Header
|
// Drawer Header
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -319,7 +368,7 @@ fun ChatsListScreen(
|
|||||||
selected = false,
|
selected = false,
|
||||||
colors = drawerItemColors,
|
colors = drawerItemColors,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch { drawerState.close() }
|
// Don't close drawer when toggling theme
|
||||||
onToggleTheme()
|
onToggleTheme()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -410,7 +459,7 @@ fun ChatsListScreen(
|
|||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
// Title - Triple click to open dev console (stays in place, just fades)
|
// Title - Triple click to open dev console (stays in place, just fades)
|
||||||
if (titleAlpha > 0.01f) {
|
if (titleAlpha > 0.01f) {
|
||||||
Column(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
val currentTime =
|
val currentTime =
|
||||||
@@ -435,7 +484,8 @@ fun ChatsListScreen(
|
|||||||
lastClickTime =
|
lastClickTime =
|
||||||
currentTime
|
currentTime
|
||||||
}
|
}
|
||||||
.graphicsLayer { alpha = titleAlpha }
|
.graphicsLayer { alpha = titleAlpha },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Rosetta",
|
"Rosetta",
|
||||||
@@ -443,32 +493,23 @@ fun ChatsListScreen(
|
|||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
if (protocolState !=
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
ProtocolState.AUTHENTICATED
|
// Status indicator dot
|
||||||
) {
|
Box(
|
||||||
Text(
|
modifier = Modifier
|
||||||
text =
|
.size(10.dp)
|
||||||
when (protocolState) {
|
.clip(CircleShape)
|
||||||
ProtocolState
|
.background(
|
||||||
.DISCONNECTED ->
|
when (protocolState) {
|
||||||
"Connecting..."
|
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) // Green
|
||||||
ProtocolState
|
ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) // Yellow
|
||||||
.CONNECTING ->
|
ProtocolState.DISCONNECTED -> Color(0xFFF44336) // Red
|
||||||
"Connecting..."
|
}
|
||||||
ProtocolState
|
)
|
||||||
.CONNECTED ->
|
.clickable {
|
||||||
"Authenticating..."
|
showStatusDialog = true
|
||||||
ProtocolState
|
}
|
||||||
.HANDSHAKING ->
|
)
|
||||||
"Authenticating..."
|
|
||||||
ProtocolState
|
|
||||||
.AUTHENTICATED ->
|
|
||||||
""
|
|
||||||
},
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = secondaryTextColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user