349 lines
18 KiB
C
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;
|
|
}
|