From 7dee49ce6346e433a5658327a993b780938d9fff Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 17 Aug 2026 21:03:05 +0300 Subject: [PATCH] =?UTF-8?q?android:=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D1=81=D0=BC=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=81=D0=B5=D1=82=D0=B8=20=E2=80=94=20NetworkChangeMonitor,=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20?= =?UTF-8?q?=D1=8F=D0=B4=D1=80=D0=B0=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/main/AndroidManifest.xml | 1 + .../utun/chat/data/NetworkChangeMonitor.kt | 101 ++++++++++++++++++ .../com/utun/chat/headless/HeadlessService.kt | 3 + .../libutun_lite/instance_lite.c | 48 +++++++++ 4 files changed, 153 insertions(+) create mode 100644 tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt diff --git a/tools/chatgui-android/app/src/main/AndroidManifest.xml b/tools/chatgui-android/app/src/main/AndroidManifest.xml index 0047a460..608b1574 100644 --- a/tools/chatgui-android/app/src/main/AndroidManifest.xml +++ b/tools/chatgui-android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt new file mode 100644 index 00000000..0a37e999 --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt @@ -0,0 +1,101 @@ +package com.utun.chat.data + +import android.net.ConnectivityManager +import android.net.LinkProperties +import android.net.Network +import android.os.Handler +import android.os.Looper +import com.utun.chat.ChatApplication + +/** + * Слушатель смены сети (ConnectivityManager.NetworkCallback). + * + * На Android нативный netlink-мониторинг сокетов недоступен (Permission denied), + * поэтому о смене сети сообщает ConnectivityManager. При смене default-сети + * (mobile↔wifi, смена IP) пересобираем конфиг сокетов и перезапускаем C-ядро, + * чтобы оно забиндилось на актуальный интерфейс. + * + * Регистрируется в HeadlessService (foreground) — работает в фоне. + */ +object NetworkChangeMonitor { + private var started = false + + /* Пропуск первого onAvailable: он приходит сразу при регистрации с текущим + * состоянием сети, а не как реальное событие смены. */ + private var primed = false + + /* Рестарт в процессе — защита от наложения (C-ядро тоже имеет CAS-гарды). */ + private var restarting = false + + /* Последний известный набор интерфейсов (ifName=ip) для лога «было → стало». */ + private var lastInterfaces: List>? = null + + private const val DEBOUNCE_MS = 500L + private val handler = Handler(Looper.getMainLooper()) + private val restartRunnable = Runnable { doRestart() } + + private val callback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + if (!primed) { primed = true; return } + schedule() + } + + override fun onLost(network: Network) { + if (!primed) return + schedule() + } + + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + if (!primed) return + schedule() + } + } + + private fun schedule() { + handler.removeCallbacks(restartRunnable) + handler.postDelayed(restartRunnable, DEBOUNCE_MS) + } + + private fun doRestart() { + if (restarting || !NativeLib.isReady()) return + restarting = true + Thread { + try { + val provider = ChatApplication.instance.configProvider + val now = provider.getAvailableInterfaces() + val prev = lastInterfaces + lastInterfaces = now + val prevStr = prev?.joinToString(", ") { (n, ip) -> "$n=$ip" } ?: "n/a" + val nowStr = now.joinToString(", ") { (n, ip) -> "$n=$ip" } + LogManager.addLog("INFO", "NET", "network change: [$prevStr] -> [$nowStr]") + NativeLib.notifyNetworkActive() + val path = provider.getString("db.path") ?: "" + val port = provider.getInt("control.port") + NativeLib.restart(path, port, provider.buildConfigText()) + } finally { + restarting = false + } + }.start() + } + + fun start() { + if (started) return + val ctx = ChatApplication.instance.applicationContext + val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return + /* Если сети ещё нет — первый onAvailable будет реальным событием. */ + primed = cm.activeNetwork == null + cm.registerDefaultNetworkCallback(callback) + started = true + LogManager.addLog("INFO", "NET", "network change listener started") + } + + fun stop() { + if (!started) return + val ctx = ChatApplication.instance.applicationContext + val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return + cm.unregisterNetworkCallback(callback) + handler.removeCallbacks(restartRunnable) + started = false + LogManager.addLog("INFO", "NET", "network change listener stopped") + } +} diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt index b4d262c1..955c4ba6 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt @@ -13,6 +13,7 @@ import com.utun.chat.MainActivity import com.utun.chat.data.LogManager import com.utun.chat.data.NativeLib import com.utun.chat.data.NetworkActivityMonitor +import com.utun.chat.data.NetworkChangeMonitor class HeadlessService : Service() { override fun onCreate() { @@ -47,6 +48,7 @@ class HeadlessService : Service() { if (ok) { LogManager.addLog("INFO", "SRV", "uTun core started") NetworkActivityMonitor.start() + NetworkChangeMonitor.start() } else { LogManager.addLog("ERROR", "SRV", "uTun start failed") } @@ -54,6 +56,7 @@ class HeadlessService : Service() { override fun onDestroy() { LogManager.addLog("INFO", "SRV", "uTun service stopping") + NetworkChangeMonitor.stop() NetworkActivityMonitor.stop() NativeLib.stop() super.onDestroy() diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index e80bffaa..72292ab4 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -499,11 +499,59 @@ void instance_lite_stop(void) { g_thread_exited = 0; } +/* Логирует текущие активные сокеты (имя=ip:port(протокол)) — состояние ДО рестарта. */ +static void log_active_sockets(struct UTUN_INSTANCE* inst, const char* label) { + if (!inst) { DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "restart: %s: (no instance)", label); return; } + char buf[768] = ""; + int off = 0; + for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { + const struct sockaddr_storage* a = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; + const char* addr = sockaddr_storage_to_str(a).str; + if (off < (int)sizeof(buf) - 96) + off += snprintf(buf + off, sizeof(buf) - (size_t)off, " %s=%s(%s)", + s->name, addr, s->is_tcp ? "TCP" : "UDP"); + } + DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "restart: %s:%s", label, buf[0] ? buf : " (none)"); +} + +/* Логирует [server:...]/addr= секции нового конфига — состояние ПОСЛЕ рестарта. */ +static void log_config_servers(const char* cfg, const char* label) { + char buf[768] = ""; + int off = 0; + char srvname[64] = ""; + const char* p = cfg; + while (p && *p) { + if (strncmp(p, "[server:", 8) == 0) { + const char* end = strchr(p, ']'); + if (end && end > p + 8) { + int n = (int)(end - p) - 8; if (n > 63) n = 63; + memcpy(srvname, p + 8, (size_t)n); srvname[n] = '\0'; + } else srvname[0] = '\0'; + } else if (strncmp(p, "addr=", 5) == 0 && srvname[0]) { + const char* eol = p; while (*eol && *eol != '\n' && *eol != '\r') eol++; + int alen = (int)(eol - p) - 5; char addr[128]; + if (alen < 1) alen = 0; if (alen > 127) alen = 127; + memcpy(addr, p + 5, (size_t)alen); addr[alen] = '\0'; + if (off < (int)sizeof(buf) - 96) + off += snprintf(buf + off, sizeof(buf) - (size_t)off, " %s=%s", srvname, addr); + srvname[0] = '\0'; + } + while (*p && *p != '\n') p++; + if (*p == '\n') p++; + } + DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "restart: %s:%s", label, buf[0] ? buf : " (none)"); +} + void instance_lite_restart(const char* new_config_text) { if (!new_config_text) return; struct UASYNC* ua = (struct UASYNC*)__atomic_load_n(&g_ua, __ATOMIC_ACQUIRE); struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE); int running = __atomic_load_n(&g_running, __ATOMIC_ACQUIRE); + + /* Диагностика смены сети: какие сокеты/адреса были → какие будут */ + log_active_sockets(inst, "OLD sockets"); + log_config_servers(new_config_text, "NEW config"); + if (!ua || !inst || !running) { IL_LOGI("restart: instance dead, doing hard restart via stop+start"); if (inst) instance_lite_stop();