Keep database reset atomic and owned beyond the activity lifecycle

This commit is contained in:
evgeny committed 2026-10-29 13:35:42 +01:00
1 parent 665137314a
commit 791ff42547
3 files changed
+31 -26

No files matched your search

@@ -92,10 +92,15 @@ object NativeLib {
check(withContext(Dispatchers.IO) { nativeStop() }) { "native stop before database reset failed" }
initialized = false
withContext(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped(); afterStop() }
val cleared = withContext(Dispatchers.IO) { nativeClearDatabase() }
if (configText != null) check(withContext(Dispatchers.IO) {
initCallbacks(dbPath, controlPort) && nativeStart(configText)
}) { "native start after database reset failed" }
val cleared = withContext(Dispatchers.IO) { nativeClearDatabase(dbPath) }
if (configText != null) {
check(withContext(Dispatchers.Main.immediate) { com.utun.chat.headless.HeadlessService.isActive() }) {
"service stopped before database reset recovery"
}
check(withContext(Dispatchers.IO) { initCallbacks(dbPath, controlPort) && nativeStart(configText) }) {
"native start after database reset failed"
}
}
coreStopping = false
cleared
}
@@ -178,7 +183,6 @@ object NativeLib {
fun getMessages(channelId: String, limit: Int = 50, anchor: String = "", exact: Boolean = false): String =
checkNotNull(nativeGetMessages(channelId, limit, messageIdBytes(anchor), exact)) { "Message snapshot unavailable channel=$channelId" }
fun feedbackCommand(json: String, requestId: Long = 0): Boolean = nativeFeedbackCommand(json.toByteArray(Charsets.UTF_8), requestId)
fun clearDatabase(): Boolean = nativeClearDatabase()
fun setDebugLevel(category: String, level: String) { nativeSetDebugLevel(category, level) }
fun getDebugCategories(): List<String> = nativeGetDebugCategories()?.split(",")?.filter { it.isNotEmpty() } ?: emptyList()
@@ -470,7 +474,7 @@ object NativeLib {
private external fun nativeSubscribeLogs(enabled: Boolean)
private external fun nativeSetUdpLogTarget(ip: String, port: Int)
private external fun nativeRegenerateKeys(configText: String): Boolean
private external fun nativeClearDatabase(): Boolean
private external fun nativeClearDatabase(dbPath: String): Boolean
private external fun nativeGetMyNodeId(): Long
private external fun nativeRequestInviteCandidates(channelId: String): Boolean
private external fun nativeRequestInviteLink(channelId: String, targetNodeId: Long, requestId: Long): Boolean
@@ -1894,32 +1894,29 @@ class ChatViewModel : ViewModel() {
fun dismissDatabaseResetError() { _databaseReset.update { it.copy(failed = false) } }
/** Принятый из UI сброс живёт в ViewModel: смена вкладки/поворот экрана не отменяет восстановление ядра. */
/** Принятый сброс принадлежит очереди Application, включая закрытие Activity; NativeLib сериализует stop/clear/start. */
fun resetDatabase() {
if (_databaseReset.value.running || _generatingKeys.value) return
val retryCleanup = _databaseReset.value.failed && NativeLib.isCoreStopping()
_databaseReset.value = DatabaseResetState(running = true)
viewModelScope.launch {
val provider = ChatApplication.instance.configProvider
provider.changes.submit("reset database") {
try {
HeadlessService.awaitStartup()
withContext(Dispatchers.IO) {
val wasRunning = NativeLib.isReady()
NativeLib.stop()
withContext(Dispatchers.Main) { clearState() }
val cleared = NativeLib.clearDatabase()
if (!cleared) LogManager.addLog("ERROR", "CFG", "database reset rejected by native backend")
if (wasRunning) {
val provider = ChatApplication.instance.configProvider
val config = provider.buildConfigText().await()
check(NativeLib.restart(provider.getString("db.path") ?: "", provider.getInt("control.port"), config)) {
"restart after database reset rejected"
}
}
_databaseReset.update { it.copy(failed = !cleared) }
if (!retryCleanup) HeadlessService.awaitStartup()
val restore = (NativeLib.isReady() || retryCleanup) && withContext(Dispatchers.IO) { provider.getServiceEnabled() }
val path = withContext(Dispatchers.IO) { provider.getString("db.path") ?: ChatApplication.instance.filesDir.absolutePath }
val config = if (restore) provider.buildConfigText().await() else null
val cleared = NativeLib.resetDatabase(path, 9999, config, ::clearState)
if (!cleared) {
_databaseReset.update { it.copy(failed = true) }
throw java.io.IOException("Database cleanup failed; native diagnostic contains the affected paths")
}
LogManager.addLog("INFO", "CFG", "Database reset completed directory=$path restart=$restore")
} catch (e: kotlinx.coroutines.CancellationException) { throw e
} catch (e: Exception) {
LogManager.addLog("ERROR", "CFG", "database reset failed: ${e.javaClass.simpleName}: ${e.message}")
_databaseReset.update { it.copy(failed = true) }
throw e
} finally { _databaseReset.update { it.copy(running = false) } }
}
}
@@ -2578,9 +2578,13 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRegenerateKey
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeClearDatabase(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return utun_bridge_clear_database(g_db_path) == 0 ? JNI_TRUE : JNI_FALSE;
JNIEnv* env, jobject thiz, jstring directory) {
(void)thiz;
const char* path = jni_utf8(env, directory);
if (!path) return JNI_FALSE;
int result = utun_bridge_clear_database(path);
u_free((void*)path);
return result == 0 ? JNI_TRUE : JNI_FALSE;
}
/* ── Voice recording JNI ── */