/* Реальный instance_lite worker и voice; wrappers наблюдают потоки/порядок и отказы attach. */ #include "utun_instance.h" #include "call/call.h" #include "call/call_audio.h" #include "chat/chat_event.h" #include "transport_layer/secure_channel.h" #include "debug_config.h" #include "mem.h" #include "u_async.h" #include "../../chatgui-android/libutun_lite/instance_lite.h" #include "../../chatgui-android/jni_bridge/android_jni_bridge.h" #include "../../../tests/test_utils.h" #include #include #include static pthread_mutex_t observation_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t changed = PTHREAD_COND_INITIALIZER; static pthread_t ui, worker; static unsigned attached, detached, destroyed, loops_destroyed, started, exited, errors, checked; static struct UTUN_INSTANCE* last_detached; static int stream_failure, voice_failure; #define CHECK_ANDROID(value) do { if (!(value)) { \ DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Android audio lifecycle line=%d: %s", __LINE__, #value); return 1; \ } } while (0) static void observe(int condition, const char* message) { pthread_mutex_lock(&observation_lock); if (!condition) { ++errors; DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Android audio lifecycle: %s", message); } pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock); } int __real_android_stream_attach(struct UTUN_INSTANCE*); int __wrap_android_stream_attach(struct UTUN_INSTANCE* inst) { worker = pthread_self(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_INFO); observe(!pthread_equal(worker, ui), "media attach executed on UI thread"); if (stream_failure) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "Android audio lifecycle: injected stream attach failure"); return -1; } return __real_android_stream_attach(inst); } int __real_call_audio_init(struct UTUN_INSTANCE*); int __wrap_call_audio_init(struct UTUN_INSTANCE* inst) { observe(pthread_equal(pthread_self(), worker) && !pthread_equal(pthread_self(), ui), "voice init outside core worker"); int result = __real_call_audio_init(inst); observe(!result && inst->call_audio_ops, "voice registration missing"); pthread_mutex_lock(&observation_lock); ++attached; pthread_mutex_unlock(&observation_lock); if (voice_failure) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "Android audio lifecycle: injected failure after voice registration"); return -1; } return result; } void __real_call_audio_destroy(struct UTUN_INSTANCE*); void __wrap_call_audio_destroy(struct UTUN_INSTANCE* inst) { observe(!pthread_equal(pthread_self(), ui), "voice detach executed on UI thread"); __real_call_audio_destroy(inst); observe(!inst->call_audio_ops, "voice registration retained after detach"); pthread_mutex_lock(&observation_lock); ++detached; last_detached = inst; pthread_mutex_unlock(&observation_lock); } void __real_utun_instance_destroy(struct UTUN_INSTANCE*); void __wrap_utun_instance_destroy(struct UTUN_INSTANCE* inst) { observe(inst == last_detached && !inst->call_audio_ops, "core destroyed before voice detach"); __real_utun_instance_destroy(inst); pthread_mutex_lock(&observation_lock); ++destroyed; pthread_mutex_unlock(&observation_lock); } void __real_uasync_destroy(struct UASYNC*, int); void __wrap_uasync_destroy(struct UASYNC* ua, int flags) { observe(destroyed && detached >= destroyed, "loop destroyed before core detach/destroy"); __real_uasync_destroy(ua, flags); pthread_mutex_lock(&observation_lock); ++loops_destroyed; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock); } int __real_pthread_detach(pthread_t); int __wrap_pthread_detach(pthread_t thread) { int result = __real_pthread_detach(thread); if (pthread_equal(pthread_self(), worker) && pthread_equal(thread, worker)) { pthread_mutex_lock(&observation_lock); ++exited; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock); } return result; } struct UTUN_INSTANCE* __real_instance_lite_get_instance(void); struct UTUN_INSTANCE* __wrap_instance_lite_get_instance(void) { observe(!pthread_equal(pthread_self(), ui), "UI bridge reads raw instance"); return __real_instance_lite_get_instance(); } char* __wrap_utun_bridge_get_local_sockets_json(void) { return u_strdup("[]"); } static void event(int type, const uint8_t* data, int length) { (void)data; (void)length; if (type != CHAT_EVT_SERVICE_STARTED) return; observe(pthread_equal(pthread_self(), worker), "service ready event outside core worker"); pthread_mutex_lock(&observation_lock); ++started; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock); } static int wait_for(const unsigned* counter, unsigned expected) { struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += 10; pthread_mutex_lock(&observation_lock); int result = 0; while (*counter < expected && !result) result = pthread_cond_timedwait(&changed, &observation_lock, &deadline); int ready = *counter >= expected; pthread_mutex_unlock(&observation_lock); if (!ready) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Android audio lifecycle timed out expected=%u", expected); return ready; } 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"); pthread_mutex_lock(&observation_lock); ++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(); observe(inst && inst->running && inst->call_audio_ops, "core stop without live voice owner"); utun_instance_stop(inst); } int main(int argc, char** argv) { CHECK_ANDROID(argc == 2); int core_stop = !strcmp(argv[1], "core_stop") || !strcmp(argv[1], "restart_core_stop"); int restart = !strcmp(argv[1], "restart") || !strcmp(argv[1], "restart_core_stop"), normal = !strcmp(argv[1], "normal"); stream_failure = !strcmp(argv[1], "stream_failure"); voice_failure = !strcmp(argv[1], "voice_failure"); CHECK_ANDROID(restart || normal || core_stop || stream_failure || voice_failure); ui = pthread_self(); worker = ui; debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_INFO); utun_instance_set_tun_init_enabled(0); size_t allocated = u_get_allocated_count(); CHECK_ANDROID(utun_bridge_call_audio_start(42) == -1); char directory[512] = "/tmp/utun_android_audio_XXXXXX", config[1200], public_key[65], private_key[65]; CHECK_ANDROID(!test_mkdtemp(directory)); struct SC_MYKEYS keys; CHECK_ANDROID(sc_generate_keypair(&keys) == SC_OK); for (unsigned i = 0; i < SC_PUBKEY_SIZE; ++i) { snprintf(public_key + i * 2, 3, "%02x", keys.public_key[i]); snprintf(private_key + i * 2, 3, "%02x", keys.private_key[i]); } snprintf(config, sizeof(config), "[global]\ndb_path=%s\nauto_sockets=no\nmy_public_key=%s\nmy_private_key=%s\n" "[server: s1]\naddr=127.0.0.1:0\ntype=public\n[chatserver]\nstorage_autoload=0\n", directory, public_key, private_key); instance_lite_set_event_handler(event); CHECK_ANDROID(!instance_lite_start(config)); if (stream_failure || voice_failure) { CHECK_ANDROID(wait_for(&exited, 1)); 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(!utun_bridge_call_audio_start(42)); uasync_post(instance_lite_get_uasync(), inspect, NULL); CHECK_ANDROID(wait_for(&checked, 1)); utun_bridge_call_audio_stop(); if (restart) { instance_lite_restart(config); CHECK_ANDROID(wait_for(&started, 2) && !utun_bridge_call_audio_start(43)); uasync_post(instance_lite_get_uasync(), inspect, NULL); CHECK_ANDROID(wait_for(&checked, 2)); utun_bridge_call_audio_stop(); } if (core_stop) { uasync_post(instance_lite_get_uasync(), stop_core, NULL); CHECK_ANDROID(wait_for(&loops_destroyed, 1u + restart)); } instance_lite_stop(); CHECK_ANDROID(attached == 1u + restart && detached == attached && destroyed == attached && loops_destroyed == attached); } CHECK_ANDROID(!errors && !instance_lite_is_running() && utun_bridge_call_audio_start(44) == -1); CHECK_ANDROID(u_get_allocated_count() == allocated); DEBUG_INFO(DEBUG_CATEGORY_CALL, "Android audio lifecycle mode=%s attach=%u detach=%u destroyed=%u loops=%u allocations=%zu->%zu PASS", argv[1], attached, detached, destroyed, loops_destroyed, allocated, u_get_allocated_count()); return 0; }