Files
XC_VM/docs/ru/guides/error-handling.md
T
Divarion-D 76844fef11 docs: restructure, fix PSR-4 drift, and unify en/ru
Overhaul the Docsify documentation (English + Russian) so it matches the current
codebase and follows one consistent pattern.

Content accuracy (post-migration):
- Rewrite development/autoloader.md to PSR-4 / Composer (the old XC_Autoloader
  scanner, igbinary tmp/cache/autoload_map and registerDirectories are gone).
- PascalCase every source path (src/core -> src/Core, domain/Stream, cli/Commands,
  public/Controllers, Infrastructure/Redis, ...) across all docs.
- Replace the removed autoload.php references with vendor/autoload.php
  (build_system, bootstrap-contexts, error-handling, modules).
- ssl-generation: note that the installer now auto-generates a unique self-signed
  certificate before Nginx starts.

Common pattern (Clean & uniform):
- Strip emoji from headings; remove the in-page Navigation blocks (the Docsify
  sidebar already provides navigation).
- One H1 + intro per doc; uniform "Related files" / "Связанные файлы" section,
  added to the code-centric docs that lacked it.

Structure:
- Remove the empty stray docs/api/; move updates_checklist.md into builds/;
  link the previously-orphaned ucs-integration.md.
- Regroup the sidebars (split the oversized guides group into Developer Guides /
  Security & Access / Integrations; fold builds into Build & Release).

Augment:
- dev-workflow: Local Setup (make dev-tools) + Quality Checks (phpstan, cs, gates).
- build_system: Composer Dependencies section (committed prod-only vendor,
  committed lock, dev tools via composer install, no build-time vendor step).

en/ru parity:
- Apply the same structure, fixes and pattern to docs/ru/ (translated), including
  a new Russian ucs-integration.md. The en and ru file sets are now identical.
2026-06-26 15:56:15 +03:00

381 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Модель обработки ошибок
Обработка ошибок в XC_VM имеет три уровня:
- **Коды ошибок** — что пошло не так (централизованный реестр именованных строк ошибок)
- **Обработчики ошибок** — как формируется HTTP-ответ клиенту (`generateError()`, `generate404()`)
- **Подсистема Logger** — runtime-перехват ошибок PHP, необработанных исключений и фатальных сбоев
---
## Обзор потока
```text
Код приложения
|
+-- generateError('CODE') // намеренный ответ с ошибкой
| -> debug-режим: стилизованная HTML-страница с кодом + описанием
| -> production: generate404() или явный HTTP-код
|
+-- PHP warning / notice / error // runtime-ошибки
| -> Logger::handleError()
| -> сопоставляет errno с уровнем (ERROR, WARNING, NOTICE, INFO)
| -> пишет base64-encoded JSON в error_log.log
| -> опционально отображает на экране
|
+-- Необработанное исключение // unhandled Throwable
| -> Logger::handleException()
| -> логирует как EXCEPTION с полной цепочкой trace
|
+-- Фатальная ошибка при shutdown // E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR
-> Logger::handleFatal()
-> логирует как FATAL (stack trace недоступен)
```
---
## Реестр кодов ошибок
Все коды объявляются в `src/Core/Error/ErrorCodes.php` как глобальный массив `$rErrorCodes`.
Формат кода:
- Ключ: строка в верхнем регистре (например: `INVALID_CREDENTIALS`)
- Значение: человекочитаемое описание на английском
Используйте только централизованные определения кодов. Не хардкодьте тексты ошибок в обработчиках endpoint'ов.
### Полный список кодов
| Код | Описание |
| --- | --- |
| `API_IP_NOT_ALLOWED` | IP не разрешён для доступа к API. |
| `ARCHIVE_DOESNT_EXIST` | Архивные файлы отсутствуют для этого stream ID. |
| `ASN_BLOCKED` | ASN заблокирован. |
| `BANNED` | Линия забанена. |
| `BLOCKED_USER_AGENT` | User-agent заблокирован. |
| `CACHE_INCOMPLETE` | Кэш генерируется... |
| `DEVICE_NOT_ALLOWED` | Устройствам MAG и Enigma запрещён доступ. |
| `DISABLED` | Линия отключена. |
| `DOWNLOAD_LIMIT_REACHED` | Достигнут лимит одновременных загрузок. |
| `E2_DEVICE_LOCK_FAILED` | Проверки device lock не пройдены. |
| `E2_DISABLED` | Устройство отключено. |
| `E2_NO_TOKEN` | Токен не указан. |
| `E2_TOKEN_DOESNT_MATCH` | Токен не совпадает с записями. |
| `E2_WATCHDOG_TIMEOUT` | Достигнут лимит времени. |
| `EMPTY_USER_AGENT` | Пустые user-agent запрещены. |
| `EPG_DISABLED` | EPG отключён. |
| `EPG_FILE_MISSING` | Кэшированные файлы EPG отсутствуют. |
| `EXPIRED` | Срок линии истёк. |
| `FORCED_COUNTRY_INVALID` | Страна не совпадает с forced country. |
| `GENERATE_PLAYLIST_FAILED` | Не удалось сгенерировать плейлист. |
| `HLS_DISABLED` | HLS отключён. |
| `HOSTING_DETECT` | Обнаружен hosting-сервер. |
| `INVALID_API_PASSWORD` | Неверный пароль API. |
| `INVALID_CREDENTIALS` | Неверное имя пользователя или пароль. |
| `INVALID_HOST` | Имя домена не распознано. |
| `INVALID_STREAM_ID` | Stream ID не существует. |
| `INVALID_TYPE_TOKEN` | Токены нельзя использовать для этого типа потока. |
| `IP_BLOCKED` | IP заблокирован. |
| `IP_MISMATCH` | Текущий IP не совпадает с IP начального подключения. |
| `ISP_BLOCKED` | ISP заблокирован. |
| `LB_TOKEN_INVALID` | AES-токен не удаётся расшифровать. |
| `LEGACY_EPG_DISABLED` | Доступ к legacy epg.php отключён. |
| `LEGACY_GET_DISABLED` | Доступ к legacy get.php отключён. |
| `LEGACY_PANEL_API_DISABLED` | Доступ к legacy panel_api.php отключён. |
| `LINE_CREATE_FAIL` | Не удалось вставить линию в базу данных. |
| `NO_CREDENTIALS` | Учётные данные не указаны. |
| `NO_SERVERS_AVAILABLE` | Сейчас нет доступных серверов для этого потока. |
| `NO_TIMESTAMP` | Не указана временная метка архива. |
| `NO_TOKEN_SPECIFIED` | Не указан AES-зашифрованный токен. |
| `NOT_ENIGMA_DEVICE` | Линия не является устройством enigma. |
| `NOT_IN_ALLOWED_COUNTRY` | Не в списке разрешённых стран. |
| `NOT_IN_ALLOWED_IPS` | Не в списке разрешённых IP. |
| `NOT_IN_ALLOWED_UAS` | Не в списке разрешённых user-agent. |
| `NOT_IN_BOUQUET` | У линии нет доступа к этому stream ID. |
| `PLAYER_API_DISABLED` | Player API отключён. |
| `PROXY_ACCESS_DENIED` | Невозможно обратиться к потоку напрямую, пока включён proxy. |
| `PROXY_DETECT` | Обнаружен proxy. |
| `PROXY_NO_API_ACCESS` | Нельзя обращаться к API через proxy. |
| `RESTREAM_DETECT` | Обнаружен рестрим. |
| `STALKER_CHANNEL_MISMATCH` | Stream ID не совпадает со stalker-токеном. |
| `STALKER_DECRYPT_FAILED` | Не удалось расшифровать stalker-токен. |
| `STALKER_INVALID_KEY` | Неверный stalker-ключ. |
| `STALKER_IP_MISMATCH` | IP не совпадает со stalker-токеном. |
| `STALKER_KEY_EXPIRED` | Срок stalker-токена истёк. |
| `STREAM_OFFLINE` | Поток сейчас offline. |
| `SUBTITLE_DOESNT_EXIST` | Файл субтитров не существует. |
| `THUMBNAIL_DOESNT_EXIST` | Файл превью не существует. |
| `THUMBNAILS_NOT_ENABLED` | Превью не включены для этого потока. |
| `TOKEN_ERROR` | AES-токен содержит неполные данные. |
| `TOKEN_EXPIRED` | Срок AES-токена истёк. |
| `TS_DISABLED` | MPEG-TS отключён. |
| `USER_ALREADY_CONNECTED` | Линия уже подключена с другого IP. |
| `USER_DISALLOW_EXT` | Расширение не в списке разрешённых. |
| `VOD_DOESNT_EXIST` | VOD-файл не существует. |
| `WAIT_TIME_EXPIRED` | Истёк таймаут запуска потока, не удалось запустить. |
Специфичные для стриминга коды (`CACHE_INCOMPLETE`, `SUBTITLE_DOESNT_EXIST`, `NO_SERVERS_AVAILABLE`, `PROXY_ACCESS_DENIED`) были перенесены из `stream/init.php` в централизованный реестр.
---
## Обработчики ошибок
Определены в `src/Core/Error/ErrorHandler.php`. Это обычные функции (а не методы класса), подключаемые рано в bootstrap.
### `generateError(string $rError, bool $rKill = true, ?int $rCode = null)`
Формирует HTTP-ответ с ошибкой. Поведение зависит от настройки `debug_show_errors`:
```text
если debug_show_errors === true
рендерит стилизованную HTML-страницу с ключом ошибки + описанием
если $rKill -> exit()
иначе (production)
если $rKill
если задан $rCode -> http_response_code($rCode) + exit()
иначе -> generate404()
// если !$rKill, в production-режиме ничего не делает
```
Параметры:
| Параметр | Тип | По умолчанию | Смысл |
| --- | --- | --- | --- |
| `$rError` | `string` | -- | Ключ из `$rErrorCodes` |
| `$rKill` | `bool` | `true` | Завершить скрипт после вывода |
| `$rCode` | `int\|null` | `null` | Явный HTTP-код ответа (обходит 404 в production) |
Примеры:
```php
generateError('INVALID_CREDENTIALS'); // production: 404 + exit
generateError('API_IP_NOT_ALLOWED', true, 403); // production: 403 + exit
generateError('STREAM_OFFLINE', false); // production: без вывода, без exit
```
### `generate404(bool $rKill = true)`
Возвращает страницу `404 Not Found` в стиле nginx и устанавливает HTTP 404. HTML включает padding-комментарии для подавления «дружелюбных» страниц ошибок в MSIE и Chrome.
```php
generate404(); // 404 + exit
generate404(false); // 404, выполнение продолжается
```
---
## Подсистема Logger
Определена в `src/Core/Logging/Logger.php`. Класс `final`, регистрирующий три глобальных PHP-обработчика для перехвата всех runtime-ошибок и записи их в файл.
### Инициализация
```php
Logger::init(bool $showErrors, string $logFile): void
```
Регистрирует:
1. `set_error_handler([Logger::class, 'handleError'])` — PHP warning, notice, error
2. `set_exception_handler([Logger::class, 'handleException'])` — необработанные `Throwable`
3. `register_shutdown_function([Logger::class, 'handleFatal'])` — фатальные ошибки при shutdown
Также настраивает `error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED)` и устанавливает `display_errors` / `display_startup_errors` на основе `$showErrors`.
### Где вызывается Logger::init()
Logger инициализируется в двух местах в зависимости от пути запроса:
| Путь входа | Файл | Как |
| --- | --- | --- |
| Bootstrap (все контексты) | `src/bootstrap.php` | `XC_Bootstrap::loadConstants()` вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')` |
| Стриминговые endpoint'ы | `src/Core/Http/RequestGuard.php` | Загружает настройки из файлового кэша, определяет `PHP_ERRORS`, затем вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')` |
В обоих случаях `PHP_ERRORS` отражает настройку `debug_show_errors` (по умолчанию `false`, когда настройки недоступны).
### Сопоставление уровней ошибок
`Logger::handleError()` сопоставляет константы PHP-ошибок строкам уровней через `mapErrorLevel()`:
| Константа(ы) PHP | Уровень лога |
| --- | --- |
| `E_ERROR`, `E_CORE_ERROR`, `E_COMPILE_ERROR` | `ERROR` |
| `E_WARNING`, `E_USER_WARNING` | `WARNING` |
| `E_NOTICE`, `E_USER_NOTICE` | `NOTICE` |
| Все другие значения `errno` | `INFO` |
Обработчик shutdown (`handleFatal()`) проверяет `error_get_last()` на эти фатальные типы и логирует их как `FATAL`:
| Константа(ы) PHP при shutdown | Уровень лога |
| --- | --- |
| `E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_COMPILE_ERROR` | `FATAL` |
Необработанные исключения, логируемые `handleException()`, всегда используют уровень `EXCEPTION`.
Ошибки, подавленные оператором `@`, игнорируются (обработчик проверяет `error_reporting() & $errno`).
### Формат лога
Каждая запись лога пишется одной строкой: `base64_encode(json_encode($data))` с переводом строки. Это предотвращает повреждение строк из многострочных сообщений.
Декодированная JSON-структура:
```json
{
"type": "WARNING",
"log_message": "Undefined variable $foo",
"file": "/home/xc_vm/Domain/Stream/StreamService.php",
"line": 142,
"log_extra": "#0 /home/xc_vm/...(line): function()\n#1 ...",
"time": 1716220800,
"env": "fpm-fcgi"
}
```
| Поле | Содержимое |
| --- | --- |
| `type` | Уровень лога: `ERROR`, `WARNING`, `NOTICE`, `INFO`, `EXCEPTION` или `FATAL` |
| `log_message` | Текст сообщения об ошибке/исключении |
| `file` | Абсолютный путь к исходному файлу |
| `line` | Номер строки, где произошла ошибка |
| `log_extra` | Stack trace (форматированная строка). Пустой для фатальных ошибок. |
| `time` | Unix timestamp |
| `env` | Имя PHP SAPI (`cli`, `fpm-fcgi` и т.д.) |
### Расположение файла лога
Путь по умолчанию: `LOGS_TMP_PATH . 'error_log.log'`
Если директория лога не существует, Logger создаёт её с правами `0775`. При запуске под root (часто в контейнерах) файл chown'ится в `xc_vm:xc_vm` с режимом `0664`.
### Вывод на экран
Когда `$showErrors = true`, Logger также рендерит ошибки напрямую:
- **CLI:** цветной вывод в терминал (красный для FATAL/ERROR, жёлтый для WARNING, синий для NOTICE)
- **Web:** inline `<div>` с моноширинным шрифтом, красной рамкой и stack trace в блоке `<pre>`
---
## Конвейер логирования: файл → база данных
Logger пишет в `error_log.log` на диск. Отдельная подсистема читает этот файл и сохраняет записи в таблицу `panel_logs`:
1. **Logger** пишет base64-encoded JSON-строки в `error_log.log`
2. **FileLogger** (`src/Core/Logging/FileLogger.php`) предоставляет вторичный логирующий интерфейс для кода приложения (ошибки PDO, ошибки EPG и т.д.), который пишет в тот же файл в том же формате
3. Записи импортируются в таблицу `panel_logs`
4. **DiagnosticsService** (`src/Core/Diagnostics/DiagnosticsService.php`) читает из `panel_logs` для:
- `downloadPanelLogs()` — получает до 1000 свежих не-EPG ошибок, затем truncate'ит таблицу
- `submitPanelLogs()` — отправляет логи на центральный API-сервер для анализа
5. Админ-панель показывает эти логи в **Management > Logs > Panel Errors**
### Шумовая фильтрация FileLogger
`FileLogger::log()` пропускает записи, соответствующие:
- Сообщения, содержащие `panel_logs` в дополнительном поле (предотвращает рекурсивное логирование)
- Сообщения, совпадающие с `timeout exceeded`, `lock wait timeout` или `duplicate entry` (шумные MySQL-ошибки)
---
## Другие логгеры
Директория `src/Core/Logging/` содержит дополнительные специализированные логгеры:
| Класс | Файл | Назначение |
| --- | --- | --- |
| `Logger` | `Logger.php` | Глобальный обработчик ошибок/исключений/fatal PHP (описан выше) |
| `FileLogger` | `FileLogger.php` | Логирование уровня приложения (ошибки PDO, EPG и т.д.) в `error_log.log` |
| `DatabaseLogger` | `DatabaseLogger.php` | События клиентских стриминговых запросов в `client_request.log` (импортируются в таблицу `client_logs`) |
| `UpdateLogger` | `UpdateLogger.php` | Операции обновления системы в `MAIN_HOME/update.log` (простой текст, не base64) |
Все логгеры, кроме `UpdateLogger`, реализуют `LoggerInterface` и пишут base64-encoded JSON.
---
## Типы исключений в кодовой базе
Кодовая база определяет небольшое число пользовательских классов исключений. Все необработанные исключения перехватываются `Logger::handleException()`, который логирует полную цепочку исключений (включая `getPrevious()`).
| Класс исключения | Базовый класс | Расположение |
| --- | --- | --- |
| `DropboxException` | `Exception` | `src/Core/Storage/DropboxClient.php` |
| `M3uParser\Exception` | `\Exception` | `src/Core/Parsing/M3uParser/src/Exception.php` |
| `DataBuildingException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Parser/DataBuildingException.php` |
| `DefinitionException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Definition/DefinitionException.php` |
| `DumpingException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Dumper/DumpingException.php` |
Большая часть кода приложения использует выбросы обобщённого `Exception` или полагается на встроенную систему ошибок PHP. Exception-обработчик Logger принимает любой `Throwable`.
---
## Debug vs Production
### Production (по умолчанию: `debug_show_errors = false`)
- `generateError()` возвращает универсальную страницу 404 (или явный HTTP-код), скрывая внутреннюю причину сбоя
- Logger по-прежнему пишет все ошибки в `error_log.log` на диск
- `display_errors` и `display_startup_errors` устанавливаются в `'0'`
- Ошибки видны только через админ-панель (страница Panel Errors) или файлы логов
### Debug (`debug_show_errors = true`)
- `generateError()` показывает стилизованную страницу с ключом ошибки и сопоставленным описанием
- Logger дополнительно рендерит ошибки на экран (цветной CLI-вывод или inline HTML)
- `display_errors` и `display_startup_errors` устанавливаются в `'1'`
Не включайте debug-отображение на production-нодах.
---
## Регистрация обработчика ошибок в bootstrap
Инфраструктура обработки ошибок загружается рано в последовательности загрузки:
1. `bootstrap.php` определяет `MAIN_HOME` и регистрирует Composer-автозагрузчик
2. `XC_Bootstrap::loadConstants()` загружает (в порядке):
- `Core/Error/ErrorCodes.php` — заполняет `$rErrorCodes`
- `Core/Error/ErrorHandler.php` — определяет `generateError()` и `generate404()`
- Файлы путей и конфигурации
- `Core/Logging/Logger.php` — определение класса
3. Вызывается `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`, регистрирующий три глобальных обработчика
4. С этого момента все PHP-ошибки, необработанные исключения и фатальные сбои перехватываются
Для стриминговых endpoint'ов, обходящих полный bootstrap, `RequestGuard.php` выполняет шаги 2-3 независимо: загружает настройки из файлового кэша, определяет `PHP_ERRORS` и вызывает `Logger::init()`.
---
## Добавление нового кода ошибки
1. Добавьте новый ключ в `src/Core/Error/ErrorCodes.php`:
```php
'MY_NEW_ERROR' => 'Human-readable description.',
```
2. Используйте его в коде:
```php
generateError('MY_NEW_ERROR');
```
Описания должны оставаться на английском для согласованности с существующим реестром.
---
## Связанные файлы
| Файл | Назначение |
| --- | --- |
| `src/Core/Error/ErrorCodes.php` | Централизованная карта кодов ошибок (`$rErrorCodes`) |
| `src/Core/Error/ErrorHandler.php` | Функции `generateError()` и `generate404()` |
| `src/Core/Logging/Logger.php` | Глобальные обработчики PHP-ошибок, исключений и фатальных |
| `src/Core/Logging/LoggerInterface.php` | Интерфейс контракта логирования |
| `src/Core/Logging/FileLogger.php` | Файловое логирование уровня приложения (PDO, EPG и т.д.) |
| `src/Core/Logging/DatabaseLogger.php` | Логирование событий клиентских стриминговых запросов |
| `src/Core/Logging/UpdateLogger.php` | Логирование операций обновления системы |
| `src/Core/Http/RequestGuard.php` | Стриминговый путь: защита от флуда, проверка хоста, инициализация Logger |
| `src/Core/Diagnostics/DiagnosticsService.php` | Читает таблицу `panel_logs` для отображения в админке и отправки в API |
| `src/bootstrap.php` | Подключает слой ошибок и Logger во всех контекстах bootstrap |