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:
Divarion_D
2026-08-27 18:07:43 +03:00
parent be2c4b3ad7
commit d4da90f37b
44 changed files with 2190 additions and 1545 deletions
+2 -2
View File
@@ -38,8 +38,8 @@ XC_VM помогает вам развернуть полноценную инф
## Технологии
- **Nginx** — обратный прокси и веб-сервер
- **PHP 8.1** — базовый сервер
- **Nginx** — обратный прокси-сервер и веб-сервер
- **PHP 8.1** — основная серверная часть
- **MariaDB** — база данных
- **KeyDB** — механизм кэширования/сеанса
- **FFmpeg 8.0** — перекодирование
+6 -6
View File
@@ -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`.
+2 -2
View File
@@ -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.
---
+19 -19
View File
@@ -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` из основного|
+32 -4
View File
@@ -6,7 +6,7 @@
## Обновление через панель управления
**Шаг 1.** Откройте раздел "Серверы" в верхнем меню панели.
**Шаг 1.** Откройте раздел **Серверы** в верхнем меню панели.
![Servers menu](../../_media/update1.png)
@@ -14,15 +14,15 @@
![Manage Servers item](../../_media/update2.png)
**Шаг 3.** Найдите целевой сервер в таблице "Серверы" и нажмите кнопку "Меню" в столбце "Действия".
**Шаг 3.** Найдите целевой сервер в таблице серверы и нажмите кнопку меню в столбце **Действия**.
![Actions button](../../_media/update3.png)
**Шаг 4.** Выберите **Серверные инструменты** в меню.
**Шаг 4.** Выберите в меню пункт **Серверные инструменты**.
![Server Tools item](../../_media/update4.png)
**Шаг 5.** В диалоговом окне "Серверные инструменты"** нажмите "Обновить сервер"**.
**Шаг 5.** В диалоговом окне **Серверные инструменты** нажмите кнопку **Сервер обновлений**.
![Update Server button](../../_media/update5.png)
@@ -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 и применяет его с теми же проверками целостности и — в основном — автоматическим резервным копированием базы данных.
+11 -11
View File
@@ -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
+46 -20
View File
@@ -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 +.
---
+9 -9
View File
@@ -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 не работает.
---
+37 -22
View File
@@ -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
```
### Новый каталог, доступный только для администратора
+26 -29
View File
@@ -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|
+13 -12
View File
@@ -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).
## Инструменты для разработки
+26 -20
View File
@@ -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
```
+9 -9
View File
@@ -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` предупреждение о сбое записи, поэтому это значение по умолчанию маскирует только временный
+195
View File
@@ -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` |Веб-точка входа: файлы маршрута + загрузочный блок модуля|
+14 -2
View File
@@ -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/` |После сохранения настроек|Нет|
+41 -17
View File
@@ -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
+22 -11
View File
@@ -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` |Определения маршрута на странице игрока|
+422
View File
@@ -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()`.
### Приоритеты трубопровода
|Диапазон|Владелец|
| ---------- | ----------------- |
| `80100` |Ядро (авторизация, разрешение, ограничение подключения)|
| `079` |Модули|
### Зарезервированные слоты на панели навигации
|Родительский узел|Гнезда для модулей|
| ------------------- | ------------------ |
| `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` является необязательным
---
+141
View File
@@ -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/` загружается только один раз.
---
-766
View File
@@ -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()`.
### Приоритеты трубопровода
|Диапазон|Владелец|
| ---------- | ----------------- |
| `80100` |Ядро (авторизация, разрешение, ограничение подключения)|
| `079` |Модули|
### Зарезервированные слоты на панели навигации
|Родительский узел|Гнезда для модулей|
| ------------------- | ------------------ |
| `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/` |Подинтерфейсы модуля|
+41 -1
View File
@@ -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` |мониторинг целостности очереди + панель мониторинга динамического буфера|
+49 -110
View File
@@ -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` |мониторинг целостности очереди + панель мониторинга динамического буфера|
+26 -26
View File
@@ -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
View File
@@ -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).
---
+177
View File
@@ -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` точка входа|
+20 -8
View File
@@ -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` |заблокированные / разрешенные проверки пользовательского агента|
+1 -1
View File
@@ -73,7 +73,7 @@
### `getPIDs(int $rServerID): array`
Анализирует информацию о системном процессе из ответа API сервера. Возвращает структурированные данные о процессе для мониторинга.
Анализирует информацию о системном процессе из ответа API сервера. Возвращает структурированные данные процесса для мониторинга.
---
+14 -14
View File
@@ -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`.
+30 -5
View File
@@ -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`), эффективным значением является **операционная** из двух —
выигрывает любой из вариантов, включающий его.
---
@@ -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|
+5 -5
View File
@@ -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`. Если для нового действия требуется стробирование ввода, необходимо явно добавить регистр.
+26 -2
View File
@@ -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`).
---
+5 -5
View File
@@ -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`).
+46 -2
View File
@@ -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
+50 -13
View File
@@ -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`.
---
+8 -8
View File
@@ -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
+37 -11
View File
@@ -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
View File
@@ -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
+32 -32
View File
@@ -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 и настройки, зависящие от среды **необходимо восстановить вручную**
- Недостающая функциональность после миграции равна **ожидается до завершения реконфигурации**
---
+15 -15
View File
@@ -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
View File
@@ -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