Files
utun2/tools/chatgui/tests/test_android_audio_lifecycle.c
T

349 lines
18 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>
#include <stdlib.h>
#include <openssl/sha.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, errors, checked;
static unsigned identity_failures;
static uint64_t first_node, current_node;
static struct UTUN_INSTANCE* last_detached;
static int stream_failure, voice_failure;
static int pause_loop, pause_instance, pause_attach;
static unsigned paused, release_worker, stop_requested, stop_entered, stop_done, joins, self_detaches, finished;
#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);
}
static void pause_worker(int enabled) {
if (!enabled) return;
pthread_mutex_lock(&observation_lock);
++paused; pthread_cond_broadcast(&changed);
while (!release_worker) pthread_cond_wait(&changed, &observation_lock);
pthread_mutex_unlock(&observation_lock);
}
struct UASYNC* __real_uasync_create(void);
struct UASYNC* __wrap_uasync_create(void) {
worker = pthread_self();
pause_worker(pause_loop);
return __real_uasync_create();
}
struct UTUN_INSTANCE* __real_utun_instance_create_from_config(struct UASYNC*, struct utun_config*);
struct UTUN_INSTANCE* __wrap_utun_instance_create_from_config(struct UASYNC* ua, struct utun_config* config) {
struct UTUN_INSTANCE* inst = __real_utun_instance_create_from_config(ua, config);
pause_worker(pause_instance);
return inst;
}
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");
pause_worker(pause_attach);
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(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(thread, worker)) {
pthread_mutex_lock(&observation_lock); ++self_detaches; pthread_mutex_unlock(&observation_lock);
}
return result;
}
int __real_pthread_join(pthread_t, void**);
int __wrap_pthread_join(pthread_t thread, void** result) {
int status = __real_pthread_join(thread, result);
if (pthread_equal(thread, worker)) {
pthread_mutex_lock(&observation_lock); ++joins; pthread_mutex_unlock(&observation_lock);
}
return status;
}
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");
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
current_node = inst->node_id;
if (!first_node) first_node = current_node;
pthread_mutex_lock(&observation_lock); ++started; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock);
}
static void log_event(int level, const char* category, const char* message) {
(void)level; (void)category;
if (strstr(message, "Android worker stop requested")) {
pthread_mutex_lock(&observation_lock); ++stop_requested; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock);
}
if (strstr(message, "Android worker finished")) {
worker = pthread_self();
pthread_mutex_lock(&observation_lock); ++finished; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock);
}
if (!strstr(message, "Android identity save failed") && !strstr(message, "Android identity key pair is invalid")) return;
pthread_mutex_lock(&observation_lock); ++identity_failures; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock);
}
static void* stop_worker(void* arg) {
(void)arg;
pthread_mutex_lock(&observation_lock); ++stop_entered; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock);
instance_lite_stop();
pthread_mutex_lock(&observation_lock); ++stop_done; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock);
return NULL;
}
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");
uint8_t public_key[SC_PUBKEY_SIZE], ed_public[SC_PUBKEY_SIZE], seed[SHA512_DIGEST_LENGTH];
observe(sc_compute_public_key_from_private(inst->my_keys.private_key, public_key) == SC_OK &&
!memcmp(public_key, inst->my_keys.public_key, sizeof(public_key)) &&
inst->node_id == sc_derive_node_id_from_pubkey(public_key) && inst->node_id == inst->config->global.my_node_id,
"X25519/node_id identity mismatch");
observe(sc_derive_ed25519_pubkey(inst->my_keys.private_key, ed_public) == SC_OK &&
!memcmp(ed_public, inst->my_ed25519_pubkey, sizeof(ed_public)), "Ed25519 public identity mismatch");
observe(SHA512(inst->my_keys.private_key, SC_PRIVKEY_SIZE, seed) &&
!memcmp(seed, inst->my_ed25519_privkey, SC_PRIVKEY_SIZE), "Ed25519 private identity mismatch");
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");
int cancel_restart = !strcmp(argv[1], "cancel_restart");
int slow = !strcmp(argv[1], "cancel_slow_attach");
int parse_failure = !strcmp(argv[1], "parse_failure");
pause_loop = !strcmp(argv[1], "cancel_loop_create");
pause_instance = !strcmp(argv[1], "cancel_instance_create");
pause_attach = !strcmp(argv[1], "cancel_attach") || slow;
int cancel = pause_loop || pause_instance || pause_attach || cancel_restart;
CHECK_ANDROID(restart || normal || core_stop || stream_failure || voice_failure || cancel || parse_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], config[1200], public_key[65], private_key[65];
const char* temporary = getenv("TMPDIR");
int length = snprintf(directory, sizeof(directory), "%s/utun_android_audio_XXXXXX", temporary ? temporary : "/tmp");
CHECK_ANDROID(length > 0 && length < (int)sizeof(directory));
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);
debug_set_log_hook(log_event);
if (parse_failure) {
for (unsigned i = 1; i <= 2; ++i) {
CHECK_ANDROID(!instance_lite_start("[global]\nmy_private_key=invalid\n"));
CHECK_ANDROID(wait_for(&finished, i));
}
instance_lite_stop();
CHECK_ANDROID(joins == 2 && !self_detaches && !started && u_get_allocated_count() == allocated);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "Android worker repeated parse failure: joins=%u detach=%u PASS", joins, self_detaches);
return 0;
}
CHECK_ANDROID(!instance_lite_start(config));
if (cancel) {
if (cancel_restart) {
CHECK_ANDROID(wait_for(&started, 1));
pause_instance = 1;
instance_lite_restart(config);
}
CHECK_ANDROID(wait_for(&paused, 1));
pthread_t stopper;
CHECK_ANDROID(!pthread_create(&stopper, NULL, stop_worker, NULL));
CHECK_ANDROID(wait_for(&stop_entered, 1));
struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += 3;
pthread_mutex_lock(&observation_lock);
while (!stop_requested && !stop_done) {
if (pthread_cond_timedwait(&changed, &observation_lock, &deadline)) break;
}
unsigned stopped_early = stop_done;
pthread_mutex_unlock(&observation_lock);
CHECK_ANDROID(stop_requested && !stopped_early);
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; }
pthread_mutex_lock(&observation_lock);
while (!stop_done) {
if (pthread_cond_timedwait(&changed, &observation_lock, &deadline)) break;
}
stopped_early = stop_done;
pthread_mutex_unlock(&observation_lock);
CHECK_ANDROID(!stopped_early && !self_detaches);
}
pthread_mutex_lock(&observation_lock); release_worker = 1; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&observation_lock);
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(!errors && u_get_allocated_count() == allocated);
/* Тот же singleton должен запуститься снова только после полного join. */
pause_loop = pause_instance = pause_attach = 0;
CHECK_ANDROID(!instance_lite_start(config) && wait_for(&started, 1u + cancel_restart));
instance_lite_stop();
CHECK_ANDROID(joins == 2 && !errors && u_get_allocated_count() == allocated);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "Android worker cancellation mode=%s joins=%u ready=%u detach=%u PASS",
argv[1], joins, started, self_detaches);
return 0;
}
if (stream_failure || voice_failure) {
CHECK_ANDROID(wait_for(&loops_destroyed, 1));
instance_lite_stop();
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();
char path[1024]; snprintf(path, sizeof(path), "%s/keys.conf", directory);
CHECK_ANDROID(!utun_mkdir(path, 0700));
CHECK_ANDROID(!instance_lite_regenerate_keys(config));
CHECK_ANDROID(wait_for(&identity_failures, 1));
CHECK_ANDROID(instance_lite_is_running() && started == 2 && attached == 2 && detached == 1 && current_node == first_node);
uasync_post(instance_lite_get_uasync(), inspect, NULL);
CHECK_ANDROID(wait_for(&checked, 3));
CHECK_ANDROID(!test_rmdir(path));
CHECK_ANDROID(!instance_lite_regenerate_keys(config));
CHECK_ANDROID(wait_for(&started, 3) && current_node != first_node && !utun_bridge_call_audio_start(44));
uasync_post(instance_lite_get_uasync(), inspect, NULL);
CHECK_ANDROID(wait_for(&checked, 4));
utun_bridge_call_audio_stop();
uint64_t regenerated_node = current_node;
/* Снимок INI был снят до смены ключей: обычный restart не должен возвращать прежнюю идентичность. */
instance_lite_restart(config);
CHECK_ANDROID(wait_for(&started, 4) && current_node == regenerated_node);
uasync_post(instance_lite_get_uasync(), inspect, NULL);
CHECK_ANDROID(wait_for(&checked, 5));
char backup[1024]; snprintf(backup, sizeof(backup), "%s/keys.backup", directory);
CHECK_ANDROID(!rename(path, backup));
FILE* invalid = fopen(path, "wb"); CHECK_ANDROID(invalid);
int written = fprintf(invalid, "public_key=%s\nprivate_key=%064d\n", public_key, 0);
int closed = fclose(invalid);
CHECK_ANDROID(written > 0 && !closed);
instance_lite_restart(config);
CHECK_ANDROID(wait_for(&identity_failures, 2));
CHECK_ANDROID(instance_lite_is_running() && started == 4 && current_node == regenerated_node && detached == 3);
uasync_post(instance_lite_get_uasync(), inspect, NULL);
CHECK_ANDROID(wait_for(&checked, 6));
CHECK_ANDROID(!test_unlink(path) && !rename(backup, path));
}
if (core_stop) {
uasync_post(instance_lite_get_uasync(), stop_core, NULL);
CHECK_ANDROID(wait_for(&loops_destroyed, 1u + 3u * restart));
}
instance_lite_stop();
CHECK_ANDROID(attached == 1u + 3u * 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_set_log_hook(NULL);
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;
}