Files
utun2/tools/chatgui/tests/test_android_audio_lifecycle.c
T
evgeny f521bb37f4 fix(call): align audio frontend ownership with core lifecycle
Keep the borrowed audio provider on UTUN_INSTANCE across chat service restarts.
Attach and detach Android voice from the core worker and reject DSP startup
after detach. Preserve TX owner epochs, pending slots and foreign-instance RX
isolation. Remove the unused cross-thread online API and keep its check local
to call.c.

Add eight lifecycle regressions with real cores and the Android host bridge.
Audit public call, DSP, clock, headless and desktop audio contracts and record
the findings and validation scope.

Validation: voice/desktop 38/38, four native call suites, native and both Android
ABI builds, full check 204/204 plus proxy/burst/load. JVM results remain Gradle
UP-TO-DATE (155 app and 15 router). No hardware runtime validation.
2026-10-26 20:22:46 +01:00

169 lines
8.3 KiB
C

/* Реальный 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 <pthread.h>
#include <string.h>
#include <time.h>
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_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);
}
int main(int argc, char** argv) {
CHECK_ANDROID(argc == 2);
int restart = !strcmp(argv[1], "restart"), normal = !strcmp(argv[1], "normal");
stream_failure = !strcmp(argv[1], "stream_failure"); voice_failure = !strcmp(argv[1], "voice_failure");
CHECK_ANDROID(restart || normal || 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();
}
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;
}