261 lines
30 KiB
Markdown
261 lines
30 KiB
Markdown
# Жизненный цикл ядра и сервисов
|
||
|
||
CLI выполняет daemon fork до создания `UASYNC`, SQLite, сокетов и фоновых потоков.
|
||
Сервисы и их workers создаются уже в дочернем процессе. Родитель ждёт подтверждения
|
||
успешной инициализации и записи PID-файла; ошибка старта возвращается вызывающему.
|
||
`test_daemon_worker` проверяет настоящий запуск без `-f`, наличие дискового потока,
|
||
штатный SIGTERM и ненулевой статус launcher при ошибке PID-файла.
|
||
|
||
Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks
|
||
останавливаемых модулей. GUI передаёт команды в этот поток.
|
||
|
||
```c
|
||
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config);
|
||
if (!inst || utun_core_start(inst) < 0) /* обработать ошибку */;
|
||
if (chat_service_start(inst) < 0) /* обработать ошибку */;
|
||
if (utun_service_start(inst) < 0) /* обработать ошибку */;
|
||
|
||
chat_service_stop(inst); /* UTUN продолжает работать */
|
||
chat_service_start(inst); /* загружает сохранённые каналы */
|
||
utun_service_stop(inst); /* чат продолжает работать */
|
||
utun_instance_destroy(inst);
|
||
```
|
||
|
||
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
|
||
в конфигурации. Desktop GUI и Android запускают ядро и чат явно, без UTUN.
|
||
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
|
||
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
|
||
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
|
||
|
||
Успех запуска требует успешной инициализации всех обязательных ресурсов: заданных
|
||
listen-сокетов, мониторинга, TUN и всех `route_subnet` при включённом TUN. Частичный
|
||
набор сокетов или маршрутов — ошибка запуска. `utun_started` устанавливается только
|
||
после завершения всех шагов. Системные маршруты хранятся отдельным списком успешно
|
||
созданных записей; stop и откат удаляют только этот список, в обратном порядке.
|
||
Маршрут, на добавлении которого ядро вернуло ошибку, не становится собственностью
|
||
UTUN и при откате не удаляется. Ошибка одного удаления не прекращает очистку остальных.
|
||
|
||
`auto_sockets=android` выбирает внешнее управление сетью: frontend регистрирует
|
||
ConnectivityManager до запроса запуска и передаёт конфигурацию сокетов через JNI.
|
||
Оба нативных монитора ОС в этом режиме не создаются. Рабочие listen-сокеты по-прежнему
|
||
должны открыться полностью. Android подтверждает запуск по `SERVICE_STARTED`, а не
|
||
по успешному созданию потока; после события передаётся свежий снимок адресов.
|
||
|
||
| Владелец | Ресурсы |
|
||
|---|---|
|
||
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
|
||
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
|
||
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
|
||
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
|
||
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
|
||
|
||
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
|
||
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
|
||
`:memory:` данные сохраняются только до уничтожения ядра.
|
||
|
||
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
|
||
не разрывает соединение, нужное другой группе. При остановке группы отменяются
|
||
её планировщики, запросы, таймеры и ожидания очередей; отправляется LEAVE,
|
||
закрываются логические каналы маршрутизатора и удаляется её ожидающий транзит
|
||
на общих соединениях. Транзит других групп сохраняется. Отложенный LEAVE может временно
|
||
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе.
|
||
|
||
ETCP удаляет транспорт после 30 секунд непрерывного DOWN только при отсутствии локальных NCD handles.
|
||
Первоначальное ожидание UP тоже учитывает владение. Повторный DOWN не продлевает
|
||
таймаут; восстановление UP отменяет его. Если срок истёк при наличии handles, транспорт
|
||
сохраняется, а исходный deadline остаётся. Освобождение последнего handle во время DOWN
|
||
оставляет транспорт до этого deadline либо запускает удаление в следующем callback,
|
||
если срок уже истёк. Внешние ETCP refs задерживают только освобождение памяти.
|
||
Явный force-close и отмена неудачной попытки последнего владельца закрывают сразу.
|
||
|
||
Входящий UP без локальных handles сохраняется: он может быть нужен пиру. CLOSE/KEEP_ALIVE
|
||
привязан при запуске ядра, поэтому мягкое закрытие работает и на чисто входящем узле.
|
||
На CLOSE при наличии handles отвечаем KEEP_ALIVE; без них закрываем транспорт.
|
||
KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE обычно занимает 5 секунд,
|
||
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
|
||
завершает ранее запрошенное мягкое закрытие.
|
||
|
||
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
|
||
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
|
||
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
|
||
отправки срок жизни пира не продлевают. Пробуждение сразу сокращает deadline до normal timeout.
|
||
После истечения последнего линка транспорт
|
||
становится DOWN, затем может удаляться через 30 секунд, когда локальных NCD handles нет. `test_etcp_server_lifecycle`
|
||
проверяет сроки, несколько путей, восстановление, владение, сон и CLOSE/KEEP_ALIVE
|
||
с управляемыми часами и контролем освобождения памяти/таймеров.
|
||
|
||
Остановка чата сначала прекращает внешние команды и фоновые задания, затем
|
||
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
|
||
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
|
||
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
|
||
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
|
||
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
|
||
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
|
||
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
|
||
транскодирование или распознавание речи может задержать stop; принудительного
|
||
прерывания этих библиотек нет.
|
||
|
||
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
|
||
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
|
||
части против вытеснения. После FIN publisher может снять своё владение, сохранив
|
||
завершённый источник для оставшихся локальных readers. Последний close освобождает
|
||
источник, индекс и кольцо. Новая публикация отсоединяет readers прежней эпохи.
|
||
Android preview собственного эфира не создаёт reader: Camera2 пишет прямо в TextureView
|
||
и encoder Surface. UI attach/detach меняет лишь camera session; audio/encoder/origin сохраняются.
|
||
UI, ожидающие команды camera handler и каждая camera session удерживают preview target отдельно.
|
||
TextureView возвращает false при destroy и снимает ссылку UI; Surface/SurfaceTexture освобождаются
|
||
после закрытия последней session. CameraDevice.onClosed снимает также оставшиеся ссылки своих
|
||
sessions, если Android не прислал отдельные session.onClosed. Повторные callbacks безопасны.
|
||
Capture stop запрещает attach и снимает своё владение target; camera HandlerThread завершается
|
||
после camera close и освобождения outputs всех созданных sessions.
|
||
Отказ preview output возвращает encoder-only session и не завершает сам эфир.
|
||
При stop/destroy отменяются delivery и media-index callbacks до освобождения store;
|
||
destroy отсоединённого cursor не обращается к store. Frontend прекращает audio callback
|
||
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
|
||
|
||
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
|
||
PTS показа; audio cursor сохраняет временной lookahead. Bridges готовят один ответ
|
||
на незавершённый запрос и не читают следующий кадр после получения ответа автоматически.
|
||
Android worker держит один рабочий input и один готовый output MediaCodec; свободный
|
||
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
|
||
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
|
||
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
|
||
Если первый доступный звук позже KEY, cursor отсчитывает стартовый audio lookahead от
|
||
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
|
||
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
|
||
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
|
||
Смена revision заменяет только video decoder и его измеренный защитный guard;
|
||
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
|
||
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
|
||
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
|
||
конец GUI требует также показа оставшихся изображений и завершения audio tail.
|
||
Отсутствие прогресса decoder при наличии работы имеет отдельную диагностику и срок 10s.
|
||
|
||
ACK-история принадлежит ядру и обновляется только при первом найденном подтверждении
|
||
пакета. REINIT/close соединения не стирает уже подтверждённые за последнюю секунду байты.
|
||
Общий `stream_publisher` Android/desktop владеет отдельным 100ms таймером TX-снимков;
|
||
close/detach отменяют его в uasync до уничтожения instance. Bridges копируют снимок
|
||
под mutex, frontend не обращается к живым очередям транспорта. Android экран подписан
|
||
на отдельный StateFlow статистики; desktop обновляет статистику отдельно от сырого превью.
|
||
Publisher не регистрирует stream event callback и не подменяет callback viewer.
|
||
Bridge удерживает внешний mutex на время producer/open/stop и destroy общего publisher;
|
||
actor не вызывает frontend callbacks под своим mutex. Reserved post, media timer,
|
||
stats timer и stream handle освобождаются до контекста publisher.
|
||
|
||
Desktop `MainWindow` владеет `StreamPublishEngine`, а окно управления лишь отображает
|
||
его состояние. Закрытие окна сохраняет эфир, новый запуск запрещён до конца FIN drain.
|
||
Qt capture callback сохраняет последний `QVideoFrame` для preview и ограниченно ставит кадр
|
||
в очередь отдельного долгоживущего image worker. Он конвертирует `QVideoFrame` в `QImage`;
|
||
video worker получает готовый image и не создаёт Qt Multimedia TLS, которое могло задержать его exit на Windows.
|
||
Video/audio workers выполняют encode/DSP вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять
|
||
видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture
|
||
обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback.
|
||
Stop запрещает producers, закрывает Qt capture и подписку на аудиозахват. Отдельный join worker
|
||
дожидается полного выхода video/audio threads, включая native TLS Qt; GUI продолжает обрабатывать события.
|
||
После join отправляется FIN с drain 2s; закрытие последнего микрофона может продолжаться параллельно.
|
||
Microphone lease и owner сохраняются до завершения обоих путей. Новый start ждёт также FIN drain.
|
||
При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync
|
||
закрывает оставшийся publisher. Destructor ждёт workers в локальном event loop без пользовательского ввода,
|
||
чтобы thread cleanup мог обратиться к GUI; start и внешние сигналы engine в это время запрещены.
|
||
Workers никогда не держат borrowed instance/handle.
|
||
Microphone lease публикации исключает call/recorder; рация согласует вход через `AudioCapture`.
|
||
Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы.
|
||
Подключение/отключение рации не останавливает устройство трансляции: атомарный слот и счётчик readers
|
||
дожидаются только callback удаляемого потребителя. Последняя подписка закрывает устройство.
|
||
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
|
||
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
|
||
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
|
||
На Android `MicrophoneCapture` владеет AudioRecord и reader-потоком, подписчики — ограниченными очередями.
|
||
Stop публикации удаляет её подписку до join Opus worker и FIN; AudioRecord остаётся живым для рации.
|
||
PTT и PCM рации проходят через одну очередь. Последний подписчик делает stop/read join/release;
|
||
приоритет глобального AudioManager сохраняется до освобождения трансляционного захвата.
|
||
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
|
||
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
|
||
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
|
||
не завершает новую сессию. Причина ошибки сохраняется в окне после окончания drain.
|
||
|
||
Desktop-звонок и рация проверяют поступление корректного PCM, включая первый callback.
|
||
Секунда без данных отключает TX и фиксирует понятную ошибку в GUI; цифровая тишина является данными.
|
||
RX продолжает работать, рация снимает PTT, автоматического переоткрытия неисправного входа нет.
|
||
Выбор другого микрофона или новая сессия снимают ошибку. Неисправное устройство закрывается
|
||
в отдельном потоке; stop не присоединяет уже запущенный close worker. Новый захват ждёт его завершения.
|
||
В звонке microphone lease удерживается до конца закрытия даже после stop. Общий AudioCapture
|
||
рации сначала отсоединяет callback потребителя; последний владелец освобождает backend в worker.
|
||
Сброс контекста и уничтожение владельца дожидаются worker до освобождения miniaudio-контекста.
|
||
Штатное закрытие исправного устройства сохраняет синхронный порядок.
|
||
|
||
`AudioDevice` подстраивает буфер по подтверждённым backend XRUN: WASAPI DATA_DISCONTINUITY
|
||
на входе, ALSA EPIPE, PulseAudio capture hole/overflow и playback underflow. Notification
|
||
только увеличивает атомарный счётчик, решение и лог выполняются в GUI recovery timer.
|
||
Исходные параметры, включая Linux capture period=10мс, сохраняются до повторных сбоев.
|
||
Первая секунда после start исключена; нужны ошибки в двух опросах за 5с. Период увеличивается
|
||
на 5мс, не чаще раза в 5с, до общей запрошенной ёмкости 100мс с учётом числа периодов.
|
||
Фактический размер согласует backend; исходный или уже достигнутый больший размер не уменьшается.
|
||
При достижении предела повторные сбои логируются без новых перезапусков. Подобранный запрос
|
||
сохраняется при recovery, сбрасывается при смене устройства/сессии; стабильный звук не вызывает reopen.
|
||
Подстройка микрофона закрывает устройство асинхронно, останавливая TX worker и сохраняя RX.
|
||
Общая capture-сессия отсоединяет callbacks до close и заново подготавливает потребителей после init.
|
||
Новая подписка во время close откладывается, stop удаляет её без ожидания backend.
|
||
Первое переоткрытие после подстройки не использует backoff; при ошибке открытия действует обычный retry.
|
||
|
||
В desktop-видеозвонке Windows/Linux камера живёт в отдельном процессе того же
|
||
`vibechat --call-camera-worker`. Служебный вход выполняется до конфига, БД и
|
||
single-instance lock. Перечисление устройств, Qt/V4L2, преобразование и освобождение
|
||
аппаратных кадров происходят только там. GUI получает собственный `QImage`; TX не
|
||
обращается к драйверу. Смена codec не перезапускает захват и не ждёт следующего GOP.
|
||
|
||
Пиксели одного кадра передаются через mapping (Windows — pagefile-backed объект ОС,
|
||
Linux — временный файл), метаданные/команды — через локальный сокет.
|
||
Worker записывает mapping до FRAME; следующий write разрешён
|
||
только после ACK, который GUI посылает после копирования в свою память. Общего mutex
|
||
нет. В worker ожидает только последний raw-кадр; поколения отсекают старые результаты.
|
||
Preview обновляется по готовности, TX получает condition-variable wake. PTS сохраняет
|
||
время захвата; кадры старше 250мс отбрасываются до encode и перед send. Отброшенный
|
||
encoded frame требует следующего KEY, поскольку encoder уже продвинул reference chain.
|
||
|
||
Отключение камеры и удаление окна не ждут worker. Независимый Session сохраняет
|
||
QProcess и mapping до finished, затем удаляет себя после отсоединения владельца.
|
||
Зависший stop получает kill через 1.5с, отсутствие прогресса — через 5с; таймеры
|
||
живут в родительском процессе. При выходе приложения kill отправляется без wait;
|
||
GUI не уничтожает живой QProcess. При отказе новый процесс запускается лишь по
|
||
явной команде пользователя и после finished прежнего. Лог категории call показывает
|
||
последнюю стадию enumerate/start/convert/release/stop и возраст кадра.
|
||
`test_call_camera_capture` проверяет настоящую IPC-цепочку с подставным устройством:
|
||
блокировки start/map/stop, поздние кадры после смены поколения, задержку 720p,
|
||
повтор после отказа, отсутствие рестарта камеры при смене codec, удаление окна,
|
||
выход приложения и независимое освобождение mapping в каждом процессе.
|
||
|
||
Статус транспорта UP подтверждает восстановление физического пути. READY сессии
|
||
группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно
|
||
его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи
|
||
защищают от сообщений старой сессии и не задают срок жизни маршрута.
|
||
|
||
Проверки: `test_services` проверяет независимый stop/start, общий NCD в CONNECTING,
|
||
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену workers,
|
||
заблокированного медиа-потока, транзита и ожиданий заменённой очереди. `test_chat_join_e2e`
|
||
проверяет join и распространение membership между узлами без UTUN-сервиса.
|
||
|
||
В Android звонковой сессией владеет `CallController` уровня приложения; события
|
||
ядра и команды Telecom обрабатываются один раз в main thread, без повторного
|
||
проигрывания событий при создании ViewModel. UI получает `StateFlow` и передаёт
|
||
пользовательские команды. `CallService` владеет `CallAudioEngine` и частичным
|
||
wake lock: перед исходящим вызовом или принятием входящего запускается foreground
|
||
режим `phoneCall|microphone`, затем сигналинг и аудио. Завершение сначала
|
||
останавливает I/O и дожидается аудиопотоков, затем освобождает native-аудио,
|
||
wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выполняется тот же
|
||
порядок; новый звонок разрешается после `SERVICE_STARTED` нового ядра.
|
||
|
||
Активность ядра задаётся явно: видимая Activity или незавершённый звонок удерживают
|
||
ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После
|
||
завершения звонка в фоне возвращается standby; экран может гаснуть во время
|
||
разговора независимо от частичного wake lock сервиса.
|
||
|
||
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический
|
||
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно.
|
||
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает
|
||
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим
|
||
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после
|
||
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
|
||
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
|
||
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
|