Files
utun2/tools/chatgui-android/doc/IMPL_PLAN.md
T
evgeny 38c9f229bc stcp: ed25519 pubkey exchange in handshake, fix instance destroy segfault, fix chat_tcp_test
- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll
- stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close
- utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP),
  add deferred drain in both Phase J and L
- stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39,
  exchange ed25519_pubkey in both handshake directions
- etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in
  etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb
- chat_sync.c: fix lk->conn NULL dereference for TCP links
- test_stcp.c: update stcp_server_create/stcp_client_connect callsites
- tools/chat_tcp_test: change transport udp→tcp
2026-08-07 00:48:27 +03:00

466 lines
18 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 — План реализации
## Порядок этапов
```
Этап 1 ── Структура проекта + CMake
│
▼
Этап 2 ── libutun_lite (C-ядро)
│
▼
Этап 3 ── Headless CLI + control socket ◄── первый работающий артефакт
│ тестируется на Linux
▼
Этап 4 ── JNI bridge (C ↔ Kotlin)
│
▼
Этап 5 ── Android UI (Jetpack Compose) ◄── второй артефакт
│
▼
Этап 6 ── Android Headless Service ◄── третий артефакт
│
▼
Этап 7 ── End-to-end тесты
```
Ключевой принцип: **сначала работает headless → потом UI**. Сетевой стек отлаживается
на Linux без Android. Только когда P2P чат работает между двумя headless-экземплярами —
переходим к Android.
---
## Этап 1. Структура проекта и сборочная система
**Цель:** Собрать пустой проект, который компилируется (пока без логики).
### 1.1. Создать файлы
```
tools/chatgui-android/
├── CMakeLists.txt # корневой: add_subdirectory(libutun_lite; headless)
├── libutun_lite/
│ ├── CMakeLists.txt # libutun_lite.a из выбранных .c
│ └── utun_lite.h # заглушка API
└── headless/
├── CMakeLists.txt # headless binary
└── headless_main.c # int main() { return 0; }
```
### 1.2. libutun_lite/CMakeLists.txt
Список всех нужных .c файлов с путями к `../../src/` и `../../lib/`.
Использовать `file(GLOB ...)` для категорий (lib/*.c, src/transport_layer/*.c, src/chat/*.c),
затем `list(REMOVE_ITEM ...)` исключить ненужные.
Результат — статическая библиотека `libutun_lite.a`.
### 1.3. headless/CMakeLists.txt
Линкует `headless_main.c` + `libutun_lite.a` + `OpenSSL::Crypto` + `pthread` + `dl`.
### 1.4. Проверка
```bash
cd tools/chatgui-android && mkdir -p build && cd build
cmake .. && make -j4
```
Должно: собраться без ошибок, `./headless/headless` выходит с 0.
---
## Этап 2. Минимальное C-ядро (libutun_lite)
**Цель:** `libutun_lite.a` собирается из отобранных .c файлов без ошибок компиляции.
### 2.1. Выверка списка файлов
Пройти по каждому .c из списка в `ARCHITECTURE.md §9`, проверить что:
- Все `#include` разрешаются (пути к lib/ и src/ корректны)
- Нет зависимостей от исключённых модулей (etcp, tun, bgp, ...)
- Если есть зависимости — либо исключить файл, либо застабить
### 2.2. Застабить зависимости
Некоторые файлы могут ссылаться на исключённые модули. Варианты:
- Добавить `#ifndef CHATGUI_ANDROID ... #endif` с заглушками
- Вынести проблемный код в `_lite` версию файла
- Создать `stubs.c` с пустыми реализациями нужных функций
### 2.3. utun_instance_lite
Создать `src/utun_instance_lite.c/h` — облегчённый instance:
- Поля: `uasync* ua`, `SC_MYKEYS my_keys`, `sqlite3* db`, `stcp_server* srv`, `void* control_srv`
- Нет: tun, etcp_connections, routing, bgp, nat, firewall, proxy
### 2.4. config_parser — упрощение
Убрать секции, не нужные в Android-версии (tun, etcp, routing, bgp, nat, firewall, proxy).
Оставить: node, control, db, debug.
### 2.5. Проверка
```bash
cd tools/chatgui-android/build && make -j4
```
Все .c компилируются, линкуется libutun_lite.a.
---
## Этап 3. Headless CLI + управляющий сокет
**Цель:** Запустить headless-экземпляр, подключиться control-клиентом, выполнить базовые команды.
### 3.1. headless_main.c
```c
int main(int argc, char** argv) {
// 1. Парсинг аргументов: -c config.cfg [-p port] [-l logfile]
// 2. Чтение конфига (config_parser)
// 3. Инициализация ключей (генерация если нет)
// 4. Открытие БД (sqlite3_open, WAL, создание таблиц)
// 5. Создание uasync
// 6. Запуск STCP сервера (stcp_server_listen)
// 7. Запуск control сокета (headless_control_init)
// 8. uasync event loop (uasync_run)
// 9. cleanup
}
```
### 3.2. headless_control.c
Управляющий TCP-сокет с текстовым протоколом (см. ARCHITECTURE.md §6).
Реализация:
- `headless_control_init(ua, port)` — создать слушающий сокет
- При новом подключении: зарегистрировать fd в uasync, выделить буфер
- При данных: парсинг строк, выполнение команд, отправка ответа
- Поддержка subscribe: сохранить список подписанных клиентов, рассылать события
- Команды первого этапа (минимальный набор):
- `status` — показать состояние (node_id, port, uptime)
- `connections` — список STCP соединений (заглушка: "none")
- `debug` / `debug_level` — управление отладкой
- `help` — список команд
- `quit` — отключиться
### 3.3. Тестирование этапа 3
```bash
# Терминал 1: запуск сервера
./headless -c test_node1.cfg -p 9999 -l node1.log
# Терминал 2: подключение control-клиентом
nc localhost 9999
> status
node_id: 0xABCD...
uptime: 5s
connections: 0
> help
...
> quit
bye
```
**Критерий:** headless запускается, слушает control-порт, отвечает на status/help/debug/quit.
### 3.4. Тестирование — два экземпляра, STCP connect
Добавить команды:
- `connect <addr> <port> [pubkey_hex]` — STCP клиент к другому headless
- `connections` — показывает peer после connect
```bash
# Терминал 1: node1 (сервер, порт 9999 control, порт 12345 stcp)
./headless -c node1.cfg -p 9999
# Терминал 2: node2 (сервер, порт 9998 control, порт 12346 stcp)
./headless -c node2.cfg -p 9998
# Терминал 3: управляем node2
nc localhost 9998
> connect 127.0.0.1 12345 <node1_pubkey_hex>
connected to <node1_id>
> connections
1: <node1_id> 127.0.0.1:12345 ready
```
**Критерий:** Два headless устанавливают STCP handshake, соединение активно.
### 3.5. Тестирование — сообщение между двумя узлами
Добавить команды:
- `create_channel <name>` — создаёт канал
- `send <channel_id> <text>` — отправляет сообщение подключённому пиру
- `messages <channel_id> [limit]` — показывает сообщения из локальной БД
```bash
# node1 (control port 9999)
> create_channel general
created channel general
> send general hello from node1 # сообщение сохраняется в БД
msg_id=1
> messages general
[1] 12:00:00 (me) hello from node1
```
После того как node2 подключён и канал синхронизирован:
```bash
# node2 (control port 9998)
> messages general
[1] 12:00:00 <node1_id> hello from node1
```
**Критерий:** Сообщение, отправленное на node1, появляется в БД node2 через STCP.
---
## Этап 4. JNI-прослойка (C → Kotlin)
**Цель:** Kotlin-код вызывает функции C-ядра через JNI, C-ядро шлёт коллбэки в Kotlin.
### 4.1. android_jni_bridge.c
JNI-функции (вызываются из Kotlin):
```c
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeInit(
JNIEnv* env, jobject thiz, jstring configPath, jstring dbPath, jint controlPort);
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy(
JNIEnv* env, jobject thiz);
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSendMessage(
JNIEnv* env, jobject thiz, jstring channelId, jstring text);
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCreateChannel(
JNIEnv* env, jobject thiz, jstring name, jstring channelId);
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectNode(
JNIEnv* env, jobject thiz, jlong nodeId, jstring addr, jint port, jbyteArray pubkey);
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback(
JNIEnv* env, jobject thiz, jobject callback);
```
Все действия (send, create, connect) отправляются в uasync-поток через `uasync_post`.
### 4.2. Коллбэки (C → Kotlin)
При `nativeInit()`:
1. JNI сохраняет `g_callback = env->NewGlobalRef(callback)`
2. `chat_event_set_handler(jni_event_handler)` — регистрирует C-обработчик
3. `jni_event_handler(int type, uint8_t* data, int len)`:
- Получает JNIEnv (через `g_jvm->AttachCurrentThread`)
- Вызывает `env->CallVoidMethod(g_callback, onEvent, type, byteArray)`
### 4.3. NativeLib.kt
```kotlin
object NativeLib {
init { System.loadLibrary("utun") }
external fun nativeInit(configPath: String, dbPath: String, controlPort: Int): Boolean
external fun nativeDestroy()
external fun nativeSendMessage(channelId: String, text: String)
external fun nativeCreateChannel(name: String, channelId: String)
external fun nativeConnectNode(nodeId: Long, addr: String, port: Int, pubkey: ByteArray)
external fun nativeSetEventCallback(callback: ChatEventCallback)
}
interface ChatEventCallback {
fun onEvent(type: Int, data: ByteArray?)
}
```
### 4.4. Тестирование (пока без Android)
Собрать `libutun.so` под Linux (не под Android NDK) и написать маленькую Java-программу,
которая грузит библиотеку и вызывает `nativeInit()`. Или написать C-тест, который
имитирует JNI-вызовы напрямую.
**Критерий:** `nativeInit()` создаёт uasync, запускает STCP сервер, открывает БД.
`nativeSendMessage()` пишет в БД. Коллбэк вызывается при событиях.
---
## Этап 5. Android UI (Jetpack Compose)
**Цель:** Минимальное Android-приложение: список каналов, чат, отправка сообщений.
### 5.1. Gradle проект
- `app/build.gradle.kts`: compileSdk 34, minSdk 26, NDK 26+
- Зависимости: Compose BOM, Material3, Navigation Compose, Room, Coroutines
- NDK: `externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } }`
### 5.2. app/src/main/cpp/CMakeLists.txt
NDK-сборка: компилирует `android_jni_bridge.c` + линкует `libutun_lite`
(сборка libutun_lite отдельно или inline в том же CMakeLists.txt).
Добавляет OpenSSL из NDK (или prebuilt .so).
### 5.3. Экраны (Compose)
**ChannelListScreen:**
- LazyColumn каналов (ChannelItem: иконка, имя, последнее сообщение, unread badge)
- FAB "+" для создания канала
- Pull-to-refresh
**ChatScreen:**
- TopAppBar с именем канала
- LazyColumn сообщений (MessageBubble: аватар, имя, текст, время)
- InputBar (TextField + IconButton send) внизу
**CreateChannelDialog:**
- AlertDialog с TextField имени
- Кнопки Create/Cancel
### 5.4. ViewModel
```kotlin
class ChatViewModel : ViewModel() {
private val _channels = MutableStateFlow<List<Channel>>(emptyList())
val channels: StateFlow<List<Channel>> = _channels
private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _messages
fun init(configPath: String, dbPath: String) { ... }
fun selectChannel(channelId: String) { ... }
fun sendMessage(text: String) { ... }
fun createChannel(name: String) { ... }
// Вызывается из JNI callback потока:
fun onNativeEvent(type: Int, data: ByteArray?) { ... }
}
```
### 5.5. Room DB (read-only)
DAO для чтения channels и messages. Использует ту же БД что и C-ядро (WAL mode).
Room открывает БД в read-only, C-ядро пишет. Конфликтов нет.
### 5.6. Тестирование
Запустить на эмуляторе или реальном устройстве. Проверить:
- Открывается список каналов (пустой)
- Создание канала (появляется в списке)
- Открытие канала (пустой чат)
- Отправка сообщения (появляется в чате)
**Критерий:** Приложение запускается, можно создать канал, написать сообщение, оно сохраняется в БД.
---
## Этап 6. Android Headless Service
**Цель:** Фоновый сервис без UI — для отладки сетевого стека на реальном устройстве.
### 6.1. HeadlessService.kt
```kotlin
class HeadlessService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Запуск foreground notification
// NativeLib.init(configPath, dbPath, controlPort)
// Уведомление: "uTun Chat running"
return START_STICKY
}
override fun onDestroy() {
NativeLib.destroy()
}
}
```
### 6.2. Отладка через adb
```bash
# На устройстве
adb shell am startservice com.utun.chat/.headless.HeadlessService
# Логи
adb logcat -s utun:*
# Control socket (проброс порта)
adb forward tcp:9999 tcp:9999
nc localhost 9999
> status
> debug etcp=trace
> connect <addr> <port> <pubkey>
```
**Критерий:** Сервис запускается, control-сокет доступен через adb forward, логи видны в logcat.
---
## Этап 7. End-to-end тесты
### 7.1. Headless тесты (Linux)
Скрипт `test_headless.sh`:
1. Запускает node1 на порту 9999 (control) и 12345 (stcp)
2. Запускает node2 на порту 9998 (control) и 12346 (stcp)
3. Через control-сокет node1: создаёт канал "general"
4. Через control-сокет node1: получает invite-ссылку
5. Через control-сокет node2: подключается к node1
6. Через control-сокет node2: join по invite
7. Через control-сокет node1: send "test message"
8. Через control-сокет node2: messages (проверка что сообщение пришло)
9. Остановка обоих экземпляров
```bash
cd tools/chatgui-android && ./test_headless.sh
```
### 7.2. Android Instrumentation тесты
```kotlin
@Test
fun testNativeInitAndSend() {
NativeLib.init(configPath, dbPath, controlPort)
// Проверить что БД создалась
val db = Room.databaseBuilder(...)
NativeLib.sendMessage("test_ch", "hello")
// Подождать и проверить что сообщение в БД
}
```
### 7.3. UI тесты (Compose)
```kotlin
@Test
fun testCreateChannelAndSend() {
composeTestRule.setContent { ChatApp() }
// Нажать FAB "+"
// Ввести имя канала
// Нажать Create
// Проверить что канал появился
// Открыть канал
// Ввести текст
// Нажать Send
// Проверить что сообщение отобразилось
}
```
---
## Контрольные точки
| Этап | Что проверяется | Где тестируется |
|------|----------------|-----------------|
| 1 | Проект собирается | Linux |
| 2 | libutun_lite.a компилируется без ошибок | Linux |
| 3 | Headless запускается, control socket отвечает | Linux |
| 3.4 | Два headless соединяются по STCP | Linux |
| 3.5 | Сообщение доставляется от node1 к node2 | Linux |
| 4 | JNI-функции вызываются, коллбэки приходят | Linux / эмулятор |
| 5 | Android приложение запускается, UI работает | Эмулятор / устройство |
| 6 | Headless Service работает, control через adb | Устройство |
| 7 | End-to-end тесты проходят | Linux + эмулятор |
## Правила разработки
- **Все C-файлы** — C99, стиль как в AGENTS.md
- **Логирование:** каждый модуль логирует инициализацию/очистку и ошибки
- **Memory:** u_malloc/u_free из lib/mem.h
- **Headless control:** все команды логируются входящие/исходящие
- **JNI:** каждая функция проверяет аргументы на NULL, логирует ошибки
- **Android UI:** ViewModel использует StateFlow, не держит ссылки на Context
- **Тесты:** каждая команда control-сокета проверяется хотя бы одним вызовом