You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

25 KiB

Задачи по проекту

Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.

Текущая задача

[+] Silero VAD в lib/ — добавлен детектор голосовой активности (Silero VAD v5, стриминговая ONNX-модель + обёртка lib/silero_vad.c/h над ONNX Runtime C API). API: silero_vad_create/create_default/destroy/reset/process/process_pcm16 (окно 512 сэмплов @16 кГц, вероятность речи [0..1], рекуррентное состояние). Модель вшита в бинарник (silero_vad_model.inc, create_default), файл не нужен. Категория лога vad. Модуль под #ifdef HAVE_SILERO_VAD — без onnxruntime компилируется в стабы, поэтому GLOB-сборки (chatgui/Android) не ломаются. Сборка: --with-silero-vad (autotools), -DHAVE_SILERO_VAD + libonnxruntime (chatgui CMake, Android NDK). make check: 111/0/1.

[+] Авто-PTT рации по VAD — сделано. Отдельный режим radio_vad_enabled (ручной PTT не меняется, при зажатой кнопке — override). Сервисный слой radio_audio.c: ресемплер 48k→16k моно (3-tap box + 3:1, стерео микшируется), накопитель окон 512, фильтр-гистерезис + машина состояний (src/radio/radio_vad.h, чистый, юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога radio_vad_threshold, стоп при тишине radio_vad_hangover_ms (200мс) или чужом разговоре (busy = RX-кадр <200мс). Pre-roll radio_vad_pre_roll_ms (200мс) — кольцевой буфер сырого PCM, флашится при срабатывании («передаём за 200мс до»). Компрессор в VAD-режиме байпасится. Desktop radio_audio_engine.cpp и Android RadioAudioEngine.kt фидят PCM постоянно в VAD-режиме; JNI radioVadMode(). Тест test_radio_vad (ресемплер + FSM). Android: onnxruntime AAR 1.23.2 в jniLibs/ + cpp/onnxruntime/include + -DHAVE_SILERO_VAD в CMake.

[+] chatgui (Qt) не собирался: C++ void в utun_node.cpp* — сделано. Причина: proxy_protocol.h (C-заголовок, включается в C++ utun_node.cpp) делал e->dgram = u_malloc(...) и struct proxy_flow* f = arg; без явных приведений — в C++ нет неявного void*→T*. Добавлены (uint8_t*)/(struct proxy_flow*) (валидны и в C). vibechat собирается (47МБ, с вшитой моделью + onnxruntime).

[+] test_auto_socket_dynamic: root-регрессия и нагрузка — изолированный namespace, корректные маршруты, двусторонний поток через backpressure, строгие seq/payload, пределы TX/RX, остановка при обрыве и drain после восстановления, баланс ресурсов. Исправлены привязка TCP к интерфейсу, удаление TCP моделей/линков/портов из БД, sender receive window при selective ACK, timestamp overflow и нулевой jitter. ASan/UBSan/LSan: PASS; дополнительно исправлены невыровненные метки аллокаторов и PID-порты в test_etcp_connect. Разбор и логи.

[ ] debug_parse_config: формат и индексы категорий — parser ожидает :, документация описывает =; преобразование битовой маски осталось после перехода enum на индексы. Разбор.

[+] 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) маршрута нет.

Звонок: две фазы исходящего вызова (индикация + гудки)

[+] Две фазы вызова — сделано. Исходящий звонок разбит на 2 фазы с индикацией в окне вызова и разными гудками: - фаза 1 «Пытаемся связаться…» (OUTGOING — INVITE/маршрут) — гудок «соединение»; - фаза 2 «Звоним…» (RINGING — у пира идёт вызов) — длинные гудки (ringback). Desktop: CallWindow::Ringing + SoundManager loop call_connecting/call_ringing (MP3 в qrc), переключение по onCallRinging, остановка по accepted/ended/error; добавлена обработка GUI_EVT_CALL_ERROR (40) — раньше при «пир офлайн» окно/гудок зависали навсегда. Android: CallRingback (MediaPlayer loop из res/raw), подписи OUTGOING/RINGING в CallScreen + ActiveCallBanner. Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing).

[+] Надёжность отмены вызова (кодограмма + watchdog) — сделано. Отмена вызова (HANGUP/CANCEL) шлётся один раз, без ретрансмита, и при флапе маршрута (RST/epoch, прямой TCP к мобиле не поднимается — только релей) терялась: вызываемый висел в RINGING вечно и отвечал BUSY на все новые INVITE. Исправлено моделью «сердцебиения»: - вызывающий шлёт кодограмму INVITE каждые 300мс весь период вызова (OUTGOING+RINGING); - у вызываемого watchdog 2с — нет кодограммы → вызов сорван (CALL_REASON_CANCEL); - завершающие кодограммы без ретрансмиссий (потерю покрывает watchdog); - запоздалые кодограммы вызова при разговоре (CONNECTING/ACTIVE) игнорируются. CALL_INVITE_RETRY_TB=3000, CALL_RING_DEAD_TB=20000, CALL_NO_ROUTE_MAX_STRIKES=20.

Рефакторинг: 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_sync callback-списки (g_props_cbks/g_apply_cbks) → в chat_core_ctx.
  • chat_join (g_keys) → в chat_core_ctx.
  • chat_msg media-счётчики/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), не влияет на результат — стоит разобрать отдельно.

[ ] lwip/proxy: утечка pcb в FIN_WAIT_2 — tcp_shutdown(pcb,0,1) (half-close по FIN от exit) не ставит TF_RXCLOSED, поэтому таймаут FIN_WAIT_2 в tcp_slowtmr не срабатывает: если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash). Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).

Открытые флаки

[+] test_chat_join_e2e — сделано. Причина: ложный критерий успеха джойнера J. wait_group_started() проверял senders_list непуст в предположении «senders_list непуст ⟺ J получил JOIN_READY». После коммита b483335c tgi_to_channel_cb (topo_group_invite.c) при NCD_EVENT_UP стал вызывать topo_group_new_conn → conn попадает в senders_list сразу после установки TCP/UDP-соединения, ДО завершения хендшейка (JOIN_INFO_REQ→RESP→REQUEST→READY). J выходил «OK» раньше, чем A получал JOIN_REQUEST → A не подписывал J → wait_signed на A/C падал (get_sign_rc=-1). Фикс: критерий J — событие CHAT_EVT_CONNECT_RESULT(result=0) (JOIN_READY), ловится через chat_event_set_handler; wait_group_started удалён. Это также убрало гонку «C вышел раньше, чем J синкает свой рекорд» (C выходит сразу после wait_signed, не дожидаясь merkle-синка J). 5 прогонов стабильно 3/3.

[ ] test_etcp_reconnect — флаки под параллельной нагрузкой make check -j4: phase 4 (reconnect после server restart) таймаутит (sent=480 recv=0), при одиночном запуске стабильно TEST PASSED. Наш код (chat_join/chat_sync) его не трогает.