Run Android service shutdown off the main thread and await it before startup

This commit is contained in:
evgeny committed 2026-10-27 19:07:05 +01:00
1 parent d6ea893f16
commit 823bae076f
3 files changed
+26 -16

No files matched your search

@@ -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)
@@ -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()
}
@@ -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. */