dm: прямой обмен сообщениями (DM) + soundtouch + архитектура звонков
- src/dm/: dm_core/dm_crypto/dm_mailbox — прямой обмен сообщениями (DM) - lib/soundtouch-master/: vendored SoundTouch (изменение темпа/высоты аудио) - doc/dm_arch.md, doc/chat_call_arch.md: архитектура DM и звонков - tests: test_dm.c, test_dm_e2e.c - chat: рефакторинг chat_* модулей, member_sync, invite_build - chatgui/chatgui-android: синхронизация с новым API чата - lib/u_async, debug_config, utun_instance: сопутствующие изменения
This commit is contained in:
1 parent
761f64cf76
commit
73fe99b33f
123 files changed
+14095
-1270
No files matched your search
@@ -0,0 +1,199 @@
|
||||
# P2P аудио-звонок пользователю — архитектура
|
||||
|
||||
Статус: спроектировано, ждёт реализации.
|
||||
|
||||
## 1. Область
|
||||
|
||||
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
|
||||
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
|
||||
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
|
||||
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
|
||||
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
|
||||
|
||||
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
|
||||
установка соединения (conn_mgr) → аудиопоток → HANGUP.
|
||||
|
||||
## 2. Разбиение файлов (отдельный каталог `src/call/`)
|
||||
|
||||
| Файл | Язык | Сборки | Назначение |
|
||||
|---|---|---|---|
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x34` |
|
||||
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
|
||||
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
|
||||
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
|
||||
| `src/call/audio_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
|
||||
|
||||
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
|
||||
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
|
||||
|
||||
Новая категория логирования: `DEBUG_CATEGORY_CALL = 31` (добавить в `debug_config.h`,
|
||||
`DEBUG_CATEGORY_COUNT` → 32).
|
||||
|
||||
## 3. Протокол (svc_id 0x34, первый байт payload = subcmd)
|
||||
|
||||
Формат приёма (как media_delivery, одноуровневый): `entry->dgram[0]=svc_id`,
|
||||
`entry->dgram[1]=subcmd`, `entry->dgram[2..]=payload`.
|
||||
|
||||
```
|
||||
CALL_INVITE 0x01 {call_id:8, group_id:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
|
||||
CALL_RINGING 0x02 {call_id:8}
|
||||
CALL_ACCEPT 0x03 {call_id:8, agreed_tracks}
|
||||
CALL_DECLINE 0x04 {call_id:8, reason:1}
|
||||
CALL_BUSY 0x05 {call_id:8}
|
||||
CALL_CANCEL 0x06 {call_id:8}
|
||||
CALL_HANGUP 0x07 {call_id:8, reason:1}
|
||||
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload}
|
||||
```
|
||||
|
||||
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
|
||||
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
|
||||
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
|
||||
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
|
||||
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
|
||||
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
|
||||
|
||||
## 4. Машина состояний
|
||||
|
||||
```
|
||||
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → CONNECTING(ACCEPT, conn_mgr_open) → ACTIVE → ENDED
|
||||
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept, conn_mgr_open) → ACTIVE → ENDED
|
||||
```
|
||||
|
||||
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
|
||||
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
|
||||
|
||||
Таймауты:
|
||||
- ring-timeout 45с (нет ответа → CANCEL);
|
||||
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
|
||||
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
|
||||
|
||||
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
|
||||
|
||||
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`;
|
||||
`CONN_EVENT_UP` → ACTIVE. Медиа идёт `etcp_route_send` (после подъёма прямого линка
|
||||
роутер идёт напрямую — RTT минимален).
|
||||
|
||||
## 5. Отправка медиа — backpressure через локальный буфер
|
||||
|
||||
Требование: не дропать аудио при переполнении роутера — держим в локальном буфере
|
||||
отправки, дренируем при появлении места.
|
||||
|
||||
Механика (`etcp_router` уже предоставляет):
|
||||
- `etcp_route_send(force=0)` возвращает `-1`, когда `send_q` роутера полон
|
||||
(`count >= ROUTER_MAX_SEND_Q_PACKETS`).
|
||||
- `etcp_router_send_q_has_room(...)` — есть ли место (порог `ROUTER_MAX_SEND_Q_PACKETS-1`).
|
||||
- `etcp_router_on_send_ready(..., &waiter, drain_cb, arg)` — одноразовый waiter:
|
||||
колбэк вызывается, когда `send_q->count <= threshold` (освободилось место).
|
||||
|
||||
В сессии:
|
||||
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью
|
||||
~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
|
||||
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
|
||||
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=0)`;
|
||||
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход.
|
||||
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости).
|
||||
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в
|
||||
`media_delivery.c:stream_send_chunk_cb`).
|
||||
|
||||
## 6. Thread-модель (single-writer)
|
||||
|
||||
| Объект | Владелец |
|
||||
|---|---|
|
||||
| `call_session` (состояние, encoder, decoder, tx_q, таймеры) | uasync-поток |
|
||||
| Opus encoder (send) | аудио-поток (encode) |
|
||||
| Opus decoder (recv) | uasync-поток (`decode → ajb_push`) |
|
||||
| `ajb` push | uasync-поток |
|
||||
| `ajb` pull + SoundTouch | аудио-поток |
|
||||
| `call_tones` | аудио-поток |
|
||||
|
||||
Поток данных:
|
||||
- TX: аудио-поток кодирует → `call_send_encoded(call_id, opus, len)` → `uasync_post` →
|
||||
uasync: `tx_q` + `call_tx_flush` (backpressure).
|
||||
- RX: uasync принимает `CALL_MEDIA` → decode → `ajb_push`; аудио-поток `call_pull_pcm` → `ajb_pull` (+ тоны).
|
||||
|
||||
`ajb` = mutex-кольцо PCM (гранулярность 20ms/50 ops в сек — мьютекс бесплатен);
|
||||
SoundTouch инстанс заперт на pull-стороне.
|
||||
|
||||
**Контракт жизненного цикла:** GUI стартует аудио-устройство по `CALL_ACCEPTED`,
|
||||
останавливает по `CALL_ENDED` и только после остановки (join аудио-потока) вызывает
|
||||
`call_release(call_id)` (post в uasync) → uasync освобождает session/encoder/decoder/jb.
|
||||
Пока устройство работает — `call_pull_pcm`/`call_send_encoded` не гоняются с освобождением.
|
||||
|
||||
## 7. Адаптивный джиттер-буфер (SoundTouch)
|
||||
|
||||
Буфер ≤ **1000 мс**, target playout ~60 мс; при росте буфера плавно tempo → **1.6x**.
|
||||
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
|
||||
API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
|
||||
|
||||
## 8. Тоны и диагностика
|
||||
|
||||
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
|
||||
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
|
||||
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл.
|
||||
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
|
||||
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
|
||||
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
|
||||
|
||||
## 9. GUI-события (extend `chat_event.h` + `gui_bridge.h` + android)
|
||||
|
||||
```
|
||||
CHAT_EVT_CALL_INCOMING 32 [ch_id_len][ch_id][caller_node_id:8][call_id:8]
|
||||
CHAT_EVT_CALL_RINGING 33 [call_id:8]
|
||||
CHAT_EVT_CALL_ACCEPTED 34 [call_id:8]
|
||||
CHAT_EVT_CALL_ENDED 35 [call_id:8][reason:1]
|
||||
CHAT_EVT_CALL_DECLINED 36 [call_id:8][reason:1]
|
||||
CHAT_EVT_CALL_STATS 37 [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2]
|
||||
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
|
||||
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
|
||||
```
|
||||
|
||||
## 10. Публичный API (call.h)
|
||||
|
||||
```
|
||||
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
|
||||
void call_accept(call_id); // callee
|
||||
void call_decline(call_id);
|
||||
void call_hangup(call_id);
|
||||
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
|
||||
// аудио-поток (общий процессинг в C-ядре):
|
||||
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
|
||||
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
|
||||
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
|
||||
void call_release(call_id); // post → uasync: освободить аудио-объекты
|
||||
```
|
||||
|
||||
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
|
||||
|
||||
## 11. Платформенный I/O
|
||||
|
||||
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
|
||||
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
|
||||
+ события в `gui_bridge_impl.cpp`.
|
||||
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
|
||||
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
|
||||
`availableCommunicationDevices`; fallback `setSpeakerphoneOn` + `startBluetoothSco`),
|
||||
детект гарнитуры (`ACTION_HEADSET_PLUG`, BT-профиль, `ACTION_SCO_AUDIO_STATE_UPDATED`),
|
||||
кнопка-цикл трубка/громкая/гарнитура, авто-выбор гарнитуры. `nativeCallSetAudioRoute(route)`.
|
||||
|
||||
## 12. Сборка
|
||||
|
||||
- `src/Makefile.am` + `tests/Makefile.am`: `call.c` (C, без `.cpp`). `src/call/` в include path.
|
||||
- `tools/chatgui/libutun/CMakeLists.txt`: +`call_audio.c`, `call_tones.c`,
|
||||
`audio_jitter.cpp`, soundtouch sources (AAFilter, FIFOSampleBuffer, FIRFilter,
|
||||
InterpolateCubic/Linear/Shannon, RateTransposer, SoundTouch, TDStretch, cpu_detect_x86,
|
||||
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
|
||||
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
|
||||
`LANGUAGES C CXX`, те же файлы.
|
||||
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
|
||||
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
|
||||
|
||||
## 13. Порядок задач
|
||||
|
||||
1. `call_proto.h` + svc_id + `DEBUG_CATEGORY_CALL`.
|
||||
2. `call.c` (машина состояний, backpressure-media, watchdog, события, API) + `chat_core` хуки.
|
||||
3. `audio_jitter.cpp` (SoundTouch) + `test_audio_jitter.c`.
|
||||
4. `call_tones.c` + `call_audio.c` (Opus + фрейминг + тон-обвязка).
|
||||
5. Desktop: `call_audio_engine.cpp` + UI + события.
|
||||
6. Android: JNI + Kotlin AudioRecord/Track + переключение источника + UI.
|
||||
7. Сборка (Makefile.am / CMake обе).
|
||||
8. `test_call.c` (два инстанса), интеграция desktop↔desktop и desktop↔android.
|
||||
@@ -0,0 +1,93 @@
|
||||
# DM — прямой p2p чат между двумя пользователями группы
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Возможность начать личный чат с любым пользователем доступных групп.
|
||||
Пока target offline сообщения временно хранятся на storage-узлах.
|
||||
|
||||
## 2. Ключевые решения
|
||||
|
||||
- Отдельная DM-подсистема (НЕ канал: без TOPO_GROUP/member_sync/merkle).
|
||||
- Идентичность детерминированная: одна ветка на пару, согласование без handshake.
|
||||
- E2E-шифрование содержимого сразу.
|
||||
- Offline-хранение: dm_mailbox на storage-узлах с флагом dm_storage.
|
||||
- Доставка: fallback — сперва проверка наличия узла в BGP-группе; доступен →
|
||||
прямой send; нет связи → storage.
|
||||
|
||||
## 3. Идентичность и ключи (детерминированные)
|
||||
|
||||
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
|
||||
shared = X25519(my_x25519_priv, peer_x25519_pub)
|
||||
content_key = SHA256(shared || "utun_dm_content")
|
||||
|
||||
Обе стороны выводят одинаковые значения из своего privkey и pubkey пира
|
||||
(известен из общей группы). Смена x25519 у пира = новая ветка.
|
||||
|
||||
## 4. Модель данных (SQLite, общий chats.db)
|
||||
|
||||
dm_conversations(conv_id TEXT PK, peer_node_id, peer_x25519 BLOB, peer_ed25519 BLOB,
|
||||
peer_name TEXT, last_out_seq INT, last_in_seq INT, created_at INT, last_ts INT)
|
||||
dm_messages(conv_id TEXT, dir INT, seq INT, ts INT, author INT, ct TEXT,
|
||||
data BLOB, sig BLOB, PRIMARY KEY(conv_id,dir,seq))
|
||||
dm_mail(recipient INT, sender INT, conv_id TEXT, seq INT, ts INT, data BLOB, sig BLOB,
|
||||
ttl INT, PRIMARY KEY(recipient,sender,seq))
|
||||
|
||||
Два независимых направленных потока (A→B, B→A) с монотонным seq.
|
||||
Без chain-хеша: для 2 пиров достаточно seq + catch-up по диапазону.
|
||||
|
||||
## 5. Доставка
|
||||
|
||||
1. Проверка доступности: peer присутствует в BGP-группе (общая группа).
|
||||
2. Доступен → прямой send (сервис ETCP_RT_ID_DM = 0x34), ждём DM_ACK{conv_id,seq}.
|
||||
Нет ACK за таймаут → fallback в mailbox.
|
||||
3. Нет связи → MAIL_PUT на 1..N storage-узлов.
|
||||
4. Catch-up при connect: DM_HELLO{conv_id,last_out_seq,last_in_seq} → пир досылает
|
||||
(in_seq+1 .. out_seq).
|
||||
5. Dedup: вставка только при seq > last_in_seq.
|
||||
|
||||
## 6. E2E-шифрование
|
||||
|
||||
data_enc = nonce(13B, счётчик по seq) || AES-CCM_ct || tag
|
||||
|
||||
secure_channel уже умеет AES-CCM. Storage/relay видят только шифротекст.
|
||||
|
||||
## 7. Storage-узлы (dm_storage)
|
||||
|
||||
- Новый флаг dm_storage=yes в adm_tags (блок B владельца), распространяется
|
||||
существующим member_sync без нового синка.
|
||||
- DM читает adm_tags мемберов общих групп (peers_<ch>) и кэширует storage-узлы.
|
||||
- Протокол mailbox (сервис ETCP_RT_ID_DM_MAILBOX = 0x35):
|
||||
|
||||
MAIL_PUT {recipient,sender,conv_id,seq,ts,data,sig}
|
||||
MAIL_PULL {recipient, per-sender cursor}
|
||||
MAIL_ACK {up_to_seq}
|
||||
|
||||
TTL 7 дней + лимит объёма, dedup по (recipient,sender,seq).
|
||||
|
||||
## 8. Старт и anti-spam
|
||||
|
||||
- dm_start(peer): создать беседу (вывести conv_id/ключи), обеспечить прямое
|
||||
соединение (переиспользовать chat_sync_connect_node/node_conn_direct).
|
||||
- Пир автопринимает на первом DM_HELLO/сообщении, только если отправитель в
|
||||
общей группе.
|
||||
|
||||
## 9. Файлы и интеграция
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| src/dm/dm_core.c/h | беседы, send/recv, таблицы, bind 0x34, conn-события, fallback |
|
||||
| src/dm/dm_crypto.c/h | derive + AES-CCM |
|
||||
| src/dm/dm_mailbox.c/h | storage-узел, push/pull/ack, bind 0x35 |
|
||||
|
||||
Плюс:
|
||||
|
||||
- src/chat/chat_member.c — бит CHAT_MEMBER_FLAG_DM_STORAGE + парсинг dm_storage.
|
||||
- utun_instance.c — dm_core_init/destroy.
|
||||
- chat_core — API dm_start/send/list + события CHAT_EVT_DM_*.
|
||||
- headless CLI — dm list/send/messages.
|
||||
- src/Makefile.am — новые источники.
|
||||
|
||||
## 10. Сервисы ETCP
|
||||
|
||||
ETCP_RT_ID_DM = 0x34
|
||||
ETCP_RT_ID_DM_MAILBOX = 0x35
|
||||
+134
-3
@@ -1,6 +1,137 @@
|
||||
# Задачи по проекту
|
||||
|
||||
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
|
||||
|
||||
## Текущая задача
|
||||
[ ] **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`. Статус: спроектировано, ждёт реализации.
|
||||
|
||||
## Рефакторинг: 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`), не влияет на результат — стоит разобрать отдельно.
|
||||
|
||||
## Открытые флаки
|
||||
- **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
|
||||
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
|
||||
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.
|
||||
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
|
||||
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
|
||||
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.
|
||||
Reference in new issue
Block a user