# Модель обработки ошибок XC_VM обработка ошибок состоит из трех уровней: - **Коды ошибок** -- что привело к сбою (централизованный реестр именованных строк ошибок) - **Обработчики ошибок** -- как генерируется HTTP-ответ клиента (`generateError()`, `generate404()`) - **Подсистема регистратора** -- фиксация во время выполнения PHP ошибок, неперехваченных исключений и фатальных сбоев --- ## Обзор потока ```text Application code | +-- generateError('CODE') // deliberate error response | -> debug mode: styled HTML page with code + description | -> production: generate404() or explicit HTTP code | +-- PHP warning / notice / error // runtime errors | -> Logger::handleError() | -> maps errno to level (ERROR, WARNING, NOTICE, INFO) | -> writes base64-encoded JSON to error_log.log | -> optionally displays on screen | +-- Uncaught exception // unhandled Throwable | -> Logger::handleException() | -> logs as EXCEPTION with full chained trace | +-- Fatal error at shutdown // E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR -> Logger::handleFatal() -> logs as FATAL (no stack trace available) ``` --- ## Реестр кодов ошибок Все коды объявляются методом `ErrorResponder::codes()` в `src/Core/Error/ErrorResponder.php`, который возвращает массив code => English-описание. Формат кода: - Ключ: строка в верхнем регистре (пример: `INVALID_CREDENTIALS`) - Значение: понятное для человека описание на английском языке Используйте только централизованные определения кода. Не следует жестко кодировать текст ошибки в обработчиках конечных точек. ### Полный список кодов |Код|Описание| | --- | --- | | `API_IP_NOT_ALLOWED` |IP-адрес не разрешен для доступа к API.| | `ARCHIVE_DOESNT_EXIST` |Для этого идентификатора потока отсутствуют архивные файлы.| | `ASN_BLOCKED` |ASN был заблокирован.| | `BANNED` |Линия была заблокирована.| | `BLOCKED_USER_AGENT` |Пользовательский агент был заблокирован.| | `CACHE_INCOMPLETE` |Генерируется кэш...| | `DEVICE_NOT_ALLOWED` |Устройствам MAG и Enigma не разрешен доступ к этому файлу.| | `DISABLED` |Линия была отключена.| | `DOWNLOAD_LIMIT_REACHED` |Достигнут лимит одновременной загрузки.| | `E2_DEVICE_LOCK_FAILED` |Проверка блокировки устройства не удалась.| | `E2_DISABLED` |Устройство было отключено.| | `E2_NO_TOKEN` |Токен не был указан.| | `E2_TOKEN_DOESNT_MATCH` |Токен не соответствует записям.| | `E2_WATCHDOG_TIMEOUT` |Истек лимит времени.| | `EMPTY_USER_AGENT` |Пустые пользовательские агенты запрещены.| | `EPG_DISABLED` |EPG был отключен.| | `EPG_FILE_MISSING` |Кэшированные файлы EPG отсутствуют.| | `EXPIRED` |Срок действия строки истек.| | `FORCED_COUNTRY_INVALID` |Страна не совпадает с принудительной страной.| | `GENERATE_PLAYLIST_FAILED` |Не удалось создать список воспроизведения.| | `HLS_DISABLED` |HLS был отключен.| | `HOSTING_DETECT` |Обнаружен хостинг-сервер.| | `INVALID_API_PASSWORD` |Неверный пароль API.| | `INVALID_CREDENTIALS` |Имя пользователя или пароль неверны.| | `INVALID_HOST` |Доменное имя не распознано.| | `INVALID_STREAM_ID` |Идентификатор потока не существует.| | `INVALID_TYPE_TOKEN` |Токены не могут быть использованы для этого типа потока.| | `IP_BLOCKED` |IP-адрес был заблокирован.| | `IP_MISMATCH` |Текущий IP-адрес не соответствует исходному IP-адресу подключения.| | `ISP_BLOCKED` |Провайдер был заблокирован.| | `LB_TOKEN_INVALID` |Токен AES не может быть расшифрован.| | `LEGACY_EPG_DISABLED` |Устаревший epg.php доступ был отключен.| | `LEGACY_GET_DISABLED` |Устаревший get.php доступ был отключен.| | `LEGACY_PANEL_API_DISABLED` |Устаревший panel_api.php доступ был отключен.| | `LINE_CREATE_FAIL` |Не удалось вставить строку в базу данных.| | `NO_CREDENTIALS` |Учетные данные не были указаны.| | `NO_SERVERS_AVAILABLE` |В настоящее время серверы для этого потока не доступны.| | `NO_TIMESTAMP` |Временная метка архива не указана.| | `NO_TOKEN_SPECIFIED` |Зашифрованный токен AES не был указан.| | `NOT_ENIGMA_DEVICE` |Линия - это не загадочное устройство.| | `NOT_IN_ALLOWED_COUNTRY` |Нет в списке разрешенных стран.| | `NOT_IN_ALLOWED_IPS` |Его нет в списке разрешенных IP-адресов.| | `NOT_IN_ALLOWED_UAS` |Отсутствует в списке разрешенных пользовательских агентов.| | `NOT_IN_BOUQUET` |У Line нет доступа к этому идентификатору потока.| | `PLAYER_API_DISABLED` |API плеера был отключен.| | `PROXY_ACCESS_DENIED` |Вы не можете получить прямой доступ к этому потоку, пока включен прокси-сервер.| | `PROXY_DETECT` |Обнаружен прокси-сервер.| | `PROXY_NO_API_ACCESS` |Не удается получить доступ к API через прокси.| | `RESTREAM_DETECT` |Обнаружен повторный поток.| | `STALKER_CHANNEL_MISMATCH` |Идентификатор потока не совпадает с токеном stalker.| | `STALKER_DECRYPT_FAILED` |Не удалось расшифровать токен сталкера.| | `STALKER_INVALID_KEY` |Недействительный ключ сталкера.| | `STALKER_IP_MISMATCH` |IP-адрес не соответствует токену stalker.| | `STALKER_KEY_EXPIRED` |Срок действия жетона сталкера истек.| | `STREAM_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`. Это простые функции (не методы класса), загружаемые в начале начальной загрузки. ### `generateError(string $rError, bool $rKill = true, ?int $rCode = null)` Выдает ответ об ошибке HTTP. Поведение зависит от параметра `debug_show_errors`: ```text if debug_show_errors === true render styled HTML page showing error key + description if $rKill -> exit() else (production) if $rKill if $rCode is set -> http_response_code($rCode) + exit() else -> generate404() // if !$rKill, does nothing in production mode ``` Параметры: |Параметр|Тип|По умолчанию|Значение| | --- | --- | --- | --- | | `$rError` | `string` |--|Ключ от `ErrorResponder::codes()`| | `$rKill` | `bool` | `true` |Завершить работу скрипта после вывода| | `$rCode` |`инт\|нулевой`| `null` |Явный код ответа HTTP (обходит 404 в рабочей среде)| Примеры: ```php generateError('INVALID_CREDENTIALS'); // production: 404 + exit generateError('API_IP_NOT_ALLOWED', true, 403); // production: 403 + exit generateError('STREAM_OFFLINE', false); // production: no output, no exit ``` ### `generate404(bool $rKill = true)` Возвращает страницу в стиле nginx `404 Not Found` и устанавливает HTTP 404. HTML-код содержит комментарии с дополнениями, чтобы скрыть страницы ошибок, отображаемые в браузере MSIE и Chrome. ```php generate404(); // 404 + exit generate404(false); // 404, continue execution ``` --- ## Подсистема регистратора Определен в `src/Core/Logging/Logger.php`. Класс `final`, который регистрирует три глобальных обработчика PHP для отслеживания всех ошибок во время выполнения и записи их в файл. ### Инициализация ```php Logger::init(bool $showErrors, string $logFile): void ``` Регистры: 1. `set_error_handler([Logger::class, 'handleError'])` -- PHP предупреждения, извещения, ошибки 2. `set_exception_handler([Logger::class, 'handleException'])` -- не перехвачено `Throwable` 3. `register_shutdown_function([Logger::class, 'handleFatal'])` -- неустранимые ошибки при завершении работы Также настраивает `error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED)` и устанавливает `display_errors` / `display_startup_errors` на основе `$showErrors`. ### Где вызывается функция Logger::init() Регистратор инициализируется в двух местах, в зависимости от пути запроса: |Путь входа|Файл|Как| | --- | --- | --- | |Bootstrap (все контексты)| `src/bootstrap.php` |`XC_Bootstrap::loadConstants()` вызовы `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`| |Конечные точки потоковой передачи| `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` | Обработчик завершения работы (`handleFatal()`) проверяет `error_get_last()` на наличие этих фатальных типов и регистрирует их как `FATAL`: |PHP постоянные значения при выключении|Уровень регистрации| | --- | --- | |`E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_COMPILE_ERROR`| `FATAL` | Неперехваченные исключения, зарегистрированные с помощью `handleException()`, всегда используют уровень `EXCEPTION`. Ошибки, подавленные с помощью оператора `@`, игнорируются (обработчик проверяет `error_reporting() & $errno`). ### Формат журнала Each log entry is written as a single line: `base64_encode(json_encode($data))` followed by a newline. This prevents line corruption from multi-line messages. Декодированная структура 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` |Трассировка стека (форматированная строка). Пусто для неустранимых ошибок.| | `time` |Временная метка Unix| | `env` |PHP Имя SAPI (`cli`, `fpm-fcgi` и т.д.)| ### Расположение файла журнала Путь по умолчанию: `LOGS_TMP_PATH . 'error_log.log'` Если каталог журнала не существует, Logger создает его с правами доступа `0775`. При запуске от имени root (распространенного в контейнерах) файлу присваивается значение `xc_vm:xc_vm` с режимом `0664`. ### Вывод на экран Когда `$showErrors` равно `true`, регистратор также отображает ошибки напрямую: - **КЛИ:** выходной сигнал терминала с цветовой кодировкой (красный - НЕИСПРАВИМОСТЬ/ОШИБКА, желтый - ПРЕДУПРЕЖДЕНИЕ, синий - УВЕДОМЛЕНИЕ) - **Сеть:** встроенный `
` с моноширинным шрифтом, красной рамкой и трассировкой стека в блоке `
`

---

## Конвейер ведения журнала: Передача файла в базу данных

Программа ведения журнала записывает данные в файл `error_log.log` на диске. Отдельная подсистема считывает этот файл и сохраняет записи в таблице базы данных `panel_logs`:

1. **Лесоруб** записывает строки JSON в кодировке base64 в `error_log.log`
2. **Файловый регистратор** (`src/Core/Logging/FileLogger.php`) предоставляет дополнительный интерфейс ведения журнала, используемый кодом приложения (ошибки PDO, ошибки EPG и т.д.), который записывает данные в тот же файл в том же формате
3. Записи заносятся в таблицу `panel_logs`
4. **Диагностическая служба** (`src/Core/Diagnostics/DiagnosticsService.php`) считывается из `panel_logs` для:
   - `downloadPanelLogs()` -- извлекает до 1000 последних ошибок, не связанных с EPG, затем обрезает таблицу
   - `submitPanelLogs()` -- отправляет логи на центральный сервер API для анализа
5. Панель администратора отображает эти журналы в разделе **Управление > Журналы > Ошибки панели**

### Фильтрация шума файлового регистратора

`FileLogger::log()` пропускает записи, которые соответствуют:

- Сообщения, содержащие `panel_logs` в дополнительном поле (предотвращает рекурсивное ведение журнала)
- Сообщения, соответствующие `timeout exceeded`, `lock wait timeout` или `duplicate entry` (зашумленные ошибки MySQL)

---

## Другие лесорубы

Каталог `src/Core/Logging/` содержит дополнительные специализированные регистраторы:

|Класс|Файл|Цель|
| --- | --- | --- |
| `Logger` | `Logger.php` |Обработчик глобальной 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` и записывают JSON в кодировке base64.

---

## Типы исключений в кодовой базе

В кодовой базе определено небольшое количество пользовательских классов исключений. Все неперехваченные исключения перехватываются командой `Logger::handleException()`, которая регистрирует всю цепочку исключений (включая `getPrevious()`).

|Класс исключений|Базовый класс|Местоположение|
| --- | --- | --- |
| `DropboxException` | `\Exception` | `src/Core/Storage/DropboxException.php` |
| `M3uParser\Exception` | `\Exception` | `src/vendor/gemorroj/m3u-parser/src/Exception.php` |
| `DataBuildingException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Parser/DataBuildingException.php` |
| `DefinitionException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Definition/DefinitionException.php` |
| `DumpingException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Dumper/DumpingException.php` |

В большинстве случаев в коде приложения используются общие ошибки `Exception` или используется встроенная система ошибок PHP. Обработчик исключений регистратора принимает любое `Throwable`.

---

## Отладка против производства

### Производство (по умолчанию: `debug_show_errors = false`)

- `generateError()` возвращает общую страницу 404 (или явный HTTP-код), скрывая внутреннюю причину сбоя
- Регистратор по-прежнему записывает все ошибки в `error_log.log` на диск
- для `display_errors` и `display_startup_errors` заданы значения `'0'`
- Ошибки видны только через панель администратора (страница ошибок панели) или файлы журналов

### Отладка (`debug_show_errors = true`)

- `generateError()` показывает стилизованную страницу с ключом ошибки и сопоставленным описанием
- Регистратор дополнительно отображает ошибки на экране (вывод CLI с цветовой кодировкой или встроенный HTML).
- для `display_errors` и `display_startup_errors` заданы значения `'1'`

Не включайте отображение отладки на рабочих узлах.

---

## Регистрация обработчика ошибок Bootstrap

Инфраструктура обработки ошибок загружается на ранней стадии загрузки:

1. `bootstrap.php` определяет `MAIN_HOME` и регистрирует автозагрузчик Composer
2. `XC_Bootstrap::loadConstants()` загружает (по порядку):
   - `Core/Error/ErrorHandler.php` -- определяет `generateError()` и `generate404()` (загружается глобально через Composer `autoload.files`); сам каталог кодов равен `ErrorResponder::codes()` в `Core/Error/ErrorResponder.php`
   - Путь и конфигурационные файлы
   - `Core/Logging/Logger.php` -- определение класса
3. вызывается `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`, регистрирующий три глобальных обработчика
4. Начиная с этого момента, регистрируются все ошибки PHP, неперехваченные исключения и фатальные сбои

Для конечных точек потоковой передачи, которые обходят полную загрузку, `RequestGuard.php` выполняет шаги 2-3 независимо: загружает настройки из файлового кэша, определяет `PHP_ERRORS` и вызывает `Logger::init()`.

---

## Добавление нового кода ошибки

1. Добавьте новую запись в массив, возвращаемый `ErrorResponder::codes()` в `src/Core/Error/ErrorResponder.php`:

```php
'MY_NEW_ERROR' => 'Human-readable description.',
```

2. Используйте это в коде:

```php
generateError('MY_NEW_ERROR');
```

Описания должны быть на английском языке для приведения в соответствие с существующим реестром.

---

## Связанные файлы

|Файл|Цель|
| --- | --- |
| `src/Core/Error/ErrorResponder.php` |Централизованная карта кодов ошибок (`ErrorResponder::codes()`)|
| `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` |Путь потоковой передачи: защита от наводнений, проверка хоста, запуск регистратора|
| `src/Core/Diagnostics/DiagnosticsService.php` |Считывает таблицу `panel_logs` для отображения администратором и отправки по API|
| `src/bootstrap.php` |Включает уровень ошибок и регистратор во всех контекстах начальной загрузки|