mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-14 12:01:31 +02:00
fix(docs): translate bold spans atomically and auto-prune orphaned ru pages
The line-by-line web translator reordered words inside `**bold**` spans and misplaced/dropped the markers, producing `**LB` or `****` (empty bold). Mask each `**...**` as ONE atomic sentinel: translate the inner text on its own, then store the whole balanced `**inner**` — the engine never sees the markers and cannot reorder or collapse them. Also harden the anthropic prompt to keep emphasis balanced. Auto-prune: after translating, delete generated docs/ru files whose docs/en source no longer exists (renamed/removed) and drop now-empty dirs, so the tree mirrors docs/en 1:1 (removes the stale development/modules.md and guides/geoip-and-device-detection.md). Bump PROMPT_VERSION to 6 to invalidate the contaminated cache and regenerate docs/ru (0 broken bold spans remaining, aside from pre-existing multi-line bold that spans a soft line break).
This commit is contained in:
+2
-2
@@ -38,8 +38,8 @@ XC_VM помогает вам развернуть полноценную инф
|
||||
|
||||
## Технологии
|
||||
|
||||
- **Nginx** — обратный прокси и веб-сервер
|
||||
- **PHP 8.1** — базовый сервер
|
||||
- **Nginx** — обратный прокси-сервер и веб-сервер
|
||||
- **PHP 8.1** — основная серверная часть
|
||||
- **MariaDB** — база данных
|
||||
- **KeyDB** — механизм кэширования/сеанса
|
||||
- **FFmpeg 8.0** — перекодирование
|
||||
|
||||
@@ -7,7 +7,7 @@ XC_VM поддерживает автоматическое и ручное ре
|
||||
|
||||
## Что сохраняется в резервной Копии
|
||||
|
||||
Резервные копии содержат полную структуру базы данных и данные, **за исключением** следующих таблиц:
|
||||
Резервные копии содержат полную структуру базы данных и данные, **кроме** следующие таблицы:
|
||||
|
||||
```text
|
||||
detect_restream_logs, epg_data, lines_activity, lines_live,
|
||||
@@ -19,7 +19,7 @@ users_credits_logs, users_logs, watch_logs
|
||||
|
||||
> **Примечание:** При восстановлении резервной копии все данные журнала будут удалены. Эти таблицы исключены, чтобы можно было управлять размерами резервных копий.
|
||||
|
||||
Резервные копии **не** включают:
|
||||
Резервные копии включают в себя **нет**:
|
||||
|
||||
- Данные файловой системы (записи, VOD файлов, EPG XML)
|
||||
- Файлы конфигурации (`config/`)
|
||||
@@ -46,7 +46,7 @@ users_credits_logs, users_logs, watch_logs
|
||||
|
||||
### Руководство пользователя (панель администратора)
|
||||
|
||||
Нажмите **Создать резервную копию сейчас** на странице резервных копий. При этом задание cron будет запущено в принудительном режиме:
|
||||
Нажмите **Создайте резервную копию прямо сейчас** на странице резервных копий. При этом задание cron будет запущено в принудительном режиме:
|
||||
|
||||
```bash
|
||||
/home/xc_vm/console.php cron:backups 1
|
||||
@@ -85,7 +85,7 @@ users_credits_logs, users_logs, watch_logs
|
||||
|
||||
### Из панели администратора
|
||||
|
||||
Нажмите **Восстановить** на любой записи резервной копии. Требуется подтверждение.
|
||||
Нажмите **Восстанавливать** на любой записи резервной копии. Требуется подтверждение.
|
||||
|
||||
Процесс:
|
||||
|
||||
@@ -98,7 +98,7 @@ users_credits_logs, users_logs, watch_logs
|
||||
BackupService::restore($filename, $config)
|
||||
```
|
||||
|
||||
> **Важно:** При восстановлении удаляется вся база данных и создается заново. Все данные, отсутствующие в резервной копии, будут потеряны.
|
||||
> **Важный:** При восстановлении вся база данных удаляется и создается заново. Все данные, отсутствующие в резервной копии, будут потеряны.
|
||||
|
||||
### Из CLI
|
||||
|
||||
@@ -121,7 +121,7 @@ sudo /home/xc_vm/console.php tools migration /path/to/backup.sql
|
||||
|
||||
### Удаленное хранение
|
||||
|
||||
- If `dropbox_keep > 0`: сохраняет только N самых последних файлов в Dropbox. Самые старые удаляются первыми.
|
||||
- Если `dropbox_keep > 0`: сохраняются только N самых последних файлов в Dropbox. Самые старые удаляются первыми.
|
||||
- If `dropbox_keep = 0`: сохраняет все удаленные файлы (неограниченное количество).
|
||||
|
||||
Очистка выполняется автоматически после каждого резервного копирования с помощью `BackupsCronJob`.
|
||||
|
||||
@@ -176,7 +176,7 @@ AuthRepository::getGroupPermissions() // builds all_reports recursively
|
||||
|
||||
### Границы
|
||||
|
||||
Что реселлеры ** не могут ** делать:
|
||||
Что делают реселлеры **не могу**:
|
||||
|
||||
- Линии доступа/пользователи за пределами их иерархии.
|
||||
- Создавайте или изменяйте пакеты.
|
||||
@@ -209,7 +209,7 @@ AuthRepository::getGroupPermissions() // builds all_reports recursively
|
||||
|`activity_logs` / `live_connections`|данные о подключении|
|
||||
| `user_logs` |журналы действий суб-реселлеров|
|
||||
|
||||
Класс `ResellerAPIWrapper` проверяет ключ API, инициализирует сеанс с помощью `ResellerAPI` и возвращает отфильтрованные ответы в формате JSON.
|
||||
Класс `ResellerAPIWrapper` проверяет API-ключ, инициализирует сеанс с помощью `ResellerAPI` и возвращает отфильтрованные ответы в формате JSON.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Диагностика сервера (`server:diagnose`)
|
||||
|
||||
Панель помечает узел proxy/LB как "отключенный" исключительно из-за устаревшего сердцебиения (`enabled` + `status = 1` + недавнее `last_check_ago`), которое никогда не сообщает вам, почему узел перестал сообщать. Команда `server:diagnose` отвечает на этот вопрос.
|
||||
Панель помечает прокси-узел/LB-узел **не в сети** исключительно из-за устаревшего сердцебиения (`enabled` + `status = 1` + недавнее `last_check_ago`), которое никогда не сообщает вам, почему узел перестал сообщать. Команда `server:diagnose` отвечает на этот вопрос.
|
||||
|
||||
```bash
|
||||
/home/xc_vm/console.php server:diagnose [server_id]
|
||||
```
|
||||
|
||||
Команда доступна только для чтения: она выполняет только проверки ping/curl/`fsockopen`, `SELECT` и `sudo -n iptables -nL`. Она никогда ничего не перезапускает и не перенастраивает.
|
||||
Это команда **доступен только для чтения**: она выполняет только проверки ping/curl/`fsockopen`, `SELECT` запросы и `sudo -n iptables -nL`. Он никогда ничего не перезапускает и не перенастраивает.
|
||||
|
||||
Реализовано с помощью `src/Cli/Commands/ServerDiagnoseCommand.php`. Команда поставляется как в основной, так и в LB сборках — весь смысл ее использования на узле заключается в локальном режиме.
|
||||
Реализовано с помощью `src/Cli/Commands/ServerDiagnoseCommand.php`. Команда поставляется в сборках **оба** MAIN и LB — локальный режим - вот и весь смысл ее размещения на узле.
|
||||
|
||||
---
|
||||
|
||||
@@ -37,8 +37,8 @@ sudo /home/xc_vm/console.php server:diagnose <server_id>
|
||||
|
||||
- **Порт ICMP + не закрыт** — узел выключен, разделен сетью или полностью защищен брандмауэром.
|
||||
- **ICMP replies but the port is dropped** — the classic: the node's own iptables blocked the main's IP (RootSignals flood/block false-positive), or nginx/the service is down. Run the local mode on the node for the exact cause.
|
||||
- **Порт открыт, но `/api` молчит** — nginx включен, PHP нет: проверьте php-fpm на узле.
|
||||
- **`/api` отвечает, но частота сердцебиения устарела ** — демон узла watchdog (программа записи сердцебиений) не запущен или не может выполнить запись в базу данных панели. Запустите локальный режим на узле.
|
||||
- **Порт открыт, но `/api` молчит** — nginx запущено, PHP - нет: проверьте php-fpm на узле.
|
||||
- **`/api` отвечает, но сердцебиение затихает** — демон узла watchdog (программа записи сердцебиений) не запущен или он не может выполнить запись в базу данных панели. Запустите локальный режим на узле.
|
||||
|
||||
### Режим B — локальная самодиагностика НА узле
|
||||
|
||||
@@ -46,18 +46,18 @@ sudo /home/xc_vm/console.php server:diagnose <server_id>
|
||||
sudo /home/xc_vm/console.php server:diagnose
|
||||
```
|
||||
|
||||
Запустите это ** на самом узле LB/proxy с отключенным доступом ** — причины обычно находятся там. Аргумент не требуется; узел идентифицирует себя с помощью `config.ini`. Проверки:
|
||||
Запустите это **на самом безмолвном узле LB/proxy** — причины обычно находятся там. Аргумент не требуется; узел идентифицирует себя по `config.ini`. Проверки:
|
||||
|
||||
1. **Моя собственная строка панели ** — включено/статус/ сердцебиение в том виде, в каком их видит главный.
|
||||
2. **Могу ли я подключиться к ГЛАВНОМУ** — подключение к базе данных (неявно подтвержденное), ICMP и TCP к широковещательному порту главного.
|
||||
3. **Отключил ли я брандмауэр main?** — сканирует цепочку `iptables INPUT` этого узла на наличие `DROP` IP-адреса main, а также файла маркера блокировки флуда. Это классическая причина "узел отключился без причины": защита от наводнений автоматически отключает общедоступные IP-адреса, и обратные вызовы главного сервера перестают поступать.
|
||||
4. **Сервис / nginx** — `systemctl is-active xc_vm` и локальная проверка TCP на собственном широковещательном порту узла.
|
||||
5. ** Сторожевой демон** — фактически записывающий сердцебиение: демон `watchdog` обновляет `last_check_ago` каждые несколько секунд. Когда его MySQL подключение к главному серверу прерывается, он ** ожидает восстановления базы данных** (повторяя попытку каждые 5 секунд) и немедленно возобновляет сердцебиение. Вместо этого были запущены более старые сборки, из—за чего ** все узлы отключались в один и тот же момент ** при любом MySQL перезапуске / сбое на главном сервере, пока `cron:servers` не восстановил их.
|
||||
6. **Хрон няни** — три дополнительные проверки, потому что мертвый watchdog остается мертвым только тогда, когда цепочка няни разорвана:
|
||||
- присутствует ли `cron:servers` в crontab пользователя **`xc_vm`** (если отсутствует, создайте заново с помощью `rm -f /home/xc_vm/tmp/crontab` и перезапустите службу);
|
||||
- активна ли системная служба cron (нет cron → crontab никогда не запускается);
|
||||
- является ли предыдущий экземпляр `cron:servers` ** зависшим из—за блокировки cron ** - зависший экземпляр блокирует каждый последующий запуск на срок до 30 минут (`acquireCronLock` истекший тайм-аут), что в точности соответствует тому, как один сбой в работе базы данных удерживает узел в автономном режиме в течение получаса. Команда выводит удерживающий PID и команду завершения.
|
||||
7. **Перекос часов** — `time_offset` относительно панели.
|
||||
1. **Мой собственный ряд панелей** — включено/статус/сердцебиение в том виде, в каком их видит основной пользователь.
|
||||
2. **Могу ли я добраться до ГЛАВНОГО** — Подключение к базе данных (неявно подтвержденное), ICMP и TCP к широковещательному порту главного сервера.
|
||||
3. **Отключил ли я главный брандмауэр?** — сканирует цепочку `iptables INPUT` этого узла на наличие `DROP` IP-адреса главного узла, а также файла-маркера блокировки потока. Это классическая причина "узел отключился без причины": защита от наводнений автоматически отключает общедоступные IP-адреса, и обратные вызовы главного сервера перестают поступать.
|
||||
4. **Услуга / nginx** — `systemctl is-active xc_vm` и локальная проверка TCP на собственном широковещательном порту узла.
|
||||
5. **Демон-сторожевой пес** — фактический регистратор сердцебиений: демон `watchdog` обновляет `last_check_ago` каждые несколько секунд. Когда его MySQL подключение к главному серверу прерывается, он **ожидает возврата базы данных** (повторяет попытку каждые 5 секунд) и немедленно возобновляет сердцебиение. Вместо этого были запущены более старые сборки, что приводило к **все узлы переходят в автономный режим в один и тот же момент** при любом MySQL перезапуске / сбое в главном меню, пока `cron:servers` не восстановило их.
|
||||
6. **Няня крон** — три дополнительных проверки, потому что мертвый watchdog остается мертвым только тогда, когда цепочка няни разорвана:
|
||||
- присутствует ли `cron:servers` в **`xc_vm` кронтаб пользователя** (если отсутствует, восстановите с помощью `rm -f /home/xc_vm/tmp/crontab` и перезапустите службу);
|
||||
- активна ли система **служба cron** (нет cron → crontab никогда не запускается);
|
||||
- является предыдущим `cron:servers` экземпляром **висел на своем замке cron** — зависший экземпляр блокирует каждый последующий запуск на срок до 30 минут (`acquireCronLock` истекший тайм-аут), что в точности соответствует тому, как один сбой в работе базы данных удерживает узел в автономном режиме в течение получаса. Команда выводит удерживающий PID и команду завершения.
|
||||
7. **Перекос часов** — `time_offset` против панели.
|
||||
|
||||
> **Примечание:** для проверки iptables требуется sudo без пароля (`sudo -n`). Без него проверка выдает сообщение о `cannot check (need sudo iptables)` вместо сбоя — запустите команду как `root` для получения полной картины.
|
||||
|
||||
@@ -65,7 +65,7 @@ sudo /home/xc_vm/console.php server:diagnose
|
||||
|
||||
## Коды вывода и выхода
|
||||
|
||||
При каждой проверке выводится одна выровненная строка `[OK]`/`[WARN]`, за которой следует пронумерованная сводка о возможных причинах с указанием точной команды устранения, если таковая существует (например, строка разблокировки `iptables -D INPUT ... -j DROP`).
|
||||
При каждой проверке выводится одна выровненная строка `[OK]`/`[WARN]`, за которой следует пронумерованная сводка **Вероятная причина (причины)** с указанием точной команды исправления там, где она существует (например, строка разблокировки `iptables -D INPUT ... -j DROP`).
|
||||
|
||||
|Код выхода|Значение|
|
||||
| --- | --- |
|
||||
@@ -104,9 +104,9 @@ Probable cause(s):
|
||||
| --- | --- | --- |
|
||||
|Звенит, порт сброшен|iptables узла заблокировал основной IP-адрес|`sudo iptables -D INPUT -s <main_ip> -j DROP` + удалить маркер `block_<ip>` (команда выводит точную строку)|
|
||||
|Ни пинга, ни порта|Отключен хост / сетевой раздел / внешний брандмауэр|Проверьте консоль хостинга, маршруты, брандмауэр провайдера|
|
||||
|Порт открыт, `/api` отключен|сбой php-fpm|Перезапустите службу `xc_vm` на узле|
|
||||
|Порт открыт, `/api` отключен|сбой в работе php-fpm|Перезапустите службу `xc_vm` на узле|
|
||||
|`/api` отлично, сердцебиение замедлилось|Сторожевой демон мертв (завершает работу при потере соединения с базой данных)|`sudo -u xc_vm console.php watchdog` на узле; проверьте разрешения базы данных (`tools mysql` на главном сервере)|
|
||||
|**Все узлы падают в один и тот же момент**|Перезапуск MySQL /сбой на главном сервере приводит к одновременному сбою watchdog на каждом узле (фатально для сборок до исправления; текущие сборки переждут это).|Проверьте журнал ошибок main MySQL во время сброса; обновите узлы, чтобы watchdog пережил перебои в работе|
|
||||
| **All nodes drop at the same moment** |Перезапуск MySQL /сбой на главном сервере приводит к одновременному сбою watchdog на каждом узле (фатально для сборок до исправления; текущие сборки переждут это).|Проверьте журнал ошибок main MySQL во время сброса; обновите узлы, чтобы watchdog пережил перебои в работе|
|
||||
|Закрылки узлов онлайн/оффлайн|Перекос часов > 30 с (или повторяющиеся сигналы MySQL)|Синхронизируйте протокол NTP на узле; проверьте стабильность MySQL на главном|
|
||||
|Статус = 4|Произошла ошибка установки/обеспечения|Повторный запуск `server:install` из основного|
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## Обновление через панель управления
|
||||
|
||||
**Шаг 1.** Откройте раздел "Серверы" в верхнем меню панели.
|
||||
**Шаг 1.** Откройте раздел **Серверы** в верхнем меню панели.
|
||||
|
||||

|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
|
||||

|
||||
|
||||
**Шаг 3.** Найдите целевой сервер в таблице "Серверы" и нажмите кнопку "Меню" в столбце "Действия".
|
||||
**Шаг 3.** Найдите целевой сервер в таблице серверы и нажмите кнопку меню в столбце **Действия**.
|
||||
|
||||

|
||||
|
||||
**Шаг 4.** Выберите **Серверные инструменты** в меню.
|
||||
**Шаг 4.** Выберите в меню пункт **Серверные инструменты**.
|
||||
|
||||

|
||||
|
||||
**Шаг 5.** В диалоговом окне "Серверные инструменты"** нажмите "Обновить сервер"**.
|
||||
**Шаг 5.** В диалоговом окне **Серверные инструменты** нажмите кнопку **Сервер обновлений**.
|
||||
|
||||

|
||||
|
||||
@@ -35,3 +35,31 @@ sudo -u xc_vm /home/xc_vm/console.php update update
|
||||
```
|
||||
|
||||
Загружает и применяет последнее обновление с GitHub. Обычно запускается автоматически через веб-панель.
|
||||
|
||||
## Откат сервера назад
|
||||
|
||||
Если при обновлении возникает проблема, вы можете откатить сервер к более ранней версии непосредственно из панели. Откат выполняется для каждого сервера, поэтому как панель **главный**, так и отдельный **Балансировщики нагрузки** могут быть понижены независимо друг от друга.
|
||||
|
||||
**Шаг 1.** Откройте раздел **Серверы** → **Управление серверами**.
|
||||
|
||||
**Шаг 2.** Найдите целевой сервер и откройте его меню **Действия** (то же меню, что использовалось для обновления).
|
||||
|
||||
**Шаг 3.** Нажмите **Версия для отката**. Откроется диалоговое окно со списком более ранних версий (сначала самых новых). Предварительные сборки помечены тегом `(beta)`.
|
||||
|
||||
**Шаг 4.** Выберите версию, к которой требуется выполнить откат, и подтвердите ее.
|
||||
|
||||
Нажатие кнопки **Отмена** вводит сигнал `rollback` (содержащий выбранную версию) в базу данных. В течение минуты CRON распознает его, и откат выполняется автоматически, повторно используя тот же процесс остановки → замены → перезапуска в качестве обновления. Отслеживайте прогресс с помощью серверной версии в таблице **Управление серверами**.
|
||||
|
||||
> 💾 На сервере **главный** перед откатом автоматически создается резервная копия базы данных, которая сохраняется в `/home/xc_vm/backups/pre_rollback_<from>_to_<to>_<timestamp>.sql`. В системах балансировки нагрузки нет базы данных, поэтому этот шаг пропущен.
|
||||
|
||||
Предлагаемые версии зависят от канала обновления сервера: на канале `stable` отображаются только стабильные версии; на канале `unstable` вы также видите предварительные версии `(beta)`.
|
||||
|
||||
> ➡️ Откат **не отменяет перенос базы данных** — они доступны только в прямом режиме. Схема разработана таким образом, чтобы поддерживать обратную совместимость, а автоматическое резервное копирование - это путь восстановления, если более старая сборка не может считывать новые данные. Используйте откат только в качестве шага восстановления.
|
||||
|
||||
### Ручной откат (CLI)
|
||||
|
||||
```bash
|
||||
sudo -u xc_vm /home/xc_vm/console.php update rollback 2.4.0
|
||||
```
|
||||
|
||||
Загружает указанный релиз с GitHub и применяет его с теми же проверками целостности и — в основном — автоматическим резервным копированием базы данных.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
В этом руководстве объясняется, как создать самозаверяющий SSL-сертификат, чтобы включить безопасные HTTPS-соединения для встроенного сервера Nginx в проекте XC_VM.
|
||||
|
||||
> **Примечание:** При новой установке уже создается ** уникальный** самозаверяющий сертификат
|
||||
> **Примечание:** При новой установке уже генерируется самоподписанный сертификат **уникальный**
|
||||
> автоматически (программа установки запускает `openssl` и записывает `server.key`/`server.crt`
|
||||
> в `bin/nginx/conf/` перед запуском Nginx), а `CertbotCronJob` позже заменяет
|
||||
> с настоящим сертификатом Let's Encrypt. Следуйте этому руководству только для ** восстановления
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
## Обзор
|
||||
|
||||
**SSL (Secure Sockets Layer)** шифрует соединение между клиентом и сервером, обеспечивая конфиденциальность данных и доверие пользователей.
|
||||
В этом руководстве показано, как создать самозаверяющий SSL-сертификат для встроенного сервера Nginx в проекте XC_VM.
|
||||
**SSL (уровень защищенных сокетов)** шифрует соединение между клиентом и сервером, обеспечивая конфиденциальность данных и доверие пользователей.
|
||||
В этом руководстве показано, как создать **самозаверяющий SSL-сертификат** для встроенного сервера **Nginx** в проекте **XC_VM**.
|
||||
|
||||
---
|
||||
|
||||
@@ -35,14 +35,14 @@ cd /home/xc_vm/bin/nginx/conf
|
||||
|
||||
## Шаг 1. Сгенерируйте закрытый ключ
|
||||
|
||||
Сгенерируйте **2048-битный закрытый ключ RSA**:
|
||||
Сгенерировать **2048-битный закрытый ключ RSA**:
|
||||
|
||||
```bash
|
||||
openssl genrsa -out server.key 2048
|
||||
```
|
||||
|
||||
После выполнения появится файл `server.key` — это ваш **закрытый ключ**.
|
||||
Храните его ** строго в тайне ** — он используется для подписи SSL-сертификата.
|
||||
Сохраните его **строго конфиденциально** — он используется для подписи SSL-сертификата.
|
||||
|
||||
---
|
||||
|
||||
@@ -75,7 +75,7 @@ DNS.1 = XC_VM
|
||||
EOF
|
||||
```
|
||||
|
||||
**Объяснение параметров:**
|
||||
**Parameter explanation:**
|
||||
|
||||
|Поле|Ценность|Цель|
|
||||
| --- | --- | --- |
|
||||
@@ -87,7 +87,7 @@ EOF
|
||||
| `CN` | XC_VM |Общее имя (основное имя хоста)|
|
||||
| `DNS.1` | XC_VM |Альтернативное имя субъекта (SAN)|
|
||||
|
||||
> ** Совет:** Для реальных доменных имен замените `DNS.1 = XC_VM` на ваш реальный домен (например, `DNS.1 = panel.example.com`), чтобы избежать предупреждений браузера.
|
||||
> **Совет:** Для реальных доменных имен замените `DNS.1 = XC_VM` на ваш реальный домен (например, `DNS.1 = panel.example.com`), чтобы избежать предупреждений браузера.
|
||||
|
||||
---
|
||||
|
||||
@@ -99,7 +99,7 @@ EOF
|
||||
openssl req -new -x509 -key server.key -out server.crt -days 3650 -config server.cnf
|
||||
```
|
||||
|
||||
**Объяснение:**
|
||||
**Explanation:**
|
||||
|
||||
- `-new -x509` — создает новый самозаверяющий сертификат.
|
||||
- `-days 3650` — срок действия сертификата (10 лет)
|
||||
@@ -122,16 +122,16 @@ openssl req -new -x509 -key server.key -out server.crt -days 3650 -config server
|
||||
|
||||
## Результат
|
||||
|
||||
Ваш **XC_VM сервер Nginx** теперь доступен по **протоколу HTTPS** с использованием недавно созданного самозаверяющего сертификата.
|
||||
Ваш **XC_VM Сервер Nginx** теперь доступен через **https** с помощью недавно созданного самозаверяющего сертификата.
|
||||
Браузеры будут отображать предупреждение “не доверенный” — это ожидаемое поведение для самозаверяющих сертификатов.
|
||||
|
||||
---
|
||||
|
||||
## Записи
|
||||
|
||||
- Самозаверяющие сертификаты подходят ** только для внутреннего использования или тестирования**.
|
||||
- Подходят самозаверяющие сертификаты **только для внутреннего использования или тестирования**.
|
||||
- Для общедоступных доменов используйте сертификаты от доверенных центров сертификации (например, [Let's Encrypt](https://letsencrypt.org/)).
|
||||
- Если вы измените имя домена/хоста (`CN` или `DNS.1`), вы ** должны повторно создать** сертификат.
|
||||
- Если вы измените имя домена/хоста (`CN` или `DNS.1`), вы получите сертификат **должен восстановиться**.
|
||||
- Чтобы проверить сгенерированный сертификат:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
|
||||
## 1. Инициирование обновления
|
||||
|
||||
Процесс начинается, когда администратор нажимает кнопку "Обновить" в веб-интерфейсе.
|
||||
Процесс начинается, когда администратор нажимает кнопку **"Обновить"** в веб-интерфейсе.
|
||||
|
||||
- Сигнал с именем `update` вставляется в таблицу `signals` в базе данных.
|
||||
- Этот сигнал действует как **триггер** для всей процедуры обновления.
|
||||
- Этот сигнал действует как **спусковой крючок** для всей процедуры обновления.
|
||||
|
||||
---
|
||||
|
||||
## 2. Триггер CRON
|
||||
|
||||
Каждую **минуту** выполняется следующее задание CRON:
|
||||
Каждые **минута** выполняется следующее задание CRON:
|
||||
|
||||
```bash
|
||||
/home/xc_vm/console.php cron:root_signals
|
||||
@@ -42,8 +42,8 @@ src/Cli/Commands/UpdateCommand.php
|
||||
|
||||
На этом этапе выполняются следующие действия:
|
||||
|
||||
1. Определите **текущий тип панели** (`MAIN` или `LB`).
|
||||
2. Извлекать обновленные метаданные из **GitHub**:
|
||||
1. Определите значение **текущий тип панели** (`MAIN` или `LB`).
|
||||
2. Извлекать обновленные метаданные из **ГитХаб**:
|
||||
- Прямая ссылка на архив обновлений.
|
||||
- Контрольная сумма SHA для проверки целостности.
|
||||
3. Загрузите архив во временный каталог.
|
||||
@@ -54,7 +54,7 @@ src/Cli/Commands/UpdateCommand.php
|
||||
sudo /usr/bin/python3 /home/xc_vm/update "/home/xc_vm/tmp/.update.tar.gz" "HASH" > /dev/null 2>&1 &
|
||||
```
|
||||
|
||||
> 💡 После завершения обновления Python программа вызывает `console.php update post-update`, что запускает [миграцию базы данных](../guides/cli-tools.md#database-updates-after-version-upgrade) и очистку после обновления.
|
||||
> 💡 После завершения обновления Python программа вызывает `console.php update post-update`, что запускает [миграцию базы данных](../guides/database-migrations.md) и очистку после обновления.
|
||||
|
||||
---
|
||||
|
||||
@@ -68,15 +68,15 @@ sudo /usr/bin/python3 /home/xc_vm/update "/home/xc_vm/tmp/.update.tar.gz" "HASH"
|
||||
|
||||
Он выполняет привилегированные системные операции:
|
||||
|
||||
1. **Повторно проверьте контрольную сумму архива.
|
||||
2. **Остановите панель **, чтобы предотвратить конфликты во время обновления.
|
||||
3. **Извлеките** архив во временный каталог:
|
||||
1. **Повторная проверка** контрольная сумма архива.
|
||||
2. **Остановите панель** для предотвращения конфликтов во время обновления.
|
||||
3. **Извлекать** архив во временный каталог:
|
||||
|
||||
```bash
|
||||
/tmp/xc_vm_update_*/
|
||||
```
|
||||
|
||||
4. **Удалите исключенные каталоги** из временной копии — двоичные файлы, конфигурации и пользовательские данные, которые нельзя перезаписывать:
|
||||
4. **Удаление исключенных каталогов** из временной копии — двоичные файлы, конфигурации и пользовательские данные, которые нельзя перезаписывать:
|
||||
|
||||
`bin/ffmpeg_bin`, `bin/nginx`, `bin/nginx_rtmp`, `bin/php`, `bin/redis`, `bin/install`, `bin/maxmind`, `bin/certbot`, `content`, `backups`, `tmp`, `config`, `signals`
|
||||
|
||||
@@ -98,8 +98,8 @@ sudo /usr/bin/python3 /home/xc_vm/update "/home/xc_vm/tmp/.update.tar.gz" "HASH"
|
||||
/home/xc_vm/console.php update post-update
|
||||
```
|
||||
|
||||
8. **Перезапустите** панель в нормальном рабочем режиме.
|
||||
9. **Очистите временный каталог и удалите архив.
|
||||
8. **Перезапуск** панель находится в нормальном рабочем режиме.
|
||||
9. **Уборка** откройте временный каталог и удалите архив.
|
||||
|
||||
> ℹ️ Один и тот же архив используется как для установки, так и для обновления. Фильтрация выполняется на сервере во время обновления — список исключений определяется непосредственно в `src/update`.
|
||||
|
||||
@@ -109,8 +109,8 @@ sudo /usr/bin/python3 /home/xc_vm/update "/home/xc_vm/tmp/.update.tar.gz" "HASH"
|
||||
|
||||
Заключительные шаги выполняются на этапе `post-update` из `UpdateCommand`:
|
||||
|
||||
1. Если включено автоматическое обновление **LB** и был обновлен главный узел (`MAIN`) → создайте сигналы `update` для всех подсистем балансировки нагрузки.
|
||||
2. Обновите **версию панели** в базе данных.
|
||||
1. Если включено значение **Автоматическое обновление LB** и был обновлен главный узел (`MAIN`), → создайте сигналы `update` для всех подсистем балансировки нагрузки.
|
||||
2. Обновите значение **панельная версия** в базе данных.
|
||||
3. Удалите устаревшие файлы.
|
||||
4. Повторно примените правильные разрешения:
|
||||
|
||||
@@ -160,12 +160,38 @@ sudo /usr/bin/python3 /home/xc_vm/update "/home/xc_vm/tmp/.update.tar.gz" "HASH"
|
||||
|
||||
---
|
||||
|
||||
## ключевые функции
|
||||
## Откат (понижение рейтинга)
|
||||
|
||||
- **Двойная проверка целостности ** (хэш проверяется как на уровне PHP, так и на уровне Python).
|
||||
- **Автоматическое распространение обновлений с MAIN на все подсистемы балансировки нагрузки.
|
||||
- **Очистка ** от устаревших файлов и нормализация разрешений.
|
||||
- **Перезапуск безопасной панели ** после установки.
|
||||
- **Гибкость и автономность ** благодаря запуску по сигналу CRON +.
|
||||
Сервер также можно откатить до версии **ранее**. Это повторяет описанный выше процесс обновления, но нацелен на выбранную версию, а не на последнюю — повторно используется тот же конвейер signal → CRON → PHP → Python и тот же инструмент `src/update` applier'а. Откат выполняется для каждого сервера, поэтому `MAIN` и каждый `LB` могут быть понижены независимо друг от друга.
|
||||
|
||||
1. **Инициация.** В **Серверы → Управление серверами** меню "Действия для каждого сервера" содержит пункт **Версия для отката**. Открывается диалоговое окно со списком более ранних версий (предварительные версии помечены как `(beta)`), выбранных с помощью действия `rollback_versions` API (`GitHubReleases::getPreviousVersions()`). При выборе версии вводится сигнал — `{"action":"rollback","version":"X.Y.Z"}` - для этого сервера.
|
||||
|
||||
2. **Триггер CRON.** `cron:root_signals` обрабатывает сигнал `rollback`, запуская:
|
||||
|
||||
```bash
|
||||
/home/xc_vm/console.php update rollback X.Y.Z
|
||||
```
|
||||
|
||||
3. **PHP layer (`UpdateCommand`, `rollback` case).**
|
||||
- Проверьте целевую версию (`X.Y.Z`, строго более старую, чем текущая версия).
|
||||
- Только для **главный**: выполните автоматическое резервное копирование базы данных в `backups/pre_rollback_<from>_to_<to>_<timestamp>.sql`, прервав его в случае сбоя. Узлы LB не имеют базы данных и пропустите это.
|
||||
- Откройте архив версии **точный** с помощью `GitHubReleases::getVersionFile()` (MAIN → `xc_vm.tar.gz`, LB → `loadbalancer.tar.gz`), загрузите его и проверьте MD5.
|
||||
- Передайте в тот же Python updater (`src/update`).
|
||||
|
||||
4. **Система + завершение.** Аналогично обновлению: скрипт на Python останавливает панель, заменяет дерево (сохраняя двоичные файлы/конфигурацию/данные) и `post-update` устанавливает версию в базе данных на откатную версию и перезапускает панель.
|
||||
|
||||
The version list is channel-aware: the `stable` channel offers only stable releases, `unstable` also offers `(beta)` pre-releases.
|
||||
|
||||
> ⚠️ Понижение версии **не отменяет перенос базы данных** (они доступны только в прямом режиме). Схема поддерживается с обратной совместимостью, а автоматическое ОСНОВНОЕ резервное копирование является способом восстановления. Программа Python выполняет копирование по дереву (`cp -a`) без удаления файлов, поэтому файлы, добавленные в более новой версии, сохраняются до следующего обновления.
|
||||
|
||||
---
|
||||
|
||||
## ключевые функции
|
||||
|
||||
- **Двойная проверка целостности** (оба уровня - PHP и Python - проверяют хэш).
|
||||
- **Автоматическое распространение** обновлений от ОСНОВНОГО до всех подсистем балансировки нагрузки.
|
||||
- **Уборка** для удаления устаревших файлов и нормализации разрешений.
|
||||
- **Безопасный перезапуск панели управления** после установки.
|
||||
- **Гибкость и автономность** благодаря запуску на основе сигнала CRON +.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# Интерактивная ссылка на API (Swagger)
|
||||
|
||||
Интерактивный, всегда синхронизируемый пользовательский интерфейс Swagger для каждого XC_VM API, созданный на основе спецификаций OpenAPI версии 3.0. Используйте вкладки API в верхней части страницы для переключения между API или откройте один из них напрямую по ссылкам ниже.
|
||||
Интерактивный, всегда синхронизированный Swagger Пользовательский интерфейс для каждого XC_VM API, созданный на основе спецификаций OpenAPI версии 3.0. Используйте **Вкладки API** вверху страницы для переключения между API или откройте один из них напрямую по ссылкам ниже.
|
||||
|
||||
|интерфейс прикладного программирования|Описание|Автор|Открыть|
|
||||
| --- | --- | --- | --- |
|
||||
|**API администратора**|XUI.ONE-совместимое администрирование — линии, пользователи, потоки, VOD, серии, серверы, настройки (104 конечные точки)|`api_key` + код доступа|[Открыть ↗](../../_media/swagger-ui.html?spec=admin)|
|
||||
|**Системный API**|Внутренний `/api.php` — поток/VOD управление, статистика, процессы, файлы, подключения (31 действие)|`password` (`live_streaming_pass`)|[Открыть ↗](../../_media/swagger-ui.html?spec=system)|
|
||||
|**API для игроков**|Проигрыватель XtreamCodes — Прямые трансляции, VOD, Сериалы, EPG|`username` + `password`|[Открыть ↗](../../_media/swagger-ui.html?spec=player)|
|
||||
|**API списка воспроизведения**|`/playlist` аутентификация + создание списка воспроизведения|`username`/`password` или `token`|[Открыть ↗](../../_media/swagger-ui.html?spec=playlist)|
|
||||
| **Admin API** |XUI.ONE-совместимое администрирование — линии, пользователи, потоки, VOD, серии, серверы, настройки (104 конечные точки)|`api_key` + код доступа|[Открыть ↗](../../_media/swagger-ui.html?spec=admin)|
|
||||
| **System API** |Внутренний `/api.php` — поток/VOD управление, статистика, процессы, файлы, соединения (31 действие)|`password` (`live_streaming_pass`)|[Открыть ↗](../../_media/swagger-ui.html?spec=system)|
|
||||
| **Player API** |Проигрыватель XtreamCodes — Прямые трансляции, VOD, Сериалы, EPG|`username` + `password`|[Открыть ↗](../../_media/swagger-ui.html?spec=player)|
|
||||
| **Playlist API** |`/playlist` аутентификация + создание списка воспроизведения|`username`/`password` или `token`|[Открыть ↗](../../_media/swagger-ui.html?spec=playlist)|
|
||||
|
||||
---
|
||||
|
||||
## Используя "Попробуй это"
|
||||
|
||||
1. Откройте спецификацию, затем используйте вкладки **API** для переключения API и вкладки **Документация / Интерактивные (Swagger)** для каждого API.
|
||||
2. Разверните любую конечную точку → ** Попробуйте ** → заполните параметры → ** Выполните**, чтобы увидеть реальный URL-адрес запроса, команду cURL и оперативный ответ.
|
||||
3. Чтобы получить доступ к API администратора, нажмите "Авторизоваться" и вставьте свой API—ключ - затем он автоматически прикрепляется к каждому запросу.
|
||||
1. Откройте спецификацию, затем используйте **Вкладки API** для переключения API и вкладки **Документация / Интерактивная (Swagger)** для каждого API.
|
||||
2. Разверните любую конечную точку → **Попробуйте это сделать** → параметры заполнения → **Выполнять**, чтобы увидеть реальный URL-адрес запроса, команду cURL и оперативный ответ.
|
||||
3. Для доступа к API администратора нажмите **Авторизовать 🔓** и вставьте свой API—ключ - затем он автоматически прикрепляется к каждому запросу.
|
||||
|
||||
> ** Примечание CORS:** CORS может заблокировать прямые вызовы "Попробуйте" из браузера на ваш сервер. Это ожидаемо — вместо этого используйте сгенерированную команду cURL, Postman или Insomnia. Ошибка не означает, что API не работает.
|
||||
> **Примечание CORS:** прямые вызовы "Попробуйте" из браузера на ваш сервер могут быть заблокированы CORS. Это ожидаемо — вместо этого используйте сгенерированную команду cURL, Postman или Insomnia. Ошибка не означает, что API не работает.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ XC_VM поддерживает две роли развертывания из
|
||||
|
||||
|Вариант|Архив|Цель|
|
||||
| --- | --- | --- |
|
||||
|**ОСНОВНОЙ**| `xc_vm.tar.gz` |Полное приложение — админ-панель, потоковое вещание, все модули, задания cron|
|
||||
|**Фунт** (балансировщик нагрузки)| `loadbalancer.tar.gz` |Сервер только для потоковой передачи — нет панели администратора, нет управления пользователями|
|
||||
| **MAIN** | `xc_vm.tar.gz` |Полное приложение — админ-панель, потоковое вещание, все модули, задания cron|
|
||||
|**фунт** (Балансировщик нагрузки)| `loadbalancer.tar.gz` |Сервер только для потоковой передачи — нет панели администратора, нет управления пользователями|
|
||||
|
||||
**MAIN** - это основной сервер, который управляет всем: пользовательским интерфейсом администратора, записями в базу данных, управлением пользователями/устройствами, обработкой EPG, резервным копированием и т.д.
|
||||
**главный** - это основной сервер, который управляет всем: пользовательским интерфейсом администратора, записями в базу данных, управлением пользователями/устройствами, EPG обработкой, резервным копированием и т.д.
|
||||
|
||||
**LB** - это облегченный потоковый узел, который получает потоки из MAIN (или других источников) и доставляет их клиентам. Он подключается к базе данных master в режиме только для чтения и не имеет панели администратора или возможностей управления.
|
||||
**фунт** - это облегченный потоковый узел, который получает потоки из MAIN (или других источников) и доставляет их клиентам. Он подключается к базе данных master в режиме только для чтения и не имеет панели администратора или возможностей управления.
|
||||
|
||||
---
|
||||
|
||||
@@ -25,9 +25,14 @@ XC_VM поддерживает две роли развертывания из
|
||||
| --- | --- | --- |
|
||||
| `make main` | `dist/xc_vm.tar.gz` |Полная ОСНОВНАЯ сборка|
|
||||
| `make lb` | `dist/loadbalancer.tar.gz` |Сборка LB (подмножество только для потоковой передачи)|
|
||||
| `make main_update` | `dist/update.tar.gz` |Постепенное ОСНОВНОЕ обновление|
|
||||
| `make lb_update` | `dist/loadbalancer_update.tar.gz` |Постепенное обновление LB|
|
||||
| `make new` |Обе полные сборки|Короткий путь: `main` + `lb`|
|
||||
| `make new` |(сбрасывает значение `dist/`)|Удалите и воссоздайте пустой каталог вывода `dist/` — запускается перед сборкой; само по себе ничего не создается|
|
||||
| `make generate_deleted_files` | `src/migrations/deleted_files.txt` |Список файлов, удаленных с момента последнего добавления тега (см. ниже)|
|
||||
|
||||
> **Обновления повторно используют весь архив.** Нет отдельной цели для инкрементного обновления - одна и та же
|
||||
> `xc_vm.tar.gz` / `loadbalancer.tar.gz` используется как для установки, так и для обновления; фильтрация происходит по
|
||||
> сервер во время обновления (см. [Механизм обновления](../administration/update-system.md)). Чтобы удалить
|
||||
> files that were *removed* between releases, `make generate_deleted_files [LAST_TAG=vX.Y.Z]` diffs
|
||||
> git и записывает `deleted_files.txt`, который применяется программой обновления.
|
||||
|
||||
Дополнительные выходы:
|
||||
|
||||
@@ -39,12 +44,12 @@ XC_VM поддерживает две роли развертывания из
|
||||
## Composer Зависимости
|
||||
|
||||
`src/vendor/` (автозагрузчик Composer PSR-4 плюс производственные зависимости) - это
|
||||
**зафиксировано** и отправлено как есть - путь развертывания не содержит Composer и никогда не выполняется
|
||||
**привержен** и поставляется как есть - путь развертывания не содержит Composer и никогда не выполняется
|
||||
`composer install`. Он поддерживается только для производства через `composer install --no-dev`, так что
|
||||
оба варианта сборки предназначены для бережливого производства без инструментов разработки.
|
||||
|
||||
- `src/composer.lock` фиксируется таким образом, чтобы `composer install` можно было воспроизвести.
|
||||
- Инструменты разработки (PHPStan, phpcs) имеют значение `require-dev` и ** отсутствуют** в
|
||||
- Инструменты разработки (PHPStan, phpcs) имеют значения `require-dev` и **нет** в
|
||||
зарегистрированный поставщик или архивы. Разработчики и CI добавляют их с помощью `make dev-tools`
|
||||
(`composer install`); шлюз `check-vendor-prod-only` завершается сбоем, если пакет разработчика
|
||||
когда-либо совершенные под `src/vendor/`.
|
||||
@@ -57,7 +62,7 @@ XC_VM поддерживает две роли развертывания из
|
||||
|
||||
### ОСНОВНАЯ сборка
|
||||
|
||||
ОСНОВНАЯ сборка содержит **весь** каталог `src/`.
|
||||
ОСНОВНАЯ сборка содержит каталог **весь** `src/`.
|
||||
|
||||
### Каталоги— включенные в сборку LB
|
||||
|
||||
@@ -65,17 +70,17 @@ XC_VM поддерживает две роли развертывания из
|
||||
|
||||
```text
|
||||
bin/ Cli/ config/ content/ Core/
|
||||
Domain/ Infrastructure/ public/ resources/
|
||||
signals/ Streaming/ tmp/ www/
|
||||
Domain/ Infrastructure/ Public/ resources/
|
||||
signals/ Streaming/ tmp/ vendor/ www/
|
||||
```
|
||||
|
||||
Плюс корневые файлы: `bootstrap.php`, `console.php`, `service`, `update`.
|
||||
|
||||
### Содержимое— исключенное из сборки LB
|
||||
|
||||
После копирования содержимое, относящееся к администратору, **удаляется** из сборки LB:
|
||||
После копирования содержимое, относящееся к администратору, становится **удаленный** из сборки LB:
|
||||
|
||||
**Удаленные каталоги:**
|
||||
**Directories removed:**
|
||||
|
||||
|Путь|Причина|
|
||||
| --- | --- |
|
||||
@@ -94,23 +99,28 @@ signals/ Streaming/ tmp/ www/
|
||||
| `resources/langs/` |Файлы языковых ресурсов|
|
||||
| `resources/libs/` |Администрирование библиотечных ресурсов|
|
||||
|
||||
**Удаленные файлы:**
|
||||
**Удаленные файлы** (они отражают `LB_FILES_TO_REMOVE` в Makefile):
|
||||
|
||||
> ⚠️ В нескольких записях здесь используется устаревший префикс `www/…` (например, `www/stream/auth.php`,
|
||||
> `www/xplugin.php`). `src/www/` больше не существует — конечные точки потоковой передачи/API перемещены под
|
||||
> `src/Public/stream/` и `src/Public/…`. Таким образом, эти `www/…` записи об удалении равны **никаких операций**
|
||||
> сегодня и заслуживают аудита в Makefile (файл, который должен быть удален из LB, на самом деле может
|
||||
> все еще отправляется по своему пути `Public/`).
|
||||
|
||||
|Файл|Причина|
|
||||
| --- | --- |
|
||||
| `Public/Controllers/Api/AdminApiController.php` |Полный admin API удален из LB|
|
||||
| `Public/Controllers/Api/ResellerRestApiController.php` |API реселлера удален из LB|
|
||||
| `Infrastructure/legacy/reseller_api.php` |Устаревший bootstrap API реселлера не нужен в LB|
|
||||
|`www/xplugin.php`, `www/probe.php`, `www/playlist.php`|Конечные точки администрирования|
|
||||
|`www/player_api.php`, `www/epg.php`, `www/enigma2.php`|Конечные точки клиентского API (обслуживаемые MAIN)|
|
||||
| `www/stream/auth.php` |Конечная точка аутентификации удалена из пакета LB|
|
||||
| `www/stream/auth.php` |Конечная точка аутентификации (устаревший путь — см. примечание)|
|
||||
|`www/admin/api.php`, `www/admin/proxy_api.php`|API администратора|
|
||||
| `bin/maxmind/GeoLite2-City.mmdb` |GeoIP БД поставляется отдельно|
|
||||
| `config/rclone.conf` |Конфигурация резервного копирования|
|
||||
| `Domain/Epg/EPG.php` |EPG класс обработки|
|
||||
| `bin/nginx/conf/gzip.conf` |Конфигурация Gzip (LB использует собственную)|
|
||||
|
||||
**Команды CLI удалены:**
|
||||
**CLI commands removed:**
|
||||
|
||||
|Файл|Причина|
|
||||
| --- | --- |
|
||||
@@ -120,7 +130,7 @@ signals/ Streaming/ tmp/ www/
|
||||
| `Cli/Commands/LbInstallFlow.php` |Помощник по установке LB (не требуется для самого LB)|
|
||||
| `Cli/Commands/ProxyInstallFlow.php` |Помощник по установке прокси-сервера (не требуется для самой LB)|
|
||||
|
||||
**Удалены задания Cron:**
|
||||
**Cron jobs removed:**
|
||||
|
||||
|Файл|Причина|
|
||||
| --- | --- |
|
||||
@@ -132,11 +142,15 @@ signals/ Streaming/ tmp/ www/
|
||||
| `Cli/CronJobs/ProvidersCronJob.php` |Синхронизация с поставщиком (только для ОСНОВНОГО)|
|
||||
| `Cli/CronJobs/SeriesCronJob.php` |Метаданные серии (только для основной версии)|
|
||||
|
||||
> ** Примечание:** Связанные с модулем cron (TMDB, Plex, Watch) теперь находятся внутри `modules/<name>/` и автоматически исключаются из сборок LB, поскольку `modules/` отсутствует в `LB_DIRS`.
|
||||
> **Примечание:** CRON, связанные с модулями (TMDB, Plex, Watch), находятся внутри `src/Modules/<name>/` и автоматически исключаются из LB-сборок - `Modules/` отсутствует в `LB_DIRS`.
|
||||
>
|
||||
> **Ministra** (`src/Ministra/`, портал Stalker — ~50 МБ ресурсов) также исключен из списка
|
||||
> **упущение**: его нет в списке `LB_DIRS`, поэтому он никогда не копировался в архив LB (там нет
|
||||
> явное правило удаления для него — отсюда и проверка отсутствия `ministra` в *Проверке сборки* ниже).
|
||||
|
||||
### Конфигурации, замененные при сборке LB
|
||||
|
||||
Эти файлы из `lb_configs/` ** заменяют** основные версии:
|
||||
Эти файлы из `lb_configs/` **заменять** ОСНОВНЫХ версий:
|
||||
|
||||
|Источник|Цель|Цель|
|
||||
| --- | --- | --- |
|
||||
@@ -228,7 +242,8 @@ www/stream/*.php
|
||||
Добавьте его в `LB_DIRS` в Makefile:
|
||||
|
||||
```makefile
|
||||
LB_DIRS = bin cli config content core domain ... your_dir
|
||||
LB_DIRS := bin Cli config content Core Domain \
|
||||
Infrastructure Public resources signals Streaming tmp vendor www your_dir
|
||||
```
|
||||
|
||||
### Новый каталог, доступный только для администратора
|
||||
|
||||
@@ -6,16 +6,18 @@
|
||||
|
||||
- Никакого DDD, никакой гексагональности, никакой чистой архитектуры — намеренно.
|
||||
- Split by context with minimal abstractions: `Controller → Service → Repository`.
|
||||
- Два артефакта сборки из одной кодовой базы: **MAIN** (полная панель) и **LB** (подмножество load balancer).
|
||||
- Два артефакта сборки из одной кодовой базы: **главный** (полная панель) и **фунт** (подмножество load balancer).
|
||||
|
||||
---
|
||||
|
||||
## Дерево исходных текстов
|
||||
## Исходное дерево
|
||||
|
||||
|Путь|Роль|
|
||||
| ---- | ---- |
|
||||
| `src/Core/` |Примитивы инфраструктуры: Контейнер DI, события, HTTP, настройка, аутентификация, ведение журнала|
|
||||
| `src/Core/` |Примитивы фреймворка: Контейнер DI, события, HTTP/маршрутизатор, конфигурация, авторизация, ведение журнала|
|
||||
| `src/Domain/` |Бизнес-контексты: Поток, VOD, линия, пользователь, сервер, безопасность и т.д.|
|
||||
| `src/Infrastructure/` |Внешние адаптеры: `DatabaseFactory`, считыватели кэша, Redis, TMDb|
|
||||
| `src/Streaming/` |Потоковая подсистема: bootstrap, аутентификация, доставка, балансировщик, защита|
|
||||
| `src/Modules/` |Дополнительный слой расширения — загружается с помощью `ModuleLoader`|
|
||||
| `src/Public/` |Передний контроллер, маршрутизатор, контроллеры, представления, ресурсы|
|
||||
| `src/Cli/` |Консольные команды и точки входа в cron|
|
||||
@@ -29,22 +31,25 @@
|
||||
|
||||
```
|
||||
Public/index.php
|
||||
└── XC_Bootstrap::boot(BootContext::ADMIN)
|
||||
└── XC_Bootstrap::boot(BootContext::Admin)
|
||||
└── ServiceContainer (DI)
|
||||
├── EventDispatcher (PSR-14)
|
||||
├── ModuleLoader → loadAll() → bootAll()
|
||||
└── Router → dispatch()
|
||||
```
|
||||
|
||||
Классы домена получают базу данных посредством внедрения `setDb()` (вызывается из
|
||||
`bootstrap.php::wireDomainDatabase()`). Нет `global $db` в пути веб-запроса.
|
||||
Классы домена и модуля принимают **нет** и `$db` в своем конструкторе. Они
|
||||
`use \XcVm\Infrastructure\Database\DatabaseAware` и вызываем `self::db()`, который лениво
|
||||
устраняет общее соединение. `bootstrap.php::wireDomainDatabase()` устанавливает это соединение
|
||||
**однажды** для каждой загрузки (через `DatabaseAware::setDb()`) — нет проводки для каждого класса и нет
|
||||
`global $db` в пути веб-запроса.
|
||||
|
||||
---
|
||||
|
||||
## Модульная система
|
||||
|
||||
Модули представляют собой изолированные каталоги под `src/Modules/` с манифестом `module.json`
|
||||
и класс, расширяющий `BaseModule`. Полную информацию смотрите в разделе [Модульная система](modules.md).
|
||||
и класс, расширяющий `BaseModule`. Полную справочную информацию (и связанные страницы о жизненном цикле / точках расширения) смотрите в [Разработка модуля](module-authoring.md).
|
||||
|
||||
```
|
||||
src/Modules/my-module/
|
||||
@@ -61,10 +66,10 @@ src/Modules/my-module/
|
||||
|
||||
|Контекст|Используется для|
|
||||
| ------- | -------- |
|
||||
| `BootContext::MINIMAL` |Скрипты, которым нужны только пути / конфигурация|
|
||||
| `BootContext::CLI` |Задания Cron и команды CLI|
|
||||
| `BootContext::STREAM` |Конечные точки потоковой передачи|
|
||||
| `BootContext::ADMIN` |Панель администратора/реселлера|
|
||||
| `BootContext::Minimal` |Скрипты, которым нужны только пути / конфигурация|
|
||||
| `BootContext::Cli` |Задания Cron и команды CLI|
|
||||
| `BootContext::Stream` |Конечные точки потоковой передачи|
|
||||
| `BootContext::Admin` |Панель администратора/реселлера|
|
||||
|
||||
---
|
||||
|
||||
@@ -76,7 +81,11 @@ src/Modules/my-module/
|
||||
|Потоковый|✅|✅|
|
||||
|Модульная система|✅|подмножество|
|
||||
|
||||
Управляется перечислением `ServerEnvironment` и полем `module.json` `environment` (`main` / `lb` / `any`).
|
||||
Управляется перечислением `ServerEnvironment` и каждым полем `module.json` `environment`
|
||||
(`main` / `lb` / `any`). At boot, `ModuleLoader::getCurrentEnvironment()` resolves the node's
|
||||
окружение из константы `SERVER_TYPE` (`'lb'` → `ServerEnvironment::LoadBalancer`, иначе
|
||||
`ServerEnvironment::Main`); модуль, у которого `environment` не соответствует узлу, пропускается, поэтому
|
||||
LB получает **подмножество** модулей.
|
||||
|
||||
---
|
||||
|
||||
@@ -84,11 +93,11 @@ src/Modules/my-module/
|
||||
|
||||
|Механизм|Как использовать|
|
||||
| --------- | ---------- |
|
||||
|События PSR-14|атрибут `EventDispatcher::listen()` или `#[ListensTo]`|
|
||||
|Оформление сервиса| `$container->decorate('id', callable, priority)` |
|
||||
|Потоковое промежуточное программное обеспечение|Реализовать `StreamMiddlewareProviderInterface`|
|
||||
|Записи Cron|Переопределить `getCronEntries()` в классе модуля|
|
||||
|Миграции баз данных|Реализовать `MigratableInterface::getMigrations()`|
|
||||
|События PSR-14|`EventDispatcher::listen()` / `#[ListensTo]` — смотрите [Система событий](event-system.md)|
|
||||
|Оформление сервиса|`$container->decorate('id', callable, priority)` — смотрите [Точки расширения модуля](module-extension-points.md#di-container-and-service-decoration)|
|
||||
|Потоковое промежуточное программное обеспечение|Реализовать `StreamMiddlewareProviderInterface` — см. [Точки расширения модуля](module-extension-points.md#stream-middleware)|
|
||||
|Записи Cron|`getCronEntries()` в классе module — смотрите [Точки расширения модуля](module-extension-points.md#cron-task)|
|
||||
|Миграции баз данных|`MigratableInterface::getMigrations()` — смотрите [Точки расширения модуля](module-extension-points.md#versioned-migrations-migratableinterface)|
|
||||
|
||||
---
|
||||
|
||||
@@ -99,15 +108,3 @@ src/Modules/my-module/
|
||||
3. Любой модуль можно отключить с помощью `config/modules.php`, не прикасаясь к ядру.
|
||||
4. Защищенные сервисы (`db`, `settings`, `config`, `auth`) не могут быть оформлены.
|
||||
5. Синхронизируйте документы EN и RU в одном и том же коммите.
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|Файл|Роль|
|
||||
| --- | --- |
|
||||
| `src/Core/` |Примитивы фреймворка (DI, события, HTTP, config, auth, ведение журнала)|
|
||||
| `src/Domain/` |Бизнес-контексты (Поток, VOD, строка, Пользователь, Сервер, безопасность)|
|
||||
| `src/Infrastructure/` |Внешние адаптеры (DatabaseFactory, CacheReader, Redis)|
|
||||
| `src/Streaming/` |Подсистема потоковой передачи|
|
||||
| `src/Modules/` |Дополнительные модули (загружаются с помощью ModuleLoader)|
|
||||
| `src/Public/` |Передний контроллер, контроллеры, виды|
|
||||
| `src/Cli/` |Консольные команды и задания cron|
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Автоматическая загрузка (PSR-4)
|
||||
|
||||
XC_VM автоматически загружает классы с помощью стандартного **Composer PSR-4** автозагрузчика; в пространстве имен кодируется путь к файлу, поэтому разрешение выполняется напрямую `file_exists` без сканирования и кэширования.
|
||||
XC_VM автоматически загружает классы с помощью стандартного автозагрузчика **Composer PSR-4**; пространство имен кодирует путь к файлу, поэтому разрешение выполняется напрямую `file_exists` без сканирования и кэширования.
|
||||
|
||||
---
|
||||
|
||||
@@ -19,16 +19,14 @@ XcVm\Public\Controllers\Admin\UserController -> src/Public/Controllers/Admin/Use
|
||||
```json
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"XcVm\\": "./",
|
||||
"M3uParser\\": "Core/Parsing/M3uParser/src/",
|
||||
"Chrisyue\\PhpM3u8\\": "Core/Parsing/PhpM3u8/src/"
|
||||
"XcVm\\": "./"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`src/vendor/` (автозагрузчик Composer + производственные зависимости) зафиксирован и
|
||||
отправлено — путь развертывания не содержит Composer и никогда не выполняется `composer install`. Там
|
||||
нет ли ** кэша карты классов ** (нет `optimize-autoloader`): пропущенный класс - это простой путь
|
||||
is **нет кэша карт классов** (no `optimize-autoloader`): пропуск класса - это простой путь
|
||||
поиск, а не повторное сканирование каталога.
|
||||
|
||||
## Добавление нового класса
|
||||
@@ -58,9 +56,9 @@ use XcVm\Domain\Billing\InvoiceService;
|
||||
|
||||
|Правило|Пример|
|
||||
| --- | --- |
|
||||
|Имя файла **должно ** совпадать с именем класса|`InvoiceService.php` → `class InvoiceService`|
|
||||
|Имя файла **должен** соответствует имени класса|`InvoiceService.php` → `class InvoiceService`|
|
||||
|Один класс на файл|PSR-4 разрешает один класс для каждого пути; разбивает файлы нескольких классов|
|
||||
|Пространство имен **должно** совпадать с путем к каталогу (с учетом регистра)|`src/Domain/Billing/` → `namespace XcVm\Domain\Billing;`|
|
||||
|Пространство имен **должен** соответствует пути к каталогу (с учетом регистра).|`src/Domain/Billing/` → `namespace XcVm\Domain\Billing;`|
|
||||
|Классы и каталоги PascalCase|`StreamService`, `DatabaseHandler`, `Core/Auth/`|
|
||||
|Соглашение о проекте: нет `declare(strict_types=1)`|—|
|
||||
|
||||
@@ -70,7 +68,7 @@ use XcVm\Domain\Billing\InvoiceService;
|
||||
|
||||
## Процедурные файлы и файлы третьих лиц
|
||||
|
||||
Некоторые файлы намеренно **не** разделены пространством имен и загружаются явным образом.
|
||||
Некоторые файлы намеренно разделены пространством имен **нет** и загружаются явным образом.
|
||||
`require`, а не автозагрузчик:
|
||||
|
||||
- процедурные точки входа, представления и загрузочный клей (например, `Public/index.php`,
|
||||
@@ -78,16 +76,19 @@ use XcVm\Domain\Billing\InvoiceService;
|
||||
- глобальные константы и функции (`Core/Config/*`, обработчик ошибок);
|
||||
- класс ioncube `XC_VM` и комплект поставки `Infrastructure/Tmdb/lib/*`.
|
||||
|
||||
Пакеты, поставляемые поставщиками `M3uParser` и `Chrisyue\PhpM3u8`, имеют свои собственные PSR-4
|
||||
префиксы (указанные выше) и автозагрузка выполняются в обычном режиме.
|
||||
Сторонние библиотеки (например, `gemorroj/m3u-parser`, `chrisyue/php-m3u8`,
|
||||
`mobiledetect/mobiledetectlib`, `geoip2/geoip2`) являются обычными Composer `require`
|
||||
зависимости, объявленные в `src/composer.json`; они находятся в `src/vendor/` и
|
||||
autoload through the Composer vendor autoloader — they are **not** listed in the
|
||||
`psr-4` блок выше.
|
||||
|
||||
## Модули
|
||||
|
||||
Классы модулей используют пространство имен `XcVm\Module\<Name>\…`, но **не** зарегистрированы
|
||||
Классы модулей используют пространство имен `XcVm\Module\<Name>\…`, но зарегистрированы в **нет**
|
||||
в `composer.json` (каталоги модулей/торговых площадок — `plex`, `watch-d2bho` —
|
||||
не соответствуют ни одному правилу PSR-4). Они разрешаются с помощью `ModuleLoader` собственных PSR-4
|
||||
распознаватель: он удаляет базовое пространство имен модуля и отображает оставшееся в
|
||||
вложенный путь в каталоге модуля. Смотрите [Модульная система](modules.md).
|
||||
дополнительный путь в каталоге модуля. Смотрите [Создание модуля](module-authoring.md).
|
||||
|
||||
## Инструменты для разработки
|
||||
|
||||
|
||||
@@ -4,22 +4,27 @@
|
||||
Каждый контекст загружает только те подсистемы, которые необходимы для его пути выполнения.
|
||||
Контекст выражается в виде значения перечисления `BootContext`.
|
||||
|
||||
> `boot()` принимает `string|BootContext`, поэтому устаревшая строка `XC_Bootstrap::CONTEXT_*`
|
||||
> константы (`CONTEXT_ADMIN`, `CONTEXT_CLI`, ...) все еще работают, но являются псевдонимами **`@deprecated`** —
|
||||
> новый код должен передавать регистр перечисления (`BootContext::Admin`). Регистры перечисления равны **ПаскалЬкас**
|
||||
> (`Minimal`, `Cli`, `Stream`, `Admin`), не в верхнем регистре.
|
||||
|
||||
---
|
||||
|
||||
## Краткий справочник
|
||||
|
||||
|Случай перечисления|Типичное использование|
|
||||
| --- | --- |
|
||||
| `BootContext::MINIMAL` |Скрипты, которым нужны только пути /конфигурация|
|
||||
| `BootContext::CLI` |Задания Cron и команды CLI|
|
||||
| `BootContext::STREAM` |Конечные точки потоковой передачи (`live`, `vod`, `timeshift`)|
|
||||
| `BootContext::ADMIN` |Панель администратора/реселлера|
|
||||
| `BootContext::Minimal` |Скрипты, которым нужны только пути /конфигурация|
|
||||
| `BootContext::Cli` |Задания Cron и команды CLI|
|
||||
| `BootContext::Stream` |Конечные точки потоковой передачи (`live`, `vod`, `timeshift`)|
|
||||
| `BootContext::Admin` |Панель администратора/реселлера|
|
||||
|
||||
---
|
||||
|
||||
## Детали контекста
|
||||
|
||||
### BootContext::МИНИМАЛЬНЫЙ
|
||||
### BootContext::Минимальный
|
||||
|
||||
Загружает константы, пути, конфигурацию, логгер и обработчики ошибок.
|
||||
Нет подключения к базе данных.
|
||||
@@ -35,12 +40,12 @@
|
||||
|
||||
```php
|
||||
require_once '/home/xc_vm/bootstrap.php';
|
||||
XC_Bootstrap::boot(BootContext::MINIMAL);
|
||||
XC_Bootstrap::boot(BootContext::Minimal);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Загрузочный текст::CLI
|
||||
### Загрузочный текст::Cli
|
||||
|
||||
Используется для задач cron и CLI.
|
||||
Добавляет инициализацию базы данных и устаревшего ядра поверх `MINIMAL`.
|
||||
@@ -54,7 +59,7 @@ XC_Bootstrap::boot(BootContext::MINIMAL);
|
||||
|
||||
```php
|
||||
require_once '/home/xc_vm/bootstrap.php';
|
||||
XC_Bootstrap::boot(BootContext::CLI, [
|
||||
XC_Bootstrap::boot(BootContext::Cli, [
|
||||
'cached' => true,
|
||||
'process' => 'xc_vm: my-job',
|
||||
]);
|
||||
@@ -62,7 +67,7 @@ XC_Bootstrap::boot(BootContext::CLI, [
|
||||
|
||||
---
|
||||
|
||||
### BootContext::ПОТОК
|
||||
### BootContext::Поток
|
||||
|
||||
Облегченный контекст для конечных точек потоковой передачи с высокой нагрузкой.
|
||||
|
||||
@@ -75,12 +80,12 @@ XC_Bootstrap::boot(BootContext::CLI, [
|
||||
|
||||
```php
|
||||
require_once '/home/xc_vm/bootstrap.php';
|
||||
XC_Bootstrap::boot(BootContext::STREAM, ['cached' => true]);
|
||||
XC_Bootstrap::boot(BootContext::Stream, ['cached' => true]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### BootContext::АДМИНИСТРАТОР
|
||||
### BootContext::Администратор
|
||||
|
||||
Полная инициализация панели администратора/реселлера.
|
||||
|
||||
@@ -97,14 +102,14 @@ XC_Bootstrap::boot(BootContext::STREAM, ['cached' => true]);
|
||||
|
||||
```php
|
||||
require_once '/home/xc_vm/bootstrap.php';
|
||||
XC_Bootstrap::boot(BootContext::ADMIN);
|
||||
XC_Bootstrap::boot(BootContext::Admin);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Матрица подсистемы
|
||||
|
||||
|Подсистема|минимальный|КЛИ|течение|администратор|
|
||||
|Подсистема|Минимальный|Кли|Течение|Администратор|
|
||||
| --- | :---: | :---: | :---: | :---: |
|
||||
|Константы/пути|✅|✅|✅|✅|
|
||||
|Лесоруб|✅|✅|✅|✅|
|
||||
@@ -127,9 +132,9 @@ XC_Bootstrap::boot(BootContext $context, array $options = []);
|
||||
|
||||
|Вариант|Тип|По умолчанию|Описание|
|
||||
| --- | --- | --- | --- |
|
||||
| `cached` | `bool` |`true` для ПОТОКА, `false` в противном случае|Использовать кэшированные настройки|
|
||||
| `redis` | `bool` |`true` для АДМИНИСТРАТОРА, `false` в противном случае|Подключить Redis|
|
||||
| `process` | `string` | `''` |Название процесса для CLI|
|
||||
| `cached` | `bool` |`true` для потока, `false` в противном случае|Использовать кэшированные настройки|
|
||||
| `redis` | `bool` |`true` для администратора, `false` в противном случае|Подключить Redis|
|
||||
| `process` | `string` | `''` |Название процесса для интерфейса командной строки|
|
||||
| `shutdown` | `callable` |встроенный|Переопределить обратный вызов завершения работы|
|
||||
|
||||
---
|
||||
@@ -139,8 +144,8 @@ XC_Bootstrap::boot(BootContext $context, array $options = []);
|
||||
`boot()` выполняется один раз для каждого процесса. Повторные вызовы игнорируются.
|
||||
|
||||
```php
|
||||
XC_Bootstrap::boot(BootContext::ADMIN);
|
||||
XC_Bootstrap::boot(BootContext::CLI); // ignored
|
||||
XC_Bootstrap::boot(BootContext::Admin);
|
||||
XC_Bootstrap::boot(BootContext::Cli); // ignored
|
||||
```
|
||||
|
||||
Для проведения тестов:
|
||||
@@ -154,9 +159,10 @@ XC_Bootstrap::reset();
|
||||
## Общедоступные методы
|
||||
|
||||
```php
|
||||
XC_Bootstrap::getContext(): ?BootContext
|
||||
XC_Bootstrap::getContext(): ?string // active context's string value, e.g. 'admin' (null before boot)
|
||||
XC_Bootstrap::isBooted(): bool
|
||||
XC_Bootstrap::isCli(): bool
|
||||
XC_Bootstrap::isCli(): bool // true when the active context is Cli
|
||||
XC_Bootstrap::isDevMode(): bool // DEV_MODE flag (see Feature Flags)
|
||||
XC_Bootstrap::getDatabase(): ?Database
|
||||
XC_Bootstrap::getContainer(): ServiceContainer
|
||||
```
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
XC_VM использует двухуровневую стратегию кэширования:
|
||||
|
||||
- **Файловый кэш (igbinary)** - основной уровень, используемый как потоковыми, так и административными потоками.
|
||||
- **Redis/KeyDB** — дополнительный высокопроизводительный уровень для определения состояния подключения и расширенных операций
|
||||
- **Файловый кэш (igbinary)** — основной уровень, используемый как потоковыми, так и административными путями
|
||||
- **Redis/KeyDB** — дополнительный высокопроизводительный уровень для определения состояния соединения и расширенных операций
|
||||
|
||||
Путь потоковой передачи считывается исключительно из файлового кэша (никаких запросов к базе данных).
|
||||
Путь администратора считывается из базы данных с дополнительным кратковременным кэшем.
|
||||
@@ -102,28 +102,28 @@ RedisManager::closeInstance() // disconnect
|
||||
|
||||
Кратковременные запросы (PHP-FPM stream/admin) открывают новое соединение для каждого процесса
|
||||
и на них не влияют тайм-ауты простоя. Демоны-долгожители — цикл `watchdog`,
|
||||
`fanout_sync` — вместо этого удерживайте **одно** соединение через синглтон для их
|
||||
`fanout_sync` — вместо этого удерживайте соединение **один** через синглтон для их
|
||||
весь срок службы, при котором возможны два режима сбоя на загруженном сервере или на межсерверном сервере
|
||||
(LB → ГЛАВНАЯ) ссылка:
|
||||
|
||||
- **Server idle-close.** Redis closes any client idle past its `timeout` (`300s`
|
||||
- **Сервер простаивает - закрывается.** Redis закрывает любой клиент, который простаивает дольше своего `timeout` (`300s`
|
||||
в комплекте `bin/redis/redis.conf`). затем phpredis прозрачно откроется снова.
|
||||
сокет в следующей команде ** без повторного воспроизведения AUTH**, поэтому более поздняя команда
|
||||
сокет в следующей команде **без повторного воспроизведения аутентификации**, поэтому более поздняя команда
|
||||
отвечает `NOAUTH` — или просто возвращает `false`.
|
||||
- **Debounced health-check gap.** `instance()` only pings every 30s, so between
|
||||
- **Устранен пробел в проверке работоспособности.** `instance()` пингуется только каждые 30 секунд, так что между
|
||||
пингует, что сброшенное соединение еще не замечено.
|
||||
|
||||
Охранники на месте:
|
||||
|
||||
- `instance()` обрабатывает любой ответ, не связанный с`PONG` пингом (беззвучное повторное подключение / `NOAUTH`
|
||||
состояние) как отключенное соединение и вызывает полное, **повторно прошедшее проверку подлинности** повторное подключение
|
||||
состояние) как отключенное соединение и принудительно выполняет полное, **повторная аутентификация** повторное подключение
|
||||
через `\XC_VM::redis_connect()` — это не просто повторная попытка на уровне сокета.
|
||||
- Вызывайте сайты, которые командами конвейера проверяют объект конвейера. Например
|
||||
`ConnectionTracker::getCapacity()` проверяет, что `$redis->multi()` вернул
|
||||
`\Redis` (сломанный сокет возвращает `false` и вызывает `zCard()` для этого bool
|
||||
был бы фатальным вне пути повторного подключения) и выдает, чтобы его цикл повторных попыток снова подключился.
|
||||
|
||||
Альтернативный вариант на стороне сервера (`timeout 0`) намеренно **не** используется —
|
||||
The server-side alternative (`timeout 0`) is deliberately **not** used — the
|
||||
вместо этого клиент становится устойчивым, и `tcp-keepalive` по-прежнему получает доступ к мертвым одноранговым узлам.
|
||||
|
||||
---
|
||||
@@ -167,7 +167,7 @@ RedisManager::closeInstance() // disconnect
|
||||
блок-листы и `proxy_servers` из файлового кэша. Перед первой сборкой
|
||||
(fresh boot, cleared tmp) those files do not exist and `CacheReader::get()`
|
||||
возвращает `null`, поэтому для каждого такого глобального массива по умолчанию используется пустой массив. Холодный кэш
|
||||
поэтому ** ошибка закрыта** — запрос не находит серверов и показывает "не в эфире". —
|
||||
следовательно, **не удается закрыть** — запрос не находит серверов и показывает "нет в эфире". —
|
||||
вместо предупреждения `foreach(null)` или `in_array($x, null)` со смертельным исходом (PHP 8)
|
||||
вниз по течению. Действительно поврежденный кэш *сборка* все еще отображается отдельно с помощью
|
||||
`FileCache` предупреждение о сбое записи, поэтому это значение по умолчанию маскирует только временный
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# Подключение и регистрация сердечника
|
||||
|
||||
Как панель собирается при загрузке: один служебный контейнер, как он заполняется и как
|
||||
модули помещают свои маршруты, события, команды, записи cron и элементы навигационной панели в основные реестры.
|
||||
|
||||
Эта страница является **сквозное повествование и потребительская сторона** одним из основных реестров. То
|
||||
авторская сторона каждой точки расширения задокументирована в другом месте и связана с
|
||||
[What lives elsewhere](#what-lives-elsewhere) — this page does not repeat it.
|
||||
|
||||
---
|
||||
|
||||
## Один контейнер
|
||||
|
||||
Все зависит от одного процесса в масштабах всего процесса `ServiceContainer`
|
||||
(`src/Core/Container/ServiceContainer.php`), синглтон PSR-11 `ContainerInterface`, полученный с помощью
|
||||
`ServiceContainer::getInstance()`. `XC_Bootstrap::boot()` создает его, заполняет его, и каждый последующий
|
||||
потребитель (`XC_Bootstrap::getContainer()`, модуль `boot()`, разрешение обработчика маршрута) считывает то же самое
|
||||
пример. Тесты сбрасывают его с помощью `ServiceContainer::resetInstance()`.
|
||||
|
||||
Для основных служб есть значение **автоматическое обнаружение поставщика услуг отсутствует**: зарегистрирован канонический набор
|
||||
обязательно с помощью bootstrap (см. ниже), а модули добавляют свои собственные сервисы в свои
|
||||
`boot()`. То, что вы видите зарегистрированным, в точности соответствует коду `set()` — ничто не подключено с помощью
|
||||
сканирование условных обозначений или аннотаций.
|
||||
|
||||
---
|
||||
|
||||
## Заполнение контейнера при загрузке
|
||||
|
||||
Это основа системы. `XC_Bootstrap::boot()` регистрация услуг осуществляется в два этапа.
|
||||
|
||||
**Early (in `boot()` itself), before any subsystem loads:**
|
||||
|
||||
|Ключ|Ценность|Источник|
|
||||
| --- | --- | --- |
|
||||
| `context` |активное строковое значение `BootContext`| `src/bootstrap.php` |
|
||||
| `options` |массив `$options`, переданный в `boot()`| `src/bootstrap.php` |
|
||||
| `config` |`ConfigReader::getAll()` (проанализировано `config.ini`)| `src/bootstrap.php` |
|
||||
|
||||
**Набор канонических служб — `XC_Bootstrap::populateContainer()`** (выполняется для каждого контекста, за исключением
|
||||
`Minimal`, т.е. как только база данных будет запущена):
|
||||
|
||||
|Ключ|Ценность|Записи|
|
||||
| --- | --- | --- |
|
||||
| `db` |дескриптор `Database`|защищенный (см. ниже)|
|
||||
| `settings` | `SettingsManager::getAll()` |защищенный|
|
||||
| `servers` | `ServerRepository::getAll()` | |
|
||||
| `bouquets` | `BouquetService::getAll()` | |
|
||||
| `categories` | `CategoryService::getFromDatabase()` | |
|
||||
| `redis` | `RedisManager::instance()` |только тогда, когда для этого контекста был загружен Redis|
|
||||
| `translator` | `Translator::class` | |
|
||||
| `events` |новый экземпляр `EventDispatcher`|также подключен к статическому фасаду (см. [События](#events))|
|
||||
|
||||
Сразу после этого **`XC_Bootstrap::assertContainerHealth()`** жестко требует, чтобы `events` было
|
||||
присутствует (плюс `db`/`redis`, когда они были загружены) и выдает ошибку, если нет — гарантия **громкий сбой**
|
||||
этот более поздний код может предполагать, что эти службы существуют, вместо того, чтобы проверять каждую из них на нулевой уровень.
|
||||
|
||||
> Записи `context`/`config`/`settings`/`servers`/`bouquets`/`categories` представляют собой простые данные
|
||||
> снимки, сделанные при загрузке, а не на ленивых заводах — они считываются, а не пересчитываются, на протяжении всего срока службы устройства.
|
||||
> запрос.
|
||||
|
||||
---
|
||||
|
||||
## Ссылка на сервисный контейнер
|
||||
|
||||
Все установщики могут быть объединены в цепочку (`return $this`).
|
||||
|
||||
|Метод|Цель|
|
||||
| --- | --- |
|
||||
| `set(id, value)` |Зарегистрируйте сервис. Значение **`Closure`** становится **ленивая фабрика синглетов** (сначала вызывается один раз для `get()`, затем кэшируется); все остальное — скаляр, объект или массив `[Class, 'method']` — сохраняется как готовое значение.|
|
||||
| `factory(id, callable)` |Зарегистрируйте фабрику, которая возвращает значение **новый экземпляр для каждого `get()`** (без кэширования).|
|
||||
| `register(array)` |Скопируйте `set()` с карты `id => value`.|
|
||||
| `get(id)` |Разрешить службу. Возвращает кэшированный синглтон, если он присутствует; в противном случае запускает фабрику один раз, кэширует ее и применяет декораторы. Защищает от циклических фабрик и выдает `CircularDependencyException`; выдает `NotFoundException` для неизвестного идентификатора.|
|
||||
| `getOrDefault(id, default)` |Как `get()`, но возвращает `default` вместо того, чтобы выбрасывать при отсутствии.|
|
||||
|`has(id)` / `keys()` / `remove(id)` / `dump()`|Самоанализ и разрушение.|
|
||||
| `decorate(id, decorator, priority)` |Завершите работу с существующей службой, наивысший приоритет которой был применен последним. **Запрещенный** в защищенных службах `['db', 'settings', 'config', 'auth']` — выберите один из вариантов оформления. Смотрите [Пункты расширения модуля](module-extension-points.md).|
|
||||
|`tag(id, tag)` / `getTagged(tag)`|Сгруппируйте службы под одной меткой для пакетного поиска.|
|
||||
|
||||
> **Метки в настоящее время не используются основной проводкой.** `tag()`/`getTagged()` существует, но путь загрузки
|
||||
> собирает вклады модулей путем проверки `instanceof` по списку загруженных модулей (см.
|
||||
> [`bootAll`](#moduleloaderbootall-the-orchestrator)), **нет** по тегу. Исходный документированный блок все еще
|
||||
> описывает теги как механизм сбора данных для подписчиков/cron/маршрутов, который описывает
|
||||
> предполагаемый дизайн, а не текущий код. Не полагайтесь на коллекцию на основе тегов, пока она не будет создана на самом деле.
|
||||
> реализованный.
|
||||
|
||||
Resolving a `[Class, 'method']` handler (used by the Router) goes **through the container**, so
|
||||
классы-обработчики разграничиваются при регистрации, а в противном случае возвращаются к `new`.
|
||||
|
||||
---
|
||||
|
||||
## `ModuleLoader::bootAll` — организатор
|
||||
|
||||
После того, как `ModuleLoader::loadAll()` обнаружил, отфильтровал и **топологически отсортированный** отобрал модули
|
||||
(see [Module Lifecycle](module-lifecycle.md)), `bootAll()` is the single place that pushes each
|
||||
вклад модуля в основные реестры. Он проверяет каждое значение **подинтерфейс** на `instanceof`
|
||||
таким образом, модуль реализует только те перехватчики, которые ему нужны (`ModuleInterface` - это их совокупность).
|
||||
|
||||
Порядок для каждого модуля внутри `bootAll(ServiceContainer $container, ?Router $router, ?StreamPipeline $pipeline)`:
|
||||
|
||||
1. **Сначала основная навигационная панель, один раз** — `(new CoreNavbarProvider())->registerNavbar(...)` перед любым модулем, поэтому узлы основного меню существуют как родительские.
|
||||
2. `ServiceProviderInterface` → `boot($container)` **затем** `registerEventSubscribers()` — службы регистрируются до подключения слушателей этого модуля.
|
||||
3. `StreamMiddlewareProviderInterface` → `registerStreamMiddleware($pipeline)` — **только в том случае, если было передано значение `$pipeline`**.
|
||||
4. `RouteProviderInterface` → `registerRoutes($router)` — **только в том случае, если было передано значение `$router`** (`$router !== null`).
|
||||
5. `NavbarProviderInterface` → `registerNavbar(...)`.
|
||||
|
||||
Два вклада равны **отдельные проходы, не являющиеся частью `bootAll`**:
|
||||
|
||||
- `registerAllCommands($registry)` — Команды CLI, каждый модуль которых заключен в try / catch, поэтому один сломанный модуль не может заблокировать весь CLI.
|
||||
- `collectCronEntries()` — строки crontab, собранные из `CronProviderInterface::getCronEntries()`, используемые командами запуска/состояния.
|
||||
|
||||
Потому что маршруты и потоковое промежуточное программное обеспечение стробируются на основе необязательных аргументов `$router`/`$pipeline`,
|
||||
**одно и то же значение `bootAll()` выполняет различную работу в зависимости от точки входа** — интерфейс командной строки не передает ни,
|
||||
таким образом, к нему подключены только подписчики services + event (и безвредная навигационная панель).
|
||||
|
||||
---
|
||||
|
||||
## События
|
||||
|
||||
`EventDispatcher` (`src/Core/Events/EventDispatcher.php`) - это синглтон со статическим фасадом.
|
||||
`populateContainer()` выполняет `new EventDispatcher()` → `EventDispatcher::setInstance($d)` →
|
||||
`$container->set('events', $d)`, таким образом, запись контейнера `events` и статический
|
||||
`EventDispatcher::dispatch()/listen()` общий доступ к хранилищу прослушивателей **один**. Модули регистрируют прослушиватели
|
||||
во время шага 2 из `bootAll`, описанного выше, с помощью атрибута `getEventSubscribers()` или `#[ListensTo]`.
|
||||
|
||||
Полная информация - регистрационные формы, приоритеты, мероприятия, которые можно отменить, встроенный каталог мероприятий — приведена ниже.
|
||||
в [системе событий](event-system.md); на этой странице описывается только *где в последовательности загрузки* прослушиватели
|
||||
подключись к сети.
|
||||
|
||||
---
|
||||
|
||||
## Регистрация команд CLI
|
||||
|
||||
Путь к интерфейсу CLI (`src/console.php`) создает свой набор команд в два этапа:
|
||||
|
||||
1. **Автоматическое обнаружение ядра.** `new CommandRegistry()`, затем глобус `Cli/Commands/*.php` и
|
||||
`Cli/CronJobs/*.php`, сопоставьте каждый каталог с его пространством имен и с помощью отражения `register()` каждый
|
||||
**неабстрактный** класс, реализующий `CommandInterface`. Таким образом, добавление основной команды - это просто
|
||||
удаление класса в одном из этих каталогов — никакой ручной регистрации.
|
||||
2. **Команды модуля.** `ModuleLoader::registerAllCommands($registry)` вызывает каждый
|
||||
`CommandProviderInterface::registerCommands()`.
|
||||
|
||||
`CommandRegistry` (`src/Cli/CommandRegistry.php`) - это простая карта `name → CommandInterface`:
|
||||
`register()`, `dispatch($argv)` (обрабатывает `--list`/`--help`, группирует справку по префиксу `group:` в
|
||||
название команды), `get($name)`, `getAll()`.
|
||||
|
||||
---
|
||||
|
||||
## Сквозная загрузка — администрирование (web)
|
||||
|
||||
`src/Public/index.php`:
|
||||
|
||||
1. `XC_Bootstrap::boot(BootContext::Admin)` — создает контейнер; устанавливает `context`/`options`/`config`; загружает константы; выполняет проверку флуда/хостинга; `bootAdmin()` (сессия, база данных, `LegacyInitializer`, Redis, API администратора/реселлера, транслятор, обработчик завершения работы, константы состояния); **`populateContainer()`**; **`assertContainerHealth()`**.
|
||||
2. `Router::getInstance()`, затем `require` основные файлы маршрутов `routes/{scope}.php` (+ `routes/api.php`).
|
||||
3. Блок загрузки модуля: `router->beginModuleRegistration()` → `new ModuleLoader; loadAll(); bootAll($container, $router)` → `router->endModuleRegistration()` → `drainRouteCollisions()`. Режим регистрации модуля выполняет **выигрывают основные маршруты** по любому маршруту модуля с одинаковым путем; коллизии фиксируются, а не перезаписываются автоматически.
|
||||
4. `Router::dispatch()` / `dispatchApi()` обрабатывает запрос, разрешая `[Class, 'method']` обработчики через контейнер.
|
||||
|
||||
Смотрите [Контексты начальной загрузки](bootstrap-contexts.md), чтобы узнать, какие именно подсистемы инициализируются в каждом контексте, и [Обработка HTTP-запросов](http-request-handling.md) для API маршрутизатора и диспетчеризации.
|
||||
|
||||
---
|
||||
|
||||
## Сквозная загрузка — CLI
|
||||
|
||||
`src/console.php`:
|
||||
|
||||
1. `require bootstrap.php`; `XC_Bootstrap::boot(BootContext::Cli)` — DB, `LegacyInitializer`, необязательно Redis, заголовок процесса, затем **такой же** `populateContainer()` (таким образом, `events` и friends также существуют в CLI).
|
||||
2. `new CommandRegistry()`; автоматическое обнаружение ядра `Cli/Commands` + `Cli/CronJobs` (глобус + отражение) → `register()`.
|
||||
3. `new ModuleLoader; loadAll(); registerAllCommands($registry)` (команды модуля, **до** `bootAll`), затем `bootAll(getContainer())` **без маршрутизатора и трубопровода** — таким образом, маршруты и потоковое промежуточное программное обеспечение пропускаются; подключаются только службы модуля + подписчики событий.
|
||||
4. `registry->dispatch($argv)` выполняет запрошенную команду.
|
||||
|
||||
---
|
||||
|
||||
## Что живет в другом месте
|
||||
|
||||
Чтобы избежать дублирования, информация об авторе и каждой подсистеме размещается на отдельных страницах:
|
||||
|
||||
|Тема|Страница|
|
||||
| --- | --- |
|
||||
| Which subsystems each context initialises; `boot()` options; idempotency |[Контексты начальной загрузки](bootstrap-contexts.md)|
|
||||
|Формы регистрации на мероприятия, приоритеты, мероприятия, которые можно отменить, каталог мероприятий|[Система событий](event-system.md)|
|
||||
|API маршрутизатора, `begin/endModuleRegistration`, диспетчеризация, разрешение обработчика|[Обработка HTTP-запросов](http-request-handling.md)|
|
||||
|Конструктор элементов навигационной панели, правила видимости, рендеринг|[Рендеринг навигационной панели](navbar-rendering.md)|
|
||||
|Обнаружение модулей, фильтрация env, топосортировка, включение/ выключение, установка / обновление|[Жизненный цикл модуля](module-lifecycle.md)|
|
||||
|DI—оформление, потоковое промежуточное программное обеспечение, cron, миграции - хуки автора модуля|[Точки расширения модуля](module-extension-points.md)|
|
||||
|Написание модуля (манифест, контракт класса, макет каталога)|[Разработка модуля](module-authoring.md)|
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|Файл|Роль|
|
||||
| --- | --- |
|
||||
| `src/Core/Container/ServiceContainer.php` |Контейнер DI: `set`/`factory`/`get`/`decorate`/`tag`|
|
||||
| `src/bootstrap.php` |`XC_Bootstrap::boot`, `populateContainer`, `assertContainerHealth`|
|
||||
| `src/Core/Module/ModuleLoader.php` |`bootAll`, `registerAllCommands`, `collectCronEntries`|
|
||||
| `src/Core/Events/EventDispatcher.php` |Диспетчер событий + статический фасад, объединенный в `events`|
|
||||
| `src/Cli/CommandRegistry.php` |Карта команд CLI + `dispatch`|
|
||||
| `src/console.php` |Точка входа в интерфейс командной строки: автоматическое обнаружение основной команды + загрузка модуля|
|
||||
| `src/Public/index.php` |Веб-точка входа: файлы маршрута + загрузочный блок модуля|
|
||||
@@ -55,7 +55,7 @@ EventDispatcher::unlisten(StreamStartedEvent::class, $myCallable);
|
||||
EventDispatcher::hasListeners(StreamStartedEvent::class); // bool
|
||||
```
|
||||
|
||||
**Приоритет** — более высокое целое число = вызывается первым. По умолчанию `0`.
|
||||
**Приоритет** — большее целое число = вызывается первым. По умолчанию `0`.
|
||||
|
||||
---
|
||||
|
||||
@@ -65,13 +65,20 @@ EventDispatcher::hasListeners(StreamStartedEvent::class); // bool
|
||||
|
||||
```php
|
||||
public function getEventSubscribers(): array {
|
||||
// One entry per event class (it is an array key). The value is either a
|
||||
// plain callable, or a [callable, int $priority] tuple (higher = called first).
|
||||
return [
|
||||
StreamStartedEvent::class => [$this, 'onStreamStarted'],
|
||||
StreamStartedEvent::class => [[$this, 'onStreamStarted'], 20], // with priority
|
||||
StreamStoppedEvent::class => [[$this, 'onStreamStopped'], 20], // with priority
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
> Класс события может появиться в этом массиве только один раз. Чтобы прикрепить **несколько**
|
||||
> прослушивающие событие **такой же** из одного модуля, используют повторяемый
|
||||
> `#[ListensTo]` attribute (Option 2) instead — `getEventSubscribers()` keeps a
|
||||
> одна запись обработчика для каждого события.
|
||||
|
||||
### Вариант 2 — атрибут #[ListensTo]
|
||||
|
||||
```php
|
||||
@@ -96,6 +103,8 @@ class MyModuleModule extends BaseModule {
|
||||
Оба механизма работают одновременно и могут сосуществовать в одном модуле.
|
||||
`ModuleLoader::bootAll()` выполняет оба прохода для каждого загруженного модуля.
|
||||
|
||||
> В приведенных выше примерах для краткости записывается `use ListensTo;` / `use AbstractEvent;`. Реальные классы — это `XcVm\Core\Events\ListensTo` и `XcVm\Core\Events\AbstractEvent` - импортируйте эти полные имена (глобального псевдонима нет).
|
||||
|
||||
---
|
||||
|
||||
## Останавливаемые события
|
||||
@@ -117,6 +126,8 @@ EventDispatcher::listen(MyGatingEvent::class, function (MyGatingEvent $e): void
|
||||
|
||||
Прослушиватели пропускаются, как только `isPropagationStopped()` возвращает значение `true`.
|
||||
|
||||
> **Ошибки прослушивателя не обнаруживаются.** `EventDispatcher::dispatch()` вызывает прослушиватели в обычном цикле без `try/catch`, поэтому, если вызывается прослушиватель, исключение распространяется за пределы `dispatch()`, а остальные прослушиватели для этого события выполняют **нет**. Поддерживайте защиту слушателей (отслеживайте свои собственные ошибки), если один из подписчиков-неудачников не должен прерывать работу других.
|
||||
|
||||
---
|
||||
|
||||
## Встроенные основные события
|
||||
@@ -128,6 +139,7 @@ EventDispatcher::listen(MyGatingEvent::class, function (MyGatingEvent $e): void
|
||||
| `PackageInstalledEvent` | `Events/Module/` |После установки marketplace|Нет|
|
||||
| `UserAuthenticatedEvent` | `Events/Auth/` |После успешного входа в систему|Да|
|
||||
| `UserLoggedOutEvent` | `Events/Auth/` |После выхода из системы|Нет|
|
||||
| `StreamStartingEvent` | `Events/Stream/` |Перед запуском потока (gate — extends `AbstractEvent`)|Да|
|
||||
| `StreamStartedEvent` | `Events/Stream/` |После начала трансляции|Нет|
|
||||
| `StreamStoppedEvent` | `Events/Stream/` |После того, как поток прекратился|Нет|
|
||||
| `SettingsChangedEvent` | `Events/Settings/` |После сохранения настроек|Нет|
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
# Иерархия исключений
|
||||
|
||||
Все исключения XC_VM расширяют диапазон `XcVmException`, так что вызывающие абоненты могут перехватывать все дерево с помощью
|
||||
один `catch` блокирует или нацелен на определенную подсистему.
|
||||
XC_VM исключения фреймворка расширяют `XcVmException` — пустую базу **маркер**
|
||||
(`class XcVmException extends \RuntimeException {}`, это не добавляет никаких дополнительных данных) — таким образом, вызывающие абоненты могут
|
||||
охватите все семейство одним `catch (XcVmException)` или нацелитесь на определенную подсистему.
|
||||
|
||||
> **Масштаб.** Эта типизированная иерархия охватывает только **Контейнер DI** и **модульная система**.
|
||||
> Это не вся панель целиком: конечные точки потоковой передачи/аутентификации сообщают о сбоях через
|
||||
> `generateError()` (без исключений), и большая часть кода домена/CLI выдает простой
|
||||
> исключения `\RuntimeException` или SPL — они по-прежнему совпадают с `catch (XcVmException)` только тогда, когда
|
||||
> класс фактически расширяет его.
|
||||
|
||||
---
|
||||
|
||||
@@ -9,20 +16,25 @@
|
||||
|
||||
```
|
||||
\Exception
|
||||
└── XcVmException
|
||||
├── Container
|
||||
│ └── ContainerException (PSR-11 ContainerExceptionInterface)
|
||||
│ ├── CircularDependencyException
|
||||
│ ├── ServiceCreationException
|
||||
│ └── NotFoundException (PSR-11 NotFoundExceptionInterface)
|
||||
└── Module
|
||||
└── ModuleException
|
||||
├── ModuleNotFoundException
|
||||
├── ModuleLoadException
|
||||
├── ModuleManifestException
|
||||
└── ModuleCycleException
|
||||
└── \RuntimeException
|
||||
└── XcVmException
|
||||
├── Container
|
||||
│ └── ContainerException (PSR-11 ContainerExceptionInterface)
|
||||
│ ├── CircularDependencyException
|
||||
│ ├── ServiceCreationException
|
||||
│ └── NotFoundException (PSR-11 NotFoundExceptionInterface) *
|
||||
└── Module
|
||||
└── ModuleException
|
||||
├── ModuleNotFoundException
|
||||
├── ModuleLoadException
|
||||
├── ModuleManifestException
|
||||
└── ModuleCycleException
|
||||
```
|
||||
|
||||
> \* `NotFoundException` расширяет `ContainerException` (таким образом, он принадлежит этому дереву), но он
|
||||
> физически находится в `src/Core/Container/Psr/NotFoundException.php` под пространством имен
|
||||
> `XcVm\Core\Container\Psr` — **нет** в `Core/Exception/Container/`.
|
||||
|
||||
---
|
||||
|
||||
## Исключения для контейнеров
|
||||
@@ -54,7 +66,7 @@ try {
|
||||
| `ModuleNotFoundException` |Отсутствует необходимый модуль зависимостей|
|
||||
| `ModuleLoadException` |Файл модуля не может быть загружен или класс не найден|
|
||||
| `ModuleManifestException` |`module.json` отсутствует, неправильно сформирован или не прошел проверку|
|
||||
| `ModuleCycleException` |Граф зависимостей имеет цикл|
|
||||
| `ModuleCycleException` |Граф зависимостей имеет топологическую сортировку, генерируемую циклом `ModuleLoader`, с циклическим путем (`a -> b -> a`) в сообщении. (В некоторых `@throws` блоках документации указано `\RuntimeException`; это просто базовый тип — `ModuleCycleException` расширяет его с помощью `XcVmException`.)|
|
||||
|
||||
---
|
||||
|
||||
@@ -85,6 +97,19 @@ try {
|
||||
|
||||
---
|
||||
|
||||
## Добавление или выбор исключения
|
||||
|
||||
- **Который нужно выбросить:** используйте наиболее конкретный существующий тип (например, `ModuleManifestException`
|
||||
для неудачного `module.json`). Если ничего не подходит и это сбой на уровне фреймворка, выбросьте
|
||||
`XcVmException` (или новый подкласс), чтобы его можно было отслеживать как одно семейство. Домен/бизнес
|
||||
ошибки, не связанные с работой фреймворка, могут привести к появлению простого сообщения `\RuntimeException` /
|
||||
`\InvalidArgumentException`.
|
||||
- **Добавление категории:** создайте класс в соответствии с `src/Core/Exception/<Subsystem>/`, расширьте
|
||||
база подсистемы (`ContainerException` / `ModuleException`) — или `XcVmException` для нового
|
||||
подсистема — и добавьте ее в дерево выше. Регистрация не требуется, все просто PHP.
|
||||
|
||||
---
|
||||
|
||||
## Местоположение
|
||||
|
||||
```
|
||||
@@ -93,8 +118,7 @@ src/Core/Exception/
|
||||
├── Container/
|
||||
│ ├── ContainerException.php
|
||||
│ ├── CircularDependencyException.php
|
||||
│ ├── ServiceCreationException.php
|
||||
│ └── NotFoundException.php
|
||||
│ └── ServiceCreationException.php
|
||||
└── Module/
|
||||
├── ModuleException.php
|
||||
├── ModuleNotFoundException.php
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
```text
|
||||
nginx -> Public/index.php
|
||||
-> URL parsing (scope + pageName)
|
||||
-> XC_Bootstrap::boot(CONTEXT_ADMIN)
|
||||
-> XC_Bootstrap::boot(BootContext::Admin)
|
||||
-> floodProtection() (block banned IPs)
|
||||
-> hostVerification() (check allowed domains)
|
||||
-> initSession()
|
||||
@@ -69,7 +69,7 @@ nginx -> Public/index.php
|
||||
|
||||
```text
|
||||
nginx -> Public/index.php
|
||||
-> XC_Bootstrap::boot(CONTEXT_ADMIN)
|
||||
-> XC_Bootstrap::boot(BootContext::Admin)
|
||||
-> new AdminApiController() or new ResellerRestApiController()
|
||||
-> $controller->index()
|
||||
-> exit
|
||||
@@ -111,8 +111,8 @@ nginx -> StreamingRequestBootstrap::init($filename)
|
||||
|
||||
1. **Защита от наводнений** -- Если файл `FLOOD_TMP_PATH/block_{IP}` существует, запрос отклоняется по протоколу HTTP 403.
|
||||
2. **Загрузка кэша настроек** -- Считывает `$rSettings` из кэша файлов, сериализованных в igbinary, по адресу `CACHE_TMP_PATH/settings`.
|
||||
3. **Проверка хоста** - При значении `$rSettings['verify_host']` true проверяется, отображается ли `HOST` в кэшированном списке `allowed_domains`. Исключения: имя хоста `xc_vm` и любой действительный IP-адрес всегда разрешены.
|
||||
4. **Флаг отображения ошибки** - Устанавливает значение константы `PHP_ERRORS` вместо `$rSettings['debug_show_errors']`.
|
||||
3. **Проверка хостинга** -- Если значение `$rSettings['verify_host']` равно true, проверяется, отображается ли `HOST` в кэшированном списке `allowed_domains`. Исключения: имя хоста `xc_vm` и любой допустимый IP-адрес всегда разрешены.
|
||||
4. **Флаг отображения ошибки** - Устанавливает константу `PHP_ERRORS` вместо константы `$rSettings['debug_show_errors']`.
|
||||
5. **Инициализация регистратора** -- Вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`.
|
||||
|
||||
Примечание: В современном bootstrap (`XC_Bootstrap::boot()`) эти обязанности выполняются методами `floodProtection()` и `hostVerification()` напрямую, а не путем включения `RequestGuard.php`.
|
||||
@@ -313,18 +313,24 @@ $router->dispatchApi($action); // returns true if matched
|
||||
|
||||
1. Нормализовать `$page` (символы подчеркивания заменить косыми чертами, зачеркнуть `.php`).
|
||||
2. Посмотрите в разделе POST routes (если используется метод POST) или GET routes. Если POST route не найден, вернитесь к GET routes.
|
||||
3. **Проверка прав доступа ** через `checkPermission()`. Если отказано, вызывает `denyAccess()` (перенаправление или 403).
|
||||
3. **Проверка прав доступа** через `checkPermission()`. Если отказано, вызывает `denyAccess()` (перенаправление или 403).
|
||||
4. **Выполнение промежуточного программного обеспечения**. Вызывается каждый вызываемый объект в массиве `middleware`. Если какой-либо из них возвращает значение `false`, выполнение прекращается.
|
||||
5. **Вызов обработчика** через `callHandler()`.
|
||||
|
||||
#### `dispatchApi($action)` порядок исполнения
|
||||
|
||||
1. Найдите в API маршруты по названию действия.
|
||||
2. **Проверка разрешений**. Если отказано, выводит `{"result": false}` и завершает работу.
|
||||
2. **Проверка прав доступа**. Если отказано, выводит `{"result": false}` и завершает работу.
|
||||
3. **Вызов обработчика**. Промежуточное программное обеспечение не выполняется.
|
||||
|
||||
Важно: `dispatchApi()` не запускает промежуточное программное обеспечение. Это намеренное отличие от отправки страниц.
|
||||
|
||||
#### Когда ничего не совпадает
|
||||
|
||||
И `dispatch()`, и `dispatchApi()` возвращают `false`, если маршрут не совпадает. `Public/index.php` затем выдает `http_response_code(404); echo '404 Not Found';` — есть **нет** универсальный контроллер. (Неправильно введенный путь к ресурсу, который достигает главного контроллера, вместо того, чтобы обслуживаться nginx, попадает на тот же 404.)
|
||||
|
||||
> **Pitfall — two sanitization APIs + a global.** Input can be reached three ways: `InputValidator` (the global request-sanitization layer), the `Request` class's static `sanitize*()` methods (kept for backward compatibility), and the global-static `RequestManager`. They are not interchangeable and the sanitization one applies depends on the bootstrap path — pick the layer the surrounding code already uses rather than mixing them, and remember `RequestManager`'s static state makes it order-dependent and awkward to isolate in tests (set it explicitly in a test rather than relying on prior request state).
|
||||
|
||||
### Регистрация маршрута модуля
|
||||
|
||||
Модули регистрируют маршруты с помощью `ModuleInterface::registerRoutes()`. Маршрутизатор поддерживает безопасный режим регистрации, предотвращающий перезапись модулями основных маршрутов:
|
||||
@@ -377,10 +383,15 @@ $collisions = $router->drainRouteCollisions();
|
||||
|
||||
|Контекст|Что он инициализирует|
|
||||
| --- | --- |
|
||||
| `CONTEXT_MINIMAL` |Автозагрузка + константы + конфигурация + регистратор. Нет подключения к базе данных.|
|
||||
| `CONTEXT_CLI` |+ База данных + `LegacyInitializer::initCore()` (очистка входных данных, настройки, пути FFmpeg). Необязательно Redis.|
|
||||
| `CONTEXT_STREAM` |+ Только база данных (упрощенная, без `LegacyInitializer`). Конечные точки потоковой передачи используют вместо этого `StreamingRequestBootstrap`.|
|
||||
| `CONTEXT_ADMIN` |+ Сессия + База данных + `LegacyInitializer::initCore()` + Redis + API администратора + Переводчик + глобальные настройки администратора. Полная инициализация.|
|
||||
| `BootContext::Minimal` |Автозагрузка + константы + конфигурация + регистратор. Нет подключения к базе данных.|
|
||||
| `BootContext::Cli` |+ База данных + `LegacyInitializer::initCore()` (очистка входных данных, настройки, пути FFmpeg). Необязательно Redis.|
|
||||
| `BootContext::Stream` |+ Только база данных (упрощенная, без `LegacyInitializer`). Конечные точки потоковой передачи используют вместо этого `StreamingRequestBootstrap`.|
|
||||
| `BootContext::Admin` |+ Сессия + База данных + `LegacyInitializer::initCore()` + Redis + API администратора + Переводчик + глобальные настройки администратора. Полная инициализация.|
|
||||
|
||||
> `boot()` принимает перечисление `BootContext` (предпочтительно). Устаревшая строка
|
||||
> константы `XC_Bootstrap::CONTEXT_{MINIMAL,CLI,STREAM,ADMIN}` равны `@deprecated`
|
||||
> псевдонимы сохранены для обеспечения обратной совместимости — вы все равно увидите их в более старых вызовах
|
||||
> места. Смотрите [Контексты начальной загрузки](bootstrap-contexts.md) для получения полной матрицы.
|
||||
|
||||
Все HTTP-контексты (не CLI) также запускают защиту от наводнений и проверку хоста перед инициализацией, зависящей от контекста.
|
||||
|
||||
@@ -400,7 +411,7 @@ $collisions = $router->drainRouteCollisions();
|
||||
| `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` |Облегченный загрузчик конечной точки потоковой передачи|
|
||||
| `src/Streaming/StreamingBootstrap.php` |Потоковое подключение к базе данных и устаревшая инициализация|
|
||||
| `src/bootstrap.php` |Унифицированный bootstrap (класс`XC_Bootstrap`)|
|
||||
| `src/Public/index.php` |Внешний контроллер для администратора/реселлера/игрока/API|
|
||||
| `src/Public/index.php` |Передний контроллер для администратора/реселлера/игрока/API|
|
||||
| `src/Public/routes/admin.php` |Определения маршрутов на странице администратора|
|
||||
| `src/Public/routes/reseller.php` |Определения маршрута на странице реселлера|
|
||||
| `src/Public/routes/player.php` |Определения маршрута на странице игрока|
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
# Разработка модуля
|
||||
|
||||
Как создать модуль XC_VM: его расположение на диске, манифест `module.json`, контракт класса модуля + метода, пространства имен и его контроллер. О том, как модуль обнаруживается/загружается/распространяется, смотрите в [Жизненный цикл модуля](module-lifecycle.md); о перехватчиках, к которым он подключается, смотрите в [Точках расширения модуля](module-extension-points.md).
|
||||
|
||||
## Обзор
|
||||
|
||||
|
||||
Модуль - это изолированный каталог под `src/Modules/` с известным контрактом. Система
|
||||
is built on **Extensible Platform** principles:
|
||||
|
||||
- Ядро (`Core/`) ничего не знает о модулях
|
||||
- Модули могут зависеть от `Core/` и `Domain/`, но никогда друг от друга (кроме как через объявленные зависимости).
|
||||
- Любой модуль можно отключить из `config/modules.php`, не прикасаясь к ядру
|
||||
- Удаление каталога модуля не приводит к фатальным ошибкам
|
||||
|
||||
---
|
||||
|
||||
## Структура каталогов модулей
|
||||
|
||||
|
||||
Имя каталога соответствует условию **`{name}_{hash5}`**, где `hash5` - это
|
||||
первые 5 символов модуля `hash_id`. Логическое имя модуля (`module.json`
|
||||
`name`, который никогда не содержит `_`) всегда разрешается из манифеста — никогда из
|
||||
базовое имя каталога. Это позволяет двум модулям с **то же имя** работать в разных
|
||||
каталоги (`watch_2541a`, `watch_9f1c0`) и установите их без столкновения с файловой системой. То
|
||||
конфигурация, график зависимостей и пространство имен - все это не соответствует каноническому `name`, поэтому каталог
|
||||
переименование не требует переноса данных. У каждого модуля **должен** есть `hash_id`: загружаемые файлы, которые отправляются
|
||||
без такового получите новый идентификатор, сгенерированный и записанный в их `module.json` перед размещением,
|
||||
таким образом, каталог без хэша никогда не создается. Устаревший пустой каталог `Modules/{name}/` из
|
||||
более старое развертывание все еще считывается, но имеет значение от **автоматическая миграция** до `{name}_{hash5}` (генерируя
|
||||
`hash_id`, если отсутствует) при следующем `console.php status` — макет без хэша удаляется, а не
|
||||
держал.
|
||||
|
||||
```text
|
||||
src/Modules/my-module_9f1c0/ # {name}_{hash5}; canonical name is "my-module"
|
||||
├── module.json # Metadata and manifest
|
||||
├── MyModule.php # Module class (source of truth)
|
||||
├── MyService.php # Business logic
|
||||
├── MyController.php # Admin pages (optional)
|
||||
├── MyCron.php # Cron logic (optional)
|
||||
├── MyCronJob.php # CLI cron wrapper (optional)
|
||||
├── database.sql # Master schema — full current CREATE/seed (optional)
|
||||
├── database_drop.sql # Teardown — DROP every table the module owns (optional)
|
||||
├── migrations/ # Forward version deltas (optional)
|
||||
│ └── 1.1.0.sql # Applied only when upgrading a panel past 1.1.0
|
||||
└── views/ # Page templates (optional)
|
||||
├── my_page.php
|
||||
└── my_page_scripts.php
|
||||
```
|
||||
|
||||
Модуль владеет своей схемой через **три роли, отражающие суть** (`bin/install/database.sql`
|
||||
+ `migrations/`):
|
||||
|
||||
|Файл|Роль|Работает на|
|
||||
| ---- | ---- | ------- |
|
||||
| `database.sql` | **One** master schema — the full current `CREATE`/seed |свежий **устанавливать**|
|
||||
| `database_drop.sql` |**Один** удаление — `DROP TABLE` для каждой таблицы, которой владеет модуль| **uninstall** |
|
||||
| `migrations/<semver>.sql` |**Папка** прямых различий между версиями|**обновление**, для версий в `(installed, current]`|
|
||||
|
||||
Правила:
|
||||
|
||||
- **Выполняется только новая установка `database.sql`**, поэтому он всегда должен отражать последнюю версию
|
||||
схема (каждая дельта загнута внутрь). Записанный `installed_version` является водяным знаком —
|
||||
ошибки никогда не воспроизводятся при новой установке.
|
||||
- **Дельты направлены только вперед** (`ALTER`/`INSERT`), названный `<semver>.sql` — демонтаж - это
|
||||
одинарный `database_drop.sql`, поэтому нет файлов для каждой версии `.down`.
|
||||
- Сохраняйте дельты **идемпотентный** (`ADD COLUMN IF NOT EXISTS`, `INSERT IGNORE`), чтобы повторные запуски были безопасными.
|
||||
- Модуль без схемы не отправляет ни один из этих файлов. Модуль, работающий только с разницей (нет `database.sql`)
|
||||
по-прежнему устанавливается путем повторного воспроизведения каждой дельты в ее версии.
|
||||
|
||||
---
|
||||
|
||||
## модуль.json
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-module",
|
||||
"hash_id": "9f1c0b7e4d2a6538c1e0a4b7d6f39e21",
|
||||
"description": "Short description",
|
||||
"version": "1.0.0",
|
||||
"requires_core": ">=2.0",
|
||||
"environment": "main",
|
||||
"priority": 0,
|
||||
"dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"has_navbar": false,
|
||||
"has_settings": false
|
||||
}
|
||||
```
|
||||
|
||||
### Поля манифеста
|
||||
|
||||
|Поле|Тип|По умолчанию|Описание|
|
||||
| ------ | ----- | :---: | ------------ |
|
||||
| `name` | `string` |—|Каноническое имя модуля (в случае с kebab, без `_`). Каталог равен `{name}_{hash5}`, но код всегда использует это значение манифеста, а не базовое имя каталога.|
|
||||
| `hash_id` | `string` |сгенерированный|**Постоянный** идентификатор модуля — случайный 32-разрядный шестнадцатеричный код, генерируемый ОДИН раз и никогда не изменяющийся при изменении версии или переименовании. Его первые 5 символов образуют суффикс каталога `{name}_{hash5}`. Не редактируйте вручную.|
|
||||
| `description` | `string` | `""` |Удобочитаемое описание|
|
||||
| `version` | `string` |—|Средняя версия (`1.0.0`)|
|
||||
| `requires_core` | `string` |—|Минимальная версия ядра (`>=2.0`)|
|
||||
| `environment` | `string` | `"main"` |`main`, `lb` или `any`|
|
||||
| `priority` | `int` | `0` |Приоритет загрузки — более высокие нагрузки раньше|
|
||||
| `dependencies` | `array` | `[]` |Жесткие зависимости; если они недоступны, зависимый объект пропускается (см. ниже)|
|
||||
| `optional_dependencies` | `array` | `[]` |Мягкие зависимости (загруженные ранее, если они есть)|
|
||||
| `has_navbar` | `bool` | `false` |Регистрирует ли модуль элементы навигационной панели|
|
||||
| `has_settings` | `bool` | `false` |Есть ли у модуля страница настроек|
|
||||
|
||||
> **`hash_id` — постоянный идентификатор модуля.** Это случайное значение из 32 шестнадцатеричных чисел,
|
||||
> сгенерированный **однажды** и **никогда** измененный впоследствии — он должен пережить сбои в версии
|
||||
> и переименовывает (чтобы оно было случайным, а не производным от `name`/`version`). Сгенерируйте его с помощью
|
||||
> `php -r 'echo bin2hex(random_bytes(16));'` и вставьте его в `module.json`, когда
|
||||
> создайте новый модуль. Не создавайте его вручную и не используйте повторно другой модуль. Это придает модулям стабильную идентичность, независимую от `name`,
|
||||
> который является основой для перемещения модулей в отдельные репозитории и для создания
|
||||
> явный для каждого модуля **источник обновления** - блок манифеста `update` (см. ниже).
|
||||
|
||||
**Hard vs soft dependencies:**
|
||||
|
||||
- `dependencies` — если какой—либо модуль недоступен (отсутствует на диске, отключен или находится в состоянии `failed`), зависимому модулю присваивается значение **пропущенный** с записанным каскадным предупреждением (все, что зависит от него, также пропускается). Остальные модули, панель администратора и интерфейс командной строки продолжают работать; единственная неудовлетворенная зависимость больше не прерывает всю загрузку.
|
||||
- `optional_dependencies` — загружается перед этим модулем, если присутствует, автоматически пропускается, если отсутствует
|
||||
|
||||
> **Остерегайтесь дрейфа.** Модуль, от которого зависят все еще включенные модули, не может быть запущен `disabled` через панель / `ModuleManager::setState()` - операция отклоняется со списком зависимостей (зеркально отображая защиту `uninstallModule()`). Это предотвращает переход в состояние "`plex` включено, но его зависимость от `watch` отключена".
|
||||
|
||||
**Priority:**
|
||||
|
||||
- Сначала при топологической сортировке учитывается график зависимостей, затем в пределах той же группы выполняется сортировка по убыванию `priority` (большее число = загружено ранее), затем по алфавиту
|
||||
|
||||
**Update source (`update` block, optional):**
|
||||
|
||||
Откуда модуль получает свои обновления. Отсутствует → `bundled` (файлы отправляются вместе с панелью и обновляются вместе с ней).
|
||||
|
||||
```json
|
||||
"update": {
|
||||
"source": "bundled | platform | git | url",
|
||||
"repository": "https://github.com/Vateron-Media/xc_vm-module-watch",
|
||||
"channel": "stable",
|
||||
"slug": "watch",
|
||||
"url": "https://…/version.json"
|
||||
}
|
||||
```
|
||||
|
||||
- `source` — `bundled` (с панелью управления), `platform` (хранилище SaaS), `git` (репозитории), `url` (автономный хостинг). Неизвестные значения возвращаются к значению `bundled`.
|
||||
- `repository` — git remote (для `git`); `slug` — хранилище slug (для `platform`, по умолчанию используется `name`); `url` — URL версии/архива (для `url`); `channel` — `stable`/`beta` (по умолчанию `stable`).
|
||||
|
||||
Блок нормализуется с помощью `ModuleLoader` и отображается с помощью `ModuleManager::listModules()`. Еженедельный cron (`cron:module_updates`) проверяет источники `git`/`url` и записывает `available_version`, что приводит к нажатию кнопки **Обновить до X** (отображается только при наличии более новой версии). Нажатие кнопки Обновить запускает `ModuleManager::updateModuleFromSource()`:
|
||||
|
||||
- `bundled` — файлы поступают вместе с панелью; Обновление просто запускает отложенные миграции.
|
||||
- `platform` — делегировано потоку установки/обновления в магазине (откат + разветвление LB внутри).
|
||||
- `git` — загружает ресурс выпуска **`module.tar.gz`** по тегу == новая версия (md5-проверяется с помощью выпуска `hashes.md5`, если присутствует).
|
||||
- `url` — перечитывает `version.json` для его `download` (https) + необязательно `md5`.
|
||||
|
||||
Для `git`/`url` выбранного `module.json` **`hash_id` должно быть равно установленному значению** (идентификация — репозиторий /URL-адрес не может выдавать себя за другой модуль), затем: резервное копирование → замена файлов → перенос → **откат при любом сбое** → распространение в LB.
|
||||
|
||||
**Стандартный набор и подготовка.** Модули, которые панель устанавливает по умолчанию, перечислены в `config/bundled_modules.php`, с ключом `hash_id` (неизменны при переименовании). На сегодняшний день все модули имеют `bundled` (их файлы находятся в архиве панели). Когда модуль извлекается в свой собственный репозиторий, измените его запись на `git`/`url`/`platform` source — `syncBundledModules()`, затем автоматически извлекает и устанавливает его с помощью `provisionStandardSet()` (это не требуется, пока все находится в комплекте на диске). `ModuleManager::findModuleByHashId()` определяет модуль по его стабильному идентификатору независимо от каталога/имени.
|
||||
|
||||
---
|
||||
|
||||
## Подинтерфейсы
|
||||
|
||||
|
||||
`ModuleInterface` разбивает площадь поверхности модуля на типизированные субдоговоры:
|
||||
|
||||
```text
|
||||
ModuleInterface
|
||||
├── ServiceProviderInterface → boot(ServiceContainer)
|
||||
├── RouteProviderInterface → registerRoutes(Router)
|
||||
├── CommandProviderInterface → registerCommands(CommandRegistry)
|
||||
└── NavbarProviderInterface → registerNavbar()
|
||||
```
|
||||
|
||||
`StreamMiddlewareProviderInterface` равно **необязательный** — оно не является частью `ModuleInterface`.
|
||||
Реализуйте это только в том случае, если модулю необходимо внедрить себя в потоковый конвейер.
|
||||
|
||||
```php
|
||||
// Optional — not in ModuleInterface
|
||||
class MyModule implements ModuleInterface, StreamMiddlewareProviderInterface {
|
||||
public function getStreamMiddleware(): array {
|
||||
return [new MyStreamMiddleware()];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Класс модуля
|
||||
|
||||
|
||||
Extend `BaseModule` — он предоставляет значения по умолчанию без операций для каждого необязательного метода, так что вы можете использовать только
|
||||
переопределите то, что на самом деле использует модуль. Требуются только `getName()` и `getVersion()`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use ServiceContainer;
|
||||
use Router;
|
||||
use CommandRegistry;
|
||||
use NavbarRegistry;
|
||||
use NavbarItem;
|
||||
|
||||
class MyModuleModule extends BaseModule {
|
||||
|
||||
public function getName(): string {
|
||||
return 'my-module';
|
||||
}
|
||||
|
||||
public function getVersion(): string {
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function boot(ServiceContainer $container): void {
|
||||
$container->set('my-module.service', function (ServiceContainer $c): MyModuleService {
|
||||
return new MyModuleService($c->get('db'));
|
||||
});
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router): void {
|
||||
$router->get('my_page', [MyModuleController::class, 'index'], [
|
||||
'permission' => ['adv', 'my_module'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function registerCommands(CommandRegistry $registry): void {
|
||||
$registry->register(new MyModuleCronJob());
|
||||
}
|
||||
|
||||
public function registerNavbar(NavbarRegistry $registry): void {
|
||||
NavbarRegistry::add(
|
||||
(new NavbarItem('management.service_setup.my_module'))
|
||||
->parent('management.service_setup')
|
||||
->url('my_page')
|
||||
->label('my_module')
|
||||
->permissions(['my_module'])
|
||||
->order(60)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Совет:** модулю без маршрутов, элементов навигационной панели и команд CLI требуется только
|
||||
> `getName()`, `getVersion()` и `boot()`.
|
||||
> Модуль изолированной подсистемы (его собственная точка входа и bootstrap, например Ministra) обычно
|
||||
> оставляет `boot()` и `registerRoutes()` унаследованными как не выполняемые операции.
|
||||
|
||||
### Метод контракта
|
||||
|
||||
|Метод|Интерфейс|Описание|
|
||||
| ------- | ----------- | ---------- |
|
||||
| `getName(): string` | `ModuleInterface` |Уникальное имя (соответствует каталогу)|
|
||||
| `getVersion(): string` | `ModuleInterface` |Версия Semver|
|
||||
| `boot(ServiceContainer)` | `ServiceProviderInterface` |Регистрация сервисов в контейнере DI|
|
||||
| `registerRoutes(Router)` | `RouteProviderInterface` |Регистрация HTTP- и API-маршрутов|
|
||||
| `registerCommands(CommandRegistry)` | `CommandProviderInterface` |Регистрация команд CLI и задач cron|
|
||||
| `registerNavbar(NavbarRegistry $registry)` | `NavbarProviderInterface` |Регистрация элементов навигационной панели|
|
||||
| `install(): void` | `ModuleInterface` |Запуск при установке модуля (миграции, начальный запуск)|
|
||||
| `uninstall(): void` | `ModuleInterface` |Запуск при удалении модуля (очистка)|
|
||||
|
||||
> **Важно — версия хранится в двух местах.** Модуль объявляет свою версию
|
||||
> **дважды**: поле `"version"` в `module.json` и возвращаемое значение из
|
||||
> `getVersion()` в классе module. **Сохраняйте их идентичными и изменяйте оба перед
|
||||
> издательский.** Во время выполнения манифест `version` имеет приоритет — установка/обновление
|
||||
> и водяной знак `installed_version` сначала читается как `module.json`, и только потом возвращается
|
||||
> to `getVersion()` — so a stale `getVersion()` silently drifts out of sync and is a
|
||||
> распространенный источник ошибок типа "выполнена /не выполнена неправильная миграция". Если модуль отправляет файл
|
||||
> migrations, `database.sql` (master schema) and the highest `migrations/<semver>.sql`
|
||||
> дельта также должна соответствовать этой версии.
|
||||
|
||||
---
|
||||
|
||||
## PHP пространства имен
|
||||
|
||||
|
||||
Каждый модуль находится в выделенном пространстве имен PHP: _BOS_0}, где _BOS_1} - это
|
||||
преобразование имени каталога модуля в PascalCase.
|
||||
|
||||
```
|
||||
src/Modules/my-module/ → namespace XcVm\Module\MyModule;
|
||||
src/Modules/watch/ → namespace XcVm\Module\Watch;
|
||||
```
|
||||
|
||||
В главном файле модуля должно быть объявлено это пространство имен и расширено `BaseModule`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use ServiceContainer;
|
||||
use Router;
|
||||
|
||||
class MyModuleModule extends BaseModule {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Все дополнительные классы в одном модуле используют одно и то же пространство имен:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
class MyModuleService { /* ... */ }
|
||||
class MyModuleController { /* ... */ }
|
||||
class MyModuleCronJob { /* ... */ }
|
||||
```
|
||||
|
||||
`use` классы, на которые вы ссылаетесь:
|
||||
|
||||
```php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use ServiceContainer;
|
||||
use NavbarRegistry;
|
||||
use NavbarItem;
|
||||
|
||||
class MyModuleModule extends BaseModule {
|
||||
public function boot(ServiceContainer $container): void {
|
||||
$container->set('my-module.service', fn () => new MyModuleService());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Имя файла основного класса модуля: `<PascalName>Module.php` — обязательно (соглашение с загрузчиком модулей)
|
||||
- All other class filenames: `<PascalName><Purpose>.php`
|
||||
- Добавьте `use ClassName;` для каждого базового класса, на который ссылается ссылка (базовый модуль, ServiceContainer, маршрутизатор и т.д.)
|
||||
- Никогда не импортируйте классы из других модулей — общайтесь через события или контейнер DI
|
||||
|
||||
---
|
||||
|
||||
## Контроллер
|
||||
|
||||
|
||||
```php
|
||||
class MyController {
|
||||
|
||||
protected string $viewsPath;
|
||||
|
||||
public function __construct() {
|
||||
$this->viewsPath = __DIR__ . '/views';
|
||||
require_once MAIN_HOME . 'Public/Views/layouts/admin.php';
|
||||
require_once MAIN_HOME . 'Public/Views/layouts/footer.php';
|
||||
}
|
||||
|
||||
public function index(): void {
|
||||
renderUnifiedLayoutHeader('admin', ['_TITLE' => 'My Module']);
|
||||
include $this->viewsPath . '/my_page.php';
|
||||
renderUnifiedLayoutFooter('admin');
|
||||
include $this->viewsPath . '/my_page_scripts.php';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|Правило| |
|
||||
| --------- | -- |
|
||||
| `__DIR__ . '/views'` |viewsPath — контроллер находится внутри каталога модуля|
|
||||
|ПОЛУЧАТЬ страницы|вызовите `renderUnifiedLayoutHeader` перед просмотром, `renderUnifiedLayoutFooter` после|
|
||||
|Действия API|нет макета — возвращаем JSON и выходим|
|
||||
|
||||
---
|
||||
|
||||
## Контрольный список модулей
|
||||
|
||||
|
||||
- [ ] Создать `src/Modules/<name>/`
|
||||
- [ ] Добавить `namespace XcVm\Module\<PascalName>;` к каждому файлу класса
|
||||
- [ ] Создать `module.json` с помощью `name`, `version`, `requires_core`, `priority`, `dependencies`, `optional_dependencies`
|
||||
- [ ] Поставьте постоянный штамп `hash_id` (`php -r 'echo bin2hex(random_bytes(16));'`; никогда не пишите его от руки)
|
||||
- [ ] Create `<PascalName>Module.php` extending `BaseModule`
|
||||
- [ ] Укажите версию в **оба** `module.json` `"version"` и `getVersion()` — они должны совпадать (измените обе версии перед публикацией)
|
||||
- [ ] Реализовать `boot()` для всех сервисов, предоставляемых модулем
|
||||
- [ ] Реализовать `registerRoutes()` для конечных точек HTTP/API
|
||||
- [ ] Ввести `registerNavbar()` для элементов панели администратора (или оставить пустым)
|
||||
- [ ] (Если кроны) Создайте `MyCron.php` + `MyCronJob.php`, зарегистрируйтесь в `registerCommands()`
|
||||
- [ ] (Если crons) Переопределяет `getCronEntries()` в классе модуля (основной файл не изменяется)
|
||||
- [ ] (Схема If) Отправляет значения `database.sql` (мастер), `database_drop.sql` (демонтаж) и `migrations/<semver>.sql` дельт
|
||||
- [ ] (При переносе PHP-логики) Реализовать `MigratableInterface::getMigrations()`
|
||||
- [ ] (Если страницы) Создайте контроллер, используя `renderUnifiedLayoutHeader/Footer`
|
||||
- [ ] (Если потоковое промежуточное программное обеспечение) Реализовать `StreamMiddlewareProviderInterface` отдельно
|
||||
- [ ] Проверить: `php -l src/Modules/<name>/<PascalName>Module.php`
|
||||
- [ ] Verify: `php console.php --list` shows the module's commands
|
||||
- [ ] Проверьте: удаление каталога модуля не приводит к фатальной ошибке
|
||||
|
||||
---
|
||||
|
||||
## часто задаваемые вопросы
|
||||
|
||||
|
||||
**Q: How do I disable a module?**
|
||||
В поле `src/config/modules.php` добавьте `'module-name' => ['state' => 'disabled']`.
|
||||
Устаревшая форма `'enabled' => false` также принята для обеспечения обратной совместимости.
|
||||
|
||||
**Q: How do I declare that my module depends on another?**
|
||||
Используйте `dependencies` в `module.json` для жестких удалений (должно присутствовать) или `optional_dependencies`
|
||||
для мягкого удаления (загружается раньше вашего, если присутствует, и автоматически пропускается, если отсутствует).
|
||||
|
||||
**Q: Can I decorate a core service?**
|
||||
Да — используйте `$container->decorate('service-id', callable, priority)` в `boot()`.
|
||||
Защищенные сервисы (`db`, `settings`, `config`, `auth`) не могут быть оформлены.
|
||||
|
||||
**Q: How do I listen to core events?**
|
||||
Вызовите `EventDispatcher::listen(EventClass::class, callable, priority)` в любом месте после начальной загрузки,
|
||||
обычно внутри `boot()` или выделенного класса подписчиков.
|
||||
|
||||
**Q: Can I dispatch custom events from a module?**
|
||||
Да. Создайте простой класс или расширьте `AbstractEvent` и вызовите `EventDispatcher::dispatch(new MyEvent(...))`.
|
||||
|
||||
**Q: What is `StreamMiddlewareProviderInterface` for?**
|
||||
Это позволяет модулю вводить значение `StreamMiddlewareInterface` в конвейер потоковой обработки
|
||||
без изменения `StreamProcess.php`. При необходимости реализуйте его вместе с `ModuleInterface`.
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|
||||
|Файл|Роль|
|
||||
| --- | --- |
|
||||
| `src/Core/Module/ModuleLoader.php` | Discovers, sorts and boots modules; PSR-4 class resolver |
|
||||
| `src/config/modules.php` |Конфигурация включения модуля / переопределения класса|
|
||||
| `src/Modules/` |Каталоги модулей|
|
||||
| `src/Core/Module/Contract/` |Подинтерфейсы модуля|
|
||||
@@ -0,0 +1,189 @@
|
||||
# Точки расширения модуля
|
||||
|
||||
Основные точки расширения, к которым подключается модуль: контейнер DI, потоковое промежуточное программное обеспечение, задачи cron, миграции версий и типизированные события. Чтобы создать модуль, смотрите [Создание модуля](module-authoring.md); для загрузки/жизненного цикла смотрите [Жизненный цикл модуля](module-lifecycle.md).
|
||||
|
||||
## Оформление контейнеров и сервизов DI
|
||||
|
||||
|
||||
Сервисы регистрируются в `boot()` через `ServiceContainer`. Контейнер поддерживает:
|
||||
|
||||
- **`set(id, factory)`** — отложенный синглтон с помощью вызываемого или прямого значения
|
||||
- **`factory(id, callable)`** — новый экземпляр для каждого `get()`
|
||||
- **`decorate(id, callable, priority)`** — завершение существующей службы
|
||||
|
||||
```php
|
||||
// Decorate a service (adds behaviour around the original)
|
||||
$container->decorate('stream.encoder', function (mixed $inner, ServiceContainer $c): MyEncoder {
|
||||
return new MyEncoder($inner, $c->get('settings'));
|
||||
}, priority: 20);
|
||||
```
|
||||
|
||||
Декораторы объединены в цепочки по приоритету (самый высокий и самый внешний). Защищенные сервисы
|
||||
(`db`, `settings`, `config`, `auth`) не удается оформить — любая попытка приводит к результату `RuntimeException`.
|
||||
|
||||
### Соответствие требованиям стандарта PSR-11
|
||||
|
||||
`ServiceContainer` реализует `ContainerInterface`:
|
||||
|
||||
```php
|
||||
public function get(string $id): mixed; // throws NotFoundException if missing
|
||||
public function has(string $id): bool;
|
||||
```
|
||||
|
||||
`NotFoundException` реализует `NotFoundExceptionInterface extends ContainerExceptionInterface`.
|
||||
|
||||
---
|
||||
|
||||
## События PSR-14
|
||||
|
||||
Модули подписываются на типизированные события с помощью атрибута `getEventSubscribers()` или `#[ListensTo]`. Это описано в полном объеме — диспетчеризация, регистрация слушателей, приоритеты, события, которые можно остановить, и встроенный каталог событий - на специальной странице [Система событий](event-system.md).
|
||||
|
||||
---
|
||||
|
||||
## Потоковое промежуточное программное обеспечение
|
||||
|
||||
|
||||
Модули могут внедрять промежуточное программное обеспечение в потоковый конвейер, реализуя
|
||||
`StreamMiddlewareProviderInterface` (отдельно от `ModuleInterface`):
|
||||
|
||||
```php
|
||||
class MyStreamMiddleware implements StreamMiddlewareInterface {
|
||||
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
|
||||
public function handle(StreamContext $ctx, callable $next): StreamContext {
|
||||
// before — read or set attributes
|
||||
$ctx->set('my.key', 'value');
|
||||
$ctx = $next($ctx);
|
||||
// after
|
||||
return $ctx;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`StreamContext` - это набор атрибутов (`get`, `set`, `has`, `abort`, `isAborted`). `StreamPipeline`
|
||||
выполняет промежуточное программное обеспечение, отсортированное по убыванию `getPriority()`.
|
||||
|
||||
### Приоритеты трубопровода
|
||||
|
||||
|Диапазон|Владелец|
|
||||
| ---------- | ----------------- |
|
||||
| `80–100` |Ядро (авторизация, разрешение, ограничение подключения)|
|
||||
| `0–79` |Модули|
|
||||
|
||||
### Зарезервированные слоты на панели навигации
|
||||
|
||||
|Родительский узел|Гнезда для модулей|
|
||||
| ------------------- | ------------------ |
|
||||
| `management.service_setup` |`order` ≥ 60|
|
||||
| `management.logs` |`order` ≥ 170|
|
||||
|
||||
---
|
||||
|
||||
## Задача Cron
|
||||
|
||||
|
||||
**Логика Cron** (`MyCron.php`) — только бизнес-логика, без подключения к интерфейсу командной строки.
|
||||
|
||||
**Обертка от CronJob** (`MyCronJob.php`) — реализует `CommandInterface`, использует `CronTrait`:
|
||||
|
||||
```php
|
||||
class MyCronJob implements CommandInterface {
|
||||
use CronTrait;
|
||||
|
||||
public function getName(): string { return 'cron:my_task'; }
|
||||
public function getDescription(): string { return 'Cron: my task'; }
|
||||
|
||||
public function execute(array $rArgs): int {
|
||||
if (!$this->assertRunAsXcVm()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
require INCLUDES_PATH . 'admin.php';
|
||||
require_once __DIR__ . '/MyCron.php';
|
||||
|
||||
$this->initCron('XC_VM[MyTask]');
|
||||
MyCron::run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Регистрация в модуле:
|
||||
|
||||
```php
|
||||
public function registerCommands(CommandRegistry $registry): void {
|
||||
$registry->register(new MyCronJob());
|
||||
}
|
||||
```
|
||||
|
||||
Объявите запись crontab, переопределив `getCronEntries()` в классе module:
|
||||
|
||||
```php
|
||||
public function getCronEntries(): array {
|
||||
return [
|
||||
'*/5 * * * *' => 'cron:my_task',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleLoader::collectCronEntries()` объединяет записи всех модулей и `StartupCommand` /
|
||||
`StatusCommand` автоматически записывайте их в системный crontab — никаких изменений в основных файлах не требуется.
|
||||
|
||||
**Формат:** ключ = выражение cron, значение = имя консольной команды, зарегистрированное с помощью `registerCommands()`.
|
||||
|
||||
---
|
||||
|
||||
## Версионные миграции (MigratableInterface)
|
||||
|
||||
|
||||
> **Два механизма, оба аддитивные.** **файловая схема**, описанный в разделе
|
||||
> [Структура каталогов модулей](module-authoring.md#module-directory-structure) (`database.sql` мастер +
|
||||
> `database_drop.sql` разборка + `migrations/<semver>.sql` дельты) используется по умолчанию для
|
||||
> обычный DDL/seed. `MigratableInterface` ниже приведен **программный** путь для обновления
|
||||
> шаги, требующие логики PHP (повторное заполнение данных, условные изменения). Модуль может использовать
|
||||
> один из них или оба; `ModuleManager::updateModule()` сначала запускает файл delta, затем
|
||||
> вызываемые миграции.
|
||||
|
||||
Модули, для обновления которых требуется PHP логическая реализация `MigratableInterface`:
|
||||
|
||||
```php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use MigratableInterface;
|
||||
use ServiceContainer;
|
||||
|
||||
class MyModuleModule extends BaseModule implements MigratableInterface {
|
||||
|
||||
public function getMigrations(): array {
|
||||
return [
|
||||
'1.1.0' => function (): void {
|
||||
// runs when upgrading from any version < 1.1.0 to >= 1.1.0
|
||||
global $db;
|
||||
$db->query("ALTER TABLE xc_my_table ADD COLUMN new_col INT DEFAULT 0");
|
||||
},
|
||||
'1.2.0' => function (): void {
|
||||
// runs when upgrading from < 1.2.0 to >= 1.2.0
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleManager::updateModule()` считывает `installed_version` из хранилища переопределений, фильтрует
|
||||
сопоставляет только записи `> fromVersion && <= toVersion`, сортирует по полу и запускает каждую из них.
|
||||
вызываемый в своей собственной транзакции базы данных. `installModule()` записи `installed_version` после
|
||||
успешная установка; `uninstallModule()` удаляет ее.
|
||||
|
||||
**Key rules:**
|
||||
|
||||
- Ключи - это полустрочные строки (`'1.1.0'`, `'2.0.0'`) — `version_compare` используется упорядочение
|
||||
- Каждая миграция выполняется в рамках своей собственной транзакции — сбой откатывает только этот шаг
|
||||
- `BaseModule` предоставляет значение по умолчанию `getMigrations(): array { return []; }`, поэтому реализация
|
||||
`MigratableInterface` является необязательным
|
||||
|
||||
---
|
||||
@@ -0,0 +1,141 @@
|
||||
# Жизненный цикл модуля
|
||||
|
||||
Как XC_VM обнаруживает, загружает, включает/отключает, устанавливает и распространяет модули во время выполнения. Чтобы создать модуль, смотрите [Разработка модуля](module-authoring.md); для его расширений смотрите [Точки расширения модуля](module-extension-points.md).
|
||||
|
||||
## Включение / выключение модулей
|
||||
|
||||
|
||||
Все обнаруженные модули загружаются по умолчанию. Используйте `src/config/modules.php` для переопределения состояния:
|
||||
|
||||
```php
|
||||
return [
|
||||
'my-module' => ['state' => 'disabled'], // preferred
|
||||
// or legacy boolean (still accepted):
|
||||
'my-module' => ['enabled' => false],
|
||||
];
|
||||
```
|
||||
|
||||
Доступные значения `state` (подкрепленные перечислением `ModuleState`):
|
||||
|
||||
|Ценность|Значение|
|
||||
| ----- | ------- |
|
||||
| `enabled` |Загрузка модуля (по умолчанию)|
|
||||
| `disabled` |Модуль обнаружен, но пропущен|
|
||||
| `installing` |Переходное состояние, заданное значением `ModuleManager` во время установки|
|
||||
| `failed` |Ошибка установки; модуль пропущен (не загружен)|
|
||||
|
||||
> **Панельная диагностика.** На странице **Модули** отображается желтый значок **⚠ Проблема зависимости** рядом со статусом модуля, если требуемая зависимость отсутствует или не включена (например, в `plex` указано `Enabled`, а в `watch` - `failed`). Во всплывающей подсказке к значку перечислены конкретные проблемы. Это поле `dependency_warnings` вычисляется с помощью `ModuleManager::listModules()`.
|
||||
|
||||
Чтобы переопределить класс, разрешенный для модуля:
|
||||
|
||||
```php
|
||||
return [
|
||||
'my-module' => ['class' => 'XcVm\\Module\\MyModuleV2\\MyModuleV2Module'],
|
||||
];
|
||||
```
|
||||
|
||||
`config/modules.php` содержит только переопределения. Пустой или отсутствующий файл означает, что все обнаруженные
|
||||
загружаются модули.
|
||||
|
||||
---
|
||||
|
||||
## Как работает загрузка
|
||||
|
||||
|
||||
`ModuleLoader` выполняет эти действия при каждом запросе:
|
||||
|
||||
1. Сканирование `src/Modules/*/module.json`
|
||||
2. Применяет переопределения из `config/modules.php`
|
||||
3. Фильтры по окружающей среде (`main` / `lb` / `any`)
|
||||
4. Определяет порядок загрузки:
|
||||
- `pruneUnsatisfiableModules()` удаляет модули, требуемые зависимости которых недоступны (каскадно, с зарегистрированным предупреждением), чтобы загрузка никогда не прерывалась
|
||||
- Топологическая сортировка (DFS) по графу зависимостей
|
||||
- В пределах одной и той же группы зависимостей выполните сортировку по убыванию `priority`, затем по алфавиту
|
||||
- Выдает `ModuleCycleException` для циклов (подкласс `\RuntimeException`; циклические зависимости остаются фатальными)
|
||||
- Отсутствующие необязательные зависимости автоматически пропускаются
|
||||
5. Resolves class name: `my-module` → FQN `XcVm\Module\MyModule\MyModuleModule`
|
||||
(kebab-case → PascalCase; может быть переопределен с помощью клавиши `class` в конфигурации)
|
||||
6. Регистрирует автозагрузчик модуля PSR-4 (сопоставляет `XcVm\Module\<Name>` с каталогом модуля)
|
||||
7. Создает экземпляр класса module
|
||||
|
||||
В веб-контексте:
|
||||
|
||||
- `bootAll($container, $router)` → вызовы `boot()`, `registerRoutes()`, `registerNavbar()`,
|
||||
и подписывается на события для каждого загруженного модуля
|
||||
|
||||
В контексте командной строки:
|
||||
|
||||
- `registerAllCommands($registry)` → вызывает `registerCommands()` для каждого загруженного модуля
|
||||
|
||||
---
|
||||
|
||||
## Marketplace: установка через расширение C
|
||||
|
||||
|
||||
Модули с платформы устанавливаются через `ModuleManager::downloadFromPlatform()`:
|
||||
|
||||
```php
|
||||
$manager->downloadFromPlatform(slug: 'my-module', version: '1.2.0', apiKey: $key);
|
||||
```
|
||||
|
||||
Под капотом:
|
||||
|
||||
1. `XC_VM::module_install($slug, $version, $apiKey)` — Расширение C загружает, расшифровывает и распаковывает файлы
|
||||
2. `installModule($slug)` — запускает `install()` в модуле
|
||||
3. `EventDispatcher::dispatch(new PackageInstalledEvent(...))` — отправляет событие
|
||||
4. `hotReload($slug, $path)` — загружает модуль в текущем запросе **без перезапуска PHP-FPM**
|
||||
|
||||
---
|
||||
|
||||
## Изолированные подсистемы
|
||||
|
||||
|
||||
Модуль может быть полностью изолированной подсистемой со своей собственной точкой входа и начальной загрузкой
|
||||
(например, Ministra). Это **соглашение**, а не маркерный интерфейс — он остается
|
||||
обычный модуль `ModuleInterface`/`BaseModule`:
|
||||
|
||||
```php
|
||||
class MyModule extends BaseModule {
|
||||
|
||||
public function getName(): string {
|
||||
return 'my-module';
|
||||
}
|
||||
|
||||
public function getVersion(): string {
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Изоляция означает, что подсистема работает через свою собственную общедоступную точку входа (например,
|
||||
`my-module/portal.php`, путь относительно `src/`, который обрабатывает свой собственный bootstrap)
|
||||
с отдельным путем начальной загрузки. Он использует общую инфраструктуру (базу данных, кэш, конфигурацию), но
|
||||
участвует ли **нет** в основных `Router`, `ModuleLoader::bootAll()` или
|
||||
`NavbarRegistry`. Реализации `boot()` и `registerRoutes()` обычно являются
|
||||
оставлено как унаследованное бездействие.
|
||||
|
||||
---
|
||||
|
||||
## Composer обнаружение пакета
|
||||
|
||||
|
||||
Модули могут распространяться в виде Composer пакетов с `"type": "xcvm-module"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "vendor/my-xcvm-module",
|
||||
"type": "xcvm-module",
|
||||
"extra": {
|
||||
"xcvm": {
|
||||
"module-path": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleLoader` автоматически сканирует `vendor/composer/installed.json` (Composer 1 и 2
|
||||
форматирует) и обнаруживает все установленные пакеты `xcvm-module` вместе со встроенным
|
||||
каталог `src/Modules/`. Пакеты дедуплицируются — модуль как в `modules/`, так и в
|
||||
`vendor/` загружается только один раз.
|
||||
|
||||
---
|
||||
@@ -1,766 +0,0 @@
|
||||
# Модульная система
|
||||
|
||||
## Обзор
|
||||
|
||||
Модуль - это изолированный каталог под `src/Modules/` с известным контрактом. Система
|
||||
построен на принципах **Расширяемой платформы**:
|
||||
|
||||
- Ядро (`Core/`) ничего не знает о модулях
|
||||
- Модули могут зависеть от `Core/` и `Domain/`, но никогда друг от друга (кроме как через объявленные зависимости).
|
||||
- Любой модуль можно отключить из `config/modules.php`, не прикасаясь к ядру
|
||||
- Удаление каталога модуля не приводит к фатальным ошибкам
|
||||
|
||||
---
|
||||
|
||||
## Структура каталогов модулей
|
||||
|
||||
The directory name follows the **`{name}_{hash5}`** convention, where `hash5` is the
|
||||
первые 5 символов модуля `hash_id`. Логическое имя модуля (`module.json`
|
||||
`name`, который никогда не содержит `_`) всегда разрешается из манифеста — никогда из
|
||||
базовое имя каталога. Это позволяет двум модулям с **одинаковым именем** работать в разных
|
||||
каталоги (`watch_2541a`, `watch_9f1c0`) и установите их без столкновения с файловой системой. То
|
||||
конфигурация, график зависимостей и пространство имен - все это не соответствует каноническому `name`, поэтому каталог
|
||||
переименование не требует переноса данных. Каждый модуль **должен ** иметь `hash_id`: загружаемые файлы, которые отправляются
|
||||
без такового получите новый идентификатор, сгенерированный и записанный в их `module.json` перед размещением,
|
||||
таким образом, каталог без хэша никогда не создается. Устаревший пустой каталог `Modules/{name}/` из
|
||||
более старое развертывание по-прежнему считывается, но оно ** автоматически переносится** в `{name}_{hash5}` (генерируя
|
||||
`hash_id`, если отсутствует) при следующем `console.php status` — макет без хэша удаляется, а не
|
||||
держал.
|
||||
|
||||
```text
|
||||
src/Modules/my-module_9f1c0/ # {name}_{hash5}; canonical name is "my-module"
|
||||
├── module.json # Metadata and manifest
|
||||
├── MyModule.php # Module class (source of truth)
|
||||
├── MyService.php # Business logic
|
||||
├── MyController.php # Admin pages (optional)
|
||||
├── MyCron.php # Cron logic (optional)
|
||||
├── MyCronJob.php # CLI cron wrapper (optional)
|
||||
├── database.sql # Master schema — full current CREATE/seed (optional)
|
||||
├── database_drop.sql # Teardown — DROP every table the module owns (optional)
|
||||
├── migrations/ # Forward version deltas (optional)
|
||||
│ └── 1.1.0.sql # Applied only when upgrading a panel past 1.1.0
|
||||
└── views/ # Page templates (optional)
|
||||
├── my_page.php
|
||||
└── my_page_scripts.php
|
||||
```
|
||||
|
||||
Модуль владеет своей схемой через **три роли, которые отражают ядро** (`bin/install/database.sql`
|
||||
+ `migrations/`):
|
||||
|
||||
|Файл|Роль|Работает на|
|
||||
| ---- | ---- | ------- |
|
||||
| `database.sql` | **One** master schema — the full current `CREATE`/seed |новая **установка**|
|
||||
| `database_drop.sql` |**Одно** удаление — `DROP TABLE` для каждой таблицы, которой владеет модуль|**удалить**|
|
||||
| `migrations/<semver>.sql` |**Папка** прямых переходов между версиями|**обновить** для версий в `(installed, current]`|
|
||||
|
||||
Правила:
|
||||
|
||||
- **Новая установка выполняется только `database.sql`**, поэтому она всегда должна соответствовать последней версии.
|
||||
схема (каждая дельта загнута внутрь). Записанный `installed_version` является водяным знаком —
|
||||
ошибки никогда не воспроизводятся при новой установке.
|
||||
- **Дельты направлены только вперед** (`ALTER`/`INSERT`), названный `<semver>.sql` — демонтаж - это
|
||||
одинарный `database_drop.sql`, поэтому нет файлов для каждой версии `.down`.
|
||||
- Сохраняйте дельты ** идемпотентными** (`ADD COLUMN IF NOT EXISTS`, `INSERT IGNORE`), чтобы повторные запуски были безопасными.
|
||||
- Модуль без схемы не отправляет ни один из этих файлов. Модуль, работающий только с разницей (нет `database.sql`)
|
||||
по-прежнему устанавливается путем повторного воспроизведения каждой дельты в ее версии.
|
||||
|
||||
---
|
||||
|
||||
## модуль.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-module",
|
||||
"hash_id": "9f1c0b7e4d2a6538c1e0a4b7d6f39e21",
|
||||
"description": "Short description",
|
||||
"version": "1.0.0",
|
||||
"requires_core": ">=2.0",
|
||||
"environment": "main",
|
||||
"priority": 0,
|
||||
"dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"has_navbar": false,
|
||||
"has_settings": false
|
||||
}
|
||||
```
|
||||
|
||||
### Поля манифеста
|
||||
|
||||
|Поле|Тип|По умолчанию|Описание|
|
||||
| ------ | ----- | :---: | ------------ |
|
||||
| `name` | `string` |—|Каноническое имя модуля (в случае с kebab, без `_`). Каталог равен `{name}_{hash5}`, но код всегда использует это значение манифеста, а не базовое имя каталога.|
|
||||
| `hash_id` | `string` |сгенерированный|**Постоянный** идентификатор модуля — случайный 32-разрядный шестнадцатеричный код, генерируемый ОДИН раз и никогда не изменяющийся при изменении версии или переименовании. Его первые 5 символов образуют суффикс каталога `{name}_{hash5}`. Не редактируйте вручную.|
|
||||
| `description` | `string` | `""` |Удобочитаемое описание|
|
||||
| `version` | `string` |—|Средняя версия (`1.0.0`)|
|
||||
| `requires_core` | `string` |—|Минимальная версия ядра (`>=2.0`)|
|
||||
| `environment` | `string` | `"main"` |`main`, `lb` или `any`|
|
||||
| `priority` | `int` | `0` |Приоритет загрузки — более высокие нагрузки раньше|
|
||||
| `dependencies` | `array` | `[]` |Жесткие зависимости; если они недоступны, зависимый объект пропускается (см. ниже)|
|
||||
| `optional_dependencies` | `array` | `[]` |Мягкие зависимости (загруженные ранее, если они есть)|
|
||||
| `has_navbar` | `bool` | `false` |Регистрирует ли модуль элементы навигационной панели|
|
||||
| `has_settings` | `bool` | `false` |Есть ли у модуля страница настроек|
|
||||
|
||||
> **`hash_id` — постоянный идентификатор модуля.** Это случайное значение из 32 шестнадцатеричных чисел,
|
||||
> сгенерированный ** один раз ** и ** никогда** впоследствии не изменявшийся — он должен пережить изменения версий
|
||||
> и переименовывает (чтобы оно было случайным, а не производным от `name`/`version`). Сгенерируйте его с помощью
|
||||
> `php -r 'echo bin2hex(random_bytes(16));'` и вставьте его в `module.json`, когда
|
||||
> создайте новый модуль. Не создавайте его вручную и не используйте повторно другой модуль. Это придает модулям стабильную идентичность, независимую от `name`,
|
||||
> который является основой для перемещения модулей в отдельные репозитории и для создания
|
||||
> явный источник обновления для каждого модуля ** - блок манифеста `update` (смотрите ниже).
|
||||
|
||||
**Жесткие и мягкие зависимости:**
|
||||
|
||||
- `dependencies` — если какой—либо модуль недоступен (отсутствует на диске, отключен или находится в состоянии `failed`), зависимый модуль ** пропускается** с каскадным предупреждением в журнале (все, что зависит от него, также пропускается). Остальные модули, панель администратора и интерфейс командной строки продолжают работать; единственная неудовлетворенная зависимость больше не прерывает всю загрузку.
|
||||
- `optional_dependencies` — загружается перед этим модулем, если присутствует, автоматически пропускается, если отсутствует
|
||||
|
||||
> **Защита от смещения.** Модуль, от которого зависят все еще включенные модули, не может быть `disabled` передан через панель / `ModuleManager::setState()` - операция отклоняется вместе со списком зависимых объектов (зеркальное отображение защиты `uninstallModule()`). Это предотвращает переход в состояние "`plex` включено, но его зависимость `watch` отключена".
|
||||
|
||||
**Приоритет:**
|
||||
|
||||
- Сначала при топологической сортировке учитывается график зависимостей, затем в пределах той же группы выполняется сортировка по убыванию `priority` (большее число = загружено ранее), затем по алфавиту
|
||||
|
||||
**Источник обновления (блок `update`, необязательный):**
|
||||
|
||||
Откуда модуль получает свои обновления. Отсутствует → `bundled` (файлы отправляются вместе с панелью и обновляются вместе с ней).
|
||||
|
||||
```json
|
||||
"update": {
|
||||
"source": "bundled | platform | git | url",
|
||||
"repository": "https://github.com/Vateron-Media/xc_vm-module-watch",
|
||||
"channel": "stable",
|
||||
"slug": "watch",
|
||||
"url": "https://…/version.json"
|
||||
}
|
||||
```
|
||||
|
||||
- `source` — `bundled` (с панелью управления), `platform` (хранилище SaaS), `git` (репозитории), `url` (автономный хостинг). Неизвестные значения возвращаются к значению `bundled`.
|
||||
- `repository` — удаленный git (для `git`); `slug` — модуль хранилища (для `platform`, по умолчанию используется `name`); `url` — URL версии/архива (для `url`); `channel` — `stable`/`beta` (по умолчанию `stable`).
|
||||
|
||||
Блок нормализуется по `ModuleLoader` и выставляется через `ModuleManager::listModules()`. Еженедельный cron (`cron:module_updates`) проверяет источники `git`/`url` и записывает данные `available_version`, что приводит к нажатию кнопки обновления ** на X** (отображается только при наличии более новой версии). Нажатие кнопки обновления запускает `ModuleManager::updateModuleFromSource()`:
|
||||
|
||||
- `bundled` — файлы поступают вместе с панелью; Обновление просто запускает отложенные миграции.
|
||||
- `platform` — делегировано потоку установки/обновления в магазине (откат + разветвление LB внутри).
|
||||
- `git` — загружает ресурс выпуска **`module.tar.gz`** по тегу == новая версия (md5-проверяется с помощью выпуска `hashes.md5`, если присутствует).
|
||||
- `url` — перечитывает `version.json` для его `download` (https) + необязательно `md5`.
|
||||
|
||||
Для `git`/`url` выбранное значение `module.json` **`hash_id` должно совпадать с установленным значением ** (идентификация — репозиторий /URL-адрес не может выдавать себя за другой модуль), затем: резервное копирование → замена файлов → миграция → ** откат при любом сбое ** → распространение до фунта стерлингов.
|
||||
|
||||
**Стандартный набор и подготовка.** Модули, которые панель устанавливает по умолчанию, перечислены в `config/bundled_modules.php`, а их ключ - в `hash_id` (неизменен при переименовании). На сегодняшний день все модули имеют `bundled` (их файлы находятся в архиве панели). Когда модуль извлекается в свой собственный репозиторий, измените его запись на `git`/`url`/`platform` source — `syncBundledModules()`, затем автоматически извлекает и устанавливает его с помощью `provisionStandardSet()` (это не требуется, пока все находится в комплекте на диске). `ModuleManager::findModuleByHashId()` определяет модуль по его стабильному идентификатору независимо от каталога/имени.
|
||||
|
||||
---
|
||||
|
||||
## Подинтерфейсы
|
||||
|
||||
`ModuleInterface` разбивает площадь поверхности модуля на типизированные субдоговоры:
|
||||
|
||||
```text
|
||||
ModuleInterface
|
||||
├── ServiceProviderInterface → boot(ServiceContainer)
|
||||
├── RouteProviderInterface → registerRoutes(Router)
|
||||
├── CommandProviderInterface → registerCommands(CommandRegistry)
|
||||
└── NavbarProviderInterface → registerNavbar()
|
||||
```
|
||||
|
||||
`StreamMiddlewareProviderInterface` является **необязательным** — он не является частью `ModuleInterface`.
|
||||
Реализуйте это только в том случае, если модулю необходимо внедрить себя в потоковый конвейер.
|
||||
|
||||
```php
|
||||
// Optional — not in ModuleInterface
|
||||
class MyModule implements ModuleInterface, StreamMiddlewareProviderInterface {
|
||||
public function getStreamMiddleware(): array {
|
||||
return [new MyStreamMiddleware()];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Класс модуля
|
||||
|
||||
Extend `BaseModule` — он предоставляет значения по умолчанию без операций для каждого необязательного метода, так что вы можете использовать только
|
||||
переопределите то, что на самом деле использует модуль. Требуются только `getName()` и `getVersion()`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use ServiceContainer;
|
||||
use Router;
|
||||
use CommandRegistry;
|
||||
use NavbarRegistry;
|
||||
use NavbarItem;
|
||||
|
||||
class MyModuleModule extends BaseModule {
|
||||
|
||||
public function getName(): string {
|
||||
return 'my-module';
|
||||
}
|
||||
|
||||
public function getVersion(): string {
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function boot(ServiceContainer $container): void {
|
||||
$container->set('my-module.service', function (ServiceContainer $c): MyModuleService {
|
||||
return new MyModuleService($c->get('db'));
|
||||
});
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router): void {
|
||||
$router->get('my_page', [MyModuleController::class, 'index'], [
|
||||
'permission' => ['adv', 'my_module'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function registerCommands(CommandRegistry $registry): void {
|
||||
$registry->register(new MyModuleCronJob());
|
||||
}
|
||||
|
||||
public function registerNavbar(): void {
|
||||
NavbarRegistry::add(
|
||||
(new NavbarItem('management.service_setup.my_module'))
|
||||
->parent('management.service_setup')
|
||||
->url('my_page')
|
||||
->label('my_module')
|
||||
->permissions(['my_module'])
|
||||
->order(60)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> ** Совет:** модулю без маршрутов, элементов навигационной панели и команд CLI требуется только
|
||||
> `getName()`, `getVersion()` и `boot()`.
|
||||
> Модуль изолированной подсистемы (его собственная точка входа и bootstrap, например Ministra) обычно
|
||||
> оставляет `boot()` и `registerRoutes()` унаследованными как не выполняемые операции.
|
||||
|
||||
### Метод контракта
|
||||
|
||||
|Метод|Интерфейс|Описание|
|
||||
| ------- | ----------- | ---------- |
|
||||
| `getName(): string` | `ModuleInterface` |Уникальное имя (соответствует каталогу)|
|
||||
| `getVersion(): string` | `ModuleInterface` |Версия Semver|
|
||||
| `boot(ServiceContainer)` | `ServiceProviderInterface` |Регистрация сервисов в контейнере DI|
|
||||
| `registerRoutes(Router)` | `RouteProviderInterface` |Регистрация HTTP- и API-маршрутов|
|
||||
| `registerCommands(CommandRegistry)` | `CommandProviderInterface` |Регистрация команд CLI и задач cron|
|
||||
| `registerNavbar()` | `NavbarProviderInterface` |Регистрация элементов навигационной панели|
|
||||
| `install(): void` | `ModuleInterface` |Запуск при установке модуля (миграции, начальный запуск)|
|
||||
| `uninstall(): void` | `ModuleInterface` |Запуск при удалении модуля (очистка)|
|
||||
|
||||
> **Важно — версия может храниться в двух местах.** Модуль объявляет свою версию
|
||||
> **дважды**: поле `"version"` в `module.json` и возвращаемое значение
|
||||
> `getVersion()` в классе module. **Сохраняйте их идентичными и изменяйте оба перед
|
||||
> издательский.** Во время выполнения манифест `version` имеет приоритет — установка/обновление
|
||||
> и водяной знак `installed_version` сначала читается как `module.json`, и только потом возвращается
|
||||
> to `getVersion()` — so a stale `getVersion()` silently drifts out of sync and is a
|
||||
> распространенный источник ошибок типа "выполнена /не выполнена неправильная миграция". Если модуль отправляет файл
|
||||
> migrations, `database.sql` (master schema) and the highest `migrations/<semver>.sql`
|
||||
> дельта также должна соответствовать этой версии.
|
||||
|
||||
---
|
||||
|
||||
## PHP пространства имен
|
||||
|
||||
Каждый модуль находится в выделенном пространстве имен PHP: _BOS_0}, где _BOS_1} - это
|
||||
преобразование имени каталога модуля в PascalCase.
|
||||
|
||||
```
|
||||
src/Modules/my-module/ → namespace XcVm\Module\MyModule;
|
||||
src/Modules/watch/ → namespace XcVm\Module\Watch;
|
||||
```
|
||||
|
||||
В главном файле модуля должно быть объявлено это пространство имен и расширено `BaseModule`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use ServiceContainer;
|
||||
use Router;
|
||||
|
||||
class MyModuleModule extends BaseModule {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Все дополнительные классы в одном модуле используют одно и то же пространство имен:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
class MyModuleService { /* ... */ }
|
||||
class MyModuleController { /* ... */ }
|
||||
class MyModuleCronJob { /* ... */ }
|
||||
```
|
||||
|
||||
`use` классы, на которые вы ссылаетесь:
|
||||
|
||||
```php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use ServiceContainer;
|
||||
use NavbarRegistry;
|
||||
use NavbarItem;
|
||||
|
||||
class MyModuleModule extends BaseModule {
|
||||
public function boot(ServiceContainer $container): void {
|
||||
$container->set('my-module.service', fn () => new MyModuleService());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Правила:**
|
||||
|
||||
- Имя файла основного класса модуля: `<PascalName>Module.php` — обязательно (соглашение с загрузчиком модулей)
|
||||
- All other class filenames: `<PascalName><Purpose>.php`
|
||||
- Добавьте `use ClassName;` для каждого базового класса, на который ссылается ссылка (базовый модуль, ServiceContainer, маршрутизатор и т.д.)
|
||||
- Никогда не импортируйте классы из других модулей — общайтесь через события или контейнер DI
|
||||
|
||||
---
|
||||
|
||||
## Оформление контейнеров и сервизов DI
|
||||
|
||||
Сервисы регистрируются в `boot()` через `ServiceContainer`. Контейнер поддерживает:
|
||||
|
||||
- **`set(id, factory)`** — отложенный синглтон с помощью вызываемого или прямого значения
|
||||
- **`factory(id, callable)`** — новый экземпляр для каждого `get()`
|
||||
- **`decorate(id, callable, priority)`** — завершение существующей службы
|
||||
|
||||
```php
|
||||
// Decorate a service (adds behaviour around the original)
|
||||
$container->decorate('stream.encoder', function (mixed $inner, ServiceContainer $c): MyEncoder {
|
||||
return new MyEncoder($inner, $c->get('settings'));
|
||||
}, priority: 20);
|
||||
```
|
||||
|
||||
Декораторы объединены в цепочки по приоритету (самый высокий и самый внешний). Защищенные сервисы
|
||||
(`db`, `settings`, `config`, `auth`) не удается оформить — любая попытка приводит к результату `RuntimeException`.
|
||||
|
||||
### Соответствие требованиям стандарта PSR-11
|
||||
|
||||
`ServiceContainer` реализует `ContainerInterface`:
|
||||
|
||||
```php
|
||||
public function get(string $id): mixed; // throws NotFoundException if missing
|
||||
public function has(string $id): bool;
|
||||
```
|
||||
|
||||
`NotFoundException` реализует `NotFoundExceptionInterface extends ContainerExceptionInterface`.
|
||||
|
||||
---
|
||||
|
||||
## События PSR-14
|
||||
|
||||
События - это простые классы PHP. Отправляйте их через `EventDispatcher`:
|
||||
|
||||
```php
|
||||
// In any module
|
||||
EventDispatcher::dispatch(new MyEvent($payload));
|
||||
|
||||
// Subscribe
|
||||
EventDispatcher::listen(MyEvent::class, function (MyEvent $e): void {
|
||||
// handle
|
||||
}, priority: 10);
|
||||
```
|
||||
|
||||
**Приоритет** — более высокое целое число = вызывается первым. По умолчанию `0`.
|
||||
|
||||
**Останавливаемые события** — продлить `AbstractEvent` и вызвать `$e->stopPropagation()`:
|
||||
|
||||
```php
|
||||
class MyGatingEvent extends AbstractEvent {
|
||||
public bool $allowed = true;
|
||||
}
|
||||
|
||||
EventDispatcher::listen(MyGatingEvent::class, function (MyGatingEvent $e): void {
|
||||
if (!$this->check()) {
|
||||
$e->allowed = false;
|
||||
$e->stopPropagation();
|
||||
}
|
||||
}, priority: 100);
|
||||
```
|
||||
|
||||
### Встроенные основные события
|
||||
|
||||
|Класс события|Когда отправлено|Останавливаемый|
|
||||
| --------------- | ---------------------- | :-----------: |
|
||||
| `ModuleLoadedEvent` |После загрузки файла модуля|❌|
|
||||
| `ModuleBootedEvent` |После вызова `boot()`|❌|
|
||||
| `PackageInstalledEvent` |После установки marketplace|❌|
|
||||
| `UserAuthenticatedEvent` |После успешного входа в систему|✅|
|
||||
| `UserLoggedOutEvent` |После выхода из системы|❌|
|
||||
| `StreamStartingEvent` |Перед началом трансляции|✅|
|
||||
| `StreamStartedEvent` |После начала трансляции|❌|
|
||||
| `StreamStoppedEvent` |После того, как поток прекратился|❌|
|
||||
| `SettingsChangedEvent` |После сохранения настроек|❌|
|
||||
|
||||
---
|
||||
|
||||
## Потоковое промежуточное программное обеспечение
|
||||
|
||||
Модули могут внедрять промежуточное программное обеспечение в потоковый конвейер, реализуя
|
||||
`StreamMiddlewareProviderInterface` (отдельно от `ModuleInterface`):
|
||||
|
||||
```php
|
||||
class MyStreamMiddleware implements StreamMiddlewareInterface {
|
||||
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
|
||||
public function handle(StreamContext $ctx, callable $next): StreamContext {
|
||||
// before — read or set attributes
|
||||
$ctx->set('my.key', 'value');
|
||||
$ctx = $next($ctx);
|
||||
// after
|
||||
return $ctx;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`StreamContext` - это набор атрибутов (`get`, `set`, `has`, `abort`, `isAborted`). `StreamPipeline`
|
||||
выполняет промежуточное программное обеспечение, отсортированное по убыванию `getPriority()`.
|
||||
|
||||
### Приоритеты трубопровода
|
||||
|
||||
|Диапазон|Владелец|
|
||||
| ---------- | ----------------- |
|
||||
| `80–100` |Ядро (авторизация, разрешение, ограничение подключения)|
|
||||
| `0–79` |Модули|
|
||||
|
||||
### Зарезервированные слоты на панели навигации
|
||||
|
||||
|Родительский узел|Гнезда для модулей|
|
||||
| ------------------- | ------------------ |
|
||||
| `management.service_setup` |`order` ≥ 60|
|
||||
| `management.logs` |`order` ≥ 170|
|
||||
|
||||
---
|
||||
|
||||
## Включение / выключение модулей
|
||||
|
||||
Все обнаруженные модули загружаются по умолчанию. Используйте `src/config/modules.php` для переопределения состояния:
|
||||
|
||||
```php
|
||||
return [
|
||||
'my-module' => ['state' => 'disabled'], // preferred
|
||||
// or legacy boolean (still accepted):
|
||||
'my-module' => ['enabled' => false],
|
||||
];
|
||||
```
|
||||
|
||||
Доступные значения `state` (подкрепленные перечислением `ModuleState`):
|
||||
|
||||
|Ценность|Значение|
|
||||
| ----- | ------- |
|
||||
| `enabled` |Загрузка модуля (по умолчанию)|
|
||||
| `disabled` |Модуль обнаружен, но пропущен|
|
||||
| `installing` |Переходное состояние, заданное значением `ModuleManager` во время установки|
|
||||
| `failed` |Ошибка установки; модуль пропущен (не загружен)|
|
||||
|
||||
> **Диагностика панели.** На странице "Модули" отображается желтый значок "Проблема с зависимостями" рядом со статусом модуля, когда требуемая зависимость отсутствует или не включена (например, `plex` означает `Enabled`, а `watch` - `failed`). Во всплывающей подсказке к значку перечислены конкретные проблемы. Это поле `dependency_warnings` вычисляется с помощью `ModuleManager::listModules()`.
|
||||
|
||||
Чтобы переопределить класс, разрешенный для модуля:
|
||||
|
||||
```php
|
||||
return [
|
||||
'my-module' => ['class' => 'XcVm\\Module\\MyModuleV2\\MyModuleV2Module'],
|
||||
];
|
||||
```
|
||||
|
||||
`config/modules.php` содержит только переопределения. Пустой или отсутствующий файл означает, что все обнаруженные
|
||||
загружаются модули.
|
||||
|
||||
---
|
||||
|
||||
## Как работает загрузка
|
||||
|
||||
`ModuleLoader` выполняет эти действия при каждом запросе:
|
||||
|
||||
1. Сканирование `src/Modules/*/module.json`
|
||||
2. Применяет переопределения из `config/modules.php`
|
||||
3. Фильтры по окружающей среде (`main` / `lb` / `any`)
|
||||
4. Определяет порядок загрузки:
|
||||
- `pruneUnsatisfiableModules()` удаляет модули, требуемые зависимости которых недоступны (каскадно, с зарегистрированным предупреждением), чтобы загрузка никогда не прерывалась
|
||||
- Топологическая сортировка (DFS) по графу зависимостей
|
||||
- В пределах одной и той же группы зависимостей выполните сортировку по убыванию `priority`, затем по алфавиту
|
||||
- Выдает `RuntimeException` для циклов (циклические зависимости остаются фатальными)
|
||||
- Отсутствующие необязательные зависимости автоматически пропускаются
|
||||
5. Resolves class name: `my-module` → FQN `XcVm\Module\MyModule\MyModuleModule`
|
||||
(kebab-case → PascalCase; может быть переопределен с помощью клавиши `class` в конфигурации)
|
||||
6. Регистрирует автозагрузчик модуля PSR-4 (сопоставляет `XcVm\Module\<Name>` с каталогом модуля)
|
||||
7. Создает экземпляр класса module
|
||||
|
||||
В веб-контексте:
|
||||
|
||||
- `bootAll($container, $router)` → вызовы `boot()`, `registerRoutes()`, `registerNavbar()`,
|
||||
и подписывается на события для каждого загруженного модуля
|
||||
|
||||
В контексте командной строки:
|
||||
|
||||
- `registerAllCommands($registry)` → вызывает `registerCommands()` для каждого загруженного модуля
|
||||
|
||||
---
|
||||
|
||||
## Marketplace: установка через расширение C
|
||||
|
||||
Модули с платформы устанавливаются через `ModuleManager::downloadFromPlatform()`:
|
||||
|
||||
```php
|
||||
$manager->downloadFromPlatform(slug: 'my-module', version: '1.2.0', apiKey: $key);
|
||||
```
|
||||
|
||||
Под капотом:
|
||||
|
||||
1. `XC_VM::module_install($slug, $version, $apiKey)` — Расширение C загружает, расшифровывает и распаковывает файлы
|
||||
2. `installModule($slug)` — запускает `install()` в модуле
|
||||
3. `EventDispatcher::dispatch(new PackageInstalledEvent(...))` — отправляет событие
|
||||
4. `hotReload($slug, $path)` — загружает модуль в текущем запросе **без перезапуска PHP-FPM.**
|
||||
|
||||
---
|
||||
|
||||
## Изолированные подсистемы
|
||||
|
||||
Модуль может быть полностью изолированной подсистемой со своей собственной точкой входа и начальной загрузкой
|
||||
(например, Ministra). Это ** соглашение**, а не маркерный интерфейс — он остается
|
||||
обычный модуль `ModuleInterface`/`BaseModule`:
|
||||
|
||||
```php
|
||||
class MyModule extends BaseModule {
|
||||
|
||||
public function getName(): string {
|
||||
return 'my-module';
|
||||
}
|
||||
|
||||
public function getVersion(): string {
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Изоляция означает, что подсистема работает через свою собственную общедоступную точку входа (например,
|
||||
`my-module/portal.php`, путь относительно `src/`, который обрабатывает свой собственный bootstrap)
|
||||
с отдельным путем начальной загрузки. Он использует общую инфраструктуру (базу данных, кэш, конфигурацию), но
|
||||
**не** участвует в основных `Router`, `ModuleLoader::bootAll()` или
|
||||
`NavbarRegistry`. Реализации `boot()` и `registerRoutes()` обычно являются
|
||||
оставлено как унаследованное бездействие.
|
||||
|
||||
---
|
||||
|
||||
## Контроллер
|
||||
|
||||
```php
|
||||
class MyController {
|
||||
|
||||
protected string $viewsPath;
|
||||
|
||||
public function __construct() {
|
||||
$this->viewsPath = __DIR__ . '/views';
|
||||
require_once MAIN_HOME . 'Public/Views/layouts/admin.php';
|
||||
require_once MAIN_HOME . 'Public/Views/layouts/footer.php';
|
||||
}
|
||||
|
||||
public function index(): void {
|
||||
renderUnifiedLayoutHeader('admin', ['_TITLE' => 'My Module']);
|
||||
include $this->viewsPath . '/my_page.php';
|
||||
renderUnifiedLayoutFooter('admin');
|
||||
include $this->viewsPath . '/my_page_scripts.php';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|Правило| |
|
||||
| --------- | -- |
|
||||
| `__DIR__ . '/views'` |viewsPath — контроллер находится внутри каталога модуля|
|
||||
|ПОЛУЧАТЬ страницы|вызовите `renderUnifiedLayoutHeader` перед просмотром, `renderUnifiedLayoutFooter` после|
|
||||
|Действия API|нет макета — возвращаем JSON и выходим|
|
||||
|
||||
---
|
||||
|
||||
## Задача Cron
|
||||
|
||||
**Cron logic** (`MyCron.php`) — только бизнес-логика, без подключения к CLI.
|
||||
|
||||
**Оболочка CronJob** (`MyCronJob.php`) — реализует `CommandInterface`, использует `CronTrait`:
|
||||
|
||||
```php
|
||||
class MyCronJob implements CommandInterface {
|
||||
use CronTrait;
|
||||
|
||||
public function getName(): string { return 'cron:my_task'; }
|
||||
public function getDescription(): string { return 'Cron: my task'; }
|
||||
|
||||
public function execute(array $rArgs): int {
|
||||
if (!$this->assertRunAsXcVm()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
require INCLUDES_PATH . 'admin.php';
|
||||
require_once __DIR__ . '/MyCron.php';
|
||||
|
||||
$this->initCron('XC_VM[MyTask]');
|
||||
MyCron::run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Регистрация в модуле:
|
||||
|
||||
```php
|
||||
public function registerCommands(CommandRegistry $registry): void {
|
||||
$registry->register(new MyCronJob());
|
||||
}
|
||||
```
|
||||
|
||||
Объявите запись crontab, переопределив `getCronEntries()` в классе module:
|
||||
|
||||
```php
|
||||
public function getCronEntries(): array {
|
||||
return [
|
||||
'*/5 * * * *' => 'cron:my_task',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleLoader::collectCronEntries()` объединяет записи всех модулей и `StartupCommand` /
|
||||
`StatusCommand` автоматически записывайте их в системный crontab — никаких изменений в основных файлах не требуется.
|
||||
|
||||
**Формат:** ключ = выражение cron, значение = имя консольной команды, зарегистрированное через `registerCommands()`.
|
||||
|
||||
---
|
||||
|
||||
## Версионные миграции (MigratableInterface)
|
||||
|
||||
> **Два механизма, оба аддитивные.** Схема на основе файлов**, описанная в разделе
|
||||
> [Структура каталогов модулей](#module-directory-structure) (`database.sql` мастер +
|
||||
> `database_drop.sql` разборка + `migrations/<semver>.sql` дельты) используется по умолчанию для
|
||||
> простой DDL/seed. `MigratableInterface` ниже приведен программный путь для обновления.
|
||||
> шаги, требующие логики PHP (повторное заполнение данных, условные изменения). Модуль может использовать
|
||||
> один из них или оба; `ModuleManager::updateModule()` сначала запускает файл delta, затем
|
||||
> вызываемые миграции.
|
||||
|
||||
Модули, для обновления которых требуется PHP логическая реализация `MigratableInterface`:
|
||||
|
||||
```php
|
||||
namespace XcVm\Module\MyModule;
|
||||
|
||||
use BaseModule;
|
||||
use MigratableInterface;
|
||||
use ServiceContainer;
|
||||
|
||||
class MyModuleModule extends BaseModule implements MigratableInterface {
|
||||
|
||||
public function getMigrations(): array {
|
||||
return [
|
||||
'1.1.0' => function (): void {
|
||||
// runs when upgrading from any version < 1.1.0 to >= 1.1.0
|
||||
global $db;
|
||||
$db->query("ALTER TABLE xc_my_table ADD COLUMN new_col INT DEFAULT 0");
|
||||
},
|
||||
'1.2.0' => function (): void {
|
||||
// runs when upgrading from < 1.2.0 to >= 1.2.0
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleManager::updateModule()` считывает `installed_version` из хранилища переопределений, фильтрует
|
||||
сопоставляет только записи `> fromVersion && <= toVersion`, сортирует по полу и запускает каждую из них.
|
||||
вызываемый в своей собственной транзакции базы данных. `installModule()` записи `installed_version` после
|
||||
успешная установка; `uninstallModule()` удаляет ее.
|
||||
|
||||
**Основные правила:**
|
||||
|
||||
- Ключи - это полустрочные строки (`'1.1.0'`, `'2.0.0'`) — `version_compare` используется упорядочение
|
||||
- Каждая миграция выполняется в рамках своей собственной транзакции — сбой откатывает только этот шаг
|
||||
- `BaseModule` предоставляет значение по умолчанию `getMigrations(): array { return []; }`, поэтому реализация
|
||||
`MigratableInterface` является необязательным
|
||||
|
||||
---
|
||||
|
||||
## Composer обнаружение пакета
|
||||
|
||||
Модули могут распространяться в виде Composer пакетов с `"type": "xcvm-module"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "vendor/my-xcvm-module",
|
||||
"type": "xcvm-module",
|
||||
"extra": {
|
||||
"xcvm": {
|
||||
"module-path": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleLoader` автоматически сканирует `vendor/composer/installed.json` (Composer 1 и 2
|
||||
форматирует) и обнаруживает все установленные пакеты `xcvm-module` вместе со встроенным
|
||||
каталог `src/Modules/`. Пакеты дедуплицируются — модуль как в `modules/`, так и в
|
||||
`vendor/` загружается только один раз.
|
||||
|
||||
---
|
||||
|
||||
## Контрольный список модулей
|
||||
|
||||
- [ ] Создать `src/Modules/<name>/`
|
||||
- [ ] Добавить `namespace XcVm\Module\<PascalName>;` к каждому файлу класса
|
||||
- [ ] Создать `module.json` с помощью `name`, `version`, `requires_core`, `priority`, `dependencies`, `optional_dependencies`
|
||||
- [ ] Поставьте постоянную отметку `hash_id` (`php -r 'echo bin2hex(random_bytes(16));'`; никогда не пишите ее от руки)
|
||||
- [ ] Create `<PascalName>Module.php` extending `BaseModule`
|
||||
- [ ] Укажите версию в ** как ** `module.json` `"version"`, так и `getVersion()` — они должны совпадать (измените обе версии перед публикацией)
|
||||
- [ ] Реализовать `boot()` для всех служб, предоставляемых модулем
|
||||
- [ ] Реализовать `registerRoutes()` для конечных точек HTTP/API
|
||||
- [ ] Внедрить `registerNavbar()` для элементов панели администратора (или оставить пустым)
|
||||
- [ ] (Если кроны) Создайте `MyCron.php` + `MyCronJob.php`, зарегистрируйтесь в `registerCommands()`
|
||||
- [ ] (Если crons) Переопределяет `getCronEntries()` в классе модуля (основной файл не изменяется)
|
||||
- [ ] (Схема If) Отправляет значения `database.sql` (мастер), `database_drop.sql` (демонтаж) и `migrations/<semver>.sql` дельт
|
||||
- [ ] (При переносе PHP-логики) Реализовать `MigratableInterface::getMigrations()`
|
||||
- [ ] (Если страницы) Создайте контроллер, используя `renderUnifiedLayoutHeader/Footer`
|
||||
- [ ] (Если потоковое промежуточное программное обеспечение) Реализовать `StreamMiddlewareProviderInterface` отдельно
|
||||
- [ ] Проверить: `php -l src/Modules/<name>/<PascalName>Module.php`
|
||||
- [ ] Verify: `php console.php --list` shows the module's commands
|
||||
- [ ] Проверьте: удаление каталога модуля не приводит к фатальной ошибке
|
||||
|
||||
---
|
||||
|
||||
## часто задаваемые вопросы
|
||||
|
||||
**Вопрос: Как мне отключить модуль?**
|
||||
В поле `src/config/modules.php` добавьте `'module-name' => ['state' => 'disabled']`.
|
||||
Устаревшая форма `'enabled' => false` также принята для обеспечения обратной совместимости.
|
||||
|
||||
** Вопрос: Как мне объявить, что мой модуль зависит от другого?**
|
||||
Используйте `dependencies` в `module.json` для жестких удалений (должно присутствовать) или `optional_dependencies`
|
||||
для мягкого удаления (загружается раньше вашего, если присутствует, и автоматически пропускается, если отсутствует).
|
||||
|
||||
** Вопрос: Могу ли я украсить основную услугу?**
|
||||
Да — используйте `$container->decorate('service-id', callable, priority)` в `boot()`.
|
||||
Защищенные сервисы (`db`, `settings`, `config`, `auth`) не могут быть оформлены.
|
||||
|
||||
**Вопрос: Как мне прослушивать основные события?**
|
||||
Вызовите `EventDispatcher::listen(EventClass::class, callable, priority)` в любом месте после начальной загрузки,
|
||||
обычно внутри `boot()` или выделенного класса подписчиков.
|
||||
|
||||
**Вопрос: Могу ли я отправлять пользовательские события из модуля?**
|
||||
Да. Создайте простой класс или расширьте `AbstractEvent` и вызовите `EventDispatcher::dispatch(new MyEvent(...))`.
|
||||
|
||||
**Вопрос: Для чего используется `StreamMiddlewareProviderInterface`?**
|
||||
Это позволяет модулю вводить значение `StreamMiddlewareInterface` в конвейер потоковой обработки
|
||||
без изменения `StreamProcess.php`. При необходимости реализуйте это вместе с `ModuleInterface`.
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|Файл|Роль|
|
||||
| --- | --- |
|
||||
| `src/Core/Module/ModuleLoader.php` | Discovers, sorts and boots modules; PSR-4 class resolver |
|
||||
| `src/config/modules.php` |Конфигурация включения модуля / переопределения класса|
|
||||
| `src/Modules/` |Каталоги модулей|
|
||||
| `src/Core/Module/Contract/` |Подинтерфейсы модуля|
|
||||
@@ -48,7 +48,7 @@
|
||||
Модуль добавляет элементы только через `registerNavbar()`:
|
||||
|
||||
```php
|
||||
public function registerNavbar(): void {
|
||||
public function registerNavbar(NavbarRegistry $registry): void {
|
||||
NavbarRegistry::add((new NavbarItem('management.service_setup.my_module'))
|
||||
->parent('management.service_setup')
|
||||
->url('my_module')
|
||||
@@ -65,6 +65,46 @@ public function registerNavbar(): void {
|
||||
}
|
||||
```
|
||||
|
||||
## API построителя навигационных элементов
|
||||
|
||||
`NavbarItem` — это объект с плавным значением (`src/Core/Module/NavbarItem.php`) - параметры цепочки отключены `new NavbarItem($key)`:
|
||||
|
||||
|Метод|Цель|
|
||||
| --- | --- |
|
||||
| `new NavbarItem($key)` |создайте узел; `$key` - это его уникальный идентификатор `section.group.item`|
|
||||
| `->parent($parentKey)` |присоединение к существующему узлу (опустить для узла верхнего уровня)|
|
||||
| `->url($url)` |целевой путь; `'#'` делает его не навигационным заголовком **группа**|
|
||||
| `->label($key, $fallback = '')` |клавиша перевода или `('', 'Literal')` для фиксированного текста|
|
||||
| `->icon($icon)` |значок CSS-класса для элемента|
|
||||
| `->permissions([...])` |ИЛИ - список разрешающих ключей; узел скрыт, если только у пользователя нет такого ключа|
|
||||
| `->order($n)` |позиция сортировки внутри родительского элемента|
|
||||
| `->desktopOnly()` |спрятаться на мобильном телефоне|
|
||||
| `->noMobileSubmenu()` |не открывайте подменю этого узла на мобильном устройстве|
|
||||
| `->submenuClass('megamenu')` |рендеринг в два столбца для длинных дочерних списков|
|
||||
| `->settingDisabled($settingKey)` |скройте узел, если этот флажок настройки панели соответствует действительности|
|
||||
| `->makeDivider()` |визуализируйте этот узел как разделитель (без ссылки)|
|
||||
|
||||
### Узел группы и разделитель
|
||||
|
||||
```php
|
||||
public function registerNavbar(NavbarRegistry $registry): void {
|
||||
// A group header (url('#')) — shown only if at least one child is visible
|
||||
NavbarRegistry::add((new NavbarItem('management.my_group'))
|
||||
->parent('management')
|
||||
->url('#')
|
||||
->label('my_group')
|
||||
->order(50));
|
||||
|
||||
// A divider inside that group
|
||||
NavbarRegistry::add((new NavbarItem('management.my_group.sep1'))
|
||||
->parent('management.my_group')
|
||||
->makeDivider()
|
||||
->order(55));
|
||||
}
|
||||
```
|
||||
|
||||
> `settingDisabled('some_setting')` скрывает узел всякий раз, когда эта настройка верна (задает функцию за переключателем). Видимость также равна **область просмотра**: проверка `permissions` OR выполняется для текущего пользователя через `Authorization::check('adv', …)`, поэтому администратор и реселлер могут видеть разные подмножества одного и того же дерева.
|
||||
|
||||
## Практические правила для модулей
|
||||
|
||||
1. Используйте уникальные значения `key` в формате `section.group.item`.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Потоковая диагностика и инструменты
|
||||
|
||||
Отдельный инструмент проверяет правильность доставки потока — что сегменты поступают по порядку и очередь доставки не прерывается. Это не зависит от пути запроса; об этом смотрите в [Подсистеме потоковой передачи](streaming-subsystem.md).
|
||||
|
||||
---
|
||||
|
||||
## `tools/stream-check/stream_queue_check.py` (Только Python, stdlib)
|
||||
|
||||
Автономный монитор для **целостность сегмента/очереди пакетов** с дополнительным **панель управления живым буфером**. Автоматическое определение HLS по сравнению с MPEG-TS.
|
||||
|
||||
```bash
|
||||
python3 tools/stream-check/stream_queue_check.py "<url>" --duration 30 # batch check
|
||||
python3 tools/stream-check/stream_queue_check.py "<url>" --json # cron / monitoring
|
||||
python3 tools/stream-check/stream_queue_check.py "<url>" --live --duration 0 # live dashboard
|
||||
```
|
||||
|
||||
Что означает "неповрежденная очередь" для каждого типа потока:
|
||||
|
||||
|Течение|Проверка очереди|
|
||||
| --- | --- |
|
||||
|HLS (`.m3u8`)|`EXT-X-MEDIA-SEQUENCE` монотонный и непрерывный (никаких удаленных или перемотанных сегментов), нет `EXT-X-DISCONTINUITY`, каждый вновь появляющийся сегмент доступен для загрузки. Основные плейлисты отображаются в их первом варианте.|
|
||||
|MPEG-TS (`.ts`, `/play/<token>/ts`)|per-PID `continuity_counter` (потерянные / дублированные / переупорядоченные пакеты = разрыв очереди), потеря байта синхронизации, индикатор транспортной ошибки и задержка доставки.|
|
||||
|
||||
Основные параметры:
|
||||
|
||||
|Флаг|Цель|
|
||||
| --- | --- |
|
||||
| `--duration N` |секунды для наблюдения (`0` = до нажатия Ctrl-C в `--live`)|
|
||||
| `--tolerance N` |разрешить N временных разрывов очереди, прежде чем сообщать о `BROKEN` (игнорируются редкие сбои источника, переданные `-c copy`)|
|
||||
| `--stall-timeout S` |перерыв в доставке засчитывается как задержка; не превышайте продолжительность сегмента (по умолчанию 15).|
|
||||
| `--live` |цветная приборная панель TUI (внизу)|
|
||||
|`--prebuffer S` / `--buffer-target S`|live: предварительный буфер для виртуального игрока и масштаб буферного графика|
|
||||
|`--json` / `--no-color`|машинный вывод / отключение ANSI|
|
||||
|
||||
Код выхода: `0` исправен, `2` проблема с очередью или задержка, `1` использование.
|
||||
|
||||
### Оперативная панель мониторинга (`--live`)
|
||||
|
||||
Моделирует виртуальный проигрыватель: проигрыватель перемещается со скоростью настенных часов, в то время как контент "принимается". Для **тс** полученная временная шкала отсчитывается от **ПЦР** (часы потоковой передачи); для **HLS** - от длительности сегментов `EXTINF`. Буферизованное время воспроизведения ("кэш") = получено − воспроизведено; если оно достигает нуля, начало воспроизведения зависает (событие отмены буферизации).
|
||||
|
||||
```text
|
||||
STREAM QUEUE / BUFFER MONITOR TS up 00:22
|
||||
cache buffer (s), last 60s:
|
||||
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▄▄▇▇▇▆▆▆▅▅▅▄▄▇▇▇▆▆▆▅ <- burst-then-drain = delivery sawtooth
|
||||
IN CACHE : [█████████████████░░░░░░░░░░░░░] 11.6s / 20s
|
||||
PLAYING : PLAYING head 00:18 received 00:29
|
||||
rate 1000 kbit/s received 4.1 MB last data 7.0s ago
|
||||
QUEUE OK cc:0 sync:0 gaps:0 disc:0 rebuffers:0
|
||||
```
|
||||
|
||||
График буфера и индикатор окрашены в зеленый (работоспособный) / желтый (низкий) / красный (недостаточный) цвета. Для HLS ряд блоков показывает сегменты, которые все еще находятся в кэше перед началом воспроизведения.
|
||||
|
||||
> **Обратите внимание — темп доставки.** Оперативная доставка клиентов теперь осуществляется с помощью
|
||||
> `xc_fanout` демон (см. [Streaming Subsystem → Daemon delivery](streaming-subsystem.md#daemon-delivery-xc_fanout)), который извлекает каждый источник по одному разу и передает его через сокет unix. `stream_queue_check.py --live` визуализирует поведение буфера, которое реальный игрок увидел бы при просмотре доставленного потока.
|
||||
|
||||
---
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|Файл|Цель|
|
||||
| --- | --- |
|
||||
| `tools/stream-check/stream_queue_check.py` |мониторинг целостности очереди + панель мониторинга динамического буфера|
|
||||
@@ -23,7 +23,7 @@ endpoint logic (live.php / vod.php / timeshift.php)
|
||||
ShutdownHandler::handle()
|
||||
```
|
||||
|
||||
nginx переписывает все URL-адреса потоковой передачи на PHP точки входа в соответствии с `www/stream/`:
|
||||
nginx переписывает все URL-адреса потоковой передачи на PHP точки входа в соответствии с `Public/stream/`:
|
||||
|
||||
|Шаблон URL-адреса|Точка входа|Цель|
|
||||
| --- | --- | --- |
|
||||
@@ -42,7 +42,6 @@ nginx переписывает все URL-адреса потоковой пер
|
||||
src/Streaming/
|
||||
├── StreamingBootstrap.php
|
||||
├── AsyncFileOperations.php
|
||||
├── TimeshiftClient.php
|
||||
├── Auth/
|
||||
│ ├── StreamAuth.php
|
||||
│ └── StreamAuthMiddleware.php
|
||||
@@ -56,6 +55,8 @@ src/Streaming/
|
||||
│ ├── HLSGenerator.php
|
||||
│ ├── OffAirHandler.php
|
||||
│ └── StreamRedirector.php
|
||||
├── Fanout/
|
||||
│ └── FanoutClient.php
|
||||
├── Health/
|
||||
│ └── ProcessChecker.php
|
||||
├── Lifecycle/
|
||||
@@ -63,8 +64,8 @@ src/Streaming/
|
||||
└── Protection/
|
||||
└── ConnectionLimiter.php
|
||||
|
||||
src/www/stream/
|
||||
├── init.php # Legacy bootstrap shim (deprecated)
|
||||
src/Public/stream/
|
||||
├── index.php # Entry router for the stream endpoints
|
||||
├── auth.php # Token validation gateway
|
||||
├── live.php # Live streaming delivery
|
||||
├── vod.php # VOD delivery
|
||||
@@ -73,6 +74,7 @@ src/www/stream/
|
||||
├── key.php # Encryption key delivery
|
||||
├── subtitle.php # Subtitle delivery
|
||||
├── thumb.php # Thumbnail delivery
|
||||
├── probe.php # Stream probe / off-air status
|
||||
└── rtmp.php # RTMP publishing endpoint
|
||||
```
|
||||
|
||||
@@ -104,7 +106,7 @@ public static function bootstrap($rFilename, $rSettings)
|
||||
|
||||
Классифицирует конечную точку:
|
||||
|
||||
- **Конечные точки зондирования:** `probe`, `player_api` (небольшая нагрузка)
|
||||
- **Конечные точки зондирования:** `probe`, `player_api` ( небольшая нагрузка)
|
||||
- **Конечные точки по умолчанию:** `live`, `thumb`, `subtitle`, `timeshift`, `vod`, `status`
|
||||
- **Привилегированные конечные точки:** `rtmp`, `portal`
|
||||
|
||||
@@ -125,7 +127,7 @@ public static function bootstrap($rFilename, $rSettings)
|
||||
|
||||
Подключается к базе данных/Redis на основе `$rSettings['redis_handler']`.
|
||||
|
||||
> **Важно:** Путь к потоковой передаче считывается исключительно из файлового кэша. При обычной работе программа не запрашивает настройки в базе данных или запросы пользователей.
|
||||
> **Важный:** Путь к потоковой передаче считывается исключительно из файлового кэша. При обычной работе программа не запрашивает настройки в базе данных или запросы пользователей.
|
||||
|
||||
---
|
||||
|
||||
@@ -179,9 +181,9 @@ Alt-Svc: h3-29, h3-T051, h3-Q050 (HTTP/3 hints)
|
||||
4. Создайте запись о подключении: `ConnectionTracker::createConnection()`.
|
||||
5. Hand delivery to the **`xc_fanout` daemon** (see below): PHP emits an
|
||||
`X-Accel-Redirect` и завершает байтовый путь — nginx передает байты в потоковом режиме.
|
||||
- **ТС:** `X-Accel-Redirect: /xc_fanout/<id>?c=<uuid>&prebuffer=N` (nginx
|
||||
- **тс:** `X-Accel-Redirect: /xc_fanout/<id>?c=<uuid>&prebuffer=N` (nginx
|
||||
перезаписывается в файл демона `/live/<id>`).
|
||||
- **HLS:** список воспроизведения указывает на выделенные сегменты; `segment.php` транслируется в прямом эфире
|
||||
- **HLS:** список воспроизведения указывает на выделенные сегменты; `segment.php` показы в прямом эфире
|
||||
сегментирует только через демон (`/xc_fanout_hls/<id>_<seq>`), иначе `404`.
|
||||
6. При выходе: `ShutdownHandler::handle()` → закрыть запись о подключении.
|
||||
|
||||
@@ -191,7 +193,7 @@ Alt-Svc: h3-29, h3-T051, h3-Q050 (HTTP/3 hints)
|
||||
|
||||
### Временной сдвиг (timeshift.php)
|
||||
|
||||
Обслуживает архивные сегменты. Использует `TimeshiftClient` для разрешения архивного файла.
|
||||
Обслуживает архивированные сегменты (timeshift / catch-up) из пути к архиву.
|
||||
|
||||
### Доставка демона — `xc_fanout`
|
||||
|
||||
@@ -199,26 +201,29 @@ Live client delivery (TS **and** HLS) is **daemon-only**: PHP authorizes the
|
||||
средство просмотра, а затем полностью покидает байтовый путь, так что средство просмотра больше не закрепляет
|
||||
PHP-FPM работник, отвечающий за жизнедеятельность потока.
|
||||
|
||||
- **Fan-out.** `xc_fanout` (a bundled Go daemon) pulls each source **once** and
|
||||
- **Расходимся веером.** `xc_fanout` (встроенный демон Go) извлекает каждый источник **однажды** и
|
||||
предоставляет его каждому пользователю через сокет unix с помощью встроенного в оперативную память сегментатора HLS.
|
||||
PHP не соответствует байтовому пути для каждого зрителя; старый цикл поиска и чтения
|
||||
(`AsyncFileOperations::awaitFileExists()`) и `HLSGenerator::generateHLS()`
|
||||
сервировочные дорожки были удалены при разделке.
|
||||
- **Два сокета.** Клиентский сокет (ориентированный на nginx) обслуживает `/live/<id>` и
|
||||
PHP не соответствует байтовому пути для каждого зрителя: рабочий процесс чтения для каждого зрителя
|
||||
цикл обслуживания и путь `HLSGenerator::generateHLS()` для обслуживания клиентов не являются
|
||||
больше не используется для оперативной доставки (`generateHLS()` сохраняется в классе, но имеет
|
||||
абонентов нет). `AsyncFileOperations::awaitFileExists()` — это **нет** удалено - это
|
||||
все еще используется для ожидания запуска потока и пути в байтах VOD/timeshift (см.
|
||||
Таблица показателей).
|
||||
- **Две розетки.** Клиентский сокет (ориентированный на nginx) обслуживает `/live/<id>` и
|
||||
`/hls/...`; управляющий сокет, предназначенный только для PHP, регистрирует источники
|
||||
(`PUT /streams/<id>` / `/ingest/<id>`), отвечает на вопросы о статусе выхода в эфир
|
||||
(`GET /streams/<id>`, `GET /probe/<id>`) и предоставляет доступ к телеметрии.
|
||||
- **Telemetry / reconciliation.** `fanout_sync` polls `GET /rates` (per-uuid
|
||||
- **Телеметрия / согласование данных.** `fanout_sync` опросы `GET /rates` (для каждого uuid
|
||||
КБИТ/с → `lines_divergence`) и `GET /connections` (согласовывает `lines_live`
|
||||
строк, поскольку PHP не может видеть разъединение в `X-Accel`).
|
||||
- **Отключен.** Если демон сообщает об отсутствии данных (`has_data=false` / устаревшие), PHP
|
||||
- **Вне эфира.** Если демон сообщает об отсутствии данных (`has_data=false` / устаревшие), PHP
|
||||
показывает страницу "не в эфире" вместо того, чтобы позволить зрителю зависнуть.
|
||||
- **Сохранено на диске HLS** только для timeshift / миниатюр / `.analyse` /
|
||||
`MonitorCommand` — не для доставки клиенту.
|
||||
|
||||
#### Наложение отправленного сообщения
|
||||
|
||||
Действие администратора "Отправить сообщение" отображает текстовый баннер на видео **одного** зрителя.
|
||||
Действие администратора "Отправить сообщение" отображает текстовый баннер на видео, которое просматривает **один** зритель.
|
||||
PHP отправляет его в сокет управления демоном
|
||||
(`FanoutClient::sendSignal` → `POST /signal/<uuid>`), и демон применяет
|
||||
ffmpeg `drawtext` наложение на следующий HLS сегмент этого просмотра (или короткий ~5-секундный фрагмент
|
||||
@@ -234,14 +239,14 @@ ffmpeg `drawtext` наложение на следующий HLS сегмент
|
||||
|
||||
Управляет текущим состоянием соединения. Серверная часть выбрана с помощью `$rSettings['redis_handler']`:
|
||||
|
||||
**Redis (предпочтительно для масштабирования):**
|
||||
**Redis (preferred for scale):**
|
||||
|
||||
- Соединения, хранящиеся в отсортированных наборах:
|
||||
- `LINE#{identity}` — подключения для пользователя
|
||||
- `STREAM#{stream_id}` — соединения для потока
|
||||
- `SERVER#{server_id}` — соединения на сервере
|
||||
|
||||
**MySQL (резервный вариант):**
|
||||
**MySQL (fallback):**
|
||||
|
||||
- Таблица: `lines_live` с полями: `activity_id`, `user_id`, `stream_id`, `server_id`, `uuid`, `pid`, `hls_end`
|
||||
|
||||
@@ -350,21 +355,14 @@ IP-блокировка на основе файлов. Файлы блоков
|
||||
|
||||
## HLS Шифрование
|
||||
|
||||
Файл: `src/Streaming/Delivery/HLSGenerator.php`
|
||||
Клиент HLS обслуживается демоном `xc_fanout` (см. [Доставка демоном](#daemon-delivery-xc_fanout)), поэтому происходит шифрование **сторона демона**:
|
||||
|
||||
```php
|
||||
public static function generateHLS($rSettings, $rM3U8, $rUsername, $rPassword,
|
||||
$rStreamID, $rUUID, $rIP, ...): string|false
|
||||
```
|
||||
1. `StreamProcess` записывает ключ потока AES-128/IV в `content/streams/<id>_.key` / `_.iv`.
|
||||
2. At ingest registration (`FanoutClient::registerIngest`), when `encrypt_hls` is on, the key/IV are handed to the daemon, which encrypts the HLS segments it serves and emits a matching `#EXT-X-KEY`.
|
||||
3. `HLSGenerator::tokenizeDaemonPlaylist()` переписывает URL-адреса сегментов плейлиста демона в ссылки с авторизацией для каждого сегмента `/hls/<token>`, которые `segment.php` передаются через прокси-сервер демона.
|
||||
4. Ключ AES доставляется игрокам с помощью `key.php` (`src/Public/stream/key.php`) с использованием того же механизма токенов.
|
||||
|
||||
Когда `encrypt_hls == true`:
|
||||
|
||||
1. Сгенерируйте ключевой токен AES-128 из IP + StreamID + salt.
|
||||
2. Замените IV содержимым из `STREAMS_PATH . $rStreamID . '_.iv'`.
|
||||
3. Encrypt each segment reference: `IP/StreamID/Segment/UUID/SERVER_ID/VideoCodec/OnDemand`.
|
||||
4. Замените названия сегментов на `/hls/{encrypted_token}`.
|
||||
|
||||
Доставка ключей происходит через `key.php` с использованием того же механизма токенов.
|
||||
> Устаревший `HLSGenerator::generateHLS()` (который создал и зашифровал плейлист на диске HLS для использования PHP) сохраняется в классе, но становится **больше не находится на пути к клиенту** после отключения демона.
|
||||
|
||||
---
|
||||
|
||||
@@ -374,12 +372,12 @@ public static function generateHLS($rSettings, $rM3U8, $rUsername, $rPassword,
|
||||
|
||||
|Особенность|Механизм|
|
||||
| --- | --- |
|
||||
|Ожидание неблокирующего файла|`AsyncFileOperations::awaitFileExists()` использует inotify (Linux) или оптимизированный опрос|
|
||||
|Трансляция-онлайн-ожидание|`AsyncFileOperations::awaitFileExists()` ожидает `_.pid`/`_.monitor`/первого сегмента при появлении потока (и в пути длиной VOD/timeshift байт). Оперативная доставка клиента осуществляется демоном, а не считывается с помощью PHP.|
|
||||
|Нулевой режим работы процессора|`time_nanosleep()` через `AsyncFileOperations::efficientSleep()`|
|
||||
|nginx буферизация|128 буферов по 32 КБАЙТ на запрос|
|
||||
|Объединение подключений в пул|Redis (предпочтительно) или постоянный MySQL|
|
||||
|Чтение только из кэша|Настройки и пользовательские данные считываются из файлового кэша без запросов к базе данных|
|
||||
|Ранний выход|Отслеживает `connection_status()` каждые 5 секунд для обнаружения отключения клиента|
|
||||
|Досрочный выход (VOD/timeshift)|Эти байтовые циклы опрашивают `connection_status()` для остановки при отключении клиента. В Live нет байтового цикла для каждого пользователя PHP (обслуживается демоном).|
|
||||
|Обновление настроек|Каждые 5 минут (300 секунд) для отслеживания изменений конфигурации без перезапуска|
|
||||
|
||||
---
|
||||
@@ -387,92 +385,34 @@ public static function generateHLS($rSettings, $rM3U8, $rUsername, $rPassword,
|
||||
## Пути к файловой системе
|
||||
|
||||
```text
|
||||
STREAMS_PATH = /home/xc_vm/www/stream/
|
||||
CONS_TMP_PATH = /home/xc_vm/tmp/
|
||||
STREAMS_PATH = /home/xc_vm/content/streams/
|
||||
VOD_PATH = /home/xc_vm/content/vod/
|
||||
ARCHIVE_PATH = /home/xc_vm/content/archive/
|
||||
VIDEO_PATH = /home/xc_vm/content/video/
|
||||
CONS_TMP_PATH = /home/xc_vm/tmp/opened_cons/
|
||||
CACHE_TMP_PATH = /home/xc_vm/tmp/cache/
|
||||
FLOOD_TMP_PATH = /home/xc_vm/tmp/flood/
|
||||
SIGNALS_PATH = /home/xc_vm/tmp/signals/
|
||||
VIDEO_PATH = /home/xc_vm/www/video/
|
||||
ARCHIVE_PATH = /home/xc_vm/www/archive/
|
||||
VOD_PATH = /home/xc_vm/www/vod/
|
||||
SIGNALS_TMP_PATH = /home/xc_vm/tmp/signals/
|
||||
SIGNALS_PATH = /home/xc_vm/signals/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Диагностика и оснастка
|
||||
|
||||
Два инструмента проверяют правильность доставки потока — что сегменты поступают по порядку
|
||||
и очередь на доставку не прерывается.
|
||||
Автономный инструмент проверки целостности потока (`tools/stream-check/stream_queue_check.py`) теперь доступен на отдельной странице - см. [Диагностика и инструменты для потоковой передачи](streaming-diagnostics.md).
|
||||
|
||||
### `tools/stream_queue_check.py` (Только Python, stdlib)
|
||||
---
|
||||
|
||||
Автономный монитор **целостности сегмента/очереди пакетов** с дополнительным ** функцией live
|
||||
панель управления буфером**. Автоматически определяет HLS по сравнению с MPEG-TS.
|
||||
## Обоснование проекта (ADR)
|
||||
|
||||
```bash
|
||||
python3 tools/stream_queue_check.py "<url>" --duration 30 # batch check
|
||||
python3 tools/stream_queue_check.py "<url>" --json # cron / monitoring
|
||||
python3 tools/stream_queue_check.py "<url>" --live --duration 0 # live dashboard
|
||||
```
|
||||
Почему оперативная доставка переместилась с tmpfs на PHP байтовый путь — решения, стоящие за текущим
|
||||
`xc_fanout` архитектура — записывается в отчетах об архитектурных решениях (repo-внутренние примечания,
|
||||
не является частью опубликованного сайта):
|
||||
|
||||
Что означает "неповрежденная очередь" для каждого типа потока:
|
||||
|
||||
|Течение|Проверка очереди|
|
||||
| --- | --- |
|
||||
|HLS (`.m3u8`)|`EXT-X-MEDIA-SEQUENCE` монотонный и непрерывный (никаких удаленных или перемотанных сегментов), нет `EXT-X-DISCONTINUITY`, каждый вновь появляющийся сегмент доступен для загрузки. Основные плейлисты отображаются в их первом варианте.|
|
||||
|MPEG-TS (`.ts`, `/play/<token>/ts`)|per-PID `continuity_counter` (потерянные / дублированные / переупорядоченные пакеты = разрыв очереди), потеря байта синхронизации, индикатор транспортной ошибки и задержка доставки.|
|
||||
|
||||
Основные параметры:
|
||||
|
||||
|Флаг|Цель|
|
||||
| --- | --- |
|
||||
| `--duration N` |секунды для наблюдения (`0` = до нажатия Ctrl-C в `--live`)|
|
||||
| `--tolerance N` |разрешить N временных разрывов очереди, прежде чем сообщать о `BROKEN` (игнорируются редкие сбои источника, переданные `-c copy`)|
|
||||
| `--stall-timeout S` |перерыв в доставке засчитывается как задержка; не превышайте продолжительность сегмента (по умолчанию 15).|
|
||||
| `--live` |цветная приборная панель TUI (внизу)|
|
||||
|`--prebuffer S` / `--buffer-target S`|live: предварительный буфер для виртуального игрока и масштаб буферного графика|
|
||||
|`--json` / `--no-color`|машинный вывод / отключение ANSI|
|
||||
|
||||
Код выхода: `0` исправен, `2` проблема с очередью или задержка, `1` использование.
|
||||
|
||||
#### Оперативная панель мониторинга (`--live`)
|
||||
|
||||
Моделирует виртуального проигрывателя: проигрыватель перемещается со скоростью настенных часов, в то время как содержимое
|
||||
"получено". Для **TS** полученная временная шкала берется из **PCR** (часы потока).;
|
||||
для **HLS** из длительностей сегментов `EXTINF`. Буферизованное время воспроизведения ("кэш") =
|
||||
получено − воспроизведено; если значение достигает нуля, то начало воспроизведения зависает (событие отмены буферизации).
|
||||
|
||||
```text
|
||||
STREAM QUEUE / BUFFER MONITOR TS up 00:22
|
||||
cache buffer (s), last 60s:
|
||||
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▄▄▇▇▇▆▆▆▅▅▅▄▄▇▇▇▆▆▆▅ <- burst-then-drain = delivery sawtooth
|
||||
IN CACHE : [█████████████████░░░░░░░░░░░░░] 11.6s / 20s
|
||||
PLAYING : PLAYING head 00:18 received 00:29
|
||||
rate 1000 kbit/s received 4.1 MB last data 7.0s ago
|
||||
QUEUE OK cc:0 sync:0 gaps:0 disc:0 rebuffers:0
|
||||
```
|
||||
|
||||
График буфера и индикатор окрашены в зеленый (исправный) / желтый (низкий) / красный цвета
|
||||
(голодает). Для HLS строка блоков показывает сегменты, которые все еще находятся в кэше перед началом
|
||||
плейхед.
|
||||
|
||||
### `console.php stream:check` (PHP, компаньон)
|
||||
|
||||
Проверяет URL-адрес источника и с помощью `--decode` извлекает и декодирует медиафайл для перехвата
|
||||
поврежденные сегменты. HLS проверяется посегментно; сообщение об ошибке с одним сокетом
|
||||
конечная точка фиксируется с помощью cURL и декодируется в автономном режиме (ffmpeg в режиме реального времени `-i` зависает на
|
||||
it). Источник: `src/Cli/Commands/StreamCheckCommand.php`.
|
||||
|
||||
```bash
|
||||
console.php stream:check "<url>" # metadata probe (type, codecs)
|
||||
console.php stream:check "<url>" --decode=30 --json
|
||||
```
|
||||
|
||||
> **Примечание — темп доставки.** Цикл доставки TS в реальном времени в `live.php` истощает
|
||||
> доступные данные без регулирования и приостанавливаются только при достижении значения ffmpeg
|
||||
> заголовок записи. Более ранняя версия отключалась на одну секунду после каждого чтения, ограничивая
|
||||
> пропускная способность составляет `read_buffer_size` в секунду, а клиенты голодают;
|
||||
> `stream_queue_check.py --live` визуализирует результирующее поведение буфера.
|
||||
- [ADR 0001 — Tmpfs-free streaming](https://github.com/Vateron-Media/XC_VM/blob/main/docs/adr/0001-tmpfs-free-streaming.md) — PHP out of the byte path, native fan-out, in-RAM HLS.
|
||||
- [ADR 0002 — `xc_fanout` daemon](https://github.com/Vateron-Media/XC_VM/blob/main/docs/adr/0002-xc-fanout-daemon.md) — the native live fan-out daemon.
|
||||
- [ADR 0003 — Полное отключение демона](https://github.com/Vateron-Media/XC_VM/blob/main/docs/adr/0003-full-daemon-cutover.md) — отмена устаревшего байтового пути для live.
|
||||
|
||||
---
|
||||
|
||||
@@ -492,5 +432,4 @@ console.php stream:check "<url>" --decode=30 --json
|
||||
| `src/Streaming/Lifecycle/ShutdownHandler.php` |очистка соединения при выходе|
|
||||
| `src/Domain/Stream/ConnectionTracker.php` |состояние соединения в Redis/MySQL|
|
||||
| `src/Core/Init/LegacyInitializer.php` |настройка глобальной переменной для потоковой передачи|
|
||||
| `src/Cli/Commands/StreamCheckCommand.php` |`stream:check` — проверка/декодирование потока на наличие фрагментарных сегментов|
|
||||
| `tools/stream_queue_check.py` |мониторинг целостности очереди + панель мониторинга динамического буфера|
|
||||
| `tools/stream-check/stream_queue_check.py` |мониторинг целостности очереди + панель мониторинга динамического буфера|
|
||||
|
||||
@@ -101,7 +101,7 @@ Authenticator::hashPassword(string $password, ?string $salt = null, int $rounds
|
||||
Использует `crypt()` с SHA-512 (`$6$`). Формат salt равен `$6$rounds=20000$<salt>$`, где `<salt>` - это 16 шестнадцатеричных символов, полученных из `openssl_random_pseudo_bytes(16)`. Пароли повторно хэшируются при каждом успешном входе в систему, что приводит к замене значения salt.
|
||||
|
||||
```php
|
||||
Authenticator::checkPassword(string $password, string $storedHash): string
|
||||
Authenticator::checkPassword(string $password, string $storedHash): bool
|
||||
```
|
||||
|
||||
Проверяет пароль в виде открытого текста на соответствие сохраненному хэшу, используя `crypt($password, $storedHash)`, с возможностью сравнения по времени с помощью `hash_equals()`. Сохраненный хэш содержит алгоритм, раунды и соль, поэтому `crypt()` воспроизводит правильный хэш для сравнения.
|
||||
@@ -119,16 +119,16 @@ Authenticator::checkPassword(string $password, string $storedHash): string
|
||||
`PlayerLoginController::processLogin()` выполняет эти проверки в порядке:
|
||||
|
||||
1. **Поиск учетных данных** -- `UserRepository::getUserInfo()` (отличается от `getAuthUserByCredentials`, используемого администратором/реселлером).
|
||||
2. **Отклонение типа линии** - Линии E2, MAG и Stalker отклоняются с определенными кодами ошибок.
|
||||
3. **Expiration check** -- `exp_date` must be null or in the future.
|
||||
4. **Admin-enabled check** -- `admin_enabled == 0` returns `CLIENT_BANNED`.
|
||||
5. **User-enabled check** -- `enabled == 0` returns `CLIENT_DISABLED`.
|
||||
6. **Список разрешенных IP-адресов** -- Если для пользователя задано значение `allowed_ips`, IP-адрес клиента должен совпадать (определяется с помощью `gethostbyname`).
|
||||
7. **Ограничение по стране ** - Два режима:
|
||||
2. **Отклонение типа линии** -- Линии E2, MAG и Stalker отклоняются с определенными кодами ошибок.
|
||||
3. **Проверка истечения срока годности** -- `exp_date` должно быть равно null или в будущем.
|
||||
4. **Проверка с поддержкой администратора** -- `admin_enabled == 0` возвращает `CLIENT_BANNED`.
|
||||
5. **Проверка, включенная пользователем** -- `enabled == 0` возвращает `CLIENT_DISABLED`.
|
||||
6. **Список разрешенных IP-адресов** -- Если для пользователя задано значение `allowed_ips`, IP-адрес клиента должен совпадать (решается с помощью `gethostbyname`).
|
||||
7. **Ограничение по стране** - Два режима:
|
||||
- Для каждого пользователя: если задано значение `forced_country`, а не `ALL`, страна GeoIP должна совпадать.
|
||||
- Глобальный: если нет переопределения для каждого пользователя, устанавливается глобальный параметр `allow_countries` (если только он не содержит `ALL`).
|
||||
8. **Проверка агента пользователя** -- Если для пользователя задано значение `allowed_ua`, то пользовательский агент HTTP должен соответствовать.
|
||||
9. **Проверка провайдера** -- флаг _BOS_0 отклоняет соединение.
|
||||
9. флаг **Проверка интернет-провайдера** -- `isp_violate` отклоняет соединение.
|
||||
10. **Проверка сервера интернет-провайдера** -- Если значение `isp_is_server` равно true и пользователь не является рестримером, соединение будет отклонено.
|
||||
|
||||
Каждый сбой запускает `BruteforceGuard::checkFlood()` перед возвратом кода ошибки.
|
||||
@@ -168,13 +168,13 @@ $_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password'
|
||||
Если установлено значение `$_SESSION['hash']`, при каждой загрузке страницы выполняются следующие проверки:
|
||||
|
||||
1. **Поиск пользователя** -- `UserRepository::getRegisteredUserById($_SESSION['hash'])`. Если пользователь больше не существует, сеанс завершается.
|
||||
2. **Permission check** -- `AuthRepository::getPermissions()` must return a valid set with `is_admin == true`.
|
||||
3. **Проверка IP-адреса** -- Сравнивает текущий IP-адрес с `$_SESSION['ip']`:
|
||||
2. **Проверка прав доступа** -- `AuthRepository::getPermissions()` должен возвращать допустимый набор с `is_admin == true`.
|
||||
3. **Проверка IP-адреса** - Сравнивает текущий IP-адрес с `$_SESSION['ip']`:
|
||||
- Если параметр `ip_subnet_match` включен: сравниваются только первые три октета (например, `192.168.1.*` соответствует `192.168.1.*`).
|
||||
- Если параметр `ip_subnet_match` отключен: требуется точное совпадение IP-адресов.
|
||||
- Если IP-адрес не совпадает и включена настройка `ip_logout`, сеанс завершается.
|
||||
- Если IP-адрес не совпадает и `ip_logout` отключен, `$_SESSION['ip']` автоматически обновляется до нового IP-адреса.
|
||||
4. **Verify hash check** -- `$_SESSION['verify']` must equal `md5($rUserInfo['username'] . '||' . $rUserInfo['password'])`. This ensures the session is invalidated if the password changes.
|
||||
4. **Проверить проверку хэша Verify** -- `$_SESSION['verify']` должно быть равно `md5($rUserInfo['username'] . '||' . $rUserInfo['password'])`. Это гарантирует, что сеанс будет аннулирован в случае изменения пароля.
|
||||
|
||||
Если какая-либо проверка завершается неудачей, сеанс очищается с помощью `SessionManager::clearContext('admin')`, и пользователь перенаправляется на индексную страницу.
|
||||
|
||||
@@ -184,7 +184,7 @@ $_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password'
|
||||
|
||||
Логика идентична проверке администратора, но используются сеансовые ключи реселлера:
|
||||
|
||||
- Проверяет `$_SESSION['reseller']` для идентификатора пользователя.
|
||||
- Проверяет `$_SESSION['reseller']` на наличие идентификатора пользователя.
|
||||
- Использует `$_SESSION['rip']` для сравнения IP-адресов.
|
||||
- Использует `$_SESSION['rverify']` для проверки хэша.
|
||||
- Проверяет разрешение `is_reseller` вместо `is_admin`.
|
||||
@@ -235,7 +235,7 @@ $_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password'
|
||||
|
||||
Проверка на отсутствие блокировки. Возвращает значение `true`, если сеанс был запущен и установлен ключ `auth`.
|
||||
|
||||
**`getUser(): ?string`**
|
||||
**`getUser(): mixed`**
|
||||
|
||||
Возвращает значение, сохраненное в ключе сеанса `auth` (идентификатор пользователя для администратора/реселлера или идентификатор строки для игрока), или `null`, если аутентификация не пройдена.
|
||||
|
||||
@@ -247,7 +247,7 @@ $_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password'
|
||||
|
||||
Устанавливает значение сеанса по логическому имени.
|
||||
|
||||
**`login(string $hash, ?string $ip = null): void`**
|
||||
**`login(mixed $hash, ?string $ip = null): void`**
|
||||
|
||||
Создает аутентифицированный сеанс, задавая значения `auth` и `activity`. При необходимости сохраняет IP-адрес клиента.
|
||||
|
||||
@@ -271,7 +271,7 @@ $_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password'
|
||||
|
||||
`SessionManager::DEFAULT_TIMEOUT` равно 60 минутам. Метод `checkTimeout()` (вызываемый автоматически `start()`) сравнивает время, прошедшее с момента `last_activity`. Если время ожидания превышено, все ключи сеанса, зависящие от контекста, сбрасываются, что приводит к выходу пользователя из системы.
|
||||
|
||||
Поскольку контекст игрока не имеет ключа `activity` в карте ключей, проверка тайм-аута не применяется к сеансам игрока.
|
||||
Поскольку контекст игрока не имеет ключа `activity` в карте ключей, проверка тайм-аута не применяется к сессиям игрока.
|
||||
|
||||
---
|
||||
|
||||
@@ -279,34 +279,34 @@ $_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password'
|
||||
|
||||
Файл: `src/Core/Auth/BruteforceGuard.php`
|
||||
|
||||
Централизованное ограничение скорости и защита от перебора. Все методы используют файловое состояние, хранящееся в `FLOOD_TMP_PATH` (`/home/xc_vm/tmp/flood/`). Разрешенные IP-адреса (IP-адреса сервера) и IP-адреса, указанные в параметре `flood_ips_exclude`, всегда исключаются.
|
||||
Централизованное ограничение скорости и защита от перебора. Все методы используют состояние на основе файла, хранящееся в `FLOOD_TMP_PATH` (`/home/xc_vm/tmp/flood/`). Разрешенные IP-адреса (IP-адреса сервера) и IP-адреса, указанные в параметре `flood_ips_exclude`, всегда исключаются.
|
||||
|
||||
### `checkFlood(?string $ip = null, bool $useCachedMode = false): null`
|
||||
### `checkFlood(?string $ip = null, bool $useCachedMode = false): void`
|
||||
|
||||
Скорость - ограничивает количество запросов по IP-адресу в пределах настраиваемого временного интервала.
|
||||
|
||||
- **Настройки:** `flood_limit` (максимальное количество запросов), `flood_seconds` (размер окна).
|
||||
- **Файл состояния:** `FLOOD_TMP_PATH . $ip` - хранит объект JSON с числом `requests` и временной меткой `last_request`.
|
||||
- **Файл состояния:** `FLOOD_TMP_PATH . $ip` - сохраняет объект JSON с числом `requests` и временной меткой `last_request`.
|
||||
- **Поведение:** Отслеживает количество запросов в пределах временного окна. Если количество превышает `flood_limit`, IP-адрес блокируется (заносится в таблицу `blocked_ips` или передается через Redis в кэшированном/потоковом режиме). Файл состояния удаляется после блокировки.
|
||||
- ** Используется: ** Логином игрока (вызывается при каждой неудачной попытке входа в систему), конечными точками потоковой передачи.
|
||||
- **Используется:** Вход игрока (вызывается при каждой неудачной попытке входа в систему), конечные точки потоковой передачи.
|
||||
|
||||
### `checkBruteforce(?string $ip = null, ?string $mac = null, ?string $username = null, bool $useCachedMode = false): null`
|
||||
### `checkBruteforce(?string $ip = null, ?string $mac = null, ?string $username = null, bool $useCachedMode = false): void`
|
||||
|
||||
Обнаруживает атаки методом перебора на основе количества уникальных MAC-адресов или имен пользователей, обнаруженных с одного IP-адреса.
|
||||
|
||||
- **Настройки:** `bruteforce_mac_attempts`, `bruteforce_username_attempts` ( максимальное количество уникальных значений), `bruteforce_frequency` (временной интервал в секундах).
|
||||
- **State file:** `FLOOD_TMP_PATH . $ip . '_mac'` or `FLOOD_TMP_PATH . $ip . '_user'` -- stores attempts as `{term: timestamp}` pairs.
|
||||
- **Файл состояния:** `FLOOD_TMP_PATH . $ip . '_mac'` или `FLOOD_TMP_PATH . $ip . '_user'` - сохраняет попытки в виде пар `{term: timestamp}`.
|
||||
- **Поведение:** Попытки с истекшим сроком действия (за пределами частотного диапазона) отсекаются с помощью `truncateAttempts()`. Если количество уникальных запросов превышает допустимое, IP-адрес блокируется.
|
||||
- **Используется:** Конечными точками потоковой аутентификации.
|
||||
- **Используется:** Конечные точки потоковой аутентификации.
|
||||
|
||||
### `checkAuthFlood(array $user, ?string $ip = null): null`
|
||||
### `checkAuthFlood(array $user, ?string $ip = null): void`
|
||||
|
||||
Ограничивает скорость запросов на аутентификацию для конкретной комбинации пользователь +IP. Предназначен для ограничения повторных попыток авторизации без полной блокировки.
|
||||
|
||||
- **Настройки:** `auth_flood_limit` (максимальное количество попыток), `auth_flood_seconds` (окно), `auth_flood_sleep` (задержка в секундах при блокировке).
|
||||
- **State file:** `FLOOD_TMP_PATH . $userId . '_' . $ip` -- stores attempts as indexed timestamps plus an optional `block_until` timestamp.
|
||||
- **Файл состояния:** `FLOOD_TMP_PATH . $userId . '_' . $ip` - сохраняет попытки в виде индексированных временных меток плюс необязательную временную метку `block_until`.
|
||||
- **Поведение:** Когда количество попыток превышает допустимое значение, устанавливается временная метка `block_until`. Последующие запросы в течение периода блокировки задерживаются на `auth_flood_sleep` секунды (через `sleep()`). IP-адрес не блокируется навсегда. Пользователи Restreamer (`is_restreamer`) освобождаются от этого требования.
|
||||
- **Используется:** Потоковой аутентификацией.
|
||||
- **Используется:** Потоковая аутентификация.
|
||||
|
||||
### `truncateAttempts(array $attempts, int $frequency, bool $list = false): array`
|
||||
|
||||
@@ -317,7 +317,7 @@ $_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password'
|
||||
Когда IP-адрес заблокирован:
|
||||
|
||||
- **Обычный режим:** Выполняет вставку в таблицу базы данных `blocked_ips` с указанием причины (`FLOOD ATTACK` или `BRUTEFORCE MAC/USER ATTACK`) и обновляет кэш `BlocklistService`.
|
||||
- **Режим кэширования/потоковой передачи (`$useCachedMode = true`):** Устанавливает сигнал Redis (`bruteforce_attack/$ip` или `flood_attack/$ip`) через `RedisManager::setSignal()` для блокировки контекста потоковой передачи без записи в базу данных.
|
||||
- **Режим кэширования/потоковой передачи (`$useCachedMode = true`):** Устанавливает сигнал Redis (`bruteforce_attack/$ip` или `flood_attack/$ip`) через `RedisManager::setSignal()` для блокировки потокового контекста без записи в базу данных.
|
||||
- В обоих режимах выполняется касание файла-маркера `FLOOD_TMP_PATH . 'block_' . $ip` для быстрой проверки на уровне файловой системы.
|
||||
|
||||
---
|
||||
|
||||
+39
-189
@@ -1,4 +1,4 @@
|
||||
# Инструменты CLI и обновления баз данных
|
||||
# Инструменты CLI и ссылка на консоль
|
||||
|
||||
Справочник по интерфейсу командной строки XC_VM, системным инструментам и процессу обновления базы данных после обновления версии. Содержит описание ежедневных операций, экстренного доступа и создания новых этапов обновления базы данных.
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
|Тип|Рассчитывать|Описание|
|
||||
| --- | --- | --- |
|
||||
|**Команды**|28|Одноразовые операции (обновление, статус, инструменты и т.д.)|
|
||||
|**Закадычные друзья**|25|Запланированные задачи (автоматически вызываемые crontab)|
|
||||
|**Демоны**|8|Длительно выполняющиеся фоновые процессы (команды, использующие `DaemonTrait`)|
|
||||
| **Commands** |28|Одноразовые операции (обновление, статус, инструменты и т.д.)|
|
||||
| **CronJobs** |25|Запланированные задачи (автоматически вызываемые crontab)|
|
||||
| **Daemons** |8|Длительно выполняющиеся фоновые процессы (команды, использующие `DaemonTrait`)|
|
||||
|
||||
> **Примечание:** Демоны - это обычные команды, которые используют `DaemonTrait`. Отдельного каталога `Daemons/` не существует.
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
| `server:install` | `ServerInstallCommand` |Установка/настройка сервера (Proxy/LB) через SSH|корень|
|
||||
| `server:diagnose` | `ServerDiagnoseCommand` |Диагностировать, почему прокси-узел/LB-узел не подключен к главному (частота сердечных сокращений, доступность, iptables, сервис)|корень|
|
||||
|
||||
> Команды, помеченные **необязательно**, условно регистрируются с помощью `file_exists()` guard: `cache_handler`, `server:install`, `migrate`.
|
||||
> `console.php` регистрирует **каждый** класс, который он обнаруживает в `Cli/Commands/` и `Cli/CronJobs/` (глобус + отражение) — есть **нет** `file_exists()` защита. Команда является "необязательной" только в том смысле, что она может быть **снято со сборки LB** (`Makefile` `LB_FILES_TO_REMOVE`) или **обеспечивается установленным модулем**. `plex_item` и `watch_item`, приведенные выше, являются **предоставляемый модулем** (Plex/Watch) — их классы команд отсутствуют в дереве committed core и существуют только тогда, когда этот модуль установлен.
|
||||
|
||||
### Команды демона (постоянные процессы)
|
||||
|
||||
@@ -82,7 +82,9 @@
|
||||
| `record` | `RecordCommand` |Запись потока в формате MP4|
|
||||
| `ondemand` | `OndemandCommand` |Прерывать трансляции без активных зрителей|
|
||||
|
||||
### Задания Cron (всего 26: 22 ядра + 4 модуля)
|
||||
### Задания Cron
|
||||
|
||||
> Таблицы команд/cron/daemon, приведенные ниже, поддерживаются вручную и могут изменяться. Источник truth — `console.php list` - запустите его, чтобы увидеть текущий реестр.
|
||||
|
||||
Все имена заданий cron имеют префикс `cron:`. Для них используется `CronTrait`, и они вызываются системой crontab.
|
||||
|
||||
@@ -102,7 +104,7 @@
|
||||
| `cron:maxmind` | `MaxMindCronJob` |Обновление баз данных MaxMind GeoIP (только по вторникам; `--force` для запуска вручную)|
|
||||
| `cron:providers` | `ProvidersCronJob` |Поставщики обновлений (необязательно)|
|
||||
| `cron:root_mysql` | `RootMysqlCronJob` |Обслуживание базы данных (root, необязательно)|
|
||||
| `cron:root_signals` | `RootSignalsCronJob` |Сигналы обработки, iptables, nginx, управление сервисами и ** двоичное самовосстановление** (root)|
|
||||
| `cron:root_signals` | `RootSignalsCronJob` |Сигналы обработки, iptables, nginx, управление службами и **бинарное самоисцеление** (root)|
|
||||
| `cron:series` | `SeriesCronJob` |Обновление данных серии (необязательно)|
|
||||
| `cron:servers` | `ServersCronJob` |Контролируйте сервер, запускайте демонов, обновляйте статистику|
|
||||
| `cron:stats` | `StatsCronJob` |Вычислять и хранить статистику|
|
||||
@@ -112,29 +114,32 @@
|
||||
| `cron:update` | `UpdateCronJob` |Проверять и применять обновления (необязательно)|
|
||||
| `cron:users` | `UsersCronJob` |Управление подключениями пользователей, синхронизацией Redis, расхождением|
|
||||
| `cron:vod` | `VodCronJob` |Содержание процесса VOD|
|
||||
| `cron:proxy` | `ProxyArchiveCronJob` |Архивирование/ротация потоковых данных прокси-сервера|
|
||||
| `cron:module_licenses` | `ModuleLicensesCronJob` |Обновить лицензии на установленные модули|
|
||||
| `cron:module_updates` | `ModuleUpdatesCronJob` |Проверьте наличие обновлений модуля|
|
||||
| `cron:tmdb` | `TmdbCronJob` |Получение метаданных TMDB (необязательно)|
|
||||
| `cron:tmdb_popular` | `TmdbPopularCronJob` |Выборка популярного содержимого TMDB (необязательно)|
|
||||
|
||||
**Модуль cron заданий** (зарегистрирован через `ModuleInterface::registerCommands()`):
|
||||
**Задания cron, предоставляемые модулем.** Регистрируются дополнительными модулями через `CronProviderInterface::getCronEntries()`; они существуют только тогда, когда этот модуль установлен, и находятся **нет** в дереве committed core (`src/Modules/` отправляются пустыми). (`cron:tmdb`/`cron:tmdb_popular` — это **ядро**, перечисленные выше, а не задания cron модуля.)
|
||||
|
||||
|Команда|Класс|Модуль|Описание|
|
||||
| --- | --- | --- | --- |
|
||||
| `cron:plex` | `PlexCronJob` |сплетение|Обрабатывать обновления Plex|
|
||||
| `cron:tmdb` | `TmdbCronJob` |тмдб|Получение метаданных TMDB (необязательно)|
|
||||
| `cron:tmdb_popular` | `TmdbPopularCronJob` |тмдб|Выборка популярного содержимого TMDB (необязательно)|
|
||||
| `cron:watch` | `WatchCronJob` |часы|Обрабатывать обновления библиотеки отслеживания|
|
||||
|
||||
> Дополнительные задания cron (условно зарегистрированные): `cron:backups`, `cron:cache_engine`, `cron:epg`, `cron:providers`, `cron:root_mysql`, `cron:series`, `cron:tmdb`, `cron:tmdb_popular`, `cron:update`.
|
||||
> "Необязательные" задания cron регистрируются **нет** условно — регистрируется каждый обнаруженный класс `CronJob`. "Необязательно" означает, что задание не выполняется, если не включена его функция/настройка (например, `cron:epg`, `cron:series`, `cron:update`), или если задание не удалено из сборки LB.
|
||||
|
||||
---
|
||||
|
||||
## Бинарное самообновление (самовосстановление)
|
||||
## Двоичное самообновление (самовосстановление)
|
||||
|
||||
Некоторые связанные двоичные файлы ** не** поставляются внутри пакета heavy runtime bundle и могут
|
||||
Некоторые связанные двоичные файлы **нет** поставляются внутри пакета heavy runtime bundle и будут
|
||||
в противном случае никогда не обновляйтесь между выпусками панели (новый узел LB или узел, оставленный включенным
|
||||
старая сборка, никогда не сходилась бы). `cron:root_signals` (root, каждую минуту) сохраняет
|
||||
они становятся текущими путем опроса их идемпотентных команд обновления для каждого двоичного файла на
|
||||
расписание с ограниченным использованием штампов - каждая загрузка выполняется только при несоответствии версии, проверяется
|
||||
контрольная сумма, запуск-тестирует новый двоичный файл, затем заменяет его атомарно (неработающая загрузка
|
||||
никогда не заменяет рабочий). Выполняется на каждом узле (главном ** и** LB).
|
||||
никогда не заменяет рабочий). Выполняется на каждом узле (main **и** LB).
|
||||
|
||||
|Двоичный|Команда|Источник|Проверить|Опрос|
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -189,7 +194,7 @@ class MyNewCommand implements CommandInterface {
|
||||
}
|
||||
```
|
||||
|
||||
Для команд **daemon** также используйте `DaemonTrait`:
|
||||
Для команд **демон** также используйте `DaemonTrait`:
|
||||
|
||||
```php
|
||||
class MyDaemonCommand implements CommandInterface {
|
||||
@@ -198,7 +203,7 @@ class MyDaemonCommand implements CommandInterface {
|
||||
}
|
||||
```
|
||||
|
||||
Для **заданий cron** используйте `CronTrait`:
|
||||
Для **задания cron** используйте `CronTrait`:
|
||||
|
||||
```php
|
||||
class MyCronJob implements CommandInterface {
|
||||
@@ -211,19 +216,12 @@ class MyCronJob implements CommandInterface {
|
||||
}
|
||||
```
|
||||
|
||||
### Шаг 2. Зарегистрируйтесь в console.php
|
||||
### Шаг 2. Регистрация происходит автоматически
|
||||
|
||||
Добавить к `console.php`:
|
||||
|
||||
```php
|
||||
// Always loaded
|
||||
$rRegistry->register(new MyNewCommand());
|
||||
|
||||
// Or conditionally (for optional features)
|
||||
if (file_exists(CLI_PATH . 'Commands/MyNewCommand.php')) {
|
||||
$rRegistry->register(new MyNewCommand());
|
||||
}
|
||||
```
|
||||
Есть **нечего добавить к `console.php`**. При запуске он выдает `Cli/Commands/*.php` и
|
||||
`Cli/CronJobs/*.php` и, посредством отражения, `register()` для каждого неабстрактного класса, реализующего
|
||||
`CommandInterface`. Переместите ваш класс в нужный каталог (с помощью `getName()`, который возвращает
|
||||
его имя команды) — это все, что требуется - смотрите [Подключение ядра → регистрация команды CLI](../development/core-wiring.md#cli-command-registration).
|
||||
|
||||
### Шаг 3. Добавить в Makefile (если LB-исключен)
|
||||
|
||||
@@ -253,14 +251,14 @@ if (file_exists(CLI_PATH . 'Commands/MyNewCommand.php')) {
|
||||
|
||||
|Подкомандование|Описание|
|
||||
| --- | --- |
|
||||
| `rescue` |Создайте временный код аварийного доступа для доступа к панели экстренной помощи. Введите URL-адрес. **Удалите этот код после использования!**|
|
||||
| `rescue` | Create a temporary rescue access code for emergency panel access. Prints the URL. **Delete this code after use!** |
|
||||
| `recaptcha` |Отключите reCAPTCHA (`recaptcha_enable = 0`), чтобы восстановить вход в панель администратора при сбое проверки captcha.|
|
||||
| `access` |Восстановите все настройки кода доступа nginx и перезагрузите nginx. Печатает URL-адреса для всех кодов панели администратора.|
|
||||
| `ports` |Восстановите настройки портов nginx (HTTP, HTTPS, RTMP) из базы данных и перезагрузите nginx.|
|
||||
| `migration` |Очистите промежуточную базу данных (`xc_vm_migrate`) и при необходимости восстановите в ней резервную копию `.sql`.|
|
||||
| `user` |Создайте пользователя rescue admin со случайными учетными данными. Введите имя пользователя и пароль. **Удалите этого пользователя после использования!**|
|
||||
| `mysql` |Повторно авторизуйте привилегии MySQL для всех серверов load balancer.|
|
||||
| `database` |Восстановите пустую базу данных XC_VM из `database.sql`. **Удаляет ВСЕ данные!** Требуется флажок `--confirm`.|
|
||||
| `database` |Восстановите пустую базу данных XC_VM из `database.sql`. **Стирает ВСЕ данные!** Требуется установить флаг `--confirm`.|
|
||||
| `flush` |Очистить все заблокированные IP—адреса - очищает правила iptables, удаляет файлы блокировки и обрезает таблицу `blocked_ips`.|
|
||||
|
||||
### Подкоманды (запускаются как `xc_vm`)
|
||||
@@ -268,7 +266,7 @@ if (file_exists(CLI_PATH . 'Commands/MyNewCommand.php')) {
|
||||
|Подкомандование|Описание|
|
||||
| --- | --- |
|
||||
| `images` |Загрузите отсутствующие изображения потоковых передач/фильмов/сериалов из базы данных TMDB. Сканирует базу данных в поисках URL-адресов изображений и загружает отсутствующие файлы.|
|
||||
| `duplicates` |Найдите и удалите повторяющиеся потоки VOD. Группируйте по идентичному источнику, сохраняйте первый, удаляйте остальные. **Деструктивный!**|
|
||||
| `duplicates` |Найдите и удалите повторяющиеся потоки VOD. Группируйте по идентичному источнику, сначала сохраняйте, а остальные удаляйте. **Разрушительный!**|
|
||||
| `bouquets` |Удалите устаревшие ссылки из букетов. Удаляет идентификаторы, которые больше не существуют в базе данных.|
|
||||
|
||||
### Примеры
|
||||
@@ -314,152 +312,16 @@ su - xc_vm -c '/home/xc_vm/console.php tools duplicates'
|
||||
su - xc_vm -c '/home/xc_vm/console.php tools bouquets'
|
||||
```
|
||||
|
||||
- ❗️ **Внимание:** `duplicates` стримы и все связанные с ними данные (журналы, статистика, эпизоды, записи) удаляются безвозвратно. Всегда создавайте резервную копию перед запуском.
|
||||
- ❗️ **Внимание:** `database --confirm` удаляет всю базу данных и заменяет ее пустой схемой. Это необратимо.
|
||||
- 💡 ** Совет:** После запуска `rescue` всегда удаляйте код через панель администратора или запустив `tools access`, как только вы восстановите доступ.
|
||||
- 💡 ** Совет:** После запуска `user` немедленно измените пароль и по завершении удалите пользователя для восстановления.
|
||||
- ⚠️ **Предупреждение:** `duplicates` стримы и все связанные с ними данные (журналы, статистика, эпизоды, записи) удаляются безвозвратно. Всегда создавайте резервную копию перед запуском.
|
||||
- ⚠️ **Предупреждение:** `database --confirm` удаляет всю базу данных и заменяет ее пустой схемой. Это необратимо.
|
||||
- 💡 **Совет:** После запуска `rescue` всегда удаляйте код через панель администратора или запустив `tools access`, как только вы восстановите доступ.
|
||||
- 💡 **Совет:** После запуска `user` немедленно измените пароль и по завершении удалите пользователя для восстановления.
|
||||
|
||||
---
|
||||
|
||||
## Обновления базы данных после Обновления Версии
|
||||
## Обновления / миграции баз данных
|
||||
|
||||
XC_VM использует файловую систему обновления базы данных для управления изменениями схемы между версиями. Обновления базы данных выполняются автоматически во время обновлений и проверок состояния системы.
|
||||
|
||||
### как это работает
|
||||
|
||||
- SQL-файлы для обновлений базы данных хранятся в `/home/xc_vm/migrations/`.
|
||||
|
||||
- Каждому файлу присваивается имя с префиксом последовательного номера, например:
|
||||
|
||||
```text
|
||||
001_drop_watch_folders_plex_token.sql
|
||||
002_panel_logs_add_file_env.sql
|
||||
003_drop_settings_segment_type.sql
|
||||
```
|
||||
|
||||
- Применяемые шаги обновления базы данных отслеживаются в таблице базы данных `migrations`. Каждый шаг выполняется **ровно один раз** - если шаг уже был применен, он пропускается.
|
||||
|
||||
- Обновления базы данных выполняются автоматически:
|
||||
- `console.php update post-update` - после обновления панели
|
||||
- `console.php status` - во время проверки состояния системы (только для главного сервера)
|
||||
|
||||
### Поток выполнения обновления базы данных
|
||||
|
||||
```text
|
||||
[ MigrationRunner::run() — DB update execution ]
|
||||
│
|
||||
▼
|
||||
[ CREATE TABLE IF NOT EXISTS `migrations` ]
|
||||
│
|
||||
▼
|
||||
[ Read all *.sql files from migrations/ ]
|
||||
│
|
||||
▼
|
||||
[ For each file not in `migrations` table: ]
|
||||
├── Execute SQL statements
|
||||
├── Record in `migrations` table
|
||||
└── Output [OK] or [WARN]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Создание нового шага обновления базы данных
|
||||
|
||||
Когда вам нужно изменить схему базы данных (добавить столбцы, создать таблицы, вставить данные и т.д.), создайте новый SQL-файл для этапа обновления базы данных.
|
||||
|
||||
### Шаг 1. Выберите имя файла
|
||||
|
||||
Используйте следующий порядковый номер и описательное название:
|
||||
|
||||
```text
|
||||
NNN_short_description.sql
|
||||
```
|
||||
|
||||
**Правила форматирования:**
|
||||
|
||||
- Числовой префикс: 3 цифры, дополненные нулем (например, `006`, `007`)
|
||||
- Разделитель: символ подчеркивания `_`
|
||||
- Название: нижний регистр, подчеркивание, описывающее, что делает шаг обновления
|
||||
- Добавочный номер: `.sql`
|
||||
|
||||
**Примеры:**
|
||||
|
||||
```text
|
||||
006_add_user_timezone.sql
|
||||
007_create_audit_log_table.sql
|
||||
008_insert_default_codec_settings.sql
|
||||
```
|
||||
|
||||
### Шаг 2. Напишите SQL-код
|
||||
|
||||
Поместите в файл необработанные инструкции SQL. Несколько инструкций разделяются символом `;`.
|
||||
|
||||
**Правила для этапов обновления базы данных SQL:**
|
||||
|
||||
- **Используйте `IF EXISTS` / `IF NOT EXISTS`**, чтобы сделать шаги обновления базы данных идемпотентными:
|
||||
|
||||
```sql
|
||||
-- Adding a column (safe)
|
||||
ALTER TABLE `settings` ADD COLUMN IF NOT EXISTS `timezone` VARCHAR(64) DEFAULT 'UTC';
|
||||
|
||||
-- Dropping a column (safe)
|
||||
ALTER TABLE `settings` DROP COLUMN IF EXISTS `old_column`;
|
||||
|
||||
-- Creating a table (safe)
|
||||
CREATE TABLE IF NOT EXISTS `audit_log` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`action` VARCHAR(255) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
- **Используйте условное обозначение `INSERT`**, чтобы избежать дублирования:
|
||||
|
||||
```sql
|
||||
INSERT INTO `streams_arguments` (argument_key, argument_name, argument_cmd)
|
||||
SELECT 'my_key', 'My Argument', '-my_flag %s'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `streams_arguments` WHERE argument_key = 'my_key');
|
||||
```
|
||||
|
||||
- **Не смешивайте DDL и DML**, которые зависят друг от друга, в одном файле. Если вам нужно добавить столбец и затем заполнить его, используйте два файла шага обновления базы данных.
|
||||
|
||||
- **Комментарии** поддерживаются с префиксом `--` (они пропускаются во время выполнения).
|
||||
|
||||
### Шаг 3. Поместите файл
|
||||
|
||||
Скопируйте SQL-файл для шага обновления базы данных в:
|
||||
|
||||
```text
|
||||
/home/xc_vm/migrations/
|
||||
```
|
||||
|
||||
> 💡 В хранилище исходных текстов это значение равно `src/migrations/`.
|
||||
|
||||
### Шаг 4. Подтвердите обновление базы данных
|
||||
|
||||
Запустите `db:migrate`, чтобы применить ожидающие обновления БД шаги:
|
||||
|
||||
```bash
|
||||
su - xc_vm -c '/home/xc_vm/console.php db:migrate'
|
||||
```
|
||||
|
||||
Или через `status first-run` (также запускает миграции):
|
||||
|
||||
```bash
|
||||
sudo /home/xc_vm/console.php status first-run
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
Migrations
|
||||
------------------------------
|
||||
[OK] 006_add_user_timezone.sql
|
||||
|
||||
```
|
||||
|
||||
Если инструкция не выполняется, шаг все равно будет записан, но покажет `[WARN]` — проверьте SQL и устраните любые проблемы.
|
||||
Файловая система обновления базы данных (создание шага `.sql`, таблицы `migrations`, потока выполнения `db:migrate`) теперь доступна на отдельной странице — см. [Обновления / миграции баз данных](database-migrations.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -471,7 +333,7 @@ Migrations
|
||||
sudo /home/xc_vm/console.php status
|
||||
```
|
||||
|
||||
Проверяет, запущен ли параметр XC_VM, подключается к базе данных, выполняет ожидающие обновления шаги, исправляет разрешения и проверяет конфигурацию nginx. Требуется после установки или восстановления.
|
||||
Проверяет, запущен ли XC_VM, подключается к базе данных, выполняет ожидающие обновления шаги, исправляет разрешения и проверяет конфигурацию nginx. Требуется после установки или восстановления.
|
||||
|
||||
С аргументом `first-run` пропускает текущую проверку, используемую для начальной настройки:
|
||||
|
||||
@@ -511,7 +373,7 @@ sudo /home/xc_vm/console.php server:diagnose <server_id>
|
||||
sudo /home/xc_vm/console.php server:diagnose
|
||||
```
|
||||
|
||||
Выясняет, почему прокси—узел/LB—узел отображается в автономном режиме на панели: проверяет частоту сердечных сокращений, доступность (ICMP/TCP/HTTP `/api`), перекос часов, очередь сигналов и - локально на узле - выполняет ли узел брандмауэр на главном IP-адресе в своем собственном iptables, запущена ли служба `xc_vm`/nginx, запущен ли демон heartbeat `watchdog` и находится ли `cron:servers` в crontab `xc_vm`. Доступно только для чтения; код выхода `0` = проблем не обнаружено, `2` = указаны вероятные причины. Более подробную информацию смотрите в [Руководстве по диагностике сервера](../administration/server-diagnostics.md).
|
||||
Обнаруживает **почему?** прокси—сервер/LB—узел, отображаемый в автономном режиме на панели: проверяет частоту сердечных сокращений, доступность (ICMP/TCP/HTTP `/api`), перекос часов, очередь сигналов и - локально на узле - выполняет ли узел брандмауэр на главном IP-адресе в своем собственном iptables, запущена ли служба `xc_vm`/nginx, запущен ли демон сердцебиения `watchdog` и находится ли `cron:servers` в crontab `xc_vm`. Доступно только для чтения; код выхода `0` = проблем не обнаружено, `2` = указаны вероятные причины. Более подробную информацию смотрите в [Руководстве по диагностике сервера](../administration/server-diagnostics.md).
|
||||
|
||||
### SSL-сертификат
|
||||
|
||||
@@ -519,21 +381,9 @@ sudo /home/xc_vm/console.php server:diagnose
|
||||
sudo /home/xc_vm/console.php certbot
|
||||
```
|
||||
|
||||
### Применяйте перенос базы данных вручную
|
||||
### Миграция баз данных
|
||||
|
||||
```bash
|
||||
su - xc_vm -c '/home/xc_vm/console.php db:migrate'
|
||||
```
|
||||
|
||||
Применяет все ожидающие `.sql` файлы из `/home/xc_vm/migrations/`. Используйте это, когда вам нужно выполнить миграцию без полного обновления системы.
|
||||
|
||||
### Обновление базы данных данными из других систем
|
||||
|
||||
```bash
|
||||
/home/xc_vm/console.php migrate
|
||||
```
|
||||
|
||||
Передает данные из промежуточной базы данных `xc_vm_migrate`. Подробности см. в [Руководстве по обновлению базы данных](../info/migration_guide.md).
|
||||
Выполните ожидающие действия `.sql` вручную или импортируйте данные из другой системы — см. [Обновления / миграции базы данных](database-migrations.md#applying-migrations-manually).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# Обновления / миграции баз данных
|
||||
|
||||
XC_VM использует файловую систему обновления базы данных для управления изменениями схемы между версиями. Обновления базы данных запускаются автоматически при обновлении панели управления и проверке состояния системы и могут быть применены вручную с помощью `db:migrate`.
|
||||
|
||||
> Информацию о точке входа в консоль, реестре команд и о том, как зарегистрировать команду, смотрите в [CLI Tools & Console Reference](cli-tools.md).
|
||||
|
||||
---
|
||||
|
||||
## как это работает
|
||||
|
||||
- SQL-файлы для обновлений базы данных хранятся в `/home/xc_vm/migrations/` (`src/migrations/` в репозитории исходных текстов).
|
||||
|
||||
- Каждому файлу присваивается имя с префиксом последовательного номера, например:
|
||||
|
||||
```text
|
||||
001_drop_watch_folders_plex_token.sql
|
||||
002_panel_logs_add_file_env.sql
|
||||
003_drop_settings_segment_type.sql
|
||||
```
|
||||
|
||||
- Применяемые шаги обновления базы данных отслеживаются в таблице базы данных `migrations`. Каждый шаг выполняется **ровно один раз** — если какой-либо шаг уже был применен, он пропускается. Нет пути возврата: миграции выполняются только в прямом направлении, поэтому по возможности поддерживайте их обратную совместимость.
|
||||
|
||||
- Обновления базы данных выполняются автоматически:
|
||||
- `console.php update post-update` — после обновления панели
|
||||
- `console.php status` — во время проверки состояния системы (только для главного сервера)
|
||||
|
||||
Основная логика находится в `MigrationRunner` (`src/Core/Database/MigrationRunner.php`).
|
||||
|
||||
### Поток выполнения обновления базы данных
|
||||
|
||||
```text
|
||||
[ MigrationRunner::run() — DB update execution ]
|
||||
│
|
||||
▼
|
||||
[ CREATE TABLE IF NOT EXISTS `migrations` ]
|
||||
│
|
||||
▼
|
||||
[ Read all *.sql files from migrations/ ]
|
||||
│
|
||||
▼
|
||||
[ For each file not in `migrations` table: ]
|
||||
├── Execute SQL statements
|
||||
├── Record in `migrations` table
|
||||
└── Output [OK] (recorded) or [FAIL] (not recorded)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Создание нового шага обновления базы данных
|
||||
|
||||
Когда вам нужно изменить схему базы данных (добавить столбцы, создать таблицы, вставить данные и т.д.), создайте новый SQL-файл для этапа обновления базы данных.
|
||||
|
||||
### Шаг 1. Выберите имя файла
|
||||
|
||||
Используйте следующий порядковый номер и описательное название:
|
||||
|
||||
```text
|
||||
NNN_short_description.sql
|
||||
```
|
||||
|
||||
**Format rules:**
|
||||
|
||||
- Числовой префикс: 3 цифры, дополненные нулем (например, `006`, `007`)
|
||||
- Разделитель: символ подчеркивания `_`
|
||||
- Название: нижний регистр, подчеркивание, описывающее, что делает шаг обновления
|
||||
- Добавочный номер: `.sql`
|
||||
|
||||
**Examples:**
|
||||
|
||||
```text
|
||||
006_add_user_timezone.sql
|
||||
007_create_audit_log_table.sql
|
||||
008_insert_default_codec_settings.sql
|
||||
```
|
||||
|
||||
### Шаг 2. Напишите SQL-код
|
||||
|
||||
Поместите в файл необработанные инструкции SQL. Несколько инструкций разделяются символом `;`.
|
||||
|
||||
**Rules for SQL DB update steps:**
|
||||
|
||||
- **Используйте `IF EXISTS` / `IF NOT EXISTS`** чтобы сделать шаги обновления базы данных идемпотентными:
|
||||
|
||||
```sql
|
||||
-- Adding a column (safe)
|
||||
ALTER TABLE `settings` ADD COLUMN IF NOT EXISTS `timezone` VARCHAR(64) DEFAULT 'UTC';
|
||||
|
||||
-- Dropping a column (safe)
|
||||
ALTER TABLE `settings` DROP COLUMN IF EXISTS `old_column`;
|
||||
|
||||
-- Creating a table (safe)
|
||||
CREATE TABLE IF NOT EXISTS `audit_log` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`action` VARCHAR(255) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
- **Использовать условное значение `INSERT`** чтобы избежать дублирования:
|
||||
|
||||
```sql
|
||||
INSERT INTO `streams_arguments` (argument_key, argument_name, argument_cmd)
|
||||
SELECT 'my_key', 'My Argument', '-my_flag %s'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `streams_arguments` WHERE argument_key = 'my_key');
|
||||
```
|
||||
|
||||
- **Не смешивайте DDL и DML**, которые зависят друг от друга в одном файле. Если вам нужно добавить столбец и затем заполнить его, используйте два файла шагов обновления базы данных.
|
||||
|
||||
- **Комментарии** поддерживаются с префиксом `--` (они пропускаются во время выполнения).
|
||||
|
||||
> Idempotency matters because a failed step is **not** recorded — it prints `[FAIL] <name> (not recorded — will retry on next run)` and re-runs on the **next** `db:migrate`. A non-idempotent step that half-applied before failing will be retried from the top, so every statement must be safe to run again (use `IF NOT EXISTS`, `INSERT ... ON DUPLICATE KEY UPDATE`, etc.).
|
||||
|
||||
### Шаг 3. Поместите файл
|
||||
|
||||
Скопируйте SQL-файл для шага обновления базы данных в:
|
||||
|
||||
```text
|
||||
/home/xc_vm/migrations/
|
||||
```
|
||||
|
||||
> 💡 В хранилище исходных текстов это значение равно `src/migrations/`.
|
||||
|
||||
### Шаг 4. Подтвердите обновление базы данных
|
||||
|
||||
Запустите `db:migrate`, чтобы применить ожидающие обновления БД шаги:
|
||||
|
||||
```bash
|
||||
su - xc_vm -c '/home/xc_vm/console.php db:migrate'
|
||||
```
|
||||
|
||||
Или через `status first-run` (также запускает миграции):
|
||||
|
||||
```bash
|
||||
sudo /home/xc_vm/console.php status first-run
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
Migrations
|
||||
------------------------------
|
||||
[OK] 006_add_user_timezone.sql
|
||||
|
||||
```
|
||||
|
||||
Если оператор завершается ошибкой, шаг выводит значение `[FAIL]` и записывается значение **нет**, поэтому он будет повторен при следующем запуске. Просмотрите SQL, исправьте его и запустите повторно `db:migrate`.
|
||||
|
||||
---
|
||||
|
||||
## Применение Миграций вручную
|
||||
|
||||
Применить все ожидающие `.sql` файлы из `/home/xc_vm/migrations/` без полного обновления системы:
|
||||
|
||||
```bash
|
||||
su - xc_vm -c '/home/xc_vm/console.php db:migrate'
|
||||
```
|
||||
|
||||
### Перенос данных из другой системы
|
||||
|
||||
Перенести данные из промежуточной базы данных `xc_vm_migrate`:
|
||||
|
||||
```bash
|
||||
/home/xc_vm/console.php migrate
|
||||
```
|
||||
|
||||
Более подробную информацию смотрите в [Руководстве по обновлению базы данных](../info/migration_guide.md).
|
||||
|
||||
---
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|Файл|Роль|
|
||||
| --- | --- |
|
||||
| `src/migrations/` |Перенос файлов базы данных `.sql`|
|
||||
| `src/Core/Database/MigrationRunner.php` |Выполняет отложенные миграции, записывает таблицу `migrations`|
|
||||
| `src/console.php` |`db:migrate` / `status` / `migrate` точка входа|
|
||||
@@ -6,14 +6,16 @@
|
||||
|
||||
## Локальная настройка
|
||||
|
||||
Зафиксированное значение `src/vendor/` предназначено только для производства, поэтому инструменты разработки (PHPStan,
|
||||
**Предпосылки:** PHP **8.1** ( коды кодовой базы `php: 8.1.33`; более новые версии не поддерживаются) и Composer доступны локально.
|
||||
|
||||
Зафиксированное значение `src/vendor/` равно **только для производства**, поэтому инструменты разработки (PHPStan,
|
||||
phpc) отсутствуют в дереве. Установите их один раз из зафиксированной блокировки:
|
||||
|
||||
```bash
|
||||
make dev-tools # = cd src && composer install
|
||||
```
|
||||
|
||||
Это добавит пакеты `require-dev` в `src/vendor/`. **Никогда не фиксируйте их** —
|
||||
Это добавит пакеты `require-dev` в пакеты `src/vendor/`. **Никогда не совершайте их** —
|
||||
зарегистрированный поставщик должен оставаться доступным только для производства (`composer install --no-dev`).
|
||||
`.gitignore` не допускает попадания пакетов разработчика в `git add`, а CI-шлюз
|
||||
(`check-vendor-prod-only`) завершается сбоем сборки, если она когда-либо была зафиксирована.
|
||||
@@ -28,15 +30,20 @@ make dev-tools # = cd src && composer install
|
||||
| `make cs` |Стиль кода — импорт/гигиена пространства имен (phpcs + Slevomat)|
|
||||
| `make cs-fix` |Примените исправления стиля на месте|
|
||||
| `make gates` |PSR-4 регрессионные параметры (ниже)|
|
||||
| `php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist` |Модульные тесты|
|
||||
| `php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist` |Модульные тесты — смотрите [Настройка PHPUnit](phpunit-phar.md)|
|
||||
|
||||
для `make phpstan` и `make cs` нужны инструменты разработчика — сначала запустите `make dev-tools`.
|
||||
|
||||
Базовый уровень PHPStan имеет значение `build/phpstan-baseline.neon` — он замораживает все *ранее существовавшие*
|
||||
проблемы, так что только **новое** из них не проходят CI. Если вы намеренно измените уровень или примете пакет
|
||||
найдя, восстановите его с помощью `make phpstan-baseline` и зафиксируйте результат. Не восстанавливайте его
|
||||
просто чтобы заглушить настоящую новую ошибку — исправьте код.
|
||||
|
||||
`make gates` связывает трех охранников:
|
||||
|
||||
- **проверка-процедурное использование ** - процедурные файлы / файлы просмотра импортируют каждый перенесенный класс, который они используют (PHP импорт является позиционным, поэтому `use` должно предшествовать использованию);
|
||||
- **verify-lb-archive** — сборка балансировщика нагрузки исключает привилегированный код (контроллеры администратора/реселлера, домен пользователя/устройства, команды установки/root).;
|
||||
- **проверка-только для поставщика-продукта** - ни один пакет `require-dev` не зафиксирован в соответствии с `src/vendor/`.
|
||||
- **проверка-процедура-использование** — процедурные файлы / файлы просмотра импортируют каждый перенесенный класс, который они используют (PHP импорт является позиционным, поэтому `use` должен предшествовать использованию).;
|
||||
- **проверить-lb-архив** — сборка балансировщика нагрузки исключает привилегированный код (контроллеры администратора/реселлера, домен пользователя/устройства, команды установки/root) — смотрите [Система сборки (MAIN vs LB)](../builds/build_system.md) для определения границы исключения;
|
||||
- **проверка-только для поставщика-продукта** — ни один пакет `require-dev` не зафиксирован в соответствии с `src/vendor/`.
|
||||
|
||||
## Развертывание кода в VDS через SFTP
|
||||
|
||||
@@ -114,7 +121,12 @@ make dev-tools # = cd src && composer install
|
||||
- **`uploadOnSave: true`** — каждое сочетание клавиш Ctrl+S мгновенно переносит файл в VDS
|
||||
- **`ignore`** — защищает файлы, зависящие от сервера. (`bin/`, `config/`, `tmp/`)
|
||||
|
||||
> ** Безопасность:** Используйте SSH-ключи вместо пароля. Каталог `.vscode/` находится в каталоге `.gitignore`, поэтому учетные данные не попадут в git.
|
||||
> ⚠️ **`watcher.autoDelete: true`** — локальное удаление файла приводит к его удалению и на VDS. Удобный
|
||||
> для поддержания синхронизации дерева, но неправильно удаленный локальный файл (или неправильное переименование) приведет к удалению
|
||||
> удаленное копирование. Сохраняйте список `ignore` неизменным или установите для него значение `false`, если вы не хотите, чтобы наблюдатель
|
||||
> для распространения удалений.
|
||||
|
||||
> **Безопасность:** Используйте SSH-ключи вместо пароля. Каталог `.vscode/` находится в каталоге `.gitignore`, поэтому учетные данные не попадут в git.
|
||||
|
||||
### Как синхронизировать папку с тестами
|
||||
|
||||
@@ -136,6 +148,6 @@ make dev-tools # = cd src && composer install
|
||||
|
||||
|Файл|Роль|
|
||||
| --- | --- |
|
||||
| `.vscode/sftp.json` |Локальная настройка → Настройка синхронизации VDS (gitignored)|
|
||||
| `.vscode/sftp.json` |Локальный → Конфигурация синхронизации VDS (gitignored)|
|
||||
| `Makefile` |`make dev-tools`, `make phpstan`, `make cs`, `make gates`|
|
||||
| `src/composer.json` |Зависимости + PSR-4 автозагрузка|
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Обнаружение устройств и блокировка STB
|
||||
|
||||
XC_VM анализирует клиентский пользовательский агент (Mobile_Detect) для адаптации пользовательского интерфейса администратора и привязывает учетные записи приставок к определенному оборудованию, чтобы строка не могла использоваться совместно на разных устройствах. Эти проверки выполняются параллельно с проверками географического местоположения в `src/Public/stream/auth.php`.
|
||||
|
||||
> Информацию о геолокации GeoIP/ISP/ASN, геомаршрутизации и обновлениях MaxMind смотрите на сопутствующей странице [GeoIP, Определение провайдера и геомаршрутизация](geoip-isp-and-geo-routing.md). Для получения информации о взаимодействии Stalker/Ministra portal, которое управляет профилями STB, смотрите [Ministra Эмуляцию STB](ministra-browser-emulation.md).
|
||||
|
||||
---
|
||||
|
||||
## Мобильное обнаружение
|
||||
|
||||
Файл: `src/vendor/mobiledetect/mobiledetectlib/src/MobileDetect.php` (Composer зависимость `mobiledetect/mobiledetectlib`)
|
||||
|
||||
Библиотека (версия 4.9.0, пространство имен `Detection\MobileDetect`) для синтаксического анализа пользовательским агентом:
|
||||
|
||||
```php
|
||||
$detect = new \Detection\MobileDetect();
|
||||
$detect->isMobile(); // phones
|
||||
$detect->isTablet(); // tablets
|
||||
$detect->isAndroid(); // brand-specific
|
||||
```
|
||||
|
||||
Используется в `src/bootstrap.php` для установки флага `$rMobile`, который переключает панель администратора/реселлера на адаптивную (мобильную) компоновку. Это **Только для пользовательского интерфейса** — это не влияет на доступ к потоковой передаче; управление доступом к STB - это логика аппаратной блокировки, описанная ниже.
|
||||
|
||||
---
|
||||
|
||||
## Устройства для телевизионной приставки (STB)
|
||||
|
||||
Две службы управляют учетными записями STB и их полями аппаратной блокировки. Каждое устройство представляет собой строку (MAG → `mag_devices`, Enigma2 → `enigma2_devices`), привязанную к строке.
|
||||
|
||||
**Энигмасервис** (`src/Domain/Device/EnigmaService.php`) — Стандартные файлы Enigma2.
|
||||
Поля блокировки: `token`, `lversion`, `cpu`, `enigma_version`, `modem_mac`, `local_ip`.
|
||||
|
||||
**Магсервис** (`src/Domain/Device/MagService.php`) — МАГНИТНЫЕ STBS.
|
||||
Поля блокировки: `ver`, `device_id2`, `device_id`, `hw_version`, `image_version`, `stb_type`, `sn`.
|
||||
|
||||
Оба типа устройств имеют три общих переключателя принудительного исполнения:
|
||||
|
||||
|Поле|Эффект|
|
||||
| --- | --- |
|
||||
| `lock_device` |аппаратная блокировка — привязывает учетную запись к идентификаторам, полученным при первом подключении.|
|
||||
| `is_isplock` |Привязка к интернет-провайдеру (смотрите страницу с географией)|
|
||||
| `forced_country` |принудительно подключите устройство к определенной стране (см. страницу с географией).|
|
||||
|
||||
### Где он настроен
|
||||
|
||||
В панели администратора: страницы **МАГНИТНЫЕ устройства** и **Устройства "Энигма"** — добавьте/отредактируйте строку устройства, переключите **Запирающее устройство** и (для порталов Ministra) установите разрешенные типы STB с помощью `allowed_stb_types`. Набор типов STB, которые портал рекламирует в окне, передается из настроек панели через `PortalHandler` в профиль клиента (см. [Ministra Эмуляция STB](ministra-browser-emulation.md)).
|
||||
|
||||
### Как осуществляется принудительная блокировка
|
||||
|
||||
1. **Сначала подключитесь** — при наличии `lock_device = 1` и отсутствии сохраненных идентификаторов аппаратные идентификаторы устройства (например, MAG `device_id`/`device_id2`/`sn`, Enigma `modem_mac`) записываются в строку устройства.
|
||||
2. **Последующие соединения** — значения, представленные в запросе/токене, должны совпадать с сохраненными. При несоответствии происходит сбой аутентификации с помощью `DEVICE_NOT_ALLOWED` (или `TOKEN_EXPIRED`, когда токен портала больше не соответствует заблокированному устройству).
|
||||
3. **шлюз stb_type** — если задано значение `allowed_stb_types`, то указанное в поле значение `stb_type` должно быть одним из допустимых значений, в противном случае портал отклоняет его.
|
||||
|
||||
**Отработанный пример (MAG):** реселлер создает учетную запись с `lock_device = 1`. Ящик клиента подключается один раз → его `device_id`/`sn` сохраняются. Если клиент копирует URL-адрес портала + MAC-адрес во второе поле, в этом поле отображается другое значение `device_id`/`sn` → auth, возвращаемое `DEVICE_NOT_ALLOWED`. Чтобы переместить строку в новое поле, администратор удаляет сохраненные идентификаторы (отредактируйте строку устройства), чтобы при следующем подключении они были восстановлены.
|
||||
|
||||
---
|
||||
|
||||
## Проверки контроля доступа (устройства)
|
||||
|
||||
Они запускаются в `src/Public/stream/auth.php` во время проверки токена, после проверки географического местоположения (1-4, на странице [geo page](geoip-isp-and-geo-routing.md)).
|
||||
|
||||
### 5. Блокировка пользовательского агента
|
||||
|
||||
```text
|
||||
check against BlocklistService::checkBlockedUAs()
|
||||
error: BLOCKED_USER_AGENT
|
||||
|
||||
if user has allowed_ua set:
|
||||
user_agent must match one entry
|
||||
error: NOT_IN_ALLOWED_UAS
|
||||
```
|
||||
|
||||
Если параметр `disallow_empty_user_agents` включен, запрос без заголовка `User-Agent` будет немедленно отклонен.
|
||||
|
||||
### 6. Проверка типа устройства
|
||||
|
||||
```text
|
||||
MAG device flag must match token
|
||||
error: DEVICE_NOT_ALLOWED or TOKEN_EXPIRED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация (настройки устройства)
|
||||
|
||||
|Установка|Тип|Описание|
|
||||
| --- | --- | --- |
|
||||
| `disallow_empty_user_agents` | `0/1` |отклонять запросы без заголовка User-Agent|
|
||||
| `allowed_stb_types` | `array` |Типы STB, которые будет принимать портал Ministra (пустые = все)|
|
||||
|
||||
Настройки для каждого устройства (`lock_device`, `is_isplock`, `forced_country`) устанавливаются в строке устройства, а не в глобальных настройках.
|
||||
|
||||
---
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|Файл|Цель|
|
||||
| --- | --- |
|
||||
| `src/vendor/mobiledetect/mobiledetectlib/src/MobileDetect.php` |`\Detection\MobileDetect` Библиотека агента пользователя (Composer dep)|
|
||||
| `src/Domain/Device/EnigmaService.php` |Управление STB-файлами Enigma2 + поля блокировки|
|
||||
| `src/Domain/Device/MagService.php` |Управление магнитными полями STB + блокировка полей|
|
||||
| `src/Public/stream/auth.php` |потоковая авторизация с проверкой устройства (5-6)|
|
||||
| `src/Domain/Security/BlocklistService.php` |заблокированные / разрешенные проверки пользовательского агента|
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
### `getPIDs(int $rServerID): array`
|
||||
|
||||
Анализирует информацию о системном процессе из ответа API сервера. Возвращает структурированные данные о процессе для мониторинга.
|
||||
Анализирует информацию о системном процессе из ответа API сервера. Возвращает структурированные данные процесса для мониторинга.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
XC_VM обработка ошибок состоит из трех уровней:
|
||||
|
||||
- **Коды ошибок** -- в чем произошел сбой (централизованный реестр именованных строк ошибок)
|
||||
- **Обработчики ошибок** -- как генерируется HTTP-ответ клиента (`generateError()`, `generate404()`)
|
||||
- **Подсистема ведения журнала** - фиксация во время выполнения ошибок PHP, неперехваченных исключений и фатальных сбоев
|
||||
- **Коды ошибок** -- что не удалось (централизованный реестр именованных строк ошибок)
|
||||
- **Обработчики ошибок** -- как формируется HTTP-ответ клиента (`generateError()`, `generate404()`)
|
||||
- **Подсистема регистратора** -- фиксация во время выполнения PHP ошибок, неперехваченных исключений и фатальных сбоев
|
||||
|
||||
---
|
||||
|
||||
@@ -196,7 +196,7 @@ Logger::init(bool $showErrors, string $logFile): void
|
||||
|
||||
### Отображение уровня ошибок
|
||||
|
||||
`Logger::handleError()` сопоставляет PHP константы ошибок со строками уровня журнала через `mapErrorLevel()`:
|
||||
`Logger::handleError()` сопоставляет PHP константы ошибок со строками уровня журнала с помощью `mapErrorLevel()`:
|
||||
|
||||
|PHP константа(ы)|Уровень регистрации|
|
||||
| --- | --- |
|
||||
@@ -253,8 +253,8 @@ Each log entry is written as a single line: `base64_encode(json_encode($data))`
|
||||
|
||||
Когда `$showErrors` равно `true`, регистратор также отображает ошибки напрямую:
|
||||
|
||||
- **CLI:** клеммный выход с цветовой кодировкой (красный - НЕИСПРАВИМОСТЬ/ОШИБКА, желтый - ПРЕДУПРЕЖДЕНИЕ, синий - УВЕДОМЛЕНИЕ)
|
||||
- **Веб:** встроенный `<div>` с моноширинным шрифтом, красной рамкой и трассировкой стека в блоке `<pre>`
|
||||
- **КЛИ:** выходной сигнал терминала с цветовой кодировкой (красный - НЕИСПРАВИМОСТЬ/ОШИБКА, желтый - ПРЕДУПРЕЖДЕНИЕ, синий - УВЕДОМЛЕНИЕ).
|
||||
- **Сеть:** встроенный `<div>` с моноширинным шрифтом, красной рамкой и трассировкой стека в блоке `<pre>`
|
||||
|
||||
---
|
||||
|
||||
@@ -262,10 +262,10 @@ Each log entry is written as a single line: `base64_encode(json_encode($data))`
|
||||
|
||||
Программа ведения журнала записывает данные в файл `error_log.log` на диске. Отдельная подсистема считывает этот файл и сохраняет записи в таблице базы данных `panel_logs`:
|
||||
|
||||
1. **Регистратор** записывает строки JSON в кодировке base64 в `error_log.log`
|
||||
2. **FileLogger** (`src/Core/Logging/FileLogger.php`) предоставляет дополнительный интерфейс ведения журнала, используемый кодом приложения (ошибки PDO, ошибки EPG и т.д.), который записывает данные в тот же файл в том же формате
|
||||
1. **Лесоруб** записывает строки JSON в кодировке base64 в `error_log.log`
|
||||
2. **Файловый регистратор** (`src/Core/Logging/FileLogger.php`) предоставляет дополнительный интерфейс ведения журнала, используемый кодом приложения (ошибки PDO, ошибки EPG и т.д.), который записывает данные в тот же файл в том же формате
|
||||
3. Записи заносятся в таблицу `panel_logs`
|
||||
4. **DiagnosticsService** (`src/Core/Diagnostics/DiagnosticsService.php`) считывает данные из `panel_logs` для:
|
||||
4. **Диагностическая служба** (`src/Core/Diagnostics/DiagnosticsService.php`) считывается из `panel_logs` для:
|
||||
- `downloadPanelLogs()` -- извлекает до 1000 последних ошибок, не связанных с EPG, затем обрезает таблицу
|
||||
- `submitPanelLogs()` -- отправляет логи на центральный сервер API для анализа
|
||||
5. Панель администратора отображает эти журналы в разделе **Управление > Журналы > Ошибки панели**
|
||||
@@ -300,11 +300,11 @@ Each log entry is written as a single line: `base64_encode(json_encode($data))`
|
||||
|
||||
|Класс исключений|Базовый класс|Местоположение|
|
||||
| --- | --- | --- |
|
||||
| `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` |
|
||||
| `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`.
|
||||
|
||||
|
||||
@@ -27,7 +27,24 @@ define('DB_ACCESS_ENABLED', false); // enables phpMiniAdmin tab/page in admin pa
|
||||
```
|
||||
|
||||
`DB_ACCESS_ENABLED` управляет доступом к phpMiniAdmin только из пользовательского интерфейса администратора.
|
||||
Это не блокирует подключения к базе данных основного приложения.
|
||||
Это не блокирует подключения к базе данных основного приложения. Его спутник `DB_ACCESS_PWD`
|
||||
(также в `AppConfig.php`) устанавливает пароль, защищающий эту страницу — оставьте его пустым, чтобы сохранить
|
||||
отключите вкладку, устанавливайте строгое значение только тогда, когда оно вам нужно.
|
||||
|
||||
### `DEV_MODE`
|
||||
|
||||
```php
|
||||
define('DEV_MODE', false); // master development-mode flag
|
||||
```
|
||||
|
||||
`DEV_MODE` - это параметр разработки во время компиляции (`bootstrap.php` преобразует его в `self::$devMode`).
|
||||
Когда `true` включается `PHP_ERRORS` (подробные ошибки на экране), запускается диагностика повторной проверки,
|
||||
и обеспечивает другие удобства для разработчиков.
|
||||
|
||||
> ⚠️ **Никогда не включайте `DEV_MODE` или `debug_show_errors` в рабочей среде** — оба раскрывают внутренние
|
||||
> ошибки/пути к посетителям. `PHP_ERRORS` заканчивается на `true`, если **любой** константа `DEV_MODE` равна
|
||||
> установлено (путь начальной загрузки) **или** значение `debug_show_errors` включено (путь защиты запроса); это
|
||||
> правило разрешения, когда они накладываются друг на друга.
|
||||
|
||||
---
|
||||
|
||||
@@ -52,11 +69,15 @@ define('DB_ACCESS_ENABLED', false); // enables phpMiniAdmin tab/page in admin pa
|
||||
|
||||
```php
|
||||
define('DB_ACCESS_ENABLED', false);
|
||||
define('XC_VM_VERSION', '2.2.1');
|
||||
define('DB_ACCESS_PWD', ''); // password for the phpMiniAdmin tab (empty = off)
|
||||
define('DEV_MODE', false); // master development-mode switch
|
||||
define('XC_VM_VERSION', '2.4.1'); // bumped every release — treat as illustrative
|
||||
define('GIT_OWNER', 'Vateron-Media');
|
||||
define('GIT_REPO_MAIN', 'XC_VM');
|
||||
define('GIT_REPO_UPDATE', 'XC_VM_Update');
|
||||
define('GIT_REPO_BIN', 'XC_VM_Binaries');
|
||||
define('GIT_REPO_FANOUT', 'XC_VM_Fanout'); // xc_fanout daemon source + binaries
|
||||
define('GIT_REPO_PROXY', 'XC_VM_Proxy');
|
||||
define('MONITOR_CALLS', 3);
|
||||
define('OPENSSL_EXTRA', '...');
|
||||
```
|
||||
@@ -65,10 +86,14 @@ define('OPENSSL_EXTRA', '...');
|
||||
|
||||
## Добавление новых флагов
|
||||
|
||||
Используйте статические константы в `AppConfig.php` для фиксированных констант инфраструктуры/среды выполнения.
|
||||
Используйте настройки (`$rSettings`) для значений, которыми необходимо управлять из пользовательского интерфейса панели.
|
||||
Используйте статические константы в `AppConfig.php` для фиксированных констант инфраструктуры/среды выполнения (отредактированных в
|
||||
код, вступающий в силу при следующем запросе). Используйте настройки (`$rSettings`) для значений, которые оператор переключает
|
||||
из панели администратора (страница **Настройки**) — они сохраняются в таблице `settings` базы данных и
|
||||
считывается с `CACHE_TMP_PATH/settings`.
|
||||
|
||||
Избегайте определения одного и того же поведения в обоих местах.
|
||||
Избегайте определения одного и того же поведения в обоих местах. Когда они неизбежно пересекаются (как в случае
|
||||
`DEV_MODE` против `debug_show_errors` → `PHP_ERRORS`), эффективным значением является **операционная** из двух —
|
||||
выигрывает любой из вариантов, включающий его.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+17
-73
@@ -1,7 +1,8 @@
|
||||
# GeoIP и обнаружение устройства
|
||||
# GeoIP, Обнаружение интернет-провайдера и гео-маршрутизация
|
||||
|
||||
XC_VM использует базы данных MaxMind GeoIP2/GeoLite2 для определения геолокации и интернет-провайдера, а также библиотеку Mobile_Detect для анализа пользовательского агента.
|
||||
Эти системы интегрированы в потоковую аутентификацию для контроля доступа, географической маршрутизации и ведения журнала действий.
|
||||
XC_VM использует базы данных MaxMind GeoIP2/GeoLite2 для определения геолокации и провайдера / ASN. Они обеспечивают потоковую аутентификацию (контроль доступа к стране/провайдеру/ASN/прокси-серверу), выбор географического сервера + прокси-сервера и ведение журнала активности.
|
||||
|
||||
> Для анализа с помощью User-Agent и аппаратной блокировки телеприставки смотрите сопутствующую страницу [Обнаружение устройств и блокировка STB](device-detection-and-stb-locking.md). И эта страница, и предыдущая сходятся в `src/Public/stream/auth.php`.
|
||||
|
||||
---
|
||||
|
||||
@@ -66,44 +67,9 @@ AND enable_isp_lock = 1
|
||||
|
||||
---
|
||||
|
||||
## Обнаружение устройств
|
||||
## Проверки контроля доступа (geo)
|
||||
|
||||
### Мобильное обнаружение
|
||||
|
||||
Файл: `src/Core/Device/MobileDetect.php`
|
||||
|
||||
Библиотека (версия 2.8.45) для анализа пользовательского агента:
|
||||
|
||||
```php
|
||||
$detect = new Mobile_Detect();
|
||||
$detect->isMobile(); // phones
|
||||
$detect->isTablet(); // tablets
|
||||
$detect->isAndroid(); // brand-specific
|
||||
```
|
||||
|
||||
Используется в `src/bootstrap.php` для обнаружения мобильных устройств с адаптивным интерфейсом администратора.
|
||||
|
||||
### Телевизионные приставки
|
||||
|
||||
**Энигмасервис** (`src/Domain/Device/EnigmaService.php`):
|
||||
|
||||
Управляет учетными записями Enigma2 STB. Блокирует поля: `token`, `lversion`, `cpu`, `enigma_version`, `modem_mac`, `local_ip`.
|
||||
|
||||
**Магсервис** (`src/Domain/Device/MagService.php`):
|
||||
|
||||
Управляет учетными записями MAG STB. Блокирует поля: `ver`, `device_id2`, `device_id`, `hw_version`, `image_version`, `stb_type`, `sn`.
|
||||
|
||||
Обе поддержки:
|
||||
|
||||
- `lock_device` — аппаратная блокировка
|
||||
- `is_isplock` — Привязка к провайдеру
|
||||
- `forced_country` — принудительный перевод пользователя в определенную страну
|
||||
|
||||
---
|
||||
|
||||
## Проверки контроля доступа
|
||||
|
||||
Все проверки выполняются в `src/www/stream/auth.php` во время проверки токена:
|
||||
Они выполняются в режиме `src/Public/stream/auth.php` во время проверки токена. Проверки 5-6 (Пользовательский агент, тип устройства) отображаются на странице [Обнаружение устройства и блокировка STB](device-detection-and-stb-locking.md).
|
||||
|
||||
### 1. Валидация в стране
|
||||
|
||||
@@ -141,25 +107,7 @@ GeoIPService::matchCIDR($asn, $ip)
|
||||
flag[4] = proxy → error: PROXY_DETECT
|
||||
```
|
||||
|
||||
Также проверяет заголовок `X-XC_VM-DETECT` для обнаружения повторного потока.
|
||||
|
||||
### 5. Блокировка пользовательского агента
|
||||
|
||||
```text
|
||||
check against BlocklistService::checkBlockedUAs()
|
||||
error: BLOCKED_USER_AGENT
|
||||
|
||||
if user has allowed_ua set:
|
||||
user_agent must match one entry
|
||||
error: NOT_IN_ALLOWED_UAS
|
||||
```
|
||||
|
||||
### 6. Проверка типа устройства
|
||||
|
||||
```text
|
||||
MAG device flag must match token
|
||||
error: DEVICE_NOT_ALLOWED or TOKEN_EXPIRED
|
||||
```
|
||||
Также проверяет заголовок `X-XC_VM-DETECT` на предмет обнаружения повторного потока.
|
||||
|
||||
---
|
||||
|
||||
@@ -217,14 +165,14 @@ GEOISP_BIN = BIN_PATH/maxmind/GeoIP2-ISP.mmdb
|
||||
### Автоматическое обновление
|
||||
|
||||
Базы данных обновляются с помощью задания cron `cron:maxmind` (`src/Cli/CronJobs/MaxMindCronJob.php`).
|
||||
Он запускается ** только по вторникам** — в день, когда MaxMind публикует новые версии. Логика разветвляется на настройки панели:
|
||||
Он запускается **только по вторникам** — в день, когда MaxMind публикует новые версии. Логика меняется в настройках панели:
|
||||
|
||||
- если `maxmind_account_id` + `maxmind_license_key` + `maxmind_editions` задано, базы данных извлекаются непосредственно из MaxMind API (`MaxMindUpdater`, загружаются только настроенные версии).;
|
||||
- если учетные данные MaxMind не заданы, они возвращаются к версиям GitHub GeoLite2 (бесплатные базы данных).
|
||||
- если для учетных данных MaxMind задано значение **нет**, оно возвращается к версиям GitHub GeoLite2 (бесплатные базы данных).
|
||||
|
||||
### Ручное (принудительное) обновление
|
||||
|
||||
Чтобы немедленно обновить базы данных `.mmdb` на рабочей панели, запустите задание cron вручную **от имени пользователя root** с флагом `--force` (это снимает ограничение "Только по вторникам").:
|
||||
Чтобы немедленно обновить базы данных `.mmdb` на рабочей панели, запустите задание cron вручную **как корень** с флагом `--force` (это снимает ограничение "Только по вторникам").:
|
||||
|
||||
```bash
|
||||
/home/xc_vm/bin/php/bin/php /home/xc_vm/console.php cron:maxmind --force
|
||||
@@ -242,25 +190,22 @@ GEOISP_BIN = BIN_PATH/maxmind/GeoIP2-ISP.mmdb
|
||||
|
||||
## Ведение журнала действий
|
||||
|
||||
Все сеансы потоковой передачи записываются в журнал GeoIP, а данные устройства - в журнал `lines_live`:
|
||||
Сеансы потоковой передачи записывают данные GeoIP (и устройства) в журнал `lines_live`:
|
||||
|
||||
|Колонка|Источник|
|
||||
| --- | --- |
|
||||
| `geoip_country_code` | `GeoIPService::getIPInfo()` |
|
||||
| `isp` |`con_isp_name` из `GeoIPService::getISP()`|
|
||||
| `external_device` |идентификатор типа устройства|
|
||||
| `user_agent` |Заголовок HTTP User-Agent|
|
||||
| `user_agent` |Заголовок HTTP-агента пользователя|
|
||||
| `user_ip` |IP-адрес клиента|
|
||||
|
||||
Вошел в систему `live.php`, `vod.php`, `timeshift.php`, и `rtmp.php`.
|
||||
|
||||
Периодически архивируется с `lines_live` по `lines_activity` с помощью `ActivityCronJob`.
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация
|
||||
|
||||
### Настройки
|
||||
## Конфигурация (географические настройки)
|
||||
|
||||
|Установка|Тип|Описание|
|
||||
| --- | --- | --- |
|
||||
@@ -272,11 +217,12 @@ GEOISP_BIN = BIN_PATH/maxmind/GeoIP2-ISP.mmdb
|
||||
| `county_override_1st` | `0/1` |автоматическое назначение forced_country при первом подключении|
|
||||
| `allow_countries` | `array` |белый список разрешенных кодов стран|
|
||||
| `detect_restream_block_user` | `0/1` |автоматическое отключение пользователя при обнаружении повторного потока|
|
||||
| `disallow_empty_user_agents` | `0/1` |отклонять запросы без использования User-Agent|
|
||||
| `maxmind_account_id` | `string` |Учетная запись MaxMind API|
|
||||
| `maxmind_license_key` | `string` |API-ключ MaxMind|
|
||||
| `maxmind_editions` | `JSON` |множество загруженных изданий|
|
||||
|
||||
(Настройки агента пользователя, такие как `disallow_empty_user_agents`, отображаются на странице устройства.)
|
||||
|
||||
---
|
||||
|
||||
## Связанные файлы
|
||||
@@ -286,11 +232,9 @@ GEOISP_BIN = BIN_PATH/maxmind/GeoIP2-ISP.mmdb
|
||||
| `src/Core/Util/GeoIP.php` |низкоуровневый поиск GeoIP с кэшированием файлов|
|
||||
| `src/Core/GeoIP/GeoIPService.php` |соответствие высокого уровня GeoIP + CIDR|
|
||||
| `src/Core/GeoIP/MaxMindUpdater.php` |Загрузчик баз данных MaxMind|
|
||||
| `src/Cli/CronJobs/MaxMindCronJob.php` |Вторник / `--force` cron обновления базы данных|
|
||||
| `src/Core/Config/Binaries.php` |GeoIP константы пути к файлу базы данных|
|
||||
| `src/Core/Device/MobileDetect.php` |Библиотека Mobile_Detect|
|
||||
| `src/Domain/Device/EnigmaService.php` |Управление STB Enigma2|
|
||||
| `src/Domain/Device/MagService.php` |Управление MAG STB|
|
||||
| `src/Domain/User/UserRepository.php` |GeoIP обогащение пользовательских записей|
|
||||
| `src/www/stream/auth.php` |потоковая авторизация со всеми проверками местоположения / устройства|
|
||||
| `src/Public/stream/auth.php` |потоковая авторизация с проверкой географического местоположения (1-4)|
|
||||
| `src/Streaming/Auth/StreamAuth.php` |Выбор сервера с поддержкой GeoIP|
|
||||
| `src/Streaming/Balancer/ProxySelector.php` |Выбор прокси-сервера с поддержкой GeoIP|
|
||||
@@ -1,6 +1,6 @@
|
||||
# Проверка и санитарная обработка входных данных
|
||||
|
||||
XC_VM использует двухуровневую защиту для входящих данных запроса. Во-первых, программа глобальной очистки удаляет опасный контент со всех PHP суперглобальных объектов во время начальной загрузки, перед запуском любого кода приложения. Во-вторых, уровень проверки на уровне действия проверяет наличие обязательных полей перед выполнением бизнес-логики.
|
||||
XC_VM использует двухуровневую защиту для входящих данных запроса. Во-первых, передача **глобальная санитарная обработка** удаляет опасный контент со всех PHP суперглобальных объектов во время начальной загрузки, перед запуском любого кода приложения. Во-вторых, уровень **проверка на уровне действий** проверяет наличие обязательных полей перед выполнением бизнес-логики.
|
||||
|
||||
Оба слоя реализованы в виде `src/Core/Validation/InputValidator.php`.
|
||||
|
||||
@@ -42,7 +42,7 @@ LegacyInitializer::initCore()
|
||||
|
||||
### Синтаксический анализ выполняется рекурсивно(&$rData, $rInput, $rIteration = 0)
|
||||
|
||||
Рекурсивно выполняет обработку данных GET и POST, применяя очистку ключей и значений к каждому листу. Для массивов выполняется рекурсия на глубину до 20 уровней. Для скалярных значений применяется `parseCleanKey()` к ключу и `parseCleanValue()` к значению.
|
||||
Рекурсивно обрабатывает данные GET и POST, применяя очистку ключей и значений к каждому листу. Для массивов выполняется рекурсия на глубину до 20 уровней. Для скалярных значений применяется `parseCleanKey()` к ключу и `parseCleanValue()` к значению.
|
||||
|
||||
Объединенный результат (сначала ПОЛУЧИТЬ, затем опубликовать с наложением) сохраняется в `RequestManager` для использования на протяжении всего жизненного цикла запроса.
|
||||
|
||||
@@ -105,7 +105,7 @@ if ($error !== null) {
|
||||
### Подтверждающие идентификаторы($ids)
|
||||
|
||||
```php
|
||||
InputValidator::confirmIDs(array $ids): array
|
||||
InputValidator::confirmIDs($ids) // untyped params/return; yields a filtered array of positive int IDs
|
||||
```
|
||||
|
||||
Фильтрует массив, чтобы он содержал только целые положительные идентификаторы. Любое значение, в котором пропущено значение `intval($id) <= 0`. Широко используется в кодовой базе (более 30 сайтов для звонков) везде, где необходимо очистить списки идентификаторов, предоставленные пользователем, перед запросами к базе данных.
|
||||
@@ -255,7 +255,7 @@ $safeIds = InputValidator::confirmIDs($userSuppliedIds);
|
||||
|
||||
Действия, явно не указанные в инструкции `switch`, попадают в `return true`, что означает, что они всегда проходят проверку. Это сделано намеренно - эти действия либо не содержат обязательных полей на уровне gate, либо выполняют свою собственную проверку на более глубоком уровне бизнес-логики.
|
||||
|
||||
**Явно выполняемые сквозные действия** (перечислены в переключателе с `return true`):
|
||||
**Явно выполняемые сквозные действия** (указан в переключателе как `return true`):
|
||||
|
||||
- `processUser`
|
||||
- `processLine`
|
||||
@@ -270,7 +270,7 @@ $safeIds = InputValidator::confirmIDs($userSuppliedIds);
|
||||
- `processLogin`
|
||||
- `submitTicket`
|
||||
|
||||
**Неявно передаваемые действия** (вообще отсутствуют в переключателе, перехватываются значением по умолчанию `return true`):
|
||||
**Неявно передаваемые действия** (вообще отсутствует в переключателе, используется значение по умолчанию `return true`):
|
||||
|
||||
Любая строка действия, не соответствующая `case`, также вернет значение `true`. Если для нового действия требуется стробирование ввода, необходимо явно добавить регистр.
|
||||
|
||||
|
||||
@@ -12,6 +12,26 @@
|
||||
|
||||
---
|
||||
|
||||
## Поток сообщений на портале (рукопожатие → профиль)
|
||||
|
||||
Модуль (или эмулятор) управляет порталом в фиксированной последовательности; понимание этого позволяет
|
||||
контрольный список шагов 4-5 по бетону:
|
||||
|
||||
1. **рукопожатие** — `GET portal.php?type=stb&action=handshake&mac=…&prehash=…` → сервер возвращает
|
||||
a **знак**. `prehash` - это вычисляемый клиентом хэш подтверждения связи (полученный модулем/эмулятором из его
|
||||
идентификатор + MAC); вы задаете значение **нет** в URL—адресе - клиент генерирует его по запросу.
|
||||
2. **get_profile получить_профиль** — `GET portal.php?type=stb&action=get_profile` с `Authorization: Bearer <token>`
|
||||
и идентификаторы устройств (`mac`, `sn`, `stb_type`, `device_id…`). Сервер проверяет подлинность
|
||||
устройство (строка MAC, `lock_device` поля, `allowed_stb_types`) и возвращает значение **профиль**, которое
|
||||
`status` указывает, авторизован ли этот ящик.
|
||||
3. Последующие вызовы (список каналов, EPG, create_link) повторно используют один и тот же токен-носитель.
|
||||
|
||||
На стороне сервера `PortalHandler.php` управляет квитированием/получением_профиля и `portal.php` запускает устройство
|
||||
проверки. (В некоторых сборках поле сначала нажимает `/server/load.php` для подтверждения балансировки нагрузки, прежде чем
|
||||
портал звонит — это нормально.)
|
||||
|
||||
---
|
||||
|
||||
## Поддерживаемые URL-адреса
|
||||
|
||||
Обычно используются два варианта (в зависимости от конфигурации nginx).:
|
||||
@@ -21,6 +41,10 @@
|
||||
|
||||
Для эмуляции браузера важно, чтобы открывался `index.html`, а шаги API переходили к `portal.php` внутри того же префикса.
|
||||
|
||||
`/ACCESS_CODE/` - это путь к панели для каждой установки **код доступа** (код из URL-адреса панели).;
|
||||
`/c/` - это короткий псевдоним, который nginx переписывает на тот же обработчик портала. Любой из них работает — используйте любой из них
|
||||
ваша настройка nginx/эмулятора ожидаема; они достигают одинакового значения `portal.php`.
|
||||
|
||||
Для эмулятора STB точкой входа также должен быть базовый префикс (или `portal.php` без параметров запроса), а не предварительно созданный запрос `action=handshake`.
|
||||
|
||||
---
|
||||
@@ -97,7 +121,7 @@ http://192.168.110.251/HgBjUjSI/
|
||||
|
||||
## Что означает `your device is not active`
|
||||
|
||||
Сообщение появляется, когда ответ профиля содержит `status = 1` (устройству не удалось выполнить аутентификацию/верификацию).
|
||||
The profile `status` field reports the outcome: **`status = 0`** — device authorized/active (normal); **`status = 1`** — device failed authentication/verification, which surfaces as *your device is not active*. (Only `0`/`1` are used for this gate.)
|
||||
|
||||
Общие причины:
|
||||
|
||||
@@ -116,7 +140,7 @@ http://192.168.110.251/HgBjUjSI/
|
||||
4. Подтверждающее подтверждение возвращает токен, а следующий `get_profile` отправляет предъявителю авторизации.
|
||||
5. Если авторизация не достигает PHP, временно включите `auth_via_query=1`.
|
||||
6. Если применяются ограничения по типу STB, добавьте `debug_key=1`.
|
||||
7. Если проблема не устранена, проверьте строку устройства в базе данных (`mag_devices`) и установите флажок `lock_device`.
|
||||
7. Если проблема не устранена, проверьте устройство на странице панель администратора: **МАГНИТНЫЕ устройства** (строка `mag_devices`, переключатель MAC + `lock_device`) и в списке Ministra настроек **разрешенные типы STB-файлов** (`allowed_stb_types`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -65,10 +65,10 @@ user -> member_group_id -> group
|
||||
Первичный метод:
|
||||
|
||||
```php
|
||||
Authorization::check(string $type, mixed $id): bool
|
||||
Authorization::check(string $rType, string|int|null $rID): bool
|
||||
```
|
||||
|
||||
**Предварительные условия:** Возвращает `false` немедленно, если `$rUserInfo`, `$rPermissions` или `$db` не инициализированы.
|
||||
**Предварительные условия:** Немедленно возвращает `false`, если `$rUserInfo`, `$rPermissions` или `$db` не инициализированы.
|
||||
|
||||
#### Тип: `user`
|
||||
|
||||
@@ -95,7 +95,7 @@ Authorization::check('adv', 'edit_bouquet');
|
||||
Authorization::check('adv', 'block_isps');
|
||||
```
|
||||
|
||||
**Важно: `is_admin` gate.** Перед проверкой массива расширенных разрешений метод требует, чтобы значение `$rPermissions['is_admin']` было равно true. Если пользователь не является администратором, `check('adv', ...)` всегда возвращает значение `false`:
|
||||
**Важно: ворота `is_admin`.** Перед проверкой массива расширенных разрешений метод требует, чтобы значение `$rPermissions['is_admin']` было равно true. Если пользователь не является администратором, `check('adv', ...)` всегда возвращает значение `false`:
|
||||
|
||||
```php
|
||||
if (!($rType == 'adv' && $rPermissions['is_admin'])) {
|
||||
@@ -155,8 +155,8 @@ PageAuthorization::checkResellerPermissions(?string $page = null): bool
|
||||
|
||||
Многие страницы сущностей используют условную логику, основанную на параметрах запроса:
|
||||
|
||||
- Если указан параметр `id`, то проверяется разрешение **редактировать**
|
||||
- Если параметр `id` отсутствует, проверяется разрешение **добавить**
|
||||
- Если указан параметр `id`, проверяется разрешение **редактировать**
|
||||
- Если параметр `id` отсутствует, проверяется разрешение **добавлять**
|
||||
- Некоторые страницы (stream, movie) также проверяют наличие параметра `import` и требуют соответствующего разрешения на импорт
|
||||
|
||||
Когда ни одно из условий не выполняется, поведение зависит от страницы: некоторые переходят к соответствующему разрешению на перечисление, другие переходят к переключателю по умолчанию (который возвращает `true`).
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
В этом руководстве показано, как запускать тесты проекта с фиксированным двоичным кодом PHP:
|
||||
|
||||
- PHP двоичный код: `/home/xc_vm/bin/php/bin/php`
|
||||
- PHP двоичный файл: `/home/xc_vm/bin/php/bin/php` (связанный PHP **на VDS**)
|
||||
- Двоичный файл PHPUnit: local `tools/.bin/phpunit.phar`
|
||||
|
||||
> **Local vs VDS.** The commands here use the VDS bundled PHP. When running on your **own
|
||||
> компьютер** (см. [Рабочий процесс разработки](dev-workflow.md)), вместо этого используйте свой локальный PHP 8.1:
|
||||
> `php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist`. CI запускает тот же набор данных через
|
||||
> зафиксированная конфигурация, поэтому зеленый локальный запуск должен соответствовать CI.
|
||||
|
||||
## Почему такая установка
|
||||
|
||||
`phpunit.phar` не включает PHP. Он всегда выполняется через интерпретатор PHP.
|
||||
@@ -12,6 +17,45 @@
|
||||
Если вы запустите `./phpunit.phar` напрямую, он может использовать другой `php` из `PATH`.
|
||||
Используйте явный двоичный путь для обеспечения согласованности во время выполнения.
|
||||
|
||||
## Схема тестирования и соглашения
|
||||
|
||||
Тесты проходят в режиме реального времени **снаружи** `src/`, в режиме `tests/`:
|
||||
|
||||
```text
|
||||
tests/
|
||||
├── phpunit.xml.dist # config: suite "XC_VM Unit", bootstrap=bootstrap.php (PHPUnit 10.5)
|
||||
├── bootstrap.php # locates vendor/autoload.php + defines constants (MAIN_HOME, PHP_BIN, paths)
|
||||
├── Support/ # test helpers (e.g. TestDb.php)
|
||||
└── Unit/ # the "XC_VM Unit" suite — one *Test.php per unit
|
||||
├── CoreEnumTest.php
|
||||
├── EventDispatcherTest.php
|
||||
└── ...
|
||||
```
|
||||
|
||||
Условные обозначения: тестовый класс равен `<Thing>Test` в `tests/Unit/<Thing>Test.php`, расширяет
|
||||
`PHPUnit\Framework\TestCase`. `bootstrap.php` подключает **Composer автозагрузчик** (таким образом
|
||||
`XcVm\…` классы разрешают — для этого требуется `src/vendor/`, т.е. сначала запустить `make dev-tools`) и
|
||||
определяет константы среды выполнения, на которые опираются тесты. Минимальный тест:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace XcVm\Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use XcVm\Core\Enum\BootContext;
|
||||
|
||||
final class MyThingTest extends TestCase
|
||||
{
|
||||
public function testItWorks(): void
|
||||
{
|
||||
self::assertSame('admin', BootContext::Admin->value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Если классы не загружаются (`Class "XcVm\…" not found`), то в `src/vendor/` отсутствует автозагрузчик —
|
||||
> запустите `make dev-tools`.
|
||||
|
||||
## 1. Проверить PHP
|
||||
|
||||
```bash
|
||||
@@ -20,7 +64,7 @@
|
||||
|
||||
## 2. Скачать PHPUnit PHAR
|
||||
|
||||
Этот проект привязан к PHP версии 8.1, поэтому используйте PHPUnit 10.
|
||||
Этот проект привязан к PHP 8.1, поэтому используйте **Модуль PHP 10** (10.5). Не извлекайте `phpunit-11.phar` — для PHPUnit 11 требуется PHP 8.2+, и он откажется запускаться на 8.1.
|
||||
|
||||
```bash
|
||||
cd /home/xc_vm
|
||||
|
||||
@@ -29,7 +29,7 @@ ProcessManager::isNamedProcessRunning(
|
||||
|
||||
Соответствует шаблону командной строки `NAME[ID]` (для работников, основанных на названии процесса).
|
||||
|
||||
### Проверьте потоковый процесс
|
||||
### Проверка потокового процесса
|
||||
|
||||
```php
|
||||
ProcessManager::isStreamRunning(int $pid, int $streamId): bool
|
||||
@@ -40,13 +40,25 @@ ProcessManager::isStreamRunning(int $pid, int $streamId): bool
|
||||
|
||||
---
|
||||
|
||||
## Утилиты для работы с PID-файлами
|
||||
## Больше проверок и помощников
|
||||
|
||||
```php
|
||||
ProcessManager::checkPidFile(string $pidFile, string $searchString): bool
|
||||
ProcessManager::matchesCmdline(int $pid, string $search): bool
|
||||
ProcessManager::isStreamAlive($pid, $streamID): bool // loose: stream ID appears in the ffmpeg/php cmdline (case-insensitive) — no output check
|
||||
ProcessManager::isMonitorAlive($pid, $streamID, $exe = null): bool // the stream's watchdog (MonitorCommand) is alive
|
||||
ProcessManager::startMonitor($streamID, $restart = 0): bool // (re)spawn the watchdog for a stream (returns true)
|
||||
ProcessManager::isNginxRunning(): bool
|
||||
ProcessManager::getProcessAge($pid): int // seconds since the process started (from /proc mtime)
|
||||
ProcessManager::findProcessPIDs(array $terms, $limit = 0): array // pids whose cmdline matches ANY of $terms (first match wins)
|
||||
ProcessManager::isAnyProcessRunning(array $terms): bool
|
||||
```
|
||||
|
||||
`isStreamRunning()` - это проверка **более строгий**: она подтверждает командную строку процесса ffmpeg
|
||||
ссылается на этот поток **выходные файлы** (`{id}_.m3u8` / `{id}_%d.ts`), т.е. на самом деле это
|
||||
создающий *этот* поток. `isStreamAlive()` - это более точное, нечувствительное к регистру соответствие подстроки в
|
||||
идентификатор потока в командной строке — дешевле, но он не проверяет вывод. Используйте `isStreamRunning()`, когда
|
||||
"создается ли этот поток?" имеет значение, `isStreamAlive()` для краткости "это процесс для этого идентификатора
|
||||
поблизости?".
|
||||
|
||||
---
|
||||
|
||||
## Завершение процесса
|
||||
@@ -62,20 +74,31 @@ ProcessManager::kill(int $pid, int $signal = SIGKILL): bool
|
||||
## Блокировка Cron
|
||||
|
||||
```php
|
||||
ProcessManager::acquireCronLock(string $pidFile, int $maxAge = 1800): void
|
||||
ProcessManager::acquireCronLock(string $pidFile, int $maxAge = 1800): bool
|
||||
```
|
||||
|
||||
Поведение:
|
||||
|
||||
- активная блокировка -> выход из текущего режима
|
||||
- устаревший замок -> удален и заменен
|
||||
- очистка блокировки -> регистрация с помощью обратного вызова shutdown
|
||||
- активная блокировка -> завершает текущий запуск
|
||||
- устаревшая блокировка (старше `$maxAge` секунд) -> удалена и заменена
|
||||
- в случае успеха -> записывает текущий PID в файл блокировки и возвращает `true`
|
||||
|
||||
> **Крайний случай.** `acquireCronLock()` регистрирует ли **нет** обработчик завершения работы — он никогда
|
||||
> автоматически снимает блокировку при выходе. Блокировка восстанавливается только в том случае, если при последующем запуске обнаруживается, что она старше
|
||||
> `$maxAge`. Так что следите за тем, чтобы `$maxAge` было удобно выше реального времени выполнения задания (медленный, но живой запуск в прошлом
|
||||
> `$maxAge` может быть восстановлено ошибочно), и не полагайтесь на то, что блокировка исчезнет в момент выполнения задания
|
||||
> заканчивает.
|
||||
|
||||
---
|
||||
|
||||
## `/proc` Проверить кэш
|
||||
|
||||
`isRunning()` использует короткое TTL-кэширование для `/proc` проверок (1 секунда), чтобы уменьшить количество повторных операций ввода-вывода в узких циклах.
|
||||
`isRunning()` использует короткий TTL-кэш для `/proc` проверок (1 секунда), чтобы уменьшить количество повторных операций ввода-вывода в узких циклах.
|
||||
|
||||
> **Ловушка.** Поскольку результат кэшируется в течение ~1 секунды, процесс, который завершается (или запускается) внутри этого
|
||||
> окно по—прежнему считывается в своем предыдущем состоянии - жесткий цикл может воздействовать на устаревшее "запущенное"/ "мертвое" окно.
|
||||
> ответ. Вызовите `ProcessManager::clearCache()`, чтобы удалить кэш, когда вам понадобится новое чтение
|
||||
> (например, сразу после удаления pid и перед его повторной проверкой).
|
||||
|
||||
---
|
||||
|
||||
@@ -83,11 +106,25 @@ ProcessManager::acquireCronLock(string $pidFile, int $maxAge = 1800): void
|
||||
|
||||
Общий формат названия процесса:
|
||||
|
||||
- `XC_VM[{id}]`
|
||||
- `Thumbnail[{id}]`
|
||||
- `TVArchive[{id}]`
|
||||
- `XC_VM[{id}]` — для каждого потока watchdog (`MonitorCommand`, порожденного `startMonitor()`)
|
||||
- `Thumbnail[{id}]` — генератор миниатюр для потока `{id}`
|
||||
- `TVArchive[{id}]` — timeshift/архивный рекордер для потока `{id}`
|
||||
|
||||
Используется вместе с помощниками по заголовку процесса CLI.
|
||||
Рабочие задают эти заголовки с помощью `cli_set_process_title()`; `isNamedProcessRunning()` и
|
||||
`findProcessPIDs()` соответствует им (смотрите список демонов в
|
||||
[Инструменты CLI и ссылка на консоль](cli-tools.md)).
|
||||
|
||||
---
|
||||
|
||||
## Запуск подпроцессов: `Thread` и `Multithread`
|
||||
|
||||
`ProcessManager` проверяет и уничтожает **существующий** процессов. Для **запуск** новых процессов из PHP:
|
||||
|
||||
- `Thread` (`src/Core/Process/Thread.php`) — тонкая оболочка `proc_open` вокруг одного
|
||||
фоновая команда (запустите ее, опросите/дождитесь ее, прочитайте ее выходные данные).
|
||||
- `Multithread` (`src/Core/Process/Multithread.php`) — выполняет несколько команд оболочки.
|
||||
одновременно и собирает выходные данные каждого из них; используйте его для разветвленной работы (например, для проверки многих
|
||||
исходники сразу), а не ручной цикл `proc_open`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ access_code = "Access Code" ; ← translate this
|
||||
actions = "Actions" ; ← translate this
|
||||
```
|
||||
|
||||
3. Перейдите в раздел **Настройки → Интерфейс → Язык интерфейса** и выберите новый код языка.
|
||||
3. Перейдите к **Настройки → Интерфейс → Язык интерфейса** и выберите новый код языка.
|
||||
|
||||
Вот и все. Перезагрузка не требуется.
|
||||
|
||||
@@ -35,10 +35,10 @@ key = "Translated text"
|
||||
another_key = "Another translated text"
|
||||
```
|
||||
|
||||
**Правила:**
|
||||
**Rules:**
|
||||
|
||||
- Заголовок раздела `[Language]` должен быть **обязательным** в первой строке.
|
||||
- Ключи являются идентификаторами `snake_case` — **не меняйте их**.
|
||||
- Заголовок раздела `[Language]` равен **требуемый** в первой строке.
|
||||
- Ключами являются идентификаторы `snake_case` — **не меняйте их**.
|
||||
- Значения должны быть заключены в **двойные кавычки**.
|
||||
- Строки отсортированы в алфавитном порядке по ключу для обеспечения согласованности.
|
||||
- Кодировка файла должна быть **UTF-8** (без спецификации).
|
||||
@@ -50,7 +50,7 @@ another_key = "Another translated text"
|
||||
|Панель загрузки|`Translator::init()` проверяет `src/resources/langs/` на наличие `*.ini` файлов|
|
||||
|Список языков|`Translator::available()` возвращает все найденные языковые коды|
|
||||
|Выбор пользователя|Язык сохраняется в файле cookie `lang` (для каждого браузера) и в столбце базы данных `settings.language` (по умолчанию).|
|
||||
|Отсутствующий ключ|Если ключ перевода используется в коде, но отсутствует в вашем файле `.ini`, система ** автоматически добавит ** его с именем ключа в качестве значения по умолчанию|
|
||||
|Отсутствующий ключ| If a translation key is used in code but missing from your `.ini` file, the system **automatically appends** it with the key name as the default value |
|
||||
|
||||
## Доступные языки
|
||||
|
||||
@@ -66,15 +66,15 @@ another_key = "Another translated text"
|
||||
|
||||
## Чаевые
|
||||
|
||||
- **Всегда используйте `en.ini` в качестве источника достоверности** — он содержит все ключи. В других файлах могут отсутствовать ключи, которые автоматически заполняются во время выполнения.
|
||||
- **Автоматическое создание отсутствующих ключей**: если в вашем файле отсутствует ключ, `Translator` автоматически добавит `key = "key"` к вашему файлу. Затем вы сможете найти и перевести эти непереведенные записи.
|
||||
- **Всегда используйте `en.ini` в качестве источника истины** — содержит все ключи. В других файлах могут отсутствовать ключи, которые автоматически заполняются во время выполнения.
|
||||
- **Автоматическое создание недостающих ключей**: если в вашем файле отсутствует ключ, `Translator` автоматически добавит `key = "key"` к вашему файлу. Затем вы сможете найти и перевести эти непереведенные записи.
|
||||
- **Поиск непереведенных ключей** — поиск строк, в которых ключ равен значению:
|
||||
|
||||
```bash
|
||||
grep -P '^(\w+)\s*=\s*"\1"$' src/resources/langs/xx.ini
|
||||
```
|
||||
|
||||
- **Проверьте свой файл ** — убедитесь, что `parse_ini_file()` может его прочитать:
|
||||
- **Проверьте свой файл** — убедитесь, что `parse_ini_file()` может это прочитать:
|
||||
|
||||
```bash
|
||||
php -r "var_dump(parse_ini_file('src/resources/langs/xx.ini', false, INI_SCANNER_RAW));" | head -20
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
# UCS Поддомены с подстановочными знаками интеграции для каждой строки
|
||||
|
||||
Эта функция поддерживает интеграцию **UCS**, предоставляя каждому клиенту уникальный поддомен для балансировки нагрузки на основе его ** идентификатора строки ** при создании перенаправлений потоков.
|
||||
Эта функция присваивает каждому клиенту значение **стабильный, уникальный поддомен балансировщика нагрузки, основанный на их идентификаторе строки** (например, строка `10` → `10.example.com`) при построении перенаправления потока.
|
||||
|
||||
> **Что здесь означает UCS?** XC_VM только *выдает* имя хоста `{line_id}.<zone>`. "UCS" относится к
|
||||
> **восходящая/внешняя система** — ваш DNS, CDN или пограничный прокси—сервер, который использует эти данные для каждой строки
|
||||
> имена хостов для маршрутизации или применения политики для каждой строки. Панель сама по себе не реализует UCS; она передает ее.
|
||||
|
||||
## Обзор
|
||||
|
||||
Когда сервер балансировки нагрузки (LB) использует домен с подстановочным знаком и включена функция **Обслуживать случайный IP / домен**, XC_VM заменяет префикс `wildcard.` на аутентифицированный идентификатор строки перед перенаправлением клиента.
|
||||
When a load-balancer (LB) server uses a wildcard domain and **Serve Random IP / Domain** is enabled, XC_VM replaces the `wildcard.` prefix with the authenticated line ID before redirecting the client.
|
||||
|
||||
|Идентификатор строки|Домен LB (настроен)|Перенаправление домена|
|
||||
|--------:|------------------------|-----------------|
|
||||
|10| `wildcard.example.com` | `10.example.com` |
|
||||
|42| `wildcard.lb1.com` | `42.lb1.com` |
|
||||
|
||||
Каждая строка получает стабильный поддомен. UCS (или вышестоящий DNS/прокси-сервер) может перенаправлять `*.example.com` к нужному серверу или применять политики для каждой строки.
|
||||
Каждая строка получает стабильный поддомен. UCS (или вышестоящий DNS/прокси-сервер) может перенаправлять `*.example.com` на правильный сервер или применять политики для каждой строки.
|
||||
|
||||
## Требования
|
||||
|
||||
1. **Сервер балансировки нагрузки** (не основной сервер), по крайней мере, с одним доменом, настроенным в соответствии с **Доменами и IP-адресами**.
|
||||
2. **Обслуживать случайный IP / домен**, включенный на этом сервере (флажок`random_ip` на странице редактирования сервера).
|
||||
1. **Сервер балансировки нагрузки** (не основной сервер), по крайней мере, с одним доменом, настроенным в соответствии с **Домены и IP-адреса**.
|
||||
2. **Обслуживать случайный IP / домен** включен на этом сервере (флажок`random_ip` на странице редактирования сервера).
|
||||
3. Запись в домене, начинающаяся с **`wildcard.`** (например, `wildcard.example.com`).
|
||||
4. DNS: подстановочная запись для базовой зоны (например, `*.example.com`), указывающая на LB или вашу границу UCS.
|
||||
5. **TLS (при обслуживании по протоколу HTTPS): подстановочный сертификат `*.example.com`.** Каждая строка преобразуется в
|
||||
*другой* хост (`10.example.com`, `42.example.com`, ...), таким образом, сертификат для каждого хоста не будет работать без
|
||||
подстановочный знак означает, что эти клиенты получают ошибки TLS. Это самая распространенная ошибка при развертывании.
|
||||
|
||||
## Установка
|
||||
|
||||
1. Откройте **Серверы → Редактировать** на целевом сервере LB.
|
||||
2. В разделе **Домены и IP-адреса** добавьте `wildcard.example.com` (используйте свою реальную зону).
|
||||
3. Включить ** Обслуживание случайного IP / домена**.
|
||||
2. В поле **Домены и IP-адреса** добавьте `wildcard.example.com` (используйте свою реальную зону).
|
||||
3. Включить **Обслуживать случайный IP / домен**.
|
||||
4. Сохраните сервер.
|
||||
5. Убедитесь, что DNS разрешает `{line_id}.example.com` для любого введенного вами идентификатора строки (wildcard DNS или UCS automation).
|
||||
|
||||
@@ -32,10 +39,10 @@
|
||||
|
||||
## Когда Это применимо
|
||||
|
||||
Подстановка выполняется только в том случае, если ** все** из приведенного ниже значения являются истинными:
|
||||
The substitution runs only when **all** of the following are true:
|
||||
|
||||
- Запрос аутентифицирован, и доступен идентификатор строки.
|
||||
- **Для выбранного LB включена функция обслуживания случайного IP / домена**.
|
||||
- **Обслуживать случайный IP / домен** включено для выбранного LB.
|
||||
- Случайно выбранный домен из списка доменов сервера содержит подстроку `wildcard.`.
|
||||
|
||||
Если клиент подключился, используя соответствующий заголовок `Host`, существующий хост сохраняется, а подстановочный знак пропускается.
|
||||
@@ -43,11 +50,27 @@
|
||||
## Пример потока
|
||||
|
||||
1. Домены сервера LB: `wildcard.example.com`, `lb2.example.com`
|
||||
2. **Обслуживать случайный IP / домен**: включен
|
||||
2. **Обслуживать случайный IP / домен**: включено
|
||||
3. Client line ID: `10`
|
||||
4. Auth выбирает `wildcard.example.com` из списка доменов
|
||||
5. URL-адрес узла перенаправления становится `10.example.com`
|
||||
|
||||
## Сложные случаи и подводные камни
|
||||
|
||||
- **Отсутствующий / неправильный подстановочный DNS-код** — если `*.zone` не удается разрешить, клиент получает неразрешимую проблему.
|
||||
хост и поток завершаются сбоем. Проверьте с помощью `dig 999.example.com` перед выдачей строк.
|
||||
- **Несколько доменов на LB** — "Обслуживать случайный IP / домен" выбирает один домен случайным образом. Если
|
||||
pick - это запись, не содержащая`wildcard.`, никакой замены не происходит, и клиент получает этот простой хост.
|
||||
Следите за тем, чтобы список доменов LB был единообразным (все с подстановочными знаками или разбирайтесь в их сочетаниях).
|
||||
- **Меняется только метка хоста** — при замене заменяется только сегмент `wildcard.`; URL-адрес
|
||||
схема (http/https) и путь не изменяются, поэтому развертывание HTTPS остается HTTPS (отсюда и сертификат
|
||||
требование, приведенное выше).
|
||||
- **`Host` -пропуск матча** — если клиент уже подключился с использованием хоста, соответствующего требованиям LB
|
||||
сконфигурированный домен, существующий хост сохраняется, а замена пропускается (например, клиент, который
|
||||
достигнутый `wildcard.example.com` непосредственно сохраняет его, а не становится `10.example.com`).
|
||||
|
||||
---
|
||||
|
||||
## Реализация
|
||||
|
||||
Логика живет в `StreamRedirector::getStreamingURL()`:
|
||||
@@ -64,8 +87,11 @@ if ($rUserID && strpos($rDomain, 'wildcard.') !== false) {
|
||||
## Записи
|
||||
|
||||
- Используйте буквальный префикс **`wildcard.`** в строке домена; заменяется только этот сегмент.
|
||||
- Идентификатор строки - это внутренний идентификатор таблицы **lines** (`users.id` в streaming auth), а не имя пользователя.
|
||||
- Идентификатор строки - это внутренний идентификатор таблицы **линии** (`users.id` в streaming auth), а не имя пользователя.
|
||||
- Подстановка подстановочных знаков применяется только к URL-адресам перенаправления LB; она не изменяет имена плейлистов или хостов API, если только эти пути также не содержат `getStreamingURL()` с идентификатором строки.
|
||||
- **Безопасность:** идентификаторы строк становятся общедоступными в именах хостов (`10.example.com`), поэтому поддомен
|
||||
это поверхность перечисления — она показывает, что идентификаторы строк являются последовательными целыми числами. Это не секрет;
|
||||
не полагайтесь на поддомен для контроля доступа (auth по-прежнему выполняется в `auth.php`).
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
|
||||
+33
-33
@@ -7,7 +7,7 @@
|
||||
## Проблемы с потоками
|
||||
|
||||
<details>
|
||||
<summary><b>❌ Мой стрим не запускается ни на MAIN, ни на LB</b></summary>
|
||||
<summary><b>❌ Мой стрим не запускается на MAIN или LB</b></summary>
|
||||
|
||||
---
|
||||
|
||||
@@ -25,7 +25,7 @@ sudo -u xc_vm /home/xc_vm/console.php monitor 291
|
||||
|
||||
### Что делает команда
|
||||
|
||||
Команда **monitor** пытается запустить поток вручную и в случае сбоя выдает сообщение об ошибке.
|
||||
Команда **монитор** пытается запустить поток вручную и в случае сбоя выдает сообщение об ошибке.
|
||||
|
||||
---
|
||||
|
||||
@@ -75,10 +75,10 @@ sudo apt install <library_name>
|
||||
|
||||
Это функции безопасности, а не баги:
|
||||
|
||||
- **TOKEN_EXPIRED** — токен сеанса имеет ограничение по времени. Пользователю необходимо повторно пройти аутентификацию.
|
||||
- **IP_MISMATCH** — IP-адрес пользователя изменился в середине потока (часто определяется как общий доступ к учетным данным).
|
||||
- **ТОКЕН_ИСПОЛЬЗОВАН** — токен сеанса имеет ограничение по времени. Пользователю необходимо повторно пройти аутентификацию.
|
||||
- **IP_MISMATCH СОВПАДЕНИЕ IP_MISMATCH** — IP-адрес пользователя изменился в середине потока (часто определяется как общий доступ к учетным данным).
|
||||
|
||||
**Соответствующие настройки:**
|
||||
**Relevant settings:**
|
||||
- `restrict_same_ip` — насколько строго соблюдается соответствие IP-адресов
|
||||
- `disallow_2nd_ip_con` — блокирует одновременные подключения с разных IP-адресов.
|
||||
|
||||
@@ -100,14 +100,14 @@ sudo apt install <library_name>
|
||||
XC_VM защита методом перебора блокирует IP-адреса после слишком большого числа неудачных попыток входа в систему. Это контролируется:
|
||||
|
||||
- `bruteforce_mac_attempts` — количество попыток для каждого MAC за определенный промежуток времени
|
||||
- `bruteforce_username_attempts` — количество попыток для каждого пользователя в течение временного окна
|
||||
- `bruteforce_username_attempts` — количество попыток для каждого имени пользователя за определенный промежуток времени
|
||||
- `flood_limit` — общее количество запросов в окне
|
||||
|
||||
** Чтобы разблокировать себя:**
|
||||
**To unblock yourself:**
|
||||
|
||||
1. **Из панели администратора:** Сервис → Управление IP-адресами → удалить из списка заблокированных.
|
||||
2. **Из интерфейса командной строки:** `sudo /home/xc_vm/console.php tools flush` — удаляет все заблокированные IP-адреса.
|
||||
3. **Если вы полностью заблокированы:** Используйте `console.php tools rescue` для создания аварийного кода доступа (см. [CLI Tools](../guides/cli-tools.md)).
|
||||
1. **Из панели администратора:** Инструменты → Управление IP-адресами → удалить из списка заблокированных.
|
||||
2. **Из CLI:** `sudo /home/xc_vm/console.php tools flush` — удаляет все заблокированные IP-адреса.
|
||||
3. **Если полностью заблокирован:** Используйте `console.php tools rescue` для создания аварийного кода доступа (см. [CLI Tools](../guides/cli-tools.md)).
|
||||
|
||||
---
|
||||
|
||||
@@ -118,23 +118,23 @@ XC_VM защита методом перебора блокирует IP-адр
|
||||
|
||||
---
|
||||
|
||||
Блок, который раньше работал, начинает блокировать свой ** IP-адрес ** после ** сброса настроек, изменения/обновления прошивки, аппаратной замены ** (или перемещения MAC в другой блок): теперь он сообщает ** другой серийный номер (`sn`) или `device_id`** больше того, что сохранила панель. При `get_profile` сервер блокирует IP-адрес, и портал возвращает 404.
|
||||
Окно, которое раньше работало, начинает получать значение **Заблокированный IP-адрес** после **сброс настроек, изменение/обновление встроенного ПО, замена оборудования** (или при перемещении MAC в другое поле): теперь оно сообщает значение **другой серийный номер (`sn`) или `device_id`**, отличное от того, которое сохранила панель. На `get_profile` сервер блокирует IP, и портал возвращает 404.
|
||||
|
||||
Это ** не ошибка** — это защита портала от клонирования **MAGSCAN**. Для этого требуется ввести серийный номер и сравнить опубликованное значение `sn` с сохраненным значением `mag_devices.sn`:
|
||||
Это **это не ошибка** — защита портала от клонирования, **MAGSCAN**. Для этого требуется ввести серийный номер и сравнить опубликованные `sn` данные с сохраненными `mag_devices.sn`:
|
||||
|
||||
- **Нет серийного номера** в запросе → запретить (`[MS] No Serial Number`).
|
||||
- **Опубликовано `sn` ≠ сохранено на устройстве `sn`** → бан (`[MS] Invalid Serial Number`).
|
||||
- **Опубликовано `sn` ≠ сохранено на устройстве `sn`** → запретить (`[MS] Invalid Serial Number`).
|
||||
|
||||
В обоих случаях IP-адрес записывается в таблицу `blocked_ips` (а оттуда в таблицу iptables), и устройство получает код 404. Если на устройстве установлен флаг **`lock_device`**, то `device_id`, `device_id2` и `hw_version` также отмечены — несоответствие не подтверждается, и устройство показывает, что "ваше устройство неактивно" (без запрета IP-адреса).
|
||||
В обоих случаях IP-адрес записывается в таблицу `blocked_ips` (а оттуда в таблицу iptables), и устройство получает код 404. Если на устройстве установлен флаг **`lock_device`**, то `device_id`, `device_id2` и `hw_version` также проверяются — несоответствие не подтверждается, и устройство показывает, что "ваше устройство неактивно" (без запрета IP-адреса).
|
||||
|
||||
**Как исправить (для легитимного ящика, данные которого действительно изменились):**
|
||||
**How to fix (for a legitimate box whose data genuinely changed):**
|
||||
|
||||
1. **Сбросьте привязку на панели:** откройте это устройство MAG в admin и **очистите его сохраненный серийный номер / `device_id`** (или удалите и повторно добавьте устройство). После этого условие "серийный номер уже записан" больше не срабатывает, и при следующем подключении новые значения будут привязаны.
|
||||
2. **Разблокируйте IP.** Самый простой способ — **через веб—панель**: откройте **Сервис → Управление IP** (`/<admin-code>/ips`), где перечислены заблокированные IP-адреса - удалите тот, который вам нужен (или очистите весь список). Настройки CLI / вручную, если вы не можете добраться до панели:
|
||||
1. **Сбросьте привязку на панели:** откройте это устройство MAG в admin и **очистите сохраненный серийный номер / `device_id`** (или удалите и повторно добавьте устройство). После этого условие "серийный номер уже записан" больше не срабатывает, и при следующем подключении будут привязаны новые значения.
|
||||
2. **Разблокируйте IP-адрес.** Самый простой способ — **через веб-панель**: откройте **Инструменты → Управление интеллектуальной собственностью** (`/<admin-code>/ips`), в котором перечислены заблокированные IP—адреса - удалите тот, который вам нужен (или очистите весь список). CLI / ручные настройки, если вы не можете добраться до панели:
|
||||
- CLI (clear all blocks): `sudo /home/xc_vm/console.php tools flush`;
|
||||
- Вручную, для каждого IP: `sudo iptables -D INPUT -s <IP> -j DROP && sudo rm -f /home/xc_vm/tmp/flood/block_<IP>`.
|
||||
|
||||
> ⚠️ Установка `enable_debug_stalker` позволяет обойти проверку `lock_device` / изображений, но не последовательный жесткий запрет MAGSCAN (который выполняется ранее) - вам все равно придется очистить сохраненные `sn` на панели.
|
||||
> ⚠️ Настройка `enable_debug_stalker` обходит проверку `lock_device` / изображений, но **НЕ** - последовательный жесткий запрет MAGSCAN (который выполняется ранее) — вам все равно придется очистить сохраненные `sn` в панели.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,12 +174,12 @@ sudo /home/xc_vm/console.php tools rescue
|
||||
|
||||
Самая распространенная проблема. Причины:
|
||||
|
||||
1. **Неверные учетные данные в `config.ini`** — проверка `host`, `port`, `db_user`, `db_pass`, `db_name`
|
||||
1. **Неверные учетные данные в `config.ini`** — проверить `host`, `port`, `db_user`, `db_pass`, `db_name`
|
||||
2. **MySQL/MariaDB не запущен** — `sudo systemctl status mariadb`
|
||||
3. **Сеть недоступна** — сервер базы данных на другом хосте и порту защищен брандмауэром.
|
||||
4. **Пользователю не хватает привилегий** — повторно предоставить их с помощью `console.php tools mysql`
|
||||
3. **Сеть недоступна** — Сервер базы данных на другом хосте и порту защищен брандмауэром
|
||||
4. **Пользователю не хватает привилегий** — повторное предоставление с помощью `console.php tools mysql`
|
||||
|
||||
**Исправлено:** Отредактируйте `/home/xc_vm/config/config.ini`, затем запустите:
|
||||
**Чинить:** Отредактируйте `/home/xc_vm/config/config.ini`, затем запустите:
|
||||
|
||||
```bash
|
||||
sudo /home/xc_vm/console.php status
|
||||
@@ -199,13 +199,13 @@ sudo /home/xc_vm/console.php status
|
||||
- Перенос зарегистрирован со статусом `[WARN]` — он не будет повторен автоматически.
|
||||
- Распространенные причины: синтаксическая ошибка, таблица уже существует, конфликт внешних ключей, отсутствует привилегия ALTER.
|
||||
|
||||
**Отладка:**
|
||||
**Debug:**
|
||||
|
||||
1. Проверьте, какая миграция завершилась неудачно, в выходных данных консоли.
|
||||
2. Откройте файл в `migrations/` и проверьте SQL-код.
|
||||
3. Устраните проблему вручную в MySQL, после чего следующее обновление продолжится с того места, где оно было остановлено.
|
||||
|
||||
Дополнительные сведения см. в разделе [Миграция базы данных](../guides/cli-tools.md#database-updates-after-version-upgrade).
|
||||
Дополнительные сведения см. в разделе [Миграция базы данных](../guides/database-migrations.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -229,7 +229,7 @@ sudo /home/xc_vm/console.php status
|
||||
|Ошибка 0|Файлы, не найденные после генерации|Проверить `/home/xc_vm/bin/certbot/logs/xc_vm.log`|
|
||||
|Ошибка 2|Неожиданная ошибка certbot|Проверьте журналы, убедитесь, что DNS разрешает доступ к вашему серверу|
|
||||
|
||||
** Также:** Удалите устаревшие файлы блокировки, если certbot был прерван:
|
||||
**Также:** Удаление устаревших файлов блокировки, если certbot был прерван:
|
||||
|
||||
```bash
|
||||
sudo rm -f /home/xc_vm/bin/certbot/*/.certbot.lock
|
||||
@@ -247,17 +247,17 @@ sudo rm -f /home/xc_vm/bin/certbot/*/.certbot.lock
|
||||
XC_VM запускает **два** nginx экземпляра:
|
||||
|
||||
1. **nginx** (`bin/nginx/`) — HTTP(ы) трафик
|
||||
2. **nginx_rtmp** (`bin/nginx_rtmp/`) — RTMP потоковая передача
|
||||
2. **nginx_rtmp** (`bin/nginx_rtmp/`) — RTMP потоковое вещание
|
||||
|
||||
Каждый из них может выйти из строя, если его порт уже используется.
|
||||
|
||||
**Диагностировать:**
|
||||
**Diagnose:**
|
||||
|
||||
```bash
|
||||
sudo netstat -tlnp | grep -E ':80|:443|:1935'
|
||||
```
|
||||
|
||||
** Исправлено:** Измените широковещательный порт в настройках панели администратора, затем заново создайте настройки:
|
||||
**Чинить:** Измените широковещательный порт в настройках панели администратора, затем заново создайте настройки:
|
||||
|
||||
```bash
|
||||
sudo /home/xc_vm/console.php tools ports
|
||||
@@ -278,7 +278,7 @@ sudo /home/xc_vm/console.php tools ports
|
||||
|
||||
Система обновлений загружается с GitHub releases. Если это не удается:
|
||||
|
||||
- **Сеть/брандмауэр ** блокирует доступ к GitHub
|
||||
- **Сеть/брандмауэр** блокирует доступ к GitHub
|
||||
- **Частичная загрузка** — соединение прервано на полпути
|
||||
- **Несоответствие MD5** — поврежденный файл (обновление благополучно прервано)
|
||||
|
||||
@@ -351,14 +351,14 @@ sudo /home/xc_vm/console.php status
|
||||
|
||||
Серверы LB опрашивают MAIN по протоколу HTTP и обрабатывают сигналы. При сбое синхронизации:
|
||||
|
||||
1. **Сеть:** LB не может подключиться к HTTP—порту MAIN - проверьте правила брандмауэра
|
||||
2. **База данных:** LB не может подключиться к MAIN'у MySQL — повторно предоставьте привилегии:
|
||||
1. **Сеть:** LB не может связаться с HTTP—портом MAIN - проверьте правила брандмауэра
|
||||
2. **База данных:** LB не удается подключиться к MySQL — повторно предоставить привилегии MAIN:
|
||||
```bash
|
||||
sudo /home/xc_vm/console.php tools mysql
|
||||
```
|
||||
3. **Тайм-аут:** Если `last_check_ago` превышает 180 секунд, сервер помечается как отключенный
|
||||
3. **Перерыв:** Если `last_check_ago` превышает 180 секунд, сервер помечается как отключенный
|
||||
|
||||
**Отладка:** Запустите на главном экране, чтобы проверить подключение:
|
||||
**Отлаживать:** Запустите на главной, чтобы проверить подключение:
|
||||
|
||||
```bash
|
||||
sudo -u xc_vm /home/xc_vm/console.php watchdog
|
||||
|
||||
@@ -6,38 +6,38 @@
|
||||
|
||||
## Важное уведомление о переносе
|
||||
|
||||
> **Прочтите это перед началом миграции.**
|
||||
> **Read this before starting the migration.**
|
||||
|
||||
XC_VM миграция переносит только **данные**.
|
||||
**Вся конфигурация намеренно исключена из процесса миграции.**
|
||||
XC_VM миграционные переводы **только данные**.
|
||||
**All configuration is intentionally excluded from migration.**
|
||||
|
||||
Это включает в себя (но не ограничивается этим):
|
||||
|
||||
- Ключи API (например, **TMDb**)
|
||||
- Ключи API (например, **ТМДб**)
|
||||
- Учетные данные внешней службы
|
||||
- Настройки, зависящие от конкретной среды
|
||||
- Конфигурация панели и системы
|
||||
- Время выполнения и состояние потока
|
||||
|
||||
Эти значения ** должны быть перенастроены вручную после миграции**.
|
||||
Эти значения **необходимо перенастроить вручную после миграции**.
|
||||
|
||||
Это ** дизайнерское решение**, а не ограничение или ошибка.
|
||||
Пропуск переконфигурации приведет к ** прерыванию выборки метаданных, обновлению заголовков потоков и связанных с ними функций **.
|
||||
Это **дизайнерское решение**, а не ограничение или ошибка.
|
||||
Пропуск реконфигурации приведет к **прерывать выборку метаданных, обновлять заголовки потоков и связанные с ними функции**.
|
||||
|
||||
---
|
||||
|
||||
## Прежде чем Вы начнете
|
||||
|
||||
> 💡 **Рекомендация:**
|
||||
> Выполните миграцию при **новой установке XC_VM**.
|
||||
> Выполните перенос на **новая установка XC_VM**.
|
||||
>
|
||||
> ⚠️ **Важно:**
|
||||
> Системные настройки и настройки панели ** НЕ перенесены**.
|
||||
> ⚠️ **Важный:**
|
||||
> Системные настройки и настройки панели равны **НЕ перенесен**.
|
||||
> Передаются только данные базы данных, поддерживаемые процессом миграции.
|
||||
|
||||
Если вы решите перейти на **существующую установку**, имейте в виду:
|
||||
Если вы решите перейти на **существующая установка**, имейте в виду:
|
||||
|
||||
- XC_VM приведет к удалению всех таблиц в основной базе данных, которые соответствуют данным из базы данных миграции.
|
||||
- XC_VM будет **удалить все таблицы** в основной базе данных, которая соответствует данным из базы данных миграции.
|
||||
- **Резервное копирование является обязательным.** Автоматический откат не предусмотрен.
|
||||
|
||||
---
|
||||
@@ -46,7 +46,7 @@ XC_VM миграция переносит только **данные**.
|
||||
|
||||
### 1. Загрузить резервную копию
|
||||
|
||||
Загрузите существующую резервную копию базы данных на сервер XC_VM, используя **SFTP**.
|
||||
Загрузите существующую резервную копию базы данных на сервер XC_VM с помощью **SFTP-протокол**.
|
||||
|
||||
Примерное местоположение:
|
||||
|
||||
@@ -64,7 +64,7 @@ XC_VM миграция переносит только **данные**.
|
||||
sudo /home/xc_vm/console.php tools migration "/tmp/backup.sql"
|
||||
```
|
||||
|
||||
Прежде чем продолжить, убедитесь, что восстановление завершилось ** без ошибок**.
|
||||
Прежде чем продолжить, убедитесь, что восстановление завершено **без ошибок**.
|
||||
|
||||
---
|
||||
|
||||
@@ -80,8 +80,8 @@ sudo /home/xc_vm/console.php tools migration "/tmp/backup.sql"
|
||||
|
||||
#### Вариант 2 — Веб-установщик
|
||||
|
||||
- Вернитесь к веб-установщику ** (ссылка показана во время настройки панели).
|
||||
- Выберите **Перенос**
|
||||
- Вернитесь к **веб-установщик** (ссылка, показанная при настройке панели).
|
||||
- Выберите **Миграция**
|
||||
- Следуйте инструкциям на экране
|
||||
|
||||
Вы будете видеть обновления о ходе выполнения в режиме реального времени.
|
||||
@@ -105,13 +105,13 @@ sudo /home/xc_vm/console.php tools access
|
||||
sudo /home/xc_vm/console.php tools user
|
||||
```
|
||||
|
||||
> ❗️ После восстановления доступа ** немедленно измените ** код доступа и учетные данные администратора.
|
||||
> ❗️ После восстановления доступа введите **немедленно измените** код доступа и учетные данные администратора.
|
||||
|
||||
---
|
||||
|
||||
## Подготовка балансировщика нагрузки
|
||||
|
||||
Подсистемы балансировки нагрузки **не перенесены**.
|
||||
Балансировщики нагрузки имеют значение **не перенесен**.
|
||||
|
||||
- При необходимости переустановите операционную систему
|
||||
- Перенастройка сети и маршрутизации
|
||||
@@ -121,9 +121,9 @@ sudo /home/xc_vm/console.php tools user
|
||||
|
||||
## Постмиграционный период (обязательно)
|
||||
|
||||
После миграции система ** не готова к работе** до тех пор, пока не будут выполнены эти шаги.
|
||||
После миграции система будет находиться в состоянии **не готов к производству** до тех пор, пока эти шаги не будут выполнены.
|
||||
|
||||
Пропуск их приведет к ** ожидаемому, но нарушенному поведению **.
|
||||
Пропуск их приведет к результату **ожидаемое, но нарушенное поведение**.
|
||||
|
||||
---
|
||||
|
||||
@@ -132,7 +132,7 @@ sudo /home/xc_vm/console.php tools user
|
||||
- Запускайте все потоки вручную
|
||||
- Убедитесь, что потоки доступны и стабильны
|
||||
|
||||
> Состояние выполнения потока ** никогда не сохраняется ** во время миграции.
|
||||
> Состояние выполнения потока во время миграции равно **никогда не сохранявшийся**.
|
||||
|
||||
---
|
||||
|
||||
@@ -145,7 +145,7 @@ sudo /home/xc_vm/console.php tools user
|
||||
- Настройки сети и обратного прокси-сервера
|
||||
- Настройка производительности
|
||||
|
||||
> Не предполагайте, что значения по умолчанию соответствуют вашим предыдущим настройкам.
|
||||
> Сделайте **нет** вывод, что значения по умолчанию соответствуют вашим предыдущим настройкам.
|
||||
> Значения по умолчанию применяются намеренно.
|
||||
|
||||
---
|
||||
@@ -154,15 +154,15 @@ sudo /home/xc_vm/console.php tools user
|
||||
|
||||
#### Ключи API Никогда Не Переносятся
|
||||
|
||||
Следующие параметры ** необходимо перенастроить вручную**:
|
||||
Следующее **необходимо перенастроить вручную**:
|
||||
|
||||
- **Ключ API TMDb**
|
||||
- **TMDb API key**
|
||||
|
||||
Это ** ожидаемое поведение**.
|
||||
Это **ожидаемое поведение**.
|
||||
|
||||
> Если выборка метаданных не работает после миграции,
|
||||
> убедитесь, что ключ API был повторно добавлен и поставщик включен.
|
||||
> Это не указывает на ошибку переноса.
|
||||
> This does **not** indicate a migration bug.
|
||||
|
||||
---
|
||||
|
||||
@@ -170,19 +170,19 @@ sudo /home/xc_vm/console.php tools user
|
||||
|
||||
### Метаданные Не извлекаются (TMDb)
|
||||
|
||||
**Причина:**
|
||||
**Cause:**
|
||||
Ключ API TMDb и конфигурация поставщика не были восстановлены.
|
||||
|
||||
**Разрешение:**
|
||||
**Resolution:**
|
||||
Повторно добавьте ключ API TMDb и включите провайдера в настройках главного сервера.
|
||||
|
||||
---
|
||||
|
||||
## Резюме
|
||||
|
||||
- Миграционный перенос ** только основных данных приложения**
|
||||
- Конфигурация ** исключена по замыслу**
|
||||
- Ключи API и настройки, зависящие от среды **, должны быть восстановлены вручную**
|
||||
- Отсутствие функциональности после миграции ** ожидается до завершения реконфигурации**
|
||||
- Миграционные переводы **только основные данные приложения**
|
||||
- Конфигурация равна **исключено по замыслу**
|
||||
- Ключи API и настройки, зависящие от среды **необходимо восстановить вручную**
|
||||
- Недостающая функциональность после миграции равна **ожидается до завершения реконфигурации**
|
||||
|
||||
---
|
||||
|
||||
@@ -24,12 +24,12 @@ Watch Folder - это автоматизированная система имп
|
||||
|
||||
### Шаг за шагом
|
||||
|
||||
1. **Администратор создает папку просмотра** в панели администратора (Папка просмотра → Добавить) или через API (`create_watch_folder`). Конфигурация включает в себя: путь к каталогу, тип контента (фильм/сериал), целевую категорию, букеты, настройки парсера и назначенный сервер.
|
||||
2. **Задание Cron `cron:watch`** выполняется периодически (контролируется с помощью `scan_offset` — секундного интервала между сканированиями). Оно запрашивает таблицу `watch_folders` для активных папок, где `last_run` превысило смещение.
|
||||
1. **Администратор создает папку наблюдения** в панели администратора (Папка просмотра → Добавить) или через API (`create_watch_folder`). Конфигурация включает в себя: путь к каталогу, тип контента (фильм/сериал), целевую категорию, букеты, настройки парсера и назначенный сервер.
|
||||
2. **Задание Cron `cron:watch`** выполняется периодически (регулируется параметром `scan_offset` — секунды между сканированиями). Он запрашивает таблицу `watch_folders` для активных папок, в которых `last_run` превышено смещение.
|
||||
3. **Обнаружение файлов** — cron использует `find` для локальных каталогов или `rclone lsjson` для облачных/удаленных подключений. Файлы фильтруются по разрешенным расширениям (по умолчанию: `mp4, mkv, avi, mpg, flv, 3gp, m4v, wmv, mov, ts`). Файлы, уже присутствующие в `streams.stream_source`, пропускаются.
|
||||
4. **Проверка стабильности ** — файлы, измененные менее 30 секунд назад, пропускаются (чтобы избежать импорта частично загруженных файлов).
|
||||
5. **Параллельная обработка** — каждый новый файл отправляется команде `watch_item` (через `shell_exec`), которая выполняет до `thread_count` элементов параллельно, используя `Multithread`.
|
||||
6. **WatchItem** анализирует имя файла с помощью PTN или guessit (смотрите документацию по синтаксическому анализу ниже), преобразует метаданные с помощью TMDB API и вставляет запись в `streams` (для фильмов) или `streams_series` + `streams_episodes` (для сериалов).
|
||||
4. **Проверка стабильности** — файлы, измененные менее 30 секунд назад, пропускаются (чтобы избежать импорта частично загруженных файлов).
|
||||
5. **Параллельная обработка** — каждый новый файл отправляется команде `watch_item` (через `shell_exec`), параллельно запуская до `thread_count` элементов с помощью `Multithread`.
|
||||
6. **Элемент наблюдения** анализирует имя файла с помощью PTN или guessit (смотрите документацию по синтаксическому анализу ниже), преобразует метаданные с помощью TMDB API и вставляет запись в `streams` (для фильмов) или `streams_series` + `streams_episodes` (для сериалов).
|
||||
7. **Назначение букета** — импортированные элементы автоматически добавляются в настроенные букеты.
|
||||
|
||||
---
|
||||
@@ -72,7 +72,7 @@ Watch Folder - это автоматизированная система имп
|
||||
|
||||
|Установка|Где|Описание|
|
||||
|---------|-------|-------------|
|
||||
| `tmdb_api_key` |Администратор → Настройки|**Требуется** — Ключ API TMDB. Без него Watch не будет работать|
|
||||
| `tmdb_api_key` |Администратор → Настройки|**Требуемый** — Ключ API TMDB. Без него Watch не будет работать|
|
||||
| `fallback_parser` |Администратор → Настройки|Синтаксический анализатор, используемый при сбое основного синтаксического анализатора|
|
||||
| `alternative_titles` |Администратор → Настройки|Поиск альтернативных названий в базе данных TMDB|
|
||||
| `max_genres` |Администратор → Настройки|Максимальное количество жанров, назначаемых для каждого элемента|
|
||||
@@ -124,8 +124,8 @@ sudo -u xc_vm /home/xc_vm/console.php cron:watch 5
|
||||
|
||||
|Синтаксический анализатор|Лучше всего для|
|
||||
|--------|----------|
|
||||
|**PTN (Почтовый индекс)**| Simple filenames with spaces: `San Andreas 2015 720p.mkv` |
|
||||
|** угадать**| Dot-separated filenames: `The.Matrix.1999.1080p.BluRay.mkv` |
|
||||
| **PTN** | Simple filenames with spaces: `San Andreas 2015 720p.mkv` |
|
||||
| **guessit** | Dot-separated filenames: `The.Matrix.1999.1080p.BluRay.mkv` |
|
||||
|
||||
Установите основной синтаксический анализатор для каждой папки просмотра. Глобальный параметр `fallback_parser` используется, когда основной синтаксический анализатор не возвращает совпадений.
|
||||
|
||||
@@ -173,7 +173,7 @@ Guessit поддерживает более сложные имена файло
|
||||
|
||||
### Возврат к имени резервной папки
|
||||
|
||||
Если имя файла не содержит отображаемого заголовка, включите ** Резервный вариант для имени папки**:
|
||||
Если имя файла не содержит отображаемого заголовка, включите **Возврат к имени резервной папки**:
|
||||
|
||||
|Пример пути к файлу|Проанализированные данные|
|
||||
|-----------------|-------------|
|
||||
@@ -195,8 +195,8 @@ Guessit поддерживает более сложные имена файло
|
||||
|
||||
Для показов на языках RTL (арабский, иврит и т.д.):
|
||||
|
||||
- Имя файла **НЕ должно содержать заголовка show**
|
||||
- Включить ** Резервное копирование к имени папки**
|
||||
- Имя файла **не должно содержать названия шоу**
|
||||
- Включить **Возврат к имени резервной папки**
|
||||
|
||||
|Пример пути к файлу|Проанализированные данные|
|
||||
|-----------------|-------------|
|
||||
@@ -209,11 +209,11 @@ Guessit поддерживает более сложные имена файло
|
||||
|
||||
### Резюме
|
||||
|
||||
- **Синтаксический анализатор PTN** — простые имена файлов, локальные форматы
|
||||
- ** синтаксический анализатор guessit ** — поддерживает имена, разделенные точками, многоязычные заголовки, возврат к имени папки
|
||||
- **Анализатор PTN** — простые имена файлов, локальные форматы
|
||||
- **синтаксический анализатор догадок** — поддерживает имена, разделенные точками, многоязычные заголовки, возврат к имени папки
|
||||
- **Языки RTL** — необходимо использовать резервную копию имени папки
|
||||
- **Структура папок сезона ** — для правильной сортировки в имени файла должно быть указано название показа
|
||||
- **Структура сезонных папок** — отображаемый заголовок должен быть в имени файла для корректной сортировки
|
||||
|
||||
---
|
||||
|
||||
💡 ** Совет:** Используйте согласованные имена файлов и папок для точного анализа и автоматической сортировки по сезонам.
|
||||
💡 **Совет:** Используйте согласованные имена файлов и папок для точного анализа и автоматической сортировки по сезонам.
|
||||
|
||||
+92
-31
@@ -38,7 +38,7 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
# Bumped when the translation prompt/rules change, to invalidate the cache.
|
||||
PROMPT_VERSION = "4"
|
||||
PROMPT_VERSION = "6"
|
||||
|
||||
LANG_NAMES = {
|
||||
"ru": "Russian",
|
||||
@@ -80,6 +80,7 @@ def cache_key(text: str, lang: str, provider: str, glossary: list[str]) -> str:
|
||||
# Each provider is ``translate(text, lang, glossary) -> text``. Add a new engine
|
||||
# by writing one function and registering it in PROVIDERS below.
|
||||
|
||||
|
||||
def _system_prompt(lang_name: str, glossary: list[str]) -> str:
|
||||
rules = [
|
||||
f"You are a professional technical translator. Translate the given "
|
||||
@@ -93,6 +94,9 @@ def _system_prompt(lang_name: str, glossary: list[str]) -> str:
|
||||
"- NEVER translate URLs, file paths, HTML tags/attributes, or link "
|
||||
"targets — translate only human-visible link text.",
|
||||
"- Keep heading text natural; anchors are derived from it automatically.",
|
||||
"- Preserve inline emphasis markers (**bold**, *italic*, _underscore_) "
|
||||
"verbatim and balanced: wrap the translated words with the SAME opening "
|
||||
"and closing markers; never drop or misplace a closing **.",
|
||||
"- Preserve trailing/leading whitespace and blank-line layout.",
|
||||
]
|
||||
if glossary:
|
||||
@@ -156,29 +160,44 @@ def provider_deepl(text: str, lang: str, glossary: list[str]) -> str:
|
||||
|
||||
_TS_SENTINEL = re.compile(r"\{(\d+)\}")
|
||||
_POSS = r"(?:['’]s|['’])?" # optional trailing possessive, dropped on restore
|
||||
_HR = re.compile(r"^\s*([-*_])( *\1){2,}\s*$") # thematic break ---
|
||||
_TABLE_SEP = re.compile(r"^\s*\|?[\s:|-]+\|?\s*$") # |---|:--:| row
|
||||
_HR = re.compile(r"^\s*([-*_])( *\1){2,}\s*$") # thematic break ---
|
||||
_TABLE_SEP = re.compile(r"^\s*\|?[\s:|-]+\|?\s*$") # |---|:--:| row
|
||||
_HEADING = re.compile(r"^(\s{0,3}#{1,6}\s+)(.*)$")
|
||||
_LIST = re.compile(r"^(\s*(?:[-*+]|\d+[.)])\s+)(.*)$")
|
||||
_QUOTE = re.compile(r"^(\s*>+\s*)(.*)$")
|
||||
|
||||
|
||||
def _protect_inline(text: str, glossary: list[str]) -> tuple[str, list[str]]:
|
||||
def _protect_inline(text, glossary, tr=None, memo=None):
|
||||
store: list[str] = []
|
||||
|
||||
def keep(m: "re.Match[str]") -> str: # store the whole match
|
||||
def keep(m: "re.Match[str]") -> str: # store the whole match
|
||||
store.append(m.group(0))
|
||||
return f"{{{len(store) - 1}}}"
|
||||
|
||||
def keep1(m: "re.Match[str]") -> str: # store group 1 (drops possessive)
|
||||
def keep1(m: "re.Match[str]") -> str: # store group 1 (drops possessive)
|
||||
store.append(m.group(1))
|
||||
return f"{{{len(store) - 1}}}"
|
||||
|
||||
text = re.sub(rf"(`[^`]*`){_POSS}", keep1, text) # inline code (+poss)
|
||||
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", keep, text) # images (whole)
|
||||
text = re.sub(r"(?<=\])\([^)]*\)", keep, text) # link target (url)
|
||||
text = re.sub(r"https?://[^\s)]+", keep, text) # bare URLs
|
||||
text = re.sub(r"</?[A-Za-z][^>]*>", keep, text) # HTML tags
|
||||
# Bold spans `**...**`: translate the inner text on its own, then store the
|
||||
# whole balanced `**...**` as ONE atomic sentinel. The MT engine never sees
|
||||
# the `**` markers, so it cannot reorder or collapse them — masking each `**`
|
||||
# separately let the words move out from between the pair and left `****`
|
||||
# (empty bold) or a dropped closing `**`. Needs a translator; without one
|
||||
# (plain masking use) bold is left untouched.
|
||||
if tr is not None:
|
||||
|
||||
def keep_bold(m: "re.Match[str]") -> str:
|
||||
inner = _ts_span(m.group(1), tr, glossary, memo if memo is not None else {})
|
||||
store.append("**" + inner + "**")
|
||||
return f"{{{len(store) - 1}}}"
|
||||
|
||||
text = re.sub(r"\*\*(.+?)\*\*", keep_bold, text) # bold (inner translated)
|
||||
|
||||
text = re.sub(rf"(`[^`]*`){_POSS}", keep1, text) # inline code (+poss)
|
||||
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", keep, text) # images (whole)
|
||||
text = re.sub(r"(?<=\])\([^)]*\)", keep, text) # link target (url)
|
||||
text = re.sub(r"https?://[^\s)]+", keep, text) # bare URLs
|
||||
text = re.sub(r"</?[A-Za-z][^>]*>", keep, text) # HTML tags
|
||||
for term in glossary:
|
||||
text = re.sub(rf"(?<![\w-])({re.escape(term)}){_POSS}(?![\w-])", keep1, text)
|
||||
return text, store
|
||||
@@ -201,15 +220,22 @@ def _ts_span(text, tr, glossary, memo):
|
||||
return text
|
||||
if text in memo:
|
||||
return memo[text]
|
||||
protected, store = _protect_inline(text, glossary)
|
||||
protected, store = _protect_inline(text, glossary, tr, memo)
|
||||
if not _TS_SENTINEL.sub("", protected).strip():
|
||||
out = text # nothing left to translate (all masked)
|
||||
else:
|
||||
base_braces = text.count("{") + text.count("}")
|
||||
src_bold = text.count("**")
|
||||
out = None
|
||||
for _ in range(4):
|
||||
cand = _restore(tr(protected), store)
|
||||
if cand.count("{") + cand.count("}") <= base_braces: # no leftovers
|
||||
# Clean result: no leftover sentinel braces AND the bold markers are
|
||||
# still balanced (a dropped `**` sentinel restores fewer `**` than
|
||||
# the source — reject it and retry, else fall back to English).
|
||||
if (
|
||||
cand.count("{") + cand.count("}") <= base_braces
|
||||
and cand.count("**") == src_bold
|
||||
):
|
||||
out = cand
|
||||
break
|
||||
if out is None:
|
||||
@@ -222,7 +248,7 @@ def _ts_line(line, tr, glossary, memo):
|
||||
"""Translate one Markdown line, preserving its structural prefix/markup."""
|
||||
if not line.strip() or _HR.match(line):
|
||||
return line
|
||||
if line.lstrip().startswith("|"): # table row
|
||||
if line.lstrip().startswith("|"): # table row
|
||||
if _TABLE_SEP.match(line):
|
||||
return line
|
||||
return "|".join(
|
||||
@@ -265,8 +291,10 @@ def provider_translators(text: str, lang: str, glossary: list[str]) -> str:
|
||||
import translators as ts # lazy
|
||||
|
||||
engines = [
|
||||
e.strip() for e in
|
||||
os.environ.get("DOCS_TRANSLATE_TS_ENGINES", "yandex,google,bing,alibaba").split(",")
|
||||
e.strip()
|
||||
for e in os.environ.get(
|
||||
"DOCS_TRANSLATE_TS_ENGINES", "yandex,google,bing,alibaba"
|
||||
).split(",")
|
||||
if e.strip()
|
||||
]
|
||||
|
||||
@@ -296,25 +324,39 @@ PROVIDERS = {
|
||||
|
||||
# ── Driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--lang", required=True, help="target language code, e.g. ru")
|
||||
ap.add_argument("--src", default=str(repo_root / "docs" / "en"),
|
||||
help="source (English) docs dir")
|
||||
ap.add_argument("--dst", default=None,
|
||||
help="destination dir (default: docs/<lang>)")
|
||||
ap.add_argument("--cache", default=str(repo_root / "build" / "docs-cache"),
|
||||
help="per-file translation cache dir")
|
||||
ap.add_argument("--glossary", default=str(repo_root / "tools" / "docs" / "glossary.txt"),
|
||||
help="do-not-translate term list")
|
||||
ap.add_argument(
|
||||
"--src",
|
||||
default=str(repo_root / "docs" / "en"),
|
||||
help="source (English) docs dir",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--dst", default=None, help="destination dir (default: docs/<lang>)"
|
||||
)
|
||||
ap.add_argument(
|
||||
"--cache",
|
||||
default=str(repo_root / "build" / "docs-cache"),
|
||||
help="per-file translation cache dir",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--glossary",
|
||||
default=str(repo_root / "tools" / "docs" / "glossary.txt"),
|
||||
help="do-not-translate term list",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
provider_name = os.environ.get("DOCS_TRANSLATE_PROVIDER", "noop")
|
||||
translate = PROVIDERS.get(provider_name)
|
||||
if translate is None:
|
||||
print(f"error: unknown DOCS_TRANSLATE_PROVIDER={provider_name!r} "
|
||||
f"(known: {', '.join(PROVIDERS)})", file=sys.stderr)
|
||||
print(
|
||||
f"error: unknown DOCS_TRANSLATE_PROVIDER={provider_name!r} "
|
||||
f"(known: {', '.join(PROVIDERS)})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
src = Path(args.src)
|
||||
@@ -349,8 +391,10 @@ def main() -> int:
|
||||
# Graceful degradation: a flaky/rate-limited web engine must never
|
||||
# break the docs build — fall back to the English source for this
|
||||
# file (and do NOT cache it, so it is retried next run).
|
||||
print(f" WARN {rel}: translation failed ({exc}); keeping English",
|
||||
file=sys.stderr)
|
||||
print(
|
||||
f" WARN {rel}: translation failed ({exc}); keeping English",
|
||||
file=sys.stderr,
|
||||
)
|
||||
failed += 1
|
||||
out = text
|
||||
else:
|
||||
@@ -362,9 +406,26 @@ def main() -> int:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(out, encoding="utf-8")
|
||||
|
||||
print(f"[{provider_name}] {args.lang}: {len(md_files)} files "
|
||||
f"({translated} translated, {cached} from cache, {failed} fell back "
|
||||
f"to English) -> {dst}")
|
||||
# Prune orphans: delete generated files whose English source no longer exists
|
||||
# (renamed/removed in docs/en), then drop any now-empty dirs — so the
|
||||
# translated tree always mirrors docs/en 1:1 and never ships stale pages.
|
||||
expected = {p.relative_to(src) for p in md_files}
|
||||
pruned = 0
|
||||
if dst.is_dir():
|
||||
for gen in sorted(dst.rglob("*.md")):
|
||||
if gen.relative_to(dst) not in expected:
|
||||
gen.unlink()
|
||||
pruned += 1
|
||||
print(f" pruned orphan {gen.relative_to(dst)}")
|
||||
for d in sorted((p for p in dst.rglob("*") if p.is_dir()), reverse=True):
|
||||
if not any(d.iterdir()):
|
||||
d.rmdir()
|
||||
|
||||
print(
|
||||
f"[{provider_name}] {args.lang}: {len(md_files)} files "
|
||||
f"({translated} translated, {cached} from cache, {failed} fell back "
|
||||
f"to English, {pruned} pruned) -> {dst}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user