From 4bece684da6600fde97c0dd571e1607f4d7898d5 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 20:07:51 +0100 Subject: [PATCH] Queue Android activity updates while holding the worker lifetime lock --- .../libutun_lite/instance_lite.c | 32 +++++++++++++++++++ .../libutun_lite/instance_lite.h | 5 +++ .../tests/test_android_audio_lifecycle.c | 19 +++++++++++ 3 files changed, 56 insertions(+) diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index d8991a9b..103d205c 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -684,6 +684,38 @@ void instance_lite_restart(const char* new_config_text) { request_restart(new_config_text, 0); } +static void set_active_trampoline(void* arg) { + struct UTUN_INSTANCE* inst = __atomic_load_n(&g_inst, __ATOMIC_ACQUIRE); + if (!inst || __atomic_load_n(&g_stop, __ATOMIC_ACQUIRE)) { + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "Android activity command cancelled: worker is stopping"); return; + } + int active = (int)(intptr_t)arg; + if (utun_set_client_activity(inst, active) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Android activity transition failed requested=%d", active); return; + } + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "Android activity applied requested=%d effective=%u", active, (unsigned)inst->client_activity); +} + +int instance_lite_set_active(int active) { + if (g_is_worker) { IL_LOGE("activity request refused: frontend thread required"); return -1; } + pthread_mutex_lock(&g_stop_mutex); + struct UASYNC* ua = __atomic_load_n(&g_ua, __ATOMIC_ACQUIRE); + if (!ua || !__atomic_load_n(&g_running, __ATOMIC_ACQUIRE) || __atomic_load_n(&g_stop, __ATOMIC_ACQUIRE)) { + pthread_mutex_unlock(&g_stop_mutex); + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "Android activity request rejected: core is not ready"); return -1; + } + struct posted_task* task = u_malloc(sizeof(*task)); + if (!task) { + pthread_mutex_unlock(&g_stop_mutex); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Android activity request allocation failed"); return -1; + } + task->callback = set_active_trampoline; + task->arg = (void*)(intptr_t)!!active; + uasync_post_reserved(ua, task); + pthread_mutex_unlock(&g_stop_mutex); + return 0; +} + /* ── Лёгкое обновление сокетов (auto_sockets=android) ── */ static void update_sockets_trampoline(void* arg) { diff --git a/tools/chatgui-android/libutun_lite/instance_lite.h b/tools/chatgui-android/libutun_lite/instance_lite.h index 148afc32..d53c4ba0 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.h +++ b/tools/chatgui-android/libutun_lite/instance_lite.h @@ -66,6 +66,11 @@ int instance_lite_generate_keys(struct utun_config* config); * Последний ещё не взятый worker запрос restart/regenerate заменяет предыдущий. */ int instance_lite_regenerate_keys(const char* config_text); +/* Frontend: поставить ACTIVE (active!=0) / STANDBY (0) без заимствования сырого uasync. + * 0 — задача зарезервирована и поставлена живому worker; -1 — ядро не готово/останавливается, ошибка памяти или неверный поток. + * При stop ожидающая команда может отмениться. Результат применения/stream hold логируется в потоке ядра. */ +int instance_lite_set_active(int active); + /* Копирует INI, будит poll-loop и пересоздаёт instance и uasync внутри worker. * До остановки проверяет конфиг и сохраняет отсутствующие ключи; отказ оставляет прежнее ядро. * При инициализации ставит запрос в очередь; при завершившемся worker делает join/start. diff --git a/tools/chatgui/tests/test_android_audio_lifecycle.c b/tools/chatgui/tests/test_android_audio_lifecycle.c index 19dd4f71..10c0f6eb 100644 --- a/tools/chatgui/tests/test_android_audio_lifecycle.c +++ b/tools/chatgui/tests/test_android_audio_lifecycle.c @@ -22,6 +22,7 @@ static pthread_cond_t changed = PTHREAD_COND_INITIALIZER; static pthread_t ui, worker; static unsigned attached, detached, destroyed, loops_destroyed, started, errors, checked; static unsigned identity_failures; +static unsigned activity_checked; static uint64_t first_node, current_node; static struct UTUN_INSTANCE* last_detached; static int stream_failure, voice_failure; @@ -180,6 +181,7 @@ static void inspect(void* arg) { (void)arg; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); observe(inst && inst->call_audio_ops && inst->call_audio_ops->on_media, "ready core without audio provider"); + observe(instance_lite_set_active(1) == -1, "activity posting accepted from native callback"); observe(!strcmp(cfg_get_string("lifecycle", "missing"), "owned"), "config provider unavailable before callback stop"); observe(utun_bridge_destroy() == -1 && instance_lite_is_running(), "bridge destroyed resources from core callback"); observe(!strcmp(cfg_get_string("lifecycle", "missing"), "owned"), "failed stop cleared config callbacks"); @@ -195,6 +197,14 @@ static void inspect(void* arg) { pthread_mutex_lock(&observation_lock); ++checked; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock); } +static void inspect_activity(void* arg) { + struct UTUN_INSTANCE* inst = instance_lite_get_instance(); + int expected = (int)(intptr_t)arg; + observe(inst && inst->client_activity_requested == expected && inst->client_activity == expected, + "activity command did not run before subsequent worker callback"); + pthread_mutex_lock(&observation_lock); ++activity_checked; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock); +} + static void stop_core(void* arg) { (void)arg; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); @@ -255,6 +265,7 @@ int main(int argc, char** argv) { instance_lite_restart(config); } CHECK_ANDROID(wait_for(&paused, 1)); + CHECK_ANDROID(instance_lite_set_active(1) == -1); pthread_t stopper; CHECK_ANDROID(!pthread_create(&stopper, NULL, stop_worker, NULL)); CHECK_ANDROID(wait_for(&stop_entered, 1)); @@ -266,6 +277,7 @@ int main(int argc, char** argv) { unsigned stopped_early = stop_done; pthread_mutex_unlock(&observation_lock); CHECK_ANDROID(stop_requested && !stopped_early); + CHECK_ANDROID(instance_lite_set_active(1) == -1); if (slow) { clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += 2; deadline.tv_nsec += 200000000; if (deadline.tv_nsec >= 1000000000) { ++deadline.tv_sec; deadline.tv_nsec -= 1000000000; } @@ -281,6 +293,7 @@ int main(int argc, char** argv) { CHECK_ANDROID(!pthread_join(stopper, NULL)); CHECK_ANDROID(stop_done == 1 && joins == 1 && !self_detaches && started == (unsigned)cancel_restart); CHECK_ANDROID(!instance_lite_is_running() && !__real_instance_lite_get_instance() && !instance_lite_get_uasync()); + CHECK_ANDROID(instance_lite_set_active(1) == -1); CHECK_ANDROID(!errors && u_get_allocated_count() == allocated); /* Тот же singleton должен запуститься снова только после полного join. */ pause_loop = pause_instance = pause_attach = 0; @@ -297,6 +310,12 @@ int main(int argc, char** argv) { CHECK_ANDROID(!started && attached == (unsigned)voice_failure && detached == 1 && destroyed == 1 && loops_destroyed == 1); } else { CHECK_ANDROID(wait_for(&started, 1) && instance_lite_is_running()); + CHECK_ANDROID(!instance_lite_set_active(1)); + uasync_post(instance_lite_get_uasync(), inspect_activity, (void*)(intptr_t)1); + CHECK_ANDROID(wait_for(&activity_checked, 1)); + CHECK_ANDROID(!instance_lite_set_active(0)); + uasync_post(instance_lite_get_uasync(), inspect_activity, NULL); + CHECK_ANDROID(wait_for(&activity_checked, 2)); CHECK_ANDROID(!utun_bridge_call_audio_start(42)); uasync_post(instance_lite_get_uasync(), inspect, NULL); CHECK_ANDROID(wait_for(&checked, 1));