Browse Source

topo_group_connect: infinite reconnect cycle + connecting status for GUI

- dedup double ETCP_CONN_STATUS_UP (UDP+TCP links) in topo_group_new_conn
- close conn_mgr handle on TIMEOUT in Phase2/3 (fix stuck 'already connecting')
- infinite Phase1→2→3 cycle with pause (1s active / 30s standby)
- CHAT_EVT_CONNECTING_NODES: yellow circle in member list while connecting
- Android: client_activity wiring via onStart/onStop + nativeSetActive
- function-level comments in topo_group_connect.c
topo_upd
evgeny 2 months ago
parent
commit
0492b19468
  1. 13
      src/routing_layer/topo_group.c
  2. 82
      src/routing_layer/topo_group_connect.c
  3. 10
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  4. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  5. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  6. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt
  7. 25
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  8. 22
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  9. 4
      tools/chatgui-android/jni_bridge/android_jni_bridge.h

13
src/routing_layer/topo_group.c

@ -445,6 +445,19 @@ void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
if (!conn->instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance is NULL"); return; }
if (!conn->instance->rt) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance->rt is NULL"); return; }
/* дедуп: тот же conn стреляет ETCP_CONN_STATUS_UP дважды (UDP-линк, затем TCP-линк).
* Если conn уже в senders_list — повторно не обрабатываем, иначе active_conn_count
* задваивается и переподключение никогда не стартует. */
{ struct ll_entry* se = group->senders_list ? group->senders_list->head : NULL;
while (se) {
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_group_new_conn: conn already in senders_list, skip double UP (%s)", conn->log_name);
return;
}
se = se->next;
}
}
topo_recovery_cancel_for_node(group, conn->peer_node_id);
struct TOPO_GROUP_NODE* peer_nq = topo_node_find_by_id(group, conn->peer_node_id);

82
src/routing_layer/topo_group_connect.c

@ -59,6 +59,7 @@ struct TOPO_GROUP_CONNECT {
int candidate_count;
struct CONN_MGR_HANDLE* handles[TGC_MAX_HANDLES];
int handle_count;
struct CONN_MGR_HANDLE* cur_handle; /* handle текущей попытки Phase 2/3 (для закрытия на TIMEOUT) */
};
static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc);
@ -72,6 +73,12 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g
/* ─── нотификация GUI о списке узлов в процессе подключения ─── */
/**
* Отправляет в GUI событие CHAT_EVT_CONNECTING_NODES — список узлов, к которым
* сейчас идёт попытка подключения. GUI по нему рисует жёлтый кружок у этих узлов.
* Пустой список (count=0) означает «сейчас ни к кому не подключаемся» —
* все жёлтые кружки снимаются.
*/
static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t* ids, int count) {
size_t cl = strlen(gc->group->channel_id); if (cl > 255) cl = 255;
size_t sz = 1 + cl + 2 + (size_t)count * 8;
@ -82,7 +89,7 @@ static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t*
for (int i = 0; i < count; i++) { memcpy(p, &ids[i], 8); p += 8; }
chat_event_post(CHAT_EVT_CONNECTING_NODES, buf, (int)sz);
u_free(buf);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: notify_connecting ch=%s count=%d",
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: notify_connecting ch=%s count=%d",
TGC_ID, gc->group->channel_id, count);
}
@ -90,6 +97,12 @@ static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t*
* Жизненный цикл
* ══════════════════════════════════════════════════════════════════════ */
/**
* Запуск авто-подключения к узлам CHAT-группы.
* Если модуль уже запущен — останавливает его и стартует заново с нуля.
* Начинает с Phase 1: параллельные попытки ко всем пирам, что были connected
* в прошлой сессии (из БД). Если таких нет — сразу переходит к Phase 2.
*/
int topo_group_connect_init(struct TOPO_GROUP* group) {
if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0])
return -1;
@ -125,6 +138,11 @@ int topo_group_connect_init(struct TOPO_GROUP* group) {
return 0;
}
/**
* Полная остановка авто-подключения: отменяет все таймеры, закрывает все
* открытые соединения и освобождает память. Вызывается при удалении группы
* и при перезапуске (init/restart).
*/
void topo_group_connect_destroy(struct TOPO_GROUP* group) {
struct TOPO_GROUP_CONNECT* gc = group->connect;
if (!gc) return;
@ -132,10 +150,16 @@ void topo_group_connect_destroy(struct TOPO_GROUP* group) {
if (gc->phase_timer) { uasync_cancel_timeout(group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; }
if (gc->pause_timer) { uasync_cancel_timeout(group->instance->ua, gc->pause_timer); gc->pause_timer = NULL; }
for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; }
u_free(gc->candidate_ids);
u_free(gc);
}
/**
* Внешний перезапуск авто-подключения (destroy + init). Используется, например,
* при смене сети, когда надо начать поиск заново. Не трогает модуль, если уже
* есть живые соединения.
*/
void topo_group_connect_restart(struct TOPO_GROUP* group) {
struct TOPO_GROUP_CONNECT* gc = group->connect;
if (!gc) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: restart ch=%s no gc → init", TGC_ID, group->channel_id); topo_group_connect_init(group); return; }
@ -145,6 +169,10 @@ void topo_group_connect_restart(struct TOPO_GROUP* group) {
topo_group_connect_init(group);
}
/**
* Сколько сейчас живых соединений с пирами группы. Нужно внешней логике
* (например, chat_sync) для решения, запускать ли переподключение.
*/
int topo_group_connect_active_count(struct TOPO_GROUP* group) {
struct TOPO_GROUP_CONNECT* gc = group->connect;
return gc ? gc->active_conn_count : 0;
@ -154,6 +182,11 @@ int topo_group_connect_active_count(struct TOPO_GROUP* group) {
* ON UP / DOWN
* ══════════════════════════════════════════════════════════════════════ */
/**
* Обработка появления соединения с пиром. Увеличивает счётчик активных и
* помечает connected=1 в БД (чтобы при следующем старте узел попал в Phase 1).
* Пропускает, если пир уже подключён через другое соединение.
*/
void topo_group_connect_on_up(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0] || !conn) return;
uint64_t peer = conn->peer_node_id;
@ -178,6 +211,11 @@ void topo_group_connect_on_up(struct TOPO_GROUP* group, struct ETCP_CONN* conn)
TGC_ID, (unsigned long long)peer, group->channel_id, (unsigned long long)group->group_id, gc->active_conn_count);
}
/**
* Обработка обрыва соединения. Уменьшает счётчик активных и помечает
* connected=0 в БД. Если живых соединений не осталось — немедленно
* перезапускает цикл поиска с Phase 1.
*/
void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0] || !conn) return;
uint64_t peer = conn->peer_node_id;
@ -224,10 +262,16 @@ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn
* Единый коллбэк для всех фаз
* ══════════════════════════════════════════════════════════════════════ */
/**
* Единый коллбэк на результат каждой попытки подключения (успех/таймаут/обрыв).
* В Phase 1 — просто считает результаты (итог подводит tgc_phase1_timeout).
* В Phase 2/3 — успех завершает цикл; провал закрывает handle (чтобы не оставить
* залипшее состояние) и ставит паузу перед следующей попыткой.
*/
static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id,
enum conn_mgr_event event, void* arg) {
struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg;
(void)h; (void)group_id;
(void)group_id;
int ok = (event == CONN_EVENT_UP);
@ -253,6 +297,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g
tgc_notify_connecting(gc, NULL, 0);
} else {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; }
tgc_start_pause(gc, (void(*)(void*))tgc_phase2_try_next, "tgc_p2_try");
}
break;
@ -263,6 +308,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g
tgc_notify_connecting(gc, NULL, 0);
} else {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; }
tgc_start_pause(gc, (void(*)(void*))tgc_phase3_try_next, "tgc_p3_try");
}
break;
@ -273,6 +319,12 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g
* Пауза: 1s ACTIVE / 30s STANDBY
* ══════════════════════════════════════════════════════════════════════ */
/**
* Пауза между попытками подключения. Длительность зависит от активности
* приложения: 1 секунда когда приложение активно, 30 секунд когда в фоне
* (экран выключен). После паузы вызывает cb (следующая попытка или перезапуск
* цикла). На время паузы уведомляет GUI пустым списком (жёлтые кружки гаснут).
*/
static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), const char* label) {
gc->phase = TGC_PHASE_PAUSE;
int active = (gc->group->instance->client_activity == CLIENT_ACTIVITY_ACTIVE);
@ -287,6 +339,11 @@ static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), co
* Phase 1 timeout
* ══════════════════════════════════════════════════════════════════════ */
/**
* Таймаут параллельных попыток Phase 1 (2 секунды). Закрывает неудачные
* handles и сбрасывает connected в БД. Если хоть один узел подключился —
* цикл завершён; иначе переходим к Phase 2.
*/
static void tgc_phase1_timeout(void* arg) {
struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg;
gc->phase_timer = NULL;
@ -330,6 +387,11 @@ static void tgc_phase1_timeout(void* arg) {
* Phase 2
* ══════════════════════════════════════════════════════════════════════ */
/**
* Последовательный перебор узлов с публичными/EIM адресами (сначала supernode,
* затем обычные публичные). Пробует следующий узел и ставит ему жёлтый кружок.
* Когда все перебраны — переходит к Phase 3.
*/
static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) {
if (gc->phase == TGC_PHASE_PAUSE) gc->phase = TGC_PHASE_TWO;
if (gc->phase != TGC_PHASE_TWO) return;
@ -358,7 +420,7 @@ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) {
uint64_t nid = gc->candidate_ids[gc->cursor++];
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 trying 0x%016llx (%d/%d) super_round=%d", TGC_ID,
(unsigned long long)nid, gc->cursor, gc->candidate_count, gc->tried_super);
if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, NULL) < 0) continue;
if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue;
tgc_notify_connecting(gc, &nid, 1);
return;
}
@ -371,6 +433,11 @@ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) {
* Phase 3 — локальные/strict NAT адреса
* ══════════════════════════════════════════════════════════════════════ */
/**
* Последовательный перебор узлов с локальными/strict NAT адресами (как Phase 2,
* но для узлов, доступных только в локальной сети). Когда все перебраны —
* пауза и полный перезапуск цикла с Phase 1 (бесконечно, пока не подключимся).
*/
static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) {
if (gc->phase == TGC_PHASE_PAUSE) gc->phase = TGC_PHASE_THREE;
if (gc->phase != TGC_PHASE_THREE) return;
@ -386,7 +453,7 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) {
uint64_t nid = gc->candidate_ids[gc->cursor++];
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 trying 0x%016llx (%d/%d)", TGC_ID,
(unsigned long long)nid, gc->cursor, gc->candidate_count);
if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, NULL) < 0) continue;
if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue;
tgc_notify_connecting(gc, &nid, 1);
return;
}
@ -398,10 +465,17 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) {
* Полный перезапуск цикла с Phase 1 (без destroy/init)
* ══════════════════════════════════════════════════════════════════════ */
/**
* Полный перезапуск цикла с Phase 1 без уничтожения модуля. Закрывает все
* открытые соединения, сбрасывает состояние и заново запускает параллельные
* попытки ко всем connected-пирам из БД. Вызывается при обрыве всех соединений
* и после исчерпания Phase 3.
*/
static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) {
gc->pause_timer = NULL;
if (gc->phase_timer) { uasync_cancel_timeout(gc->group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; }
for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]);
if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; }
gc->handle_count = 0; gc->pending = 0; gc->connected_count = 0;
gc->cursor = 0; gc->tried_super = 0;
u_free(gc->candidate_ids); gc->candidate_ids = NULL; gc->candidate_count = 0;

10
tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt

@ -146,4 +146,14 @@ class MainActivity : ComponentActivity() {
}
}
}
override fun onStart() {
super.onStart()
if (NativeLib.isReady()) NativeLib.setActive(true)
}
override fun onStop() {
super.onStop()
if (NativeLib.isReady()) NativeLib.setActive(false)
}
}

2
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt

@ -68,7 +68,7 @@ data class ChatMember(val nodeId: Long, val name: String, val online: Boolean =
val directUp: Boolean = false, val indirectUp: Boolean = false,
val directPresence: Boolean = false, val indirectPresence: Boolean = false,
val bgpPresence: Boolean = false, val bgpRtt: Int = 0,
val bgpNodes: Int = 0) {
val bgpNodes: Int = 0, val connecting: Boolean = false) {
val flags: Int get() = (if (isSuper) 1 else 0) or (if (isAdmin) 2 else 0) or (if (isModer) 4 else 0)
}

4
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -53,6 +53,9 @@ object NativeLib {
fun ping() { nativePing() }
fun isResponsive(): Boolean = nativeIsResponsive()
/* ── App foreground/background (Android lifecycle) ── */
fun setActive(active: Boolean) { nativeSetActive(active) }
fun sendMessage(channelId: String, text: String) { nativeSendMessage(channelId, text) }
fun createChannel(name: String, channelId: String) { nativeCreateChannel(name, channelId) }
fun connectNode(address: String, port: Int, pubkeyHex: String) { nativeConnectNode(address, port, pubkeyHex) }
@ -140,6 +143,7 @@ object NativeLib {
private external fun nativePing()
private external fun nativeIsResponsive(): Boolean
private external fun nativeIsRunning(): Boolean
private external fun nativeSetActive(active: Boolean)
/* ── Channel members JNI ── */
private external fun nativeGetChannelMembers(channelId: String): String?

3
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

@ -122,11 +122,13 @@ private fun MemberListItem(
onEdit: () -> Unit
) {
val onlineColor = when {
member.connecting -> Color(0xFFFFEB3B) // yellow: connecting
member.directUp || member.indirectUp -> Color(0xFF4CAF50) // green: up
member.directPresence || member.indirectPresence || member.bgpPresence -> Color(0xFFFFA726) // orange: presence but down
else -> Color(0xFF9E9E9E) // gray: offline
}
val connLabel = when {
member.connecting -> "connecting"
member.directUp -> "direct"
member.indirectUp -> "indirect"
member.directPresence || member.indirectPresence || member.bgpPresence -> "offline"
@ -171,6 +173,7 @@ private fun MemberListItem(
if (member.isSuper) parts.add("Super")
if (member.isStorage) parts.add("Storage")
if (member.isSelf) parts.add("self")
else if (member.connecting) parts.add("connecting…")
else if (member.directUp) parts.add("direct↑")
else if (member.indirectUp) parts.add("indirect↑")
else if (member.directPresence || member.indirectPresence || member.bgpPresence) parts.add("down")

25
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -65,6 +65,9 @@ class ChatViewModel : ViewModel() {
private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap())
val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses
private val _connecting = MutableStateFlow<Map<String, Set<Long>>>(emptyMap())
val connecting: StateFlow<Map<String, Set<Long>>> = _connecting
private val _localSockets = MutableStateFlow<List<LocalSocketState>>(emptyList())
val localSockets: StateFlow<List<LocalSocketState>> = _localSockets
@ -281,6 +284,24 @@ class ChatViewModel : ViewModel() {
_selectedMember.value = _selectedMember.value?.copy(online = ml.connected && ml.linksUp > 0)
}
}
27 -> { /* CONNECTING_NODES: [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
if (data == null || data.size < 3) return
val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl + 2) return
val chId = String(data, 1, cl)
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val count = buf.getShort().toInt() and 0xFFFF
val set = HashSet<Long>()
for (i in 0 until count) {
if (buf.position() + 8 > data.size) break
set.add(buf.getLong())
}
_connecting.value = _connecting.value + (chId to set)
val cur = _currentChannel.value
if (cur != null && cur.id == chId) {
_members.value = _members.value.map { it.copy(connecting = it.nodeId in set) }
}
}
}
}
@ -324,17 +345,19 @@ class ChatViewModel : ViewModel() {
fun refreshMembers(chId: String) {
val r = repo ?: return
val statuses = _nodeStatuses.value
val connecting = _connecting.value[chId] ?: emptySet()
viewModelScope.launch {
val raw = withContext(Dispatchers.IO) { r.getMembers(chId) }
_members.value = raw.map { m ->
val ns = statuses[m.nodeId]
if (ns != null) m.copy(
val base = if (ns != null) m.copy(
online = ns.online, rtt = ns.bestRtt,
directUp = ns.directUp, indirectUp = ns.indirectUp,
directPresence = ns.directPresence, indirectPresence = ns.indirectPresence,
bgpPresence = ns.bgpPresence, bgpRtt = ns.bgpRtt,
bgpNodes = ns.bgpNodes
) else m
base.copy(connecting = base.nodeId in connecting)
}
}
}

22
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -1177,6 +1177,20 @@ void utun_bridge_on_network_change(void) {
bridge_log(BLEV_DEBUG, "network change notified to auto_socket");
}
static void bridge_set_active_trampoline(void* arg) {
int active = (int)(intptr_t)arg;
struct UTUN_INSTANCE* inst = chat_core_get_inst();
if (!inst) return;
utun_set_client_activity(inst, active);
bridge_log(BLEV_DEBUG, "client_activity set to %s", active ? "ACTIVE" : "STANDBY");
}
void utun_bridge_set_active(int active) {
struct UASYNC* ua = instance_lite_get_uasync();
if (!ua) return;
uasync_post(ua, bridge_set_active_trampoline, (void*)(intptr_t)active);
}
void utun_bridge_collect_conn_list(void) {
instance_lite_collect_conn_list();
}
@ -1990,6 +2004,14 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeOnNetworkChanged(
utun_bridge_on_network_change();
}
/* ── App foreground/background (Android lifecycle) → client_activity ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetActive(
JNIEnv* env, jobject thiz, jboolean active) {
(void)env; (void)thiz;
utun_bridge_set_active(active ? 1 : 0);
}
/* ── Connection monitor ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnList(

4
tools/chatgui-android/jni_bridge/android_jni_bridge.h

@ -133,6 +133,10 @@ char* utun_bridge_get_local_sockets_json(void);
void utun_bridge_on_network_change(void);
/* ── App foreground/background (Android lifecycle) → client_activity ── */
void utun_bridge_set_active(int active);
/* ── Connection monitor (post collect functions to uasync thread) ── */
void utun_bridge_collect_conn_list(void);

Loading…
Cancel
Save