ha(H1): исправления по ревью — жизненный цикл сессии, самодостаточный wheel

- C1: единственный владелец stop+destroy — reader-поток; close() только
  выставляет флаг и ждёт; повторный close и close из колбэка безопасны
  (регресс-тесты; TSan на close с мёртвым модулем — 0 гонок)
- M1: wheel собирается с bundled mbedtls (нет внешней soname mbedcrypto);
  попутно исправлена bundled-ветка CMake (-lSTATIC)
- C-API: счётчики потерянных событий/логов, лог-кольцо без union-балласта,
  атомарные fn/ctx лог-синка
- provision: валидация формата облачных ответов; CLI: человеческие ошибки
  (exit 2), --out с правами 0600, console_script pyfglair
- упаковка: MANIFEST.in (sdist с C++-исходниками), очистка stale .so,
  readme в метаданных; find_library без ложных .bak
- тесты: close-сценарии, доставка логов, ошибки CLI/provision, C-API очередь
This commit is contained in:
2026-09-29 11:48:32 +03:00
parent 0292404756
commit b84bd8d555
18 changed files with 356 additions and 56 deletions
+62 -19
View File
@@ -56,6 +56,7 @@ class Config:
_GLOBAL_LOG: Optional[LogHandler] = None
_LOG_PUMP: Optional["_LogPump"] = None
_LOG_PUMP_LOCK = threading.Lock()
class _LogPump(threading.Thread):
@@ -66,7 +67,14 @@ class _LogPump(threading.Thread):
def run(self) -> None:
out = ffi.new("fgl_log_event_t[]", 8)
dropped = 0
while True:
total = int(lib.fgl_log_events_dropped())
if total > dropped:
_LOGGER.warning(
"переполнение очереди логов: потеряно %d", total - dropped
)
dropped = total
count = lib.fgl_log_wait_events(out, 8, 500)
handler = _GLOBAL_LOG
if handler is None:
@@ -84,10 +92,11 @@ class _LogPump(threading.Thread):
def set_log_handler(handler: Optional[LogHandler]) -> None:
"""Глобальный приёмник логов ядра (0=debug..3=error)."""
global _GLOBAL_LOG, _LOG_PUMP
_GLOBAL_LOG = handler
if handler is not None and _LOG_PUMP is None:
_LOG_PUMP = _LogPump()
_LOG_PUMP.start()
with _LOG_PUMP_LOCK:
_GLOBAL_LOG = handler
if handler is not None and _LOG_PUMP is None:
_LOG_PUMP = _LogPump()
_LOG_PUMP.start()
def set_log_level(level: int) -> None:
@@ -119,18 +128,27 @@ class _SessionReader(threading.Thread):
def run(self) -> None:
out = ffi.new("fgl_event_t[]", 16)
finalize = ffi.NULL
dropped = 0
try:
while True:
with self._session._lock:
if self._session._closed:
# Освобождает сессию reader: close() мог быть вызван
# из колбэка в этом же потоке (loop отсутствует).
# Единственный владелец stop+destroy при закрытии —
# reader (защищает от гонки с close() и позволяет
# close() из колбэка в этом же потоке).
finalize = self._session._session
self._session._session = ffi.NULL
return
raw = self._session._session
if raw == ffi.NULL:
return
total = int(lib.fgl_session_events_dropped(raw))
if total > dropped:
_LOGGER.warning(
"переполнение очереди событий: потеряно %d",
total - dropped,
)
dropped = total
count = lib.fgl_session_wait_events(raw, out, 16, 250)
for i in range(count):
try:
@@ -252,6 +270,14 @@ class Session:
def closed(self) -> bool:
return self._closed
@property
def events_dropped(self) -> int:
"""Сколько событий потеряно из-за переполнения очереди (диагностика)."""
with self._lock:
if self._closed or self._session == ffi.NULL:
return 0
return int(lib.fgl_session_events_dropped(self._session))
@property
def config(self) -> Config:
return self._config
@@ -263,26 +289,43 @@ class Session:
return bool(lib.fgl_session_start(self._session))
def stop(self) -> None:
with self._lock:
if self._closed:
return
raw = self._session
lib.fgl_session_stop(raw)
"""Полное завершение сессии (синоним close: stop + destroy)."""
self.close()
def close(self) -> None:
"""Останавливает сессию, джойнит reader и освобождает ядро.
Из колбэка в reader-потоке (Session без loop) возвращается сразу:
освобождение завершит сам reader. Повторный вызов — no-op.
"""
with self._lock:
if self._closed:
return
self._closed = True
raw = self._session
lib.fgl_session_stop(raw)
if self._reader is threading.current_thread():
return # сессию освободит reader (см. _SessionReader.run)
self._reader.join(timeout=3.0)
if self._reader.is_alive():
_LOGGER.error(
"читатель событий не завершился; сессия не освобождена"
)
return
self._reader.join(timeout=10.0)
with self._lock:
if self._reader.is_alive():
_LOGGER.error(
"читатель событий не завершился за 10 с; сессия не "
"освобождена"
)
return
raw = self._session
self._session = ffi.NULL
if raw != ffi.NULL:
# Reader завершился, не освободив сессию (неожиданное исключение).
lib.fgl_session_destroy(raw)
def __del__(self) -> None:
try:
if not self._closed:
_LOGGER.warning(
"Session не закрыта: вызовите close()/async_stop()"
)
except Exception:
pass
async def async_stop(self) -> None:
loop = asyncio.get_running_loop()