47 changed files with 707 additions and 336 deletions
@ -0,0 +1,59 @@ |
|||||||
|
# Жизненный цикл ядра и сервисов |
||||||
|
|
||||||
|
Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks |
||||||
|
останавливаемых модулей. GUI передаёт команды в этот поток. |
||||||
|
|
||||||
|
```c |
||||||
|
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); |
||||||
|
if (!inst || utun_core_start(inst) < 0) /* обработать ошибку */; |
||||||
|
if (chat_service_start(inst) < 0) /* обработать ошибку */; |
||||||
|
if (utun_service_start(inst) < 0) /* обработать ошибку */; |
||||||
|
|
||||||
|
chat_service_stop(inst); /* UTUN продолжает работать */ |
||||||
|
chat_service_start(inst); /* загружает сохранённые каналы */ |
||||||
|
utun_service_stop(inst); /* чат продолжает работать */ |
||||||
|
utun_instance_destroy(inst); |
||||||
|
``` |
||||||
|
|
||||||
|
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён |
||||||
|
в конфигурации. Desktop GUI и Android запускают ядро и чат явно, без UTUN. |
||||||
|
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта |
||||||
|
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл |
||||||
|
приложения; освобождение ядра выполняет `utun_instance_destroy()`. |
||||||
|
|
||||||
|
| Владелец | Ресурсы | |
||||||
|
|---|---| |
||||||
|
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг | |
||||||
|
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport | |
||||||
|
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API | |
||||||
|
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery | |
||||||
|
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки | |
||||||
|
|
||||||
|
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь |
||||||
|
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте |
||||||
|
`:memory:` данные сохраняются только до уничтожения ядра. |
||||||
|
|
||||||
|
Общий транспорт живёт, пока есть его владельцы. Закрытие запроса одной группы |
||||||
|
не разрывает соединение, нужное другой группе. При остановке группы отменяются |
||||||
|
её планировщики, запросы, таймеры и ожидания очередей; отправляется LEAVE, |
||||||
|
закрываются логические каналы маршрутизатора и удаляется её ожидающий транзит |
||||||
|
на общих соединениях. Транзит других групп сохраняется. Отложенный LEAVE может временно |
||||||
|
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе. |
||||||
|
|
||||||
|
Остановка чата сначала прекращает внешние команды и фоновые задания, затем |
||||||
|
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core. |
||||||
|
`media_async_destroy()` дожидается завершения native workers и вызывает каждый |
||||||
|
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует. |
||||||
|
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное |
||||||
|
транскодирование или распознавание речи может задержать stop; принудительного |
||||||
|
прерывания этих библиотек нет. |
||||||
|
|
||||||
|
Статус транспорта UP подтверждает восстановление физического пути. READY сессии |
||||||
|
группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно |
||||||
|
его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи |
||||||
|
защищают от сообщений старой сессии и не задают срок жизни маршрута. |
||||||
|
|
||||||
|
Проверки: `test_services` проверяет независимый stop/start, общий NCD в CONNECTING, |
||||||
|
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену workers, |
||||||
|
заблокированного медиа-потока, транзита и ожиданий заменённой очереди. `test_chat_join_e2e` |
||||||
|
проверяет join и распространение membership между узлами без UTUN-сервиса. |
||||||
@ -0,0 +1,173 @@ |
|||||||
|
#include <assert.h> |
||||||
|
#include <string.h> |
||||||
|
#include <stdlib.h> |
||||||
|
#include <unistd.h> |
||||||
|
#include "utun_instance.h" |
||||||
|
#include "config_parser.h" |
||||||
|
#include "topo_group.h" |
||||||
|
#include "topo_node_sqlite.h" |
||||||
|
#include "route_lib.h" |
||||||
|
#include "etcp.h" |
||||||
|
#include "etcp_router.h" |
||||||
|
#include "media_delivery/media_index.h" |
||||||
|
#include "media_delivery/media_delivery_proto.h" |
||||||
|
#include "node_conn_direct.h" |
||||||
|
#include "chat/chat_core.h" |
||||||
|
#include "chat/db_sync.h" |
||||||
|
#include "media_async/media_async.h" |
||||||
|
#include "../lib/debug_config.h" |
||||||
|
|
||||||
|
struct job { int worked, done, error; }; |
||||||
|
static void work(void* arg) { ((struct job*)arg)->worked = 1; } |
||||||
|
static void done(void* arg, int err) { struct job* job = arg; job->done++; job->error = err; } |
||||||
|
static void ready(struct ll_queue* queue, void* arg) { (void)queue; (*(int*)arg)++; } |
||||||
|
|
||||||
|
/* Pending transit shares the physical connection but belongs to its group. */ |
||||||
|
static void pending_transit(struct ETCP_CONN* conn, uint64_t group_id, int* calls) { |
||||||
|
if (!conn->transit_queues) conn->transit_queues = queue_new(conn->instance->ua, 16, 0, 24, "test_transit"); |
||||||
|
assert(conn->transit_queues); |
||||||
|
struct TRANSIT_QUEUE* transit = (struct TRANSIT_QUEUE*)queue_entry_new(sizeof(*transit) - sizeof(struct ll_entry)); |
||||||
|
assert(transit); transit->group_id = group_id; transit->conn = conn; |
||||||
|
transit->src_node_id = 123; transit->dst_node_id = conn->peer_node_id; |
||||||
|
transit->q = queue_new(conn->instance->ua, 0, 0, 0, "test_transit_packets"); assert(transit->q); |
||||||
|
assert(queue_data_put_with_index(conn->transit_queues, &transit->ll) == 0); |
||||||
|
struct ll_entry* packet = ll_alloc_lldgram(1); assert(packet); packet->dgram[0] = 0; packet->len = 1; |
||||||
|
assert(queue_data_put(transit->q, packet) == 0); |
||||||
|
assert(queue_waiter_wait(conn->send_input_q, &transit->waiter, ready, calls) == 0); |
||||||
|
} |
||||||
|
|
||||||
|
static struct TOPO_GROUP* chat_group(struct UTUN_INSTANCE* inst) { |
||||||
|
for (struct ll_entry* e = inst->topo_groups->group_list->head; e; e = e->next) { |
||||||
|
struct TOPO_GROUP* group = (struct TOPO_GROUP*)e; |
||||||
|
if (group->group_type == TOPO_GROUP_TYPE_CHAT) return group; |
||||||
|
} |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
int main(void) { |
||||||
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); |
||||||
|
struct UASYNC* ua = uasync_create(); assert(ua); |
||||||
|
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, |
||||||
|
"[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" |
||||||
|
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" |
||||||
|
"[routing]\nmy_subnet=192.168.42.0/24\n[server: udp]\naddr=127.0.0.1:0\ntype=public\n" |
||||||
|
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"); |
||||||
|
assert(inst && !topo_groups_get_default(inst->topo_groups) && !inst->chat_core); |
||||||
|
inst->config->global.db_sync_enabled = 0; |
||||||
|
assert(utun_core_start(inst) == 0 && utun_core_start(inst) == 0); |
||||||
|
struct ETCP_SOCKET* socket = inst->etcp_sockets; |
||||||
|
sqlite3* db = inst->topo_sqlite_db; |
||||||
|
struct TOPO_NODE* identity = topo_node_registry_find(inst->topo_groups, inst->node_id); |
||||||
|
assert(identity && identity->v4_addrs); |
||||||
|
assert(chat_service_start(inst) == 0 && chat_service_start(inst) == 0); |
||||||
|
assert(!topo_groups_get_default(inst->topo_groups)); |
||||||
|
chat_core_create_channel_auto(inst, "lifecycle"); |
||||||
|
struct TOPO_GROUP* chat = chat_group(inst); assert(chat); |
||||||
|
uint64_t chat_id = chat->group_id; |
||||||
|
assert(utun_service_start(inst) == 0 && utun_service_start(inst) == 0); |
||||||
|
assert(inst->rt->count > 0); |
||||||
|
struct TOPO_GROUP* vpn = topo_groups_get_default(inst->topo_groups); assert(vpn); |
||||||
|
|
||||||
|
struct SC_MYKEYS keys; assert(sc_generate_keypair(&keys) == SC_OK); |
||||||
|
struct TOPO_ADDR4 address = { .addr = {127, 0, 0, 1}, .port = 9, .protocol = TOPO_PROTO_UDP }; |
||||||
|
struct TOPO_NODE node = { .v4_addrs = &address }; |
||||||
|
memcpy(node.public_key, keys.public_key, 32); node.node_id = sc_derive_node_id_from_pubkey(keys.public_key); |
||||||
|
uint8_t sig[64] = {1}; |
||||||
|
assert(topo_node_sqlite_member_block_put(db, chat->channel_id, node.node_id, sig, 1, sig, 1, |
||||||
|
keys.public_key, keys.public_key, "{}", NULL, 0, sig) == 0); |
||||||
|
struct NODE_CONN_DIRECT* seed = NULL; |
||||||
|
assert(node_conn_direct_open_node(inst, node.node_id, NULL, NULL, &seed, &node, NULL) >= 0); |
||||||
|
struct ETCP_CONN* conn = node_conn_direct_get_conn(seed); assert(conn); |
||||||
|
struct TOPO_PEER_REQUEST *chat_request = NULL, *vpn_request = NULL; |
||||||
|
assert(topo_group_peer_open(chat, node.node_id, &chat_request) == 0); |
||||||
|
assert(topo_group_peer_open(vpn, node.node_id, &vpn_request) == 0); |
||||||
|
node_conn_direct_close(seed); |
||||||
|
utun_service_stop(inst); utun_service_stop(inst); |
||||||
|
assert(!conn->close_requested && topo_group_peer_conn(chat_request) == conn); |
||||||
|
assert(topo_group_peer_phase(vpn_request) == TOPO_PEER_FAILED && inst->rt->count == 0); |
||||||
|
topo_group_peer_close(vpn_request); |
||||||
|
assert(inst->etcp_sockets == socket && inst->topo_sqlite_db == db && inst->chat_started); |
||||||
|
assert(topo_node_registry_find(inst->topo_groups, inst->node_id) == identity); |
||||||
|
assert(topo_node_update_my_addresses(inst) >= 0); |
||||||
|
|
||||||
|
assert(utun_service_start(inst) == 0); |
||||||
|
vpn = topo_groups_get_default(inst->topo_groups); |
||||||
|
assert(topo_group_peer_open(vpn, node.node_id, &vpn_request) == 0); |
||||||
|
/* Stop a real stream while its router send queue is blocked. */ |
||||||
|
char path[] = "/tmp/utun-service-stream-XXXXXX"; |
||||||
|
int fd = mkstemp(path); assert(fd >= 0); |
||||||
|
char bytes[8192] = {0}; assert(write(fd, bytes, sizeof(bytes)) == sizeof(bytes)); close(fd); |
||||||
|
chat_core_save_ui_state(inst, "media_base", "/tmp"); |
||||||
|
uint8_t media_id[16] = {1}, block_id[16] = {2}, hash[32] = {3}; |
||||||
|
assert(media_index_register_downloaded(db, media_id, block_id, hash, chat->channel_id, |
||||||
|
strrchr(path, '/') + 1, inst->node_id, sizeof(bytes), sizeof(bytes), 0, 0) == 0); |
||||||
|
struct ETCP_ROUTER_CONN* routed = etcp_router_conn_get(inst, chat_id, node.node_id, ETCP_RT_ID_MEDIA_DELIVERY); |
||||||
|
assert(routed); |
||||||
|
struct queue_waiter_handle waiter = {0}; int ready_calls = 0; |
||||||
|
struct ll_queue* old_queue = routed->send_q; |
||||||
|
queue_set_threshold(old_queue, -1, 0); |
||||||
|
etcp_router_on_send_ready(inst, chat_id, node.node_id, ETCP_RT_ID_MEDIA_DELIVERY, &waiter, ready, &ready_calls); |
||||||
|
assert(waiter.internal); |
||||||
|
etcp_router_conn_restart(inst, chat_id, node.node_id, ETCP_RT_ID_MEDIA_DELIVERY); |
||||||
|
assert(routed->send_q != old_queue); |
||||||
|
queue_set_threshold(routed->send_q, -1, 0); |
||||||
|
etcp_router_on_send_ready(inst, chat_id, node.node_id, ETCP_RT_ID_MEDIA_DELIVERY, &waiter, ready, &ready_calls); |
||||||
|
assert(!old_queue->waiter_head && waiter.internal); |
||||||
|
etcp_router_cancel_send_ready(inst, chat_id, node.node_id, ETCP_RT_ID_MEDIA_DELIVERY, &waiter); |
||||||
|
assert(!routed->send_q->waiter_head && !waiter.internal); |
||||||
|
queue_set_threshold(routed->send_q, 0, 0); |
||||||
|
routed->send_q->waiter_defer = 1; |
||||||
|
etcp_router_on_send_ready(inst, chat_id, node.node_id, ETCP_RT_ID_MEDIA_DELIVERY, &waiter, ready, &ready_calls); |
||||||
|
assert(waiter.call_soon_id); |
||||||
|
etcp_router_cancel_send_ready(inst, chat_id, node.node_id, ETCP_RT_ID_MEDIA_DELIVERY, &waiter); |
||||||
|
struct media_pkt_block_req request = { .subcmd = MEDIA_SUBCMD_BLOCK_REQ, .group_id = chat_id }; |
||||||
|
memcpy(request.media_id, media_id, 16); memcpy(request.block_id, block_id, 16); |
||||||
|
struct ll_entry* packet = ll_alloc_lldgram(ROUTER_SVC_PAYLOAD_OFF + sizeof(request)); assert(packet); |
||||||
|
memset(packet->dgram, 0, ROUTER_SVC_PAYLOAD_OFF); |
||||||
|
packet->dgram[0] = ETCP_RT_ID_MEDIA_DELIVERY; |
||||||
|
memcpy(packet->dgram + ROUTER_SVC_SRC_OFF, &node.node_id, 8); |
||||||
|
memcpy(packet->dgram + ROUTER_SVC_GROUP_OFF, &chat_id, 8); |
||||||
|
memcpy(packet->dgram + ROUTER_SVC_PAYLOAD_OFF, &request, sizeof(request)); |
||||||
|
packet->len = ROUTER_SVC_PAYLOAD_OFF + sizeof(request); |
||||||
|
inst->router_bindings.callbacks[ETCP_RT_ID_MEDIA_DELIVERY](conn, packet); |
||||||
|
assert(inst->md.streams && inst->md.active_streams == 1); |
||||||
|
struct job cancelled = {0}; |
||||||
|
media_async_submit(inst->media_async, ua, work, &cancelled, done, &cancelled); |
||||||
|
queue_set_threshold(conn->send_input_q, -1, 0); |
||||||
|
pending_transit(conn, chat_id, &ready_calls); |
||||||
|
pending_transit(conn, vpn->group_id, &ready_calls); |
||||||
|
chat_service_stop(inst); chat_service_stop(inst); |
||||||
|
assert(queue_entry_count(conn->transit_queues) == 1); |
||||||
|
assert(((struct TRANSIT_QUEUE*)conn->transit_queues->head)->group_id == vpn->group_id); |
||||||
|
assert(!inst->md.streams && routed->closed); unlink(path); |
||||||
|
assert(cancelled.worked && cancelled.done == 1 && cancelled.error == MEDIA_ASYNC_CANCELLED); |
||||||
|
assert(!chat_group(inst) && !inst->chat_core && !inst->chat_sync && !inst->media_async && !inst->md.initialized); |
||||||
|
assert(!db_sync_instance_find(inst, chat_id)); |
||||||
|
assert(!conn->close_requested && topo_group_peer_conn(vpn_request) == conn); |
||||||
|
assert(topo_group_peer_phase(chat_request) == TOPO_PEER_FAILED); |
||||||
|
topo_group_peer_close(chat_request); |
||||||
|
/* Remove the other group's pending transit before processing the event loop. */ |
||||||
|
etcp_router_close_group(inst, vpn->group_id); |
||||||
|
assert(queue_entry_count(conn->transit_queues) == 0); |
||||||
|
queue_set_threshold(conn->send_input_q, 0, 0); |
||||||
|
for (int i = 0; i < 3; i++) { |
||||||
|
assert(chat_service_start(inst) == 0 && chat_group(inst)->group_id == chat_id); |
||||||
|
assert(db_sync_instance_find(inst, chat_id)); |
||||||
|
chat_service_stop(inst); |
||||||
|
assert(inst->etcp_sockets == socket && inst->topo_sqlite_db == db && inst->utun_started); |
||||||
|
uasync_poll(ua, 0); |
||||||
|
} |
||||||
|
topo_group_peer_close(vpn_request); |
||||||
|
utun_service_stop(inst); |
||||||
|
struct job completed = {0}; |
||||||
|
struct media_async* jobs = media_async_create(); assert(jobs); |
||||||
|
media_async_submit(jobs, ua, work, &completed, done, &completed); |
||||||
|
while (!completed.done) uasync_poll(ua, 100); |
||||||
|
assert(completed.worked && completed.done == 1 && !completed.error); |
||||||
|
media_async_destroy(jobs); |
||||||
|
utun_instance_destroy(inst); uasync_poll(ua, 0); |
||||||
|
assert(!ready_calls); |
||||||
|
assert(ua->timer_alloc_count == ua->timer_free_count && ua->socket_alloc_count == ua->socket_free_count); |
||||||
|
uasync_destroy(ua, 0); |
||||||
|
return 0; |
||||||
|
} |
||||||
Loading…
Reference in new issue