- 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
466 lines
18 KiB
Markdown
466 lines
18 KiB
Markdown
# 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-сокета проверяется хотя бы одним вызовом
|