158 lines
16 KiB
Markdown
158 lines
16 KiB
Markdown
# RF001 launcher/health diagnostic
|
||
|
||
Дата проверки: 29 сентября 2026 года.
|
||
|
||
## Ограничения проверки
|
||
|
||
- Производственный RX-агент не останавливался и не изменялся.
|
||
- Задание Планировщика Windows не изменялось.
|
||
- Диагностический процесс не обращался к HTTP-очереди.
|
||
- Доступ к SDR не выполнялся: `hardware_accessed=false`, `RF RX=0`, `RF TX=0`.
|
||
- Bootstrap `rf001-bootstrap-2026.09.29.5` не создавался; `.4` повторно не запускался.
|
||
- Clean release A `rf001-agent-2026.09.29.1` использовался только для чтения кода health writer и проверки `READY.json`/manifest.
|
||
|
||
## Безопасное состояние после проверки
|
||
|
||
- Активный производственный агент: `2026.09.25.1`; heartbeat на MAIN свежий, active task отсутствует.
|
||
- Незавершённых задач: 0. Незавершённых аппаратных задач: 0.
|
||
- Задание `SDR Rover RX Agent`: `Running`; его действие по-прежнему запускает legacy agent, а не release A.
|
||
- Производственная цепочка legacy agent на RX: venv wrapper PID 7980 -> базовый Python PID 17568.
|
||
- `hackrf_transfer.exe`: 0 процессов. Процессов RX capture: 0.
|
||
- Диагностических процессов: 0.
|
||
- Clean release A: 10 файлов из 10 совпадают с `versions/rf001-agent-2026.09.29.1` внутри подписанного bootstrap `.4` по относительному пути, размеру и SHA256.
|
||
- Conflict quarantine сохранён. SHA256 его diagnostic manifest: `b92f15dd4b7cffef375fd108f454a41623ed7328220c73c3f2b847dc59e956bc`.
|
||
- `rf001-bootstrap-2026.09.29.5.zip` отсутствует.
|
||
|
||
## Доказанная первопричина
|
||
|
||
Два последовательных изолированных запуска через настоящий `start_release()` дали одинаковый результат. Поля `release_id`, `agent_version`, `launch_nonce` и `state` совпали. Единственным несовпавшим полем стало `health.pid == Popen.pid`.
|
||
|
||
На проверенном Windows/venv способе запуска `Popen.pid` принадлежит процессу `venv\\Scripts\\pythonw.exe`, а фактический интерпретатор, который исполняет health writer, является его единственным прямым дочерним процессом с образом базового `Python313\\python.exe`. Связь установлена не по именам: сверены PID, PPID, время создания, полный путь образа, командная строка и одноразовый `launch_nonce`.
|
||
|
||
### Запуск 1
|
||
|
||
- Launcher процесса диагностики: PID 7848.
|
||
- `Popen.pid`: 10284, образ `C:\\Users\\Professional\\Documents\\Codex\\SDR_Rover_Video\\.venv\\Scripts\\pythonw.exe`, PPID 7848, создан `2026-09-29T13:04:44.696889Z`.
|
||
- Health writer: PID 8452, образ `C:\\Users\\Professional\\AppData\\Local\\Programs\\Python\\Python313\\python.exe`, PPID 10284, создан `2026-09-29T13:04:44.702070Z`.
|
||
- Health обновлён `2026-09-29T16:04:44.866156+03:00`.
|
||
- Wrapper уже наблюдался 0,167845 с и отсутствовал к завершению прогона через 2,589228 с после создания.
|
||
- Agent child уже наблюдался 0,164086 с и отсутствовал к завершению прогона через 2,585469 с после создания.
|
||
- Точный момент завершения каждого процесса не опрашивался, поэтому приведены проверенные нижняя и верхняя границы, а не предполагаемая длительность.
|
||
|
||
### Запуск 2
|
||
|
||
- Launcher процесса диагностики: PID 7848.
|
||
- `Popen.pid`: 3504, образ `C:\\Users\\Professional\\Documents\\Codex\\SDR_Rover_Video\\.venv\\Scripts\\pythonw.exe`, PPID 7848, создан `2026-09-29T13:04:47.290751Z`.
|
||
- Health writer: PID 14616, образ `C:\\Users\\Professional\\AppData\\Local\\Programs\\Python\\Python313\\python.exe`, PPID 3504, создан `2026-09-29T13:04:47.295668Z`.
|
||
- Health обновлён `2026-09-29T16:04:47.461326+03:00`.
|
||
- Wrapper уже наблюдался 0,169277 с и отсутствовал к завершению прогона через 2,574777 с после создания.
|
||
- Agent child уже наблюдался 0,165658 с и отсутствовал к завершению прогона через 2,571158 с после создания.
|
||
- Точный момент завершения каждого процесса не опрашивался.
|
||
|
||
Оба `launch_nonce` различались:
|
||
|
||
- `96acc3af17150f42d393238bcbdad3ff`;
|
||
- `e4a618547e4bcda4b6914bf81bbd7963`.
|
||
|
||
В обоих случаях штатный `start_release()` завершился fail-closed:
|
||
|
||
```text
|
||
RuntimeError: health file does not match the launched process
|
||
```
|
||
|
||
После каждого прогона процессов, принадлежащих диагностическому запуску, не осталось. Исходные health-файлы и снимки процессов сохранены на RX в:
|
||
|
||
```text
|
||
C:\Users\Professional\Downloads\SDR_Rover_Diagnostics\launcher_health_20260929T130000Z\evidence
|
||
```
|
||
|
||
## Сравнение полей health
|
||
|
||
| Поле | Запуск 1 | Запуск 2 |
|
||
|---|---:|---:|
|
||
| `release_id == rf001-agent-2026.09.29.1` | PASS | PASS |
|
||
| `agent_version == 2026.09.29.1` | PASS | PASS |
|
||
| `launch_nonce` | PASS | PASS |
|
||
| `state == RUNNING` | PASS | PASS |
|
||
| `health.pid == Popen.pid` | FAIL: 8452 != 10284 | FAIL: 14616 != 3504 |
|
||
|
||
Следовательно, `Popen.pid` соответствует варианту C: это родитель фактического процесса агента. Он не является произвольным или посторонним процессом; это venv-обёртка с ожидаемой командной строкой, которая существует одновременно с прямым дочерним health writer.
|
||
|
||
## Строгая модель идентичности
|
||
|
||
Основным вариантом по-прежнему должен быть запуск, при котором `Popen.pid` принадлежит фактическому долгоживущему агенту. Для проверенной Windows/venv топологии, где это условие не выполняется, допустима только следующая строгая альтернатива:
|
||
|
||
1. Сохранить точные проверки `release_id`, `agent_version`, одноразового `launch_nonce` и `state=RUNNING`.
|
||
2. Снять идентичность процесса `Popen` сразу после запуска: PID, путь образа, командную строку и время создания.
|
||
3. Если health PID отличается, принять только один живой прямой дочерний процесс этого конкретного wrapper.
|
||
4. Для дочернего процесса проверить время создания после начала данного запуска, точную команду с тем же agent entrypoint и nonce, ожидаемый путь интерпретатора и наличие процесса в момент принятия health.
|
||
5. Не принимать внука, произвольного потомка, самый новый `python.exe`, совпадение только по имени или один числовой PID.
|
||
6. При завершении или откате повторно сверить PID и время создания перед остановкой wrapper и строго связанных с ним потомков. Это закрывает повторное использование PID.
|
||
|
||
Минимальное производственное исправление можно ограничить `rf001_agent_launcher.py`: добавить проверку уникального прямого потомка и такую же строгую очистку потомков при ошибке. Формат health менять не требуется, поскольку в нём уже есть nonce и собственный PID. Если получение надёжного времени создания и пути образа без дополнительного компонента окажется невозможно, health writer следует расширить полями process identity; отключать проверку PID нельзя.
|
||
|
||
## Конечный автомат launcher
|
||
|
||
Названия ниже описывают фактические фазы кода. Не все они сейчас записываются как отдельное значение в `launcher_status.json`.
|
||
|
||
| Фаза | Допустимый вход | Процессы | Canonical release / active pointer | Scheduled Task | Health | Действие при ошибке |
|
||
|---|---|---|---|---|---|---|
|
||
| `LEGACY_ACTIVE` | Действующий агент здоров и idle | Legacy agent запущен | Указатель на legacy | Launcher не меняет определение задания | Legacy `RUNNING` | Запрос обновления принимается только при `safe_to_switch=true`, совпадающем `from_release_id` и idle health |
|
||
| `TARGET_VERIFIED` | Подписанный target прошёл `validate_ready_release()` | Новый процесс ещё не создан | Target уже существует в canonical releases; указатель ещё legacy | Без изменений | Legacy health | Отказ до остановки legacy |
|
||
| `STARTING_TARGET` | Target проверен | Legacy останавливается; создаётся wrapper target | Указатель атомарно меняется на target; status `STARTING_UPDATE` | Без изменений | Старый health больше не считается подтверждением | Переход в `TARGET_FAILED` |
|
||
| `WAITING_FOR_HEALTH` | `Popen` создан, nonce уникален | Wrapper и возможный прямой child | Указатель target | Без изменений | Ожидаются свежие точные поля и строгая process identity | Timeout, exit или mismatch завершают target fail-closed |
|
||
| `TARGET_HEALTHY` | Все health-поля и identity прошли | Target продолжает работу | Указатель target; status `RUNNING` | Без изменений | Target `RUNNING` | Дальнейший выход ведёт в `RESTARTING` |
|
||
| `TARGET_FAILED` | Startup exit, timeout или mismatch | Target должен быть остановлен без orphan | Target ещё был активным указателем | Без изменений | Ошибочный health не принимается | Начать rollback |
|
||
| `ROLLING_BACK` | Target не прошёл health gate | Target очищается, запускается legacy | Указатель возвращается на legacy | Без изменений | Для legacy требуется новый nonce и новый health | Ошибка rollback должна оставаться явной, а не считаться успехом |
|
||
| `LEGACY_RESTORED` | Legacy снова прошёл health gate | Legacy работает | Указатель legacy; status `ROLLED_BACK` | Без изменений | Свежий legacy `RUNNING` | Архивировать запрос и продолжить legacy |
|
||
|
||
Существенный пробел текущего production-кода: `TARGET_FAILED` останавливает только объект `Popen`; общая строгая очистка потомка в launcher пока не реализована. В двух диагностических запусках orphan не осталось, но это наблюдение не заменяет будущую явную очистку по identity.
|
||
|
||
## Проверенные гонки и регрессии
|
||
|
||
Локальный набор `pytest` проверяет:
|
||
|
||
- точное совпадение процесса: PASS;
|
||
- неверные nonce, release, version и state: FAIL;
|
||
- stale health: FAIL;
|
||
- посторонний PID: FAIL;
|
||
- единственный ожидаемый прямой child: PASS только со строгой lineage, nonce, командой и временем создания;
|
||
- неожиданный grandchild: FAIL;
|
||
- повторное использование PID с другим creation time: FAIL;
|
||
- target умер до health: FAIL;
|
||
- target записал health, но умер: FAIL;
|
||
- health в состоянии `STARTING`, до полной готовности: FAIL;
|
||
- rollback не принимает health с nonce прошлого запуска: FAIL;
|
||
- выбор процессов для cleanup не затрагивает посторонний процесс;
|
||
- cleanup останавливает только строго принадлежащие wrapper и child;
|
||
- два успешных логических запуска детерминированы и имеют разные nonce;
|
||
- health JSON заменяется атомарно.
|
||
|
||
Команда проверки:
|
||
|
||
```text
|
||
python -m pytest tests/test_rf001_launcher_health_diagnostic.py tests/test_rf001_agent_launcher.py tests/test_rf001_rx_http_agent.py tests/test_rf001_agent_update.py -q
|
||
```
|
||
|
||
Результат: `48 passed`, предупреждение только о недоступности `.pytest_cache` в текущей песочнице.
|
||
|
||
Windows integration test использовал реальный RX venv, установленный launcher source, canonical release A и настоящий agent health writer. Выполнено два последовательных изолированных запуска. HTTP polling, SDR и аппаратные команды в этом режиме отсутствовали.
|
||
|
||
## Файлы и следующий этап
|
||
|
||
В этой диагностической фазе добавлены только:
|
||
|
||
- `tools/rf001_launcher_health_diagnostic.py`;
|
||
- `tests/test_rf001_launcher_health_diagnostic.py`;
|
||
- этот отчёт.
|
||
|
||
Для будущего производственного исправления потребуются как минимум:
|
||
|
||
- `tools/rf001_agent_launcher.py`;
|
||
- `tests/test_rf001_agent_launcher.py`;
|
||
- тесты упаковки, подтверждающие неизменность подписанного состава и fail-closed rollback.
|
||
|
||
`tools/rf001_rx_http_agent.py` потребуется менять только при выборе расширенного health process identity; доказанная минимальная схема может обойтись без изменения агента.
|
||
|
||
После отдельного согласования исправления понадобится новый уникальный bootstrap, поскольку launcher на RX должен быть заменён проверенным пакетом. Bootstrap `.5` в рамках этой фазы не создавался.
|