18 KiB
Задачи по проекту
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
Текущая задача
[+] Q8/Android не подключается к локальному чатгуи (LAN) — сделано. Причина: тип сокета
в Android-конфиге был захардкожен type=public, из-за чего sock_match ставил
has_priv=0 и не создавал линки к приватным (LAN) адресам пиров (create_links … → 0 links).
Исправлено: (1) ip_is_public() вынесен в lib/platform_compat (единый классификатор
IPv4 с корректным ntohl); (2) в auto_socket_reconcile_config (Android-режим) тип сокета
выставляется по адресу (public→PUBLIC, приватный/link-local→NAT), конфиг type игнорируется;
(3) sock_collect_views больше не смотрит на type из конфига: has_priv — по классификации
локального адреса, has_inet=1 (исходящие считаем доступными), is_eim — по NAT-детекции
(hairpin). is_local_subnet упрощён до !ip_is_public(). Тест test_sock_match обновлён
(+ ip_is_public кейсы). check.sh: 86 passed / 1 failed / 1 skipped.
[ ] DM: прямой p2p чат с любым пользователем группы — отдельная DM-подсистема.
ТЗ и архитектура: /doc/dm_arch.md. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
тесты test_dm (крипто) и test_dm_e2e (интеграция). Остались задачи ниже.
[ ] Звонок (P2P аудио) — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный
каталог src/call/. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с.
Android: переключение источника (трубка/громкая/гарнитура).
Архитектура: /doc/chat_call_arch.md.
[+] **Сигналинг + релей медиа** — сделано. `call_proto.h` (svc `ETCP_RT_ID_CALL
0x36`, subcmd INVITE/RINGING/ACCEPT/DECLINE/BUSY/CANCEL/HANGUP/MEDIA),
`call.c` (машина состояний, реестр сессий, conn_mgr, ring-timeout 45с,
watchdog no-traffic 20с, потеря соединения → end, события
CHAT_EVT_CALL_* 34–40), `DEBUG_CATEGORY_CALL 32`. `test_call` (2 инстанса,
happy path + decline + media-релей). Приём медиа — через `call_set_media_recv_cb`.
[+] **Headless API** — сделано. Команды control-сокета `call_start/accept/decline/
hangup` + события `call_incoming/ringing/accepted/ended/declined` (JSON);
аудио-сокет `call/call_headless.c` (TCP, бинарный фрейм
`[type:1][call_id:8][len:2][data]`, HELLO/FRAME), конфиг `call_audio_bind`.
Разрыв conn_mgr — отложенно 500мс (`CALL_TEARDOWN_DELAY_TB`) после ENDED,
чтобы HANGUP успел долететь; защита от stale-handle через conn-status.
Тесты: `test_call` (сигналинг+медиа), `test_call_headless` (полный e2e через
control+audio сокеты).
[+] **Аудио-контур (time-stretch в сервисном слое)** — сделано. `src/call/audio_jitter.cpp`
(SoundTouch, C API `ajb_*`: адаптивный джиттер-буфер ≤1000мс, target 60мс,
tempo 1.0→1.6x — догон live без потери данных при сетевой паузе),
`src/call/call_tones.c` (глитч при сталле >500мс + тон завершения),
`src/call/call_audio.c` (Opus 48kHz/20мс + jitter + тоны + pull API финального PCM).
`call.c`: `call_audio_ops` (on_media c seq/ts, get_stats) + событие
`CHAT_EVT_CALL_STATS` раз в 1с [rtt_ms, buffer_ms, tempo_x100].
Desktop `call_audio_engine.cpp` — только miniaudio I/O (получает финальный PCM).
Тест `test_audio_jitter` (ёмкость/догон/стационар). Демон остаётся C-only.
[+] **Android** — сделано. JNI (`utun_bridge_call_*`/`nativeCall*`), AudioRecord/Track
(CallAudioEngine), роутинг источника (CallAudioRouter: EARPIECE/SPEAKER/HEADSET,
BT SCO), CallScreen + IncomingCallDialog + RingtonePlayer + IncomingCallManager,
`libutun_lite/call_audio.c` (собственный аудио-контур Opus 48kHz/20мс).
[+] **Единый голосовой стек (voice stack) + SoundTouch на Android** — сделано.
Унификация аудио-контура звонка: `libutun_voice` (статическая C++-либа с
C-интерфейсом `call_audio.h` = SoundTouch + `voice_jitter.cpp` + `call_audio.c`
+ `call_tones.c`), собирается через `src/call/voice_sources.cmake` и линкуется
desktop/Android/headless-CLI. Джиттер-буфер держит КОДИРОВАННЫЕ (Opus) кадры,
декодирует только на pull (непосредственно перед воспроизведением) — память ~48x
меньше и uasync-поток разгружен. Android переведён с push-FIFO (Kotlin ArrayDeque)
на pull-модель (`nativeCallAudioPull`), своего джиттера больше нет → убрана
асимметрия задержки (desktop↔Android). Headless-аудио-сокет отдаёт готовый PCM
(декод Opus→PCM, без тактирования; клиент сам задаёт темп). Диагностика:
`CALL_STATS` расширен [dropped, underruns] (раз в 1с), счётчики headless.
Удалены `audio_jitter.cpp` и `libutun_lite/call_audio.c`. Тест `test_voice_jitter`
(ёмкость/догон/стационар). Android — `ANDROID_STL=c++_static`.
[ ] **DM-звонки** (fallback `group_id==0`) — не сделано: `call_is_online`/`call_start`
требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет.
Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
Сделано (глобальные синглтоны переведены на UTUN_INSTANCE):
chat_setting→inst->chat_settings(struct chat_setting_state, state-level API для парсера конфига + instance-level для runtime).chat_event→inst->chat_event_handler(обработчик теперь получаетinst).chat_core(g_cc) →inst->chat_core(struct chat_core_ctx*).chat_sync(g_cs) →inst->chat_sync.member_synccallback-списки (g_props_cbks/g_apply_cbks) → вchat_core_ctx.chat_join(g_keys) → вchat_core_ctx.chat_msgmedia-счётчики/backfill → вchat_core_ctx.dm_core(g_dm) →inst->dm;dm_mailbox(g_mb) →inst->dm_mailbox.chat_headless_control(g_hc) →inst->headless.chat_whisper— сигнатурыinit/available/triggerпринимаютinst.- API протащено через
inst(chat_core_/dm_), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser). test_dm_e2e.cпереписан на однопоточный: одинuasync, триutun_instance(A,B,C), master state-machine, без fork. Фаза 2 (B offline) —utun_instance_destroy(B1)+ пересоздание B2.- Попутно исправлен 1-байтовый overflow в
dm_mailbox.c: mb_route_send(u_malloc(1+body_len)→1+1+body_len).
Статус сборки: make -C src (libutun.a + бинарник utun) и make -C tests — OK.
Осталось (по порядку)
[+] test_dm_e2e: зависание в utun_instance_destroy(B1) (фаза P_B_OFFLINE) — сделано.
Причин было несколько (не одна):
1. lib/u_async.c: если ближайший таймер уже истёк на входе в uasync_poll,
get_next_timeout возвращал {0,0}, timeout_ms=-1 → epoll_wait(-1) блокировался
навсегда, а process_timeouts (вызывается только после epoll_wait) не запускался.
Фикс: if (timeout_ms == -1 && heap не пуст) timeout_ms = 0.
2. routing_layer/etcp_router.c: router_send_conn с group_id=0 (глобальная
маршрутизация DM/mailbox) не находил маршрут (topo_groups_find(0)==NULL).
Фикс: fallback на instance_find_conn(inst, remote) при group==NULL.
3. dm/dm_core.c: dm_on_conn_status PULL-ил только беседы с подключившимся пиром;
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения
PULL для всех бесед, чей пир не подключён напрямую.
4. dm/dm_mailbox.c: не было ACK после PULL_RESP → storage не чистил dm_mail.
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK.
5. utun_instance.c: dm_core_init ставил deliver-cb до dm_mailbox_init →
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core.
test_dm_e2e проходит (несколько прогонов), test_dm тоже.
[+] chatgui-android: переезд на per-instance API — сделано.
instance_lite.c/h (+instance_lite_get_instance()), jni_bridge.c, standby.c,
headless_control.c, voice_recorder.c, attachment_sender.c, video_sender.c,
photo_sender.c — все call-сайты переведены на inst. libutun_lite компилируется,
jni_bridge.c — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.)
[+] test_chat_join: segfault (регресс рефакторинга) — сделано.
chat_join хранил join_keys в chat_core_ctx (CC(inst)), а тест использует chat_join
без chat_core → NULL-deref. Фикс: join_keys вынесен в UTUN_INSTANCE.join_keys
(у chat_join собственный init/destroy).
[ ] BGP-гонка: дропнутый REQUEST_TABLE (отдельная задача, как договорились).
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка
«conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки
`topo_group_send_table_request()`.
**Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк,
затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего
переподключение не стартует. Поэтому `return` в дедупе — правильный.
**В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу
(NODEINFO/узлы группы). Сценарий «разнесённого старта»:
1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn`
→ `topo_group_send_table_request`, `topo_group.c:602`).
2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка).
C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G →
дропает запрос.
3. C наконец создаёт G (`topo_groups_create_group` → обход connections →
`topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`.
A отвечает своей таблицей → **C узнаёт A**.
4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G.
Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость).
**Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно
идемпотентно `topo_group_send_table_request(group, conn)`:
```c
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
topo_group_send_table_request(group, conn); /* повторно, идемпотентно */
DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name);
return;
}
```
`topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE,
пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден.
**Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за
детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE —
поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный
воспроизводимый тест для этой гонки пока не написан.
[ ] «packet undecryptable» на линках C (вторичное). A, узнав B через BGP, через
topo_group_connect пытается поднять прямой A↔B линк (seskey 4777737b), но B не
настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением
(не пытаться соединяться с узлами, для которых нет [client]/ncd-конфигурации,
либо обрабатывать неудачу без спама).
[+] Обновить GUI call-сайты tools/chatgui (Qt, desktop) — сделано.
- В chat_core.h вынесены trampoline-структуры из .c: update_my_name_arg,
save_ui_state_arg, chat_setting_arg (были в chat_profile.c/chat_core.c).
- gui_bridge.h/impl: gui_bridge_set_inst()/gui_bridge_get_inst() (+g_inst).
- utun_node.cpp: gui_bridge_set_inst(m_instance), chat_event_set_handler(m_instance,…),
chat_core_sync_my_addresses(m_instance).
- Обновлены ~45 call-сайтов в 16 файлах (mainwindow, messagelist, messagedelegate,
memberlistmodel, memberpropsdialog, accountlist, invitedialog, inviteby,
joindialog, settingsdialog, channelsettingsdialog, soundsettingspage,
audiodevicesettingspage, storagesettingspage, statuspage, connmonitorwindow).
- Сборка cmake --build . --target vibechat — OK.
Попутно: chat_sync_connect_from_invite писал cs->pending_* на вызывающем (GUI) потоке —
перенёс в cm_invite_trampoline (uasync-поток), убрав гонку; заодно исправил утечку
inv->addrs_data и добавил null-check cs в трамплине.
[+] Прогон всех тестов — сделано. make clean && make -j4 OK; все тесты проходят
(кроме test_auto_socket_dynamic — пропуск «requires root»). test_dm_e2e стабилен
в нескольких прогонах. В test_dm_e2e при teardown остаётся timer-leak (~68 узлов,
router_no_route), не влияет на результат — стоит разобрать отдельно.
Открытые флаки
[ ] test_chat_join_e2e — стабильно падает (2/3 сценария): «J not signed by A»
get_sign_rc=-1 (signed_by-верификация invite). Не связан с socket-классификацией
(обнаружен при прогоне после фикса Q8/LAN). Скорее всего регресс join-протокола.
[ ] test_etcp_reconnect — флаки под параллельной нагрузкой make check -j4:
phase 4 (reconnect после server restart) таймаутит (sent=480 recv=0), при одиночном
запуске стабильно TEST PASSED. Наш код (chat_join/chat_sync) его не трогает.