26 KiB
Жизненный цикл ядра и сервисов
CLI выполняет daemon fork до создания UASYNC, SQLite, сокетов и фоновых потоков.
Сервисы и их workers создаются уже в дочернем процессе. Родитель ждёт подтверждения
успешной инициализации и записи PID-файла; ошибка старта возвращается вызывающему.
test_daemon_worker проверяет настоящий запуск без -f, наличие дискового потока,
штатный SIGTERM и ненулевой статус launcher при ошибке PID-файла.
Все операции выполняются в потоке единственного UASYNC экземпляра, вне callbacks
останавливаемых модулей. GUI передаёт команды в этот поток.
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. 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 хранит только последний кадр, video/audio workers выполняют encode/DSP
вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять
видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture
обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback.
Сначала stop запрещает producers, закрывает Qt capture и подписку на аудиозахват, затем join workers,
освобождает microphone lease и завершает публикацию через FIN с drain 2s.
При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync
закрывает оставшийся publisher. 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-видеозвонке 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 отменяет ожидающие задачи и очищает подписки старого ядра.