feat(xray-proxy): добавлен безопасный жизненный цикл управляемого Xray

- Зачем:
  - stop/status опирались на обход argv и могли счесть чужой или
    переиспользованный PID управляемым процессом, а параллельный start
    и аварийная гибель супервизора не имели определённого поведения.
- Что:
  - введена модель идентичности: PID + starttime (поле 22 /proc/PID/stat),
    /proc/PID/exe для ребёнка, канонический путь скрипта для супервизора,
    boot_id как граница загрузки; перечитка перед каждым сигналом.
  - добавлены lifetime-flock супервизора, три исхода проверки состояния
    (здоровая пара / подтверждённый сирота / непроверяемое), отказ при
    занятом порте пробным bind и очистка stale только под lock.
  - тесты расширены до 49 (blank flock, подмена starttime, чужой exe,
    boot_id, SIGKILL супервизора, занятый порт, парсер stat); спека
    приведена к модели идентичности; тикет 03 закрыт, леджер дополнен.
- Проверка:
  - python3 -m unittest discover -s tests — 49 OK.
  - bash tests/test_proxy_launchers.sh — 8 PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Fable 5 committed 2026-07-20 07:02:33 -04:00
1 parent f2d303c233
commit 5ace0cfae0
6 files changed
+890 -100

No files matched your search

@@ -306,14 +306,30 @@ CLI предоставляет `install`, `start`, `stop`, `status`, `env` и `r
останавливает Xray. Отдельный постоянный журнал и команда `logs` не нужны:
достаточно истории tmux.
Супервизор владеет блокировкой, дочерним процессом и служебным каталогом
`~/.local/state/xray-proxy/`. Файл состояния содержит PID и время старта обоих
процессов, путь к бинарнику и порт, но не VLESS-ссылку и не её секреты. `status`
и `stop` сверяют PID со временем старта и
ожидаемым исполняемым файлом, поэтому повторное использование PID не считается
управляемым процессом. `stop` посылает SIGTERM супервизору, а тот завершает Xray
и очищает состояние. Если супервизор аварийно исчез, `stop` может завершить
только дочерний Xray, идентичность которого подтверждена теми же данными.
Супервизор на всё время работы удерживает неблокирующую эксклюзивную блокировку
`fcntl.flock` в служебном каталоге `~/.local/state/xray-proxy/`. Второй `start`
отказывает до запуска процессов и изменения state или сгенерированного
конфига. `state.json` нужен для восстановления после сбоя, но не служит
блокировкой.
Файл состояния содержит ровно `supervisor_pid`, `xray_pid`, `xray_path`,
`config_path`, `address`, `port`, `supervisor_path`, `supervisor_starttime`,
`xray_starttime` и `boot_id`. Время старта — поле 22 `/proc/PID/stat`; поле
`comm` сначала отделяется по последней `)`. Для обоих процессов проверяются PID,
точное время старта и `boot_id`. У ребёнка дополнительно сверяется канонический
`/proc/PID/exe` с `xray_path`. У Python-супервизора операнд скрипта из
`/proc/PID/cmdline` разрешается относительно `/proc/PID/cwd` и сравнивается с
каноническим `supervisor_path`. Ошибка чтения или разбора означает несовпадение.
Непосредственно перед каждым сигналом идентичность читается повторно.
Проверка состояния различает три исхода. Для здоровой пары `stop` посылает
SIGTERM супервизору. Если супервизор исчез, но ребёнок подтверждён, `status`
сообщает о сироте с ненулевым кодом, `env` и `run` отказывают, а `stop` посылает
сигнал ребёнку напрямую без требования доступности порта и удаляет state и
конфиг. При живом, но неподтверждённом PID сигналы не посылаются, файлы
сохраняются, а диагностика требует ручной проверки и удаления. Если оба PID
мертвы или `boot_id` не совпадает, устаревшие state и конфиг можно безопасно
удалить без сигналов.
У пользователя может быть только один управляемый экземпляр. Повторный `start`
не заменяет его другим профилем. Для смены профиля нужны явные `stop` и `start`.