From 823bae076f428704c13d9141f41ae09cbd2d22c8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 19:07:05 +0100 Subject: [PATCH] Run Android service shutdown off the main thread and await it before startup --- .../main/java/com/utun/chat/data/NativeLib.kt | 24 ++++++++++--------- .../com/utun/chat/headless/HeadlessService.kt | 8 ++++++- .../jni_bridge/android_jni_bridge.c | 10 ++++---- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 7b5220ca4..18dd8db5b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -3,6 +3,7 @@ package com.utun.chat.data import com.utun.chat.ChatApplication import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext interface ChatEventCallback { fun onEvent(type: Int, data: ByteArray?) @@ -41,11 +42,11 @@ object NativeLib { return nativeStart(configText) } - fun stop() { - prepareForCoreStop() - if (!isReady()) return - nativeStop() - runBlocking(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() } + /** Снимает frontend media на main, дожидается worker на IO даже до ready; ошибка запрещает следующий init. */ + suspend fun stop() { + withContext(Dispatchers.Main.immediate) { prepareForCoreStop() } + check(withContext(Dispatchers.IO) { nativeStop() }) { "native worker stop failed" } + withContext(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() } } fun setEventCallback(cb: ChatEventCallback) { @@ -54,10 +55,11 @@ object NativeLib { }) } - fun destroy() { - prepareForCoreStop() - nativeDestroy() - runBlocking(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() } + /** Как stop, дополнительно освобождает JNI global refs только после подтверждённого join. */ + suspend fun destroy() { + withContext(Dispatchers.Main.immediate) { prepareForCoreStop() } + check(withContext(Dispatchers.IO) { nativeDestroy() }) { "native worker destroy failed" } + withContext(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() } } fun restart(dbPath: String, controlPort: Int, configText: String): Boolean { @@ -347,10 +349,10 @@ object NativeLib { private external fun nativeInit(dbPath: String, controlPort: Int, config: Any, logCb: Any): Boolean private external fun nativeStart(configText: String): Boolean - private external fun nativeStop() + private external fun nativeStop(): Boolean private external fun nativeRequestCpuLoad(): Boolean private external fun nativeSetEventCallback(cb: Any) - private external fun nativeDestroy() + private external fun nativeDestroy(): Boolean private external fun nativeSendMessage(channelId: String, text: ByteArray, replyTo: ByteArray, requestId: Long): Boolean private external fun nativeEditMessage(channelId: String, messageId: ByteArray, version: Long, text: ByteArray, requestId: Long): Boolean private external fun nativeCreateChannel(name: String, channelId: String) 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 98c547ec7..abf719205 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 @@ -33,10 +33,15 @@ class HeadlessService : Service() { }, onFailure = ::stopSelf, log = { level, message -> LogManager.addLog(level, "SRV", message) }) companion object { private var currentService: HeadlessService? = null + // Живёт дольше экземпляра Service: новый init не пересекается с очисткой предыдущего worker. + private val shutdownScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private val shutdown = CoreShutdown(shutdownScope, NativeLib::stop, + log = { level, message -> LogManager.addLog(level, "SRV", message) }) /** Main thread: дождаться готового ядра текущего Service; ошибка/остановка прерывает вызывающую операцию. * Без Service ожидания нет: настройки сохраняются для следующего запуска. */ suspend fun awaitStartup() { + shutdown.await() val service = currentService ?: return service.startup.awaitReady() check(currentService === service && NativeLib.isReady()) { "service core stopped while awaiting startup" } @@ -64,6 +69,7 @@ class HeadlessService : Service() { startForeground(1, notification()) startup.start(prepare = { + shutdown.await() val provider = ChatApplication.instance.configProvider val (dbDir, listenPort) = withContext(Dispatchers.IO) { (provider.getString("db.path") ?: filesDir.absolutePath) to provider.getInt("node.listen_port") @@ -82,7 +88,7 @@ class HeadlessService : Service() { LogManager.addLog("INFO", "SRV", "uTun service stopping") NetworkChangeMonitor.stop() NetworkActivityMonitor.stop() - NativeLib.stop() + shutdown.request() super.onDestroy() } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index e9a083b23..726bffbf1 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -1889,11 +1889,12 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStart( /* ── nativeStop() ── */ -JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStop( +JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStop( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; - utun_bridge_destroy(); + if (utun_bridge_destroy()) return JNI_FALSE; udp_log_stop(); + return JNI_TRUE; } /* ── nativeInit (legacy, just saves callbacks) ── */ @@ -2067,14 +2068,15 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback( /* ── Pass-through JNI wrappers (unchanged from before) ── */ -JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( +JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( JNIEnv* env, jobject thiz) { (void)thiz; - utun_bridge_destroy(); + if (utun_bridge_destroy()) return JNI_FALSE; udp_log_stop(); if (g_config_callback) { (*env)->DeleteGlobalRef(env, g_config_callback); g_config_callback = NULL; } if (g_log_callback) { (*env)->DeleteGlobalRef(env, g_log_callback); g_log_callback = NULL; } if (g_event_callback) { (*env)->DeleteGlobalRef(env, g_event_callback); g_event_callback = NULL; } + return JNI_TRUE; } /* Пустой массив означает отсутствие ссылки; иначе принимаем только полный глобальный ID. */