3 Commits

Author SHA1 Message Date
ua1zbe
0a24a6f6c0 Bump version to 1.0.14 (16) 2026-10-09 13:20:28 +03:00
ua1zbe
b847320360 reconnect: соблюдать паузу между попытками (backoff)
tryConnectOnce игнорировала результат runTcpLoop и всегда возвращала true,
из-за чего reconnectLoop не делал паузу и переподключался заново каждые
~30 мс, заваливая сервер MSG_ERROR 'Access denied'. Теперь возвращаем
!running, поэтому после разрыва/отказа выдерживается RECONNECT_DELAY_MS.
2026-10-09 13:17:50 +03:00
ua1zbe
203e5f6ed5 reconnect: не уходить в «ошибку авторизации» при коротком обрыве
После короткого обрыва сервер ещё держит старую сессию и на рукопожатии
присылает MSG_ERROR. Клиент трактовал любую ошибку до STATE_READY как
фатальную (stopFatal → STATE_AUTH_FAILED) и прекращал переподключения.

Теперь ошибки рукопожатия (MSG_ERROR, протокольные сбои, слишком большой
кадр) считаются временными, если успешная сессия уже была (everReady):
показываем STATE_ERROR и продолжаем автопереподключение. Фатально — только
до первой успешной авторизации (реально неверные креды).
2026-10-09 13:05:05 +03:00
2 changed files with 40 additions and 7 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "ru.ua1zbe.svxremote"
minSdk = 26
targetSdk = 34
versionCode = 15
versionName = "1.0.13"
versionCode = 16
versionName = "1.0.14"
}
buildTypes {

View File

@@ -85,6 +85,15 @@ class ReflectorClient(
@Volatile
private var fatal = false
/**
* Была ли хотя бы одна успешная сессия за текущее подключение.
* Если да — креды уже проверены, поэтому ошибки на рукопожатии считаем
* временными (сервер мог ещё держать старую сессию после обрыва) и
* продолжаем автопереподключение вместо показа «ошибки авторизации».
*/
@Volatile
private var everReady = false
@Volatile
private var tcpSocket: Socket? = null
private var udpSocket: DatagramSocket? = null
@@ -107,6 +116,7 @@ class ReflectorClient(
this.settings = settings
manualDisconnect.set(false)
fatal = false
everReady = false
if (running.get()) {
// Перезапуск с новыми настройками (например, смена профиля)
running.set(false)
@@ -254,7 +264,10 @@ class ReflectorClient(
callbacks.onState(STATE_TCP_CONNECTED)
startTcpSenderThread(socket, s)
runTcpLoop(socket, s)
true
// runTcpLoop завершилась: если всё ещё должны работать — значит
// соединение разорвано/отклонено, вернём false, чтобы reconnectLoop
// выдержал RECONNECT_DELAY_MS и не бомбил сервер переподключениями.
!running.get()
} catch (e: Exception) {
try {
socket.close()
@@ -399,6 +412,7 @@ class ReflectorClient(
serverCodecs = info.codecs
callbacks.onServerInfo(info)
state = STATE_READY
everReady = true
callbacks.onState(STATE_READY)
// Сначала сообщаем о себе (версия ПО для дашборда), затем
// выбираем TG для передачи и подписываемся на прослушивание
@@ -441,9 +455,17 @@ class ReflectorClient(
ReflectorProtocol.MSG_ERROR -> {
val err = MsgParser.errorMessage(msg.fields)
Log.w(TAG, "MSG_ERROR: $err")
Log.w(TAG, "MSG_ERROR: $err (state=$state, everReady=$everReady)")
if (state < STATE_READY) {
if (everReady) {
// Креды проверены ранее: ошибка на рукопожатии почти
// всегда временная (сервер ещё держит старую сессию
// после короткого обрыва) — переподключаемся заново.
callbacks.onState(STATE_ERROR, err.ifEmpty { "Переподключение" })
abortSockets()
} else {
stopFatal(err.ifEmpty { "Access denied" })
}
return false
}
callbacks.onError(err)
@@ -452,7 +474,13 @@ class ReflectorClient(
else -> Unit // неизвестные сообщения игнорируются
}
} catch (e: Exception) {
if (everReady) {
// Сбой после успешного подключения — не фатально, переподключаемся.
callbacks.onState(STATE_ERROR, "Ошибка протокола: ${e.message}")
abortSockets()
} else {
stopFatal("Ошибка протокола: ${e.message}")
}
return false
}
return true
@@ -478,7 +506,12 @@ class ReflectorClient(
}
private fun protocolError(detail: String): Boolean {
if (everReady) {
callbacks.onState(STATE_ERROR, detail)
abortSockets()
} else {
stopFatal(detail)
}
return false
}