Files
utun2/tools/chatgui-android/doc/ARCHITECTURE.md
T

190 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# chatgui-android — архитектура
## Состав и сборка
Android-клиент использует общее ядро из `lib/` и `src/`: ETCP с UDP-линками и
STCP-линками поверх TCP, NCD, группы/BGP, ETCP-router, чат, DM и media-сервисы.
Отдельной Android-реализации chat_sync/merkle_sync и отдельного lite-instance нет.
STCP передаёт ETCP-пакеты; его потоковое шифрование — AES-CTR, пакетное ETCP — AES-CCM.
Контракты транспорта описаны в `../../../src/transport_layer/stcp.h` и `etcp.h`.
`libutun_lite/utun_sources.cmake` собирает `lib/*.c`, LMDB и рекурсивно `src/*.c`,
исключая `utun.c`, каталоги lwip_orig/uip/transport_layer/BBR и файлы отдельного
голосового стека. Это источник актуального состава, а не список «только TCP».
Файлы Android-обёртки перечислены в `_cfg_src`; старая копия `invite_link_c.c` туда не входит.
NDK-сборка в `app/src/main/cpp/CMakeLists.txt` создаёт OBJECT-библиотеку `utun_core`
и итоговую `libutun.so`: ядро, JNI, UDP-лог, отдельная статическая `utun_voice`,
OpenSSL Crypto, Opus, SpeexDSP, ONNX Runtime. JNI распределён между
`android_jni_bridge.c`, `android_stream_jni.c`, `android_call_video_jni.c` и
`android_call_output_jni.c`. Голосовой стек собирается по `src/call/voice_sources.cmake`.
Параметры приложения определяет `app/build.gradle.kts`: minSdk 26, compile/targetSdk 36,
NDK 29.0.14206865, JVM 17, ABI arm64-v8a/x86_64. UI — Kotlin, Jetpack Compose и Material3;
CameraX отвечает за захват фото/видео, MediaCodec — за live video, AudioRecord/AudioTrack — за аудио.
## Владение ядром и потоки
`NativeLib.init(dbPath, controlPort)` сохраняет config/log callbacks и настраивает
voice_recorder. Он не запускает ядро и не открывает control-listener; controlPort
в этой ветке только логируется. Затем `NativeLib.start(configText)` передаёт INI
в `instance_lite_start()`.
Один worker pthread создаёт uasync и `UTUN_INSTANCE`, принудительно задаёт
CLIENT_TYPE_MOBILE и запускает `utun_core_start()` + `chat_service_start()`.
UTUN-сервис включается отдельно через `[gui] utun_enabled=1`, по умолчанию выключен.
Наличие TUN/routing/NAT-кода в сборке само по себе сервис не включает.
До `CHAT_EVT_SERVICE_STARTED` worker привязывает stream bridge и call_video_port,
а при UTUN_WITH_VOICE — call_audio. Успех start означает создание потока либо уже
существующий запуск; он не подтверждает готовность сервисов. Готовность проверяется
по событию и `instance_lite_is_running()`.
Команды UI обычно копируют аргументы и ставят работу в uasync. При этом C JSON-getters
и некоторые настройки читают SQLite/живые структуры синхронно в вызывающем потоке;
C `utun_bridge_set_member_nick()` пишет локальный nickname напрямую. C setter chat_setting
также синхронный, тогда как JNI nativeSetChatSetting использует trampoline.
Нельзя считать весь bridge автоматически выполняющимся в потоке ядра.
Заимствованные inst/ua не удерживают ресурс: frontend должен сериализовать вызовы
по ним с stop/restart; обход живых списков требует исключить их параллельное изменение.
JNI получает JNIEnv для native-потока и вызывает Kotlin `onEvent(type, ByteArray?)`.
Payload копируется в byte array; его формат зависит от CHAT_EVT_* из `src/chat/chat_event.h`.
Это не универсальный JSON. Отдельный C `utun_event_fn` не является обработчиком
бинарных событий instance_lite. ViewModel переносит обновление UI в свой coroutine context.
Перед stop/restart Kotlin сбрасывает controllers и останавливает внешние media workers.
Worker снимает media-привязки, уничтожает instance, снимает standby/UDP-таймеры и уничтожает uasync.
`instance_lite_stop()` ждёт до двух секунд: при timeout отсоединяет поток, поэтому возврат
не гарантирует завершённый cleanup. Без опубликованного instance он возвращается сразу.
Restart пересоздаёт instance и uasync. Дополнительный опрос is_running в bridge до 5с
может увидеть ещё старое работающее ядро; подтверждение нового запуска — SERVICE_STARTED.
Общие правила освобождения: `../../../doc/service_lifecycle.md`.
## Данные и конфигурация
Ядро владеет общей SQLite `db_path/chats.db`; путь без db_path даёт in-memory БД.
Chat использует тот же handle, что и топология. Схему создают общие модули
`src/routing_layer/topo_node_sqlite.c`, `src/chat/`, `src/dm/` и media-подсистемы.
Room/DbRepository и отдельного Kotlin SQL-соединения в приложении нет:
ChatRepository получает JSON через JNI и разбирает события, ViewModel хранит StateFlow.
Имена/форматы таблиц не следует дублировать в Android-коде.
ConfigProvider хранит настройки в DataStore, читает ключи и формирует общий INI
через buildConfigText/buildConfigFromServers. Callback-ключи вроде `node.name` и `db.path`
не являются именами INI-секций. Native config API копирует таблицу callbacks по значению;
функции и их окружение остаются у провайдера. Init без config и destroy снимают прежнюю
регистрацию; смену провайдера нужно сериализовать с cfg_get_*. Минимальный пример INI:
```ini
[global]
my_node_name=Android
# При отсутствии ключей начальный запуск сгенерирует X25519-пару.
db_path=/data/user/0/com.utun.chat/files
db_sync_enabled=1
chatserver_enabled=1
auto_sockets=android
[gui]
utun_enabled=0
[allowed_keys]
allow_all=yes
[ntp]
enabled=no
[debug]
chat=info
socket=info
```
Секции `[server:<имя>]` с addr/type/transport/netif строятся из интерфейсов устройства.
NetworkChangeMonitor передаёт новый INI в updateSockets: uasync согласует сокеты через
`auto_socket_reload_config`, сохраняя ядро/чат. В auto_sockets=android общий
`auto_socket_on_network_change()` не запускает scan: нужен именно reload из Kotlin.
Настройки голоса, рации, stream, standby и хранения передаются в `[chat]`,
UDP-лог — в `[log_udp] ip/port`; полный набор задаёт ConfigProvider.
Начальная генерация ключей пытается сохранить их в `db_path/keys.conf`.
`instance_lite_regenerate_keys()` меняет ключи/node_id на месте, без пересоздания соединений.
## Каналы, приглашения и DM
Создание канала ставится в uasync; channel_id, переданный старому bridge API создания,
игнорируется: ядро генерирует ID. `connect_node(address,port,pubkey)` остался заглушкой.
Для подключения канала/участника используются общие chat_core/topo_group API.
`InviteLink.kt` получает предпросмотр через `NativeLib.decodeInvite()` — JNI вызывает
общий `src/chat/invite_link.c`. Ссылка `utun://...` содержит версию 0x03, пароль,
channel_id, join_key, REALITY-параметры и блоки публичного ключа/адресов.
JOIN передаёт исходную ссылку и request ID через потокобезопасный chat_join_port;
принятый запрос завершается CONNECT_RESULT с тем же request. Отрицательный возврат
submit означает отказ без события. Детали: `src/chat/chat_join.h`, `chat_join_port.h`.
Регистрация join_key подтверждается до выдачи ссылки. JOIN добавляет подписанного мембера,
Merkle распространяет запись; доступ к группе определяется локальной таблицей участников.
JOIN_READY добавления и READY групповой сессии — разные состояния.
DM-команды используют conv_id и поколение беседы. История приходит событием
CHAT_EVT_DM_MESSAGES, список — CHAT_EVT_DM_CONVERSATIONS. Frontend отбрасывает
ответы старого поколения/другой беседы. Voice/video принимают target `dm:<conv_id>`;
отдельные dm_* функции принимают conv_id без префикса. Общее устройство DM:
`../../../doc/dm_arch.md`.
## Вложения и media
Канальные text/file/photo используют chat_core_submit_trampoline. reply_to — ровно
32 байта исходного message_id, нули означают отсутствие ответа. request_id связывает
публикацию с CHAT_EVT_MSG_SUBMITTED; успешная постановка не означает доставку пиру.
Обычный файл и готовое фото остаются у caller и должны жить неизменными до завершения
асинхронного копирования/индексации. Фото здесь не декодируется и не масштабируется.
voice_recorder принимает int16 mono 48kHz, отсоединяет PCM/compressor при stop и
передаёт общий attachment_send. Запись короче 500мс по монотонным часам отбрасывается.
Готовый MP4 передаётся без транскода; при принятом video-запросе cache-файл принадлежит
задаче и удаляется также при ошибке/отмене. То же правило у отдельной DM-отправки файла.
При отказе постановки файл остаётся caller. Generation защищает публикацию в DM;
канальный reply_to/request_id фиксируется до начала подготовки.
Звонок и рация используют общий C голосовой стек. Kotlin владеет AudioRecord/AudioTrack,
feed/pull workers и маршрутом Android Telecom; DSP и сетевой сигналинг разделены.
Вызовы lifecycle DSP выполняются после остановки его внешних feed/pull callers.
Контракты PCM/PTS: `src/call/call_audio.h`, `src/radio/radio_audio.h`, `jni_bridge/android_jni_bridge.h`.
Live stream bridge владеет одним publisher и независимыми viewers поверх общих
`stream_publisher`/`stream_viewer`. UI использует session IDs, не inst/ua.
Данные идут через pull, а каталог источников — через CHAT_EVT_STREAM_SOURCES.
Pull возвращает owned буфер записей с 28-байтовым BE-заголовком; освобождение — u_free.
Demand ограничивает батч viewer; GAP/CLOSED требуют сбросить прежний frontend-буфер.
Точный формат, лимиты и срок жизни signal: `libutun_lite/stream_bridge.h`.
## Фон и диагностика
Android HeadlessService — foreground service с уведомлением и общим native-ядром,
а не Linux control-loop. Он запускает NetworkChangeMonitor и NetworkActivityMonitor.
Переход UI в фон передаёт client_activity в uasync. Standby чередует ACTIVE/SLEEP;
сам event loop продолжает работать. Ожидания SLEEP будятся после перехода в ACTIVE,
сетевой activity может разбудить после min_sleep. Контракт отмены: `libutun_lite/standby.h`.
DEBUG_* фильтруются общими уровнями/категориями. Bridge переводит их в logcat и,
при включённой подписке, Kotlin LogManager. Debug hook также пишет в UDP-буфер:
16KiB threshold либо timer 200мс, неблокирующая отправка без повторов. ERROR вызывает flush.
Target — числовой IPv4; attach/detach таймера выполняет uasync worker при каждом запуске/рестарте.
Ошибки самого UDP-логгера идут в отдельный utun-udplog, минуя debug hook.
## Linux headless и проверки
Каталог `headless/` — отдельный каркас: headless_main запускает select-loop и JSON-line
control socket, но не создаёт uasync/instance и не запускает чат. `-p` меняет порт,
`-l` только печатает имя файла. ping/status/connections/debug_level возвращают заглушки,
subscribe/quit управляют клиентом, ряд команд даёт not implemented. Текстовые команды
вида `send <channel> <text>` этот каркас не поддерживает. JOIN/settings требуют уже
запущенное ядро и исполнения в его потоке; каркас не заменяет рабочий headless API.
Для Linux сетевых сценариев используются основной `src/utun`,
`[chatserver] headless_control_bind=127.0.0.1:9999` и `tools/chatcli`.
Для APK: `cd tools/chatgui-android && ./gradlew assembleDebug`.
Проверки native bridge/standby доступны в `tests/Makefile.am`;
проверка независимости media workers на устройстве описана в
`../tests/stream_worker_smoke/README.md` и не проверяет сетевую доставку/UI.