# 03 — Безопасный жизненный цикл управляемого процесса **What to build:** Управляемый Xray предсказуемо переживает повторные команды, сигналы, устаревшее состояние и аварийное исчезновение супервизора, не затрагивая чужие процессы и не оставляя секретных данных вне закрытого служебного каталога. **Blocked by:** 02 — Управляемый Xray: полный штатный путь; 06 — Упрощение. **Status:** done **Резолюция (2026-07-20):** реализовано и принято. Дельта: CLI 976→1235, тесты 437→521 + 799→1112; сьюты: 49 unittest OK + 8 shell PASS (baseline 34/8). Конвейер: spec-ревью постановки (NEEDS_REWORK, 7 MAJOR + 3 MINOR — постановка переписана до реализации) → реализация на medium → саморевью (3 находки, починены) → слепые ревью: TASK 1 MINOR + 2 NIT, CODE 4 MAJOR + 1 MINOR (гонки идентичности/очистки) → триаж: FIXED 8, REJECTED 0 → обе перепроверки FIXES_CONFIRMED. ## Модель идентичности процессов Идентичность определяется отдельно для ребёнка-Xray и для супервизора; «обход argv в поисках любого совпадения» (`verify_managed_state`) заменяется этим механизмом. - **Общее для обоих:** PID + время старта. Время старта — поле 22 (`starttime`) из `/proc/PID/stat`, целое число тиков с момента загрузки; хранится в state как есть и сравнивается на точное равенство. Парсер обязан сначала изолировать поле `comm` по последней `)` (comm может содержать пробелы и скобки) и только потом индексировать поля; любая ошибка чтения/разбора трактуется как несовпадение (fail-closed). - **Ребёнок Xray:** канонический путь `/proc/PID/exe` (realpath) совпадает с сохранённым `xray_path`. - **Супервизор:** его exe — интерпретатор Python, поэтому проверяется иначе: в state сохраняется канонический путь скрипта CLI (`supervisor_path`, realpath на момент старта); при проверке операнд скрипта из `/proc/PID/cmdline` резолвится (относительные пути — через `/proc/PID/cwd`) и сравнивается с `supervisor_path`. Поддерживаемые формы запуска (относительный путь, PATH, симлинк) не ломаются — сравнение идёт по realpath. - **Границы загрузки:** в state сохраняется `boot_id` (`/proc/sys/kernel/random/boot_id`). Несовпадение с текущим означает устаревшее состояние: записанные PID трактуются как мёртвые, сигналы не посылаются. - **Гонка check-then-kill:** непосредственно перед каждым `os.kill` идентичность перечитывается заново; несовпадение — отказ без сигнала. Остаточная гонка «процесс умер между перечиткой и kill» принята как риск модели доверенных пользователей (pidfd не используем: Python 3.8+, переносимость). ## Чекбоксы - [x] Взаимное исключение: супервизор на всё время жизни держит неблокирующий эксклюзивный `fcntl.flock` на lock-файле в служебном каталоге. Второй одновременный `start` завершается понятным отказом, не запуская процессов и не трогая чужие state/конфиг. `state.json` остаётся данными для восстановления после сбоя, а не механизмом блокировки. Последовательные повторный `start` и смена профиля при живом экземпляре по-прежнему отвергаются. - [x] Схема state (права 0600, каталоги 0700) — ровно эти ключи: `supervisor_pid`, `xray_pid`, `xray_path`, `config_path`, `address`, `port` (существующие) + `supervisor_path`, `supervisor_starttime`, `xray_starttime`, `boot_id` (новые). Ссылка VLESS и её секреты в state не попадают. - [x] `status` и `stop` применяют модель идентичности выше; чужой, устаревший или переиспользованный PID не признаётся управляемым процессом (в пределах принятой остаточной гонки). - [x] Три исхода проверки состояния, у каждого — своё поведение: 1. **Здоровая пара** (супервизор и ребёнок живы, идентичность подтверждена): `stop` сигналит супервизору, как сейчас. 2. **Сирота** (супервизор исчез, ребёнок жив и подтверждён по PID + starttime + boot_id + exe): `stop` сигналит ребёнку напрямую и удаляет state и сгенерированный конфиг; доступность порта для этого НЕ требуется. `status` — отдельная ненулевая диагностика «сирота»; `env` и `run` — отказ. 3. **Непроверяемое/устаревшее состояние:** если записанный PID жив, но идентичность не подтверждена — никаких сигналов, state сохраняется, диагностика с инструкцией ручного удаления; если записанные PID мертвы (или boot_id не совпал) — безопасная очистка устаревших файлов разрешена. - [x] Конфликт локального порта: `start` при уже занятом порте завершается ненулевым кодом, диагностика называет порт занятым/недоступным, чужой слушатель не затронут, свои сгенерированные файлы убраны. Инспекция владения слушателем (/proc/net/tcp) запрещена. - [x] Секреты: полная VLESS-ссылка никуда не персистится; UUID, ключи, секретоподобные значения отсутствуют в stdout, stderr, argv, `state.json` и lock-файле. Единственное допустимое место — `client.json` с правами 0600 внутри каталога 0700; он удаляется при штатном завершении и при `stop` сироты. - [x] Автоматические тесты (без root, на обычной dev-машине): - блокировка: удержанный извне flock → `start` отказывает, ничего не запустив и не изменив; - переиспользованный PID моделируется правкой сохранённого `starttime` у живого процесса (реальную гонку PID не воспроизводим); отдельно — мёртвый PID, неверный исполняемый файл, несовпавший `boot_id`; - SIGKILL супервизора → `stop` добивает подтверждённого сироту и чистит state/конфиг; - занятый порт (заранее открытый сокет) → контракт из чекбокса выше; - юнит-тест парсера `/proc/PID/stat` на синтетических `comm` с пробелами и `)`; - фикстура фейкового Xray адаптирована под проверку `/proc/PID/exe` (ребёнок должен быть настоящим исполняемым файлом — например, `sys.executable` как «бинарник xray» со скриптами-операндами; детали на усмотрение исполнителя); - все сохраняемые ниже поведения остаются зелёными. ## Сохранить, не переписывать Уже реализовано и покрыто тестами в 02/06 — не переделывать, только не сломать: проброс SIGINT/SIGTERM/SIGHUP ребёнку; очистка state и конфига при штатном завершении; права 0600/0700; последовательные отказы (повторный start, смена профиля, несовпадение машинного адреса/порта со state); loopback-only; секреты вне stdout/stderr/argv; foreground-супервизор; fail-fast `pclaude`/`pcodex`; внешний путь без машинного конфига. Запрещено восстанавливать срезанное в 06: владение слушателем через `/proc/net/tcp`, `profile_fingerprint`, инспекцию zombie-состояний, SIGALRM-машинерию, поддержку второй версии Xray. ## Не трогать - `bash/.bash_functions`, установщик, README — вне объёма. - Никаких коммитов. ## Отчёт исполнителя 1. Изменённые файлы со строкой «зачем» на каждый. 2. Команды тестов и счётчики (`python3 -m unittest discover -s tests`, `bash tests/test_proxy_launchers.sh`; baseline: 34 OK / 8 PASS — новых станет больше, указать числа). 3. Чекбоксы: закрыт / не закрыт и почему. 4. Принятые по ходу решения и сомнения, если есть.