42 KiB
Задачи по проекту
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
Текущая задача
[+] DM: надёжная доставка сообщений, первый этап pm.txt — 01.10.2026:
атомарные история/seq/outbox, подписанные точные квитанции после commit,
live BGP-маршрут и durable очередь на суперузле с проверкой подписанной роли.
Повторы, out-of-order, rollback БД, неверные ACK и поздний PUT проверены;
40 offline-сообщений доставлены после restart источника, первый offline DM —
после restart суперузла при выключенном источнике. Копии удалены по квитанции.
Подготовка ciphertext медиа использует ключ target и независимые nonce порций;
проверены границы 32 КиБ, пустые файлы, неверные ключи/UUID/hash и повреждения.
Clean-сборка ядра/Qt, test_dm, test_dm_e2e и полный check прошли:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load PASS.
Текущие гарантии и оставшаяся интеграция: doc/dm_arch.md.
[+] DM: сетевая доставка и pending-медиа по pm.txt — 01.10.2026:
отдельные owners dm_media/dm_mailbox_media и файловый transfer с CM handle,
подписанные метаданные и независимая квитанция файла после durable приёма.
Прямая доставка или S→M custody, restart S/M при выключенном A, доставка B,
повтор удаления до DELETE_ACK; поздние STORE/STORED не возрождают копии.
TTL M по умолчанию 7 дней, резервирование квоты, отмена transfer и stop workers.
Копирование, шифрование, хеширование, проверка и durable файловые операции —
в media_async; отправка по 8 КиБ через call_soon. Тест 8 МиБ/0,5 с сохраняет
heartbeat uasync (максимальный интервал 10,3 мс в проверенном прогоне).
Qt/Android: оригинальные файловые вложения, сохранение и статусы доставки;
PM-снимки из ядра асинхронные. Чистые сборки Qt и Android APK прошли.
test_dm_media/test_dm_worker, rollback/quota/cancel и полный check:
115 PASS, 0 FAIL, 1 SKIP; proxy/burst/load PASS. GUI на устройствах не проверялся.
Протокол, владение и проверки: doc/dm_arch.md.
[ ] uasync: ложные ERROR об утечке перед cleanup — 01.10.2026: test_dm_e2e и существующий test_conn_mgr проходят, но uasync_destroy() пишет Timer leak до освобождения отменённых heap/immediate таймеров. После cleanup счётчики сходятся, финальная проверка проходит. Перенести предварительные счётчики в DEBUG; ERROR оставить для реального расхождения после cleanup, отдельно показывать активные и отменённые таймеры.
[+] Статус подключения звонка в Qt и Android — фактический путь router: прямое / reverse / indirect / нет подключения. Учитывается BGP fallback; события только при изменении, общий таймер 1с, сброс при завершении. Проверены смена путей настоящего звонка, медиа, ASAN, Qt, Android APK и полный check.
[+] Единицы RTT в окне звонка — call_post_stats() переводит
ETCP_CONN.rtt_last из 0.1мс в миллисекунды до отправки CALL_STATS.
Исправление общее для Qt и Android. DEBUG-категория debug раз в секунду
показывает исходный RTT, RTT в мс, транспортного соседа и метрики аудио.
[+] Восстановление путей conn_mgr на протяжении жизни handle — начальный
TIMEOUT отделён от срока цикла проб. Поздний обрыв запускает новый цикл;
рабочий резерв и NCD сохраняются, при отсутствии резерва INDIRECT стартует сразу.
Циклы ограничены 15с, паузы 1→2→4→8→15с; новая версия узла или локальные сокеты
обходят паузу. Прямой READY останавливает поиск и получает приоритет.
Исходящие request_id и входящие сроки независимы; устаревшие ответы игнорируются.
UP/DOWN отражают общую доступность, TIMEOUT приходит один раз до первого UP.
Последний close отменяет восстановление, включая закрытие из callback.
Проверены десять сетевых сценариев с реальными данными: поздний обрыв,
повторные циклы, смена адресов/сокетов, потеря маршрута посредника, старый ACK,
начальный TIMEOUT с поздним UP и close. ASAN без ошибок; собраны Qt и Android APK.
Полный check: 113 PASS, 0 FAIL, 1 SKIP; пропущенный auto_socket_dynamic отдельно
прошёл с root в network namespace. Интеграционные proxy, burst и load прошли.
Алгоритм: src/routing_layer/conn_mgr_doc.md.
[ ] Общий лимит check.sh после clean — 01.10.2026: расчёт по времени предыдущего
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
до test_uasync_socket_race. Повторный запуск на готовой сборке с большим лимитом
прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов
либо учитывать clean при расчёте.
[ ] Паузы аудио Qt при звонке с SM — 01.10.2026, звонок
939a3ea36f3eeac3, прямой LAN TCP 192.168.40.250:44604 ↔ 192.168.40.247:44173.
Tcpdump: общие паузы входящего трафика до 645мс; ping до роутера в среднем
100мс, до SM 128мс. ПК на 2.4ГГц, SM на 5ГГц. Одновременно CallAudio: cb
падает до 20–40/с вместо 100/с, буфер Qt растёт до 2с, на SM повторяется refill.
В 8с strace аудиопоток продолжает обслуживать PulseAudio без долгих syscall;
причина редких аудиоколлбэков пока не установлена. Добавить измерения интервалов
и длительности callback, кадров capture/playback и сводку CALL_STATS; сравнить
сетевые задержки после подключения ПК к 5ГГц или кабелю.
Повторный звонок b2a423ed2d36ed83 (01:25:31–01:27:39): DIRECT сохраняется,
RTT 2–3мс; в начале на SM gap tone длится 1.8–2.0с примерно каждые 6с.
На Qt частота callback падает вплоть до 4/с, входящий PCM продолжает приходить:
буфер 65→465→1465→2079мс за 3с, затем растёт до нескольких секунд, dropped=0.
Локализовано прекращение обслуживания duplex-аудио Qt; ожидание backend и
задержка обработки PCM пока не разделены. Часы SM отстают от Qt примерно на 1.24с.
Добавлена диагностика backend/устройств, времени между callback и длительности
capture/playback, фактической частоты callback и PCM кадров за интервал.
Счётчики сбрасываются до старта устройства; нужна повторная запись с новой сборкой.
[+] Ложный сигнал потери звука при запуске звонка — 01.10.2026, звонок
bc3efadcac629947: Qt запускает аудиоколлбэки в 01:54:56.933, включает gap tone в
01:54:57.415 и выключает в 01:54:58.072 при первом воспроизведении PCM.
На SM между ACCEPT и первым захваченным кадром проходит около 1.15с;
часы SM отстают примерно на 1.23с. Qt стабильно обслуживает 100 callback/с,
RTT около 1мс. Исправлено: до первого PCM начальное ожидание проходит в тишине,
последующий перебой включает тон через 500мс. Стартовый сбой по-прежнему
ограничен существующим watchdog отсутствия медиа. Регрессия проверяет ожидание
1.5с, начальное накопление, настоящий перебой, неполный блок, новый звонок и завершение.
Ускорение запуска Android: router.start занимает около 775мс; система сообщает
длительное удержание AudioManager.updateAudioPortCache. Ещё около 370мс проходит
до запуска capture worker. Гудки теперь останавливаются до startAudio;
router использует снимок устройств с обновлением по событиям и явной переоценке,
capture запускается до подготовки/старта playback. Ошибка playback останавливает capture.
Добавлены длительности отдельных стадий, первого capture и PCM. Ядро, Qt и APK
собраны после clean; три аудиотеста прошли. Выигрыш на SM требует замера в новом звонке.
В звонке Qt 180f046ef22defc2 первый PCM пришёл через 1245мс без стартового gap tone.
Полный check вне песочницы: 113 PASS, 0 FAIL, 1 SKIP; auto_socket_dynamic отдельно
прошёл с root в private netns. Интеграционные proxy, burst и load прошли.
[ ] Оставшаяся задержка запуска аудио SM — после обновления 01.10.2026,
звонок 2b9ad4e21a54d8ba: стартового gap tone нет на обеих сторонах; Qt
стабильно работает около 100 callback/с, max_idle=15.3мс, буфер 55мс, RTT=2мс.
На SM первый capture появляется через 1140мс от startAudio (1164мс от ACCEPT).
router.start=781мс, получение снимка устройств=0мс, AudioRecord create=24мс,
startRecording=7мс; от начала startRecording до первого кадра=328мс.
Capture worker стартует раньше playback, но общий выигрыш пока не виден.
AudioTrack create=21мс, play=327мс; система сообщает AudioPortCache contention=497мс
и восстановление AudioTrack при setPreferredDevice. Разделить замеры регистрации
callback, BT-профиля и применения маршрута; отдельно проверить первый AudioRecord.read.
На SM входящий буфер за время подготовки вырос до 1149мс и к концу звонка снизился
до 570мс; ускорение старта playback также сократит начальное накопление.
Реализован независимый запуск AudioTrack и AudioRecord в play/capture потоках;
частичный старт отменяется до освобождения устройств/native. AEC измеряется после готовности обоих.
Добавлены callbackMs/btProfileMs/applyMs/refreshMs, длительность первого read и время первой записи playback.
Резерв теперь зависит от изменения задержки доставки по timestamps CALL_MEDIA, отдельно от глубины очереди;
стартовое накопление и догон не повышают запас. Сохранность FIFO и uint32 wrap проверяются регрессиями.
Сборки ядра, Qt и Android после clean успешны; три аудиотеста прошли. Полный check:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load прошли.
Регрессия refill учитывает остаток SoundTouch: два новых кадра могут уже заполнить запас 60мс.
SM и Q8 обновлены 01.10.2026 в 03:03. Выигрыш по времени требует пробного звонка с новой версией.
[ ] Гонка invite-файла в test_chat_join_e2e — wait_file() проверяет только
существование файла, а wf() создаёт его до записи. Читатель иногда получает
пустую ссылку и ошибку префикса utun://. Публиковать готовый файл атомарно.
[ ] Некорректное время в выводе check-proxy — успешные basic_1mb и
stress_sessions иногда печатают огромные положительные/отрицательные миллисекунды,
хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода.
[+] SpeexDSP AEC (эхоподавление) в lib/ — акустическое эхоподавление звонка.
Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в lib/speexdsp/,
обёртка lib/speex_aec.c/h (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних
зависимостей). Собственная линия задержки рендера на delay_frames кадров +
синхронный speex_echo_cancellation (встроенный буфер SpeexDSP фиксирован в
2 кадра и для Android не годится); дрейф потоков — через переполнение/недозаполнение
линии (drop/dup + passthrough). Категория лога aec.
Ядро: call_audio_set_aec_enabled/delay_frames, AEC в feed_pcm (захват→encode)
и pull_pcm (рендер-референс), lifecycle в start/release. Desktop — настройка GUI
aec_enabled ([chatserver], читается в call_audio_start). Android — включается
только на спикерфоне (route==SPEAKER, live через JNI callAudioSetAec),
задержка меряется AudioTrack/AudioRecord.getTimestamp; CallAudioEngine.playLoop
переведён на absolute-deadline (устранён систематический дрейф). Тест
test_speex_aec (подавление эха 25.5 дБ, сохранность речи 0.956, delay-line, NULL-гарды).
Сборки: autotools + chatgui CMake + Android CMake/APK.
[+] 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) удалена; передаётся текущий PCM.
Компрессор в 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.
Android: настройки VAD (включение, порог, задержка после тишины); микрофон в PTT
создаётся только для передачи. PTT запускается при касании, после отпускания
передаёт ещё 250мс; повторное касание отменяет завершение (двойной тап без FIN).
VAD работает 15 минут при горящем экране;
SCREEN_OFF освобождает захват, SCREEN_ON автоматически открывает новое окно.
RX остаётся активен; TX освобождается отдельно через radio_audio_capture_stop().
[+] 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_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), не влияет на результат — стоит разобрать отдельно.
[ ] 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) его не трогает.