style: apply K&R + tab formatting across the codebase (make cs-fix)

Mechanical, behaviour-preserving reformat produced by 'make cs-fix' under
the new build/phpcs.xml.dist ruleset: K&R braces, tab indentation, and the
other whitespace normalisations. No logic changes.
This commit is contained in:
Divarion_D
2026-09-13 14:14:23 +03:00
parent 5843f18bb7
commit 758a9cab0b
513 changed files with 35922 additions and 36029 deletions
-1
View File
@@ -16,7 +16,6 @@ namespace XcVm\Cli;
*/
interface CommandInterface {
/**
* Уникальное имя команды.
*
-1
View File
@@ -20,7 +20,6 @@ namespace XcVm\Cli;
*/
class CommandRegistry {
/** @var CommandInterface[] name → command */
private $rCommands = [];
+7 -8
View File
@@ -25,7 +25,6 @@ use XcVm\Domain\Stream\StreamProcess;
*/
class ArchiveCommand implements CommandInterface {
/** @inheritDoc */
public function getName(): string {
return 'archive';
@@ -111,7 +110,7 @@ class ArchiveCommand implements CommandInterface {
// currently listed in the playlist to avoid reading a partial file.
$rPlaylistContent = @file_get_contents($rPlaylist);
preg_match_all('/_(\d+)\.ts/', ($rPlaylistContent ?: ''), $rSegMatches);
$rSegNum = !empty($rSegMatches[1]) ? (int)end($rSegMatches[1]) + 1 : 0;
$rSegNum = !empty($rSegMatches[1]) ? (int) end($rSegMatches[1]) + 1 : 0;
$this->logStatus('Starting from source segment #' . $rSegNum . '.');
// Open the first archive minute file; subsequent ones are opened on rotation.
@@ -165,7 +164,7 @@ class ArchiveCommand implements CommandInterface {
if ($rWaitCount % 300 === 0) {
$rFreshContent = @file_get_contents($rPlaylist);
preg_match_all('/_(\d+)\.ts/', ($rFreshContent ?: ''), $rFreshMatches);
$rFreshSeg = !empty($rFreshMatches[1]) ? (int)end($rFreshMatches[1]) : -1;
$rFreshSeg = !empty($rFreshMatches[1]) ? (int) end($rFreshMatches[1]) : -1;
if ($rFreshSeg >= 0 && $rFreshSeg > $rSegNum + 1) {
// Forward gap: ffmpeg skipped segment numbers (old files purged
@@ -269,7 +268,7 @@ class ArchiveCommand implements CommandInterface {
*
* @param string $rMessage Human-readable status message.
*/
private function logStatus($rMessage): void {
private function logStatus(string $rMessage): void {
echo '[archive][' . getmypid() . '][' . gmdate('Y-m-d H:i:s') . '] ' . $rMessage . "\n";
}
@@ -283,7 +282,7 @@ class ArchiveCommand implements CommandInterface {
* @param int $rStreamID Stream identifier used to build the archive path.
* @param int $rDuration Maximum retention duration in days.
*/
private function deleteSegments($rStreamID, $rDuration): void {
private function deleteSegments(int $rStreamID, int $rDuration): void {
$rSegmentCount = intval(count(scandir(ARCHIVE_PATH . $rStreamID . '/')) - 2);
if ($rDuration * 24 * 60 < $rSegmentCount) {
$rDelta = $rSegmentCount - $rDuration * 24 * 60;
@@ -308,7 +307,7 @@ class ArchiveCommand implements CommandInterface {
*
* @param int $rStreamID Stream identifier.
*/
private function checkRunning($rStreamID): void {
private function checkRunning(int $rStreamID): void {
clearstatcache(true);
$rPID = null;
if (file_exists(STREAMS_PATH . $rStreamID . '_.archive')) {
@@ -326,7 +325,7 @@ class ArchiveCommand implements CommandInterface {
file_put_contents(STREAMS_PATH . $rStreamID . '_.archive', getmypid());
$this->logStatus('Wrote current PID to archive marker file.');
}
/**
* Checks whether a given PID is an active TVArchive worker for this stream.
*
@@ -337,7 +336,7 @@ class ArchiveCommand implements CommandInterface {
* @param int $rStreamID Stream identifier to match against the process title.
* @return bool True if the process is an archive worker for this stream.
*/
private function isArchiveProcessForStream($rPID, $rStreamID): bool {
private function isArchiveProcessForStream(int $rPID, int $rStreamID): bool {
if (!is_numeric($rPID) || 0 >= intval($rPID) || !file_exists('/proc/' . $rPID)) {
return false;
}
+1 -2
View File
@@ -17,7 +17,6 @@ use XcVm\Core\Updates\UpdateChannels;
*/
class BinariesCommand implements CommandInterface {
public function getName(): string {
return 'binaries';
}
@@ -101,7 +100,7 @@ class BinariesCommand implements CommandInterface {
$rLogFile = $rLogDir . 'binaries_update_' . date('Ymd_His') . '.log';
$rDetachedCommand = 'nohup ' . $rCommand . ' > ' . escapeshellarg($rLogFile) . ' 2>&1 < /dev/null & echo $!';
echo "Starting binaries updater in background...\n";
$rPid = trim((string)shell_exec($rDetachedCommand));
$rPid = trim((string) shell_exec($rDetachedCommand));
if (empty($rPid)) {
echo "Failed to start binaries updater in background.\n";
+1 -1
View File
@@ -73,7 +73,7 @@ class CacheHandlerCommand implements CommandInterface {
}
try {
$rUpdatedLines = array();
$rUpdatedLines = [];
foreach (SignalQueue::pending() as list($rFileMD5, $rKey, $rData)) {
list($rHeader) = explode('/', $rKey);
switch ($rHeader) {
+9 -10
View File
@@ -17,7 +17,6 @@ use XcVm\Domain\Server\ServerRepository;
*/
class CertbotCommand implements CommandInterface {
public function getName(): string {
return 'certbot';
}
@@ -50,26 +49,26 @@ class CertbotCommand implements CommandInterface {
if (file_exists(BIN_PATH . 'certbot/logs/xc_vm.log')) {
unlink(BIN_PATH . 'certbot/logs/xc_vm.log');
}
foreach (array('logs', 'config', 'work') as $rPath) {
foreach (['logs', 'config', 'work'] as $rPath) {
if (file_exists(BIN_PATH . 'certbot/' . $rPath . '/.certbot.lock')) {
unlink(BIN_PATH . 'certbot/' . $rPath . '/.certbot.lock');
}
}
$rActiveDomains = array();
$rActiveDomains = [];
foreach ($rData['domain'] as $rDomain) {
if (!empty($rDomain) && !filter_var($rDomain, FILTER_VALIDATE_IP)) {
$rActiveDomains[] = $rDomain;
}
}
$rError = null;
$rOutput = array();
$rOutput = [];
$rResult = false;
if (0 < count($rActiveDomains)) {
$rCertbotWebroot = MAIN_HOME . 'certbot-webroot';
if (!is_dir($rCertbotWebroot)) {
mkdir($rCertbotWebroot, 0775, true);
}
foreach (array('--dry-run ', '') as $rDry) {
foreach (['--dry-run ', ''] as $rDry) {
if (ServerRepository::getAll()[SERVER_ID]['http_broadcast_port'] == 80) {
$rCommand = 'sudo certbot ' . $rDry . '--config-dir ' . BIN_PATH . 'certbot/config --work-dir ' . BIN_PATH . 'certbot/work --logs-dir ' . BIN_PATH . 'certbot/logs certonly --agree-tos --expand --non-interactive --register-unsafely-without-email --webroot -w ' . $rCertbotWebroot;
} else {
@@ -79,7 +78,7 @@ class CertbotCommand implements CommandInterface {
$rCommand .= ' -d ' . basename($rDomain);
}
$rCommand .= ' 2>&1';
$rOutput = array();
$rOutput = [];
exec($rCommand, $rOutput, $rReturn);
if (empty($rDry)) {
@@ -135,11 +134,11 @@ class CertbotCommand implements CommandInterface {
} else {
$rError = 3;
}
if (in_array($rError, array(0, 1))) {
if (in_array($rError, [0, 1])) {
$db->query('SELECT `certbot_ssl` FROM `servers` WHERE `id` = ?;', SERVER_ID);
$rCertInfo = json_decode($db->get_row()['certbot_ssl'], true);
if (!$rCertInfo) {
$rSelectedDomain = array(null, null);
$rSelectedDomain = [null, null];
foreach (scandir(BIN_PATH . 'certbot/config/live/') as $rDir) {
if ($rDir != '.' && $rDir != '..') {
$rSplit = explode('-', $rDir);
@@ -151,7 +150,7 @@ class CertbotCommand implements CommandInterface {
if (in_array(strtolower($rDomain), array_map('strtolower', $rActiveDomains))) {
$rInfo = DiagnosticsService::getCertificateInfo(BIN_PATH . 'certbot/config/live/' . $rDir . '/fullchain.pem');
if (($rInfo['serial'] && $rSelectedDomain[0] < $rInfo['expiration']) && !$rSelectedDomain[0]) {
$rSelectedDomain = array($rInfo['expiration'], $rInfo);
$rSelectedDomain = [$rInfo['expiration'], $rInfo];
}
}
}
@@ -171,7 +170,7 @@ class CertbotCommand implements CommandInterface {
}
}
}
$rReturn = array('status' => $rResult, 'error' => $rError, 'output' => $rOutput);
$rReturn = ['status' => $rResult, 'error' => $rError, 'output' => $rOutput];
if (!is_dir(BIN_PATH . 'certbot/logs')) {
mkdir(BIN_PATH . 'certbot/logs', 0775, true);
}
+13 -11
View File
@@ -20,7 +20,6 @@ use XcVm\Streaming\Codec\FFprobeRunner;
*/
class CreatedCommand implements CommandInterface {
public function getName(): string {
return 'created';
}
@@ -71,7 +70,7 @@ class CreatedCommand implements CommandInterface {
$rServerInfo['cchannel_rsources'] = json_decode($rServerInfo['cchannel_rsources'], true);
if (!$rServerInfo['cchannel_rsources']) {
$rServerInfo['cchannel_rsources'] = array();
$rServerInfo['cchannel_rsources'] = [];
}
$rSourcesLeft = array_diff($rStreamInfo['stream_source'], $rServerInfo['cchannel_rsources']);
@@ -133,13 +132,14 @@ class CreatedCommand implements CommandInterface {
. (isset($rEncode['speed']) ? ' @ ' . $rEncode['speed'] : '') . "\n";
$db->db_connect();
$db->query('UPDATE `streams_servers` SET `progress_info` = ? WHERE `server_stream_id` = ?', json_encode(array('cc_encode' => array(
$db->query('UPDATE `streams_servers` SET `progress_info` = ? WHERE `server_stream_id` = ?', json_encode(['cc_encode' => [
'source' => $rDone + 1,
'total' => $rTotal,
'pct' => $rPct,
'out_time' => gmdate('H:i:s', (int) $rOutSecs),
'speed' => ($rEncode['speed'] ?? null),
))), $rServerInfo['server_stream_id']);
]
]), $rServerInfo['server_stream_id']);
$db->close_mysql();
}
}
@@ -187,17 +187,19 @@ class CreatedCommand implements CommandInterface {
$rInt = $rSeconds = 0;
$rList = explode("\n", file_get_contents(CREATED_PATH . $rStreamID . '_.list'));
$rReturn = array();
$rReturn = [];
foreach ($rList as $rItem) {
$parts = explode("'", $rItem);
if (!isset($parts[1])) continue;
if (!isset($parts[1])) {
continue;
}
$rFilename = $parts[1];
if (file_exists($rFilename)) {
$rFileInfo = FFprobeRunner::probeStream($rFilename);
$rReturn[] = array(
$rReturn[] = [
'position' => $rInt,
'filename' => basename($rFilename),
'path' => $rFilename,
@@ -205,7 +207,7 @@ class CreatedCommand implements CommandInterface {
'seconds' => $rFileInfo['of_duration'],
'start' => $rSeconds,
'finish' => $rSeconds + $rFileInfo['of_duration']
);
];
$rSeconds += $rFileInfo['of_duration'];
$rInt++;
@@ -227,12 +229,12 @@ class CreatedCommand implements CommandInterface {
*/
private function readEncodeProgress(string $rFile): array {
if (!is_file($rFile)) {
return array();
return [];
}
$rHandle = @fopen($rFile, 'r');
if (!$rHandle) {
return array();
return [];
}
if (filesize($rFile) > 4096) {
fseek($rHandle, -4096, SEEK_END);
@@ -240,7 +242,7 @@ class CreatedCommand implements CommandInterface {
$rTail = stream_get_contents($rHandle);
fclose($rHandle);
$rOutput = array();
$rOutput = [];
foreach (array_filter(array_map('trim', explode("\n", (string) $rTail))) as $rRow) {
$rParts = explode('=', $rRow, 2);
if (count($rParts) == 2) {
-1
View File
@@ -16,7 +16,6 @@ use XcVm\Core\Database\MigrationRunner;
*/
class DbMigrateCommand implements CommandInterface {
public function getName(): string {
return 'db:migrate';
}
+10 -11
View File
@@ -19,7 +19,6 @@ use XcVm\Streaming\Fanout\IngestFeeder;
*/
class DelayCommand implements CommandInterface {
public function getName(): string {
return 'delay';
}
@@ -72,9 +71,9 @@ class DelayCommand implements CommandInterface {
$db->close_mysql();
$rDelayDuration = intval($rStreamInfo['delay_minutes']) + 5;
$this->cleanUpSegments($rStreamID, $rDelayDuration);
$rSegmentSettings = array('seg_time' => intval(SettingsManager::get('seg_time')), 'seg_list_size' => intval(SettingsManager::get('seg_list_size')), 'seg_delete_threshold' => intval(SettingsManager::get('seg_delete_threshold')));
$rSegmentSettings = ['seg_time' => intval(SettingsManager::get('seg_time')), 'seg_list_size' => intval(SettingsManager::get('seg_list_size')), 'seg_delete_threshold' => intval(SettingsManager::get('seg_delete_threshold'))];
$rTotalSegments = intval($rSegmentSettings['seg_list_size']) + 5;
$rOldSegments = array();
$rOldSegments = [];
if (file_exists($rPlaylistOld)) {
$rOldSegments = $this->getSegments($rPlaylistOld, -1);
}
@@ -89,7 +88,7 @@ class DelayCommand implements CommandInterface {
});
$rFeeder->connect();
$rFedSegment = null;
$rFeedQueue = array();
$rFeedQueue = [];
$rFeedCurrent = null;
$rPrevMD5 = null;
@@ -102,7 +101,7 @@ class DelayCommand implements CommandInterface {
$rSegmentSettings['seg_time'] = $rDuration;
}
}
$rM3U8 = array('vars' => array('#EXTM3U' => '', '#EXT-X-VERSION' => 3, '#EXT-X-MEDIA-SEQUENCE' => '0', '#EXT-X-TARGETDURATION' => $rSegmentSettings['seg_time']), 'segments' => $this->getData($rPlaylistDelay, $rOldSegments, $rTotalSegments, $rPlaylistOld));
$rM3U8 = ['vars' => ['#EXTM3U' => '', '#EXT-X-VERSION' => 3, '#EXT-X-MEDIA-SEQUENCE' => '0', '#EXT-X-TARGETDURATION' => $rSegmentSettings['seg_time']], 'segments' => $this->getData($rPlaylistDelay, $rOldSegments, $rTotalSegments, $rPlaylistOld)];
if (!empty($rM3U8['segments'])) {
$rData = '';
$rSequence = 0;
@@ -152,7 +151,7 @@ class DelayCommand implements CommandInterface {
* @return void
*/
private function queueForDaemon(array $rSegments, ?int &$rFedSegment, array &$rQueue): void {
$rNew = array();
$rNew = [];
foreach ($rSegments as $rSegment) {
if (preg_match('/_(\d+)\.ts$/', (string) ($rSegment['file'] ?? ''), $rMatch)) {
$rNumber = intval($rMatch[1]);
@@ -176,7 +175,7 @@ class DelayCommand implements CommandInterface {
}
if (is_string($rData) && strlen($rData) >= 188) {
$rData = substr($rData, 0, strlen($rData) - strlen($rData) % 188); // whole packets
$rQueue[] = array('data' => $rData, 'dur' => max(0.5, floatval($rSegment['seconds'])), 'burst' => $rSeed);
$rQueue[] = ['data' => $rData, 'dur' => max(0.5, floatval($rSegment['seconds'])), 'burst' => $rSeed];
}
$rFedSegment = $rNumber;
}
@@ -196,7 +195,7 @@ class DelayCommand implements CommandInterface {
$rNow = microtime(true);
if ($rCurrent === null && count($rQueue) > 0) {
$rItem = array_shift($rQueue);
$rCurrent = array('data' => $rItem['data'], 'sent' => 0, 'start' => $rNow, 'dur' => $rItem['dur'], 'burst' => $rItem['burst']);
$rCurrent = ['data' => $rItem['data'], 'sent' => 0, 'start' => $rNow, 'dur' => $rItem['dur'], 'burst' => $rItem['burst']];
}
if ($rCurrent === null) {
$rFeeder->flush();
@@ -236,7 +235,7 @@ class DelayCommand implements CommandInterface {
}
private function getData($rPlaylistDelay, &$rOldSegments, $rTotalSegments, $rPlaylistOld): array {
$rSegments = array();
$rSegments = [];
if (!empty($rOldSegments)) {
$rSegments = array_shift($rOldSegments);
unlink(DELAY_PATH . $rSegments['file']);
@@ -268,7 +267,7 @@ class DelayCommand implements CommandInterface {
}
private function getSegments($rPlaylist, $rCounter = 0): array {
$rSegments = array();
$rSegments = [];
if (file_exists($rPlaylist)) {
$rFP = fopen($rPlaylist, 'r');
while (!feof($rFP) && count($rSegments) != $rCounter) {
@@ -278,7 +277,7 @@ class DelayCommand implements CommandInterface {
$rSeconds = rtrim($rSeconds, ',');
$rSegmentFile = trim(fgets($rFP));
if (file_exists(DELAY_PATH . $rSegmentFile)) {
$rSegments[] = array('seconds' => $rSeconds, 'file' => $rSegmentFile);
$rSegments[] = ['seconds' => $rSeconds, 'file' => $rSegmentFile];
}
}
}
+5 -5
View File
@@ -41,7 +41,7 @@ class FanoutSyncCommand implements CommandInterface {
private const INTERVAL = 10;
/** @var array<string,int> Daemon viewer uuid => when it was first seen without a row. */
private array $rOrphanSince = array();
private array $rOrphanSince = [];
public function getName(): string {
return 'fanout_sync';
@@ -151,7 +151,7 @@ class FanoutSyncCommand implements CommandInterface {
* @return string[] The uuids dropped on this pass.
*/
public function dropOrphans(array $rActive, array $rConns, ?int $rNow = null, ?callable $rDrop = null): array {
$rKnown = array();
$rKnown = [];
foreach ($rConns as $rConn) {
if (is_array($rConn) && !empty($rConn['uuid']) && empty($rConn['hls_end'])) {
$rKnown[$rConn['uuid']] = true;
@@ -162,8 +162,8 @@ class FanoutSyncCommand implements CommandInterface {
$rDrop = $rDrop ?? static function (string $rUUID): void {
FanoutClient::dropConnection($rUUID);
};
$rSeen = array();
$rDropped = array();
$rSeen = [];
$rDropped = [];
foreach ($rActive as $rUUID) {
$rUUID = (string) $rUUID;
if ($rUUID === '' || isset($rKnown[$rUUID])) {
@@ -290,7 +290,7 @@ class FanoutSyncCommand implements CommandInterface {
if (!is_array($rKeys)) {
return null;
}
$rOut = array();
$rOut = [];
foreach ($rKeys as $rUUID) {
$rConn = ConnectionTracker::getConnection($rUUID);
if (is_array($rConn)) {
+23 -24
View File
@@ -7,19 +7,18 @@ use XcVm\Core\Updates\GitHubReleases;
use XcVm\Core\Updates\UpdateChannels;
class LbInstallFlow {
// Per-distribution package lists, mirrored from the MAIN installer (install -> PACKAGES),
// with the mariadb-server/client/common packages removed (LB nodes use the main server's DB).
public static function getPackages(string $rDistID = 'debian', string $rVersion = ''): array {
$rLists = array(
'debian' => array('iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'libgeoip-dev', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'sysstat', 'alsa-utils', 'v4l-utils', 'certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libogg0', 'libnuma1', 'xz-utils', 'zip', 'unzip', 'libssh2-1', 'libsodium23', 'cpufrequtils', 'mcrypt', 'cron', 'git', 'curl'),
'debian11' => array('iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'libgeoip-dev', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'alsa-utils', 'v4l-utils', 'certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libogg0', 'libnuma1', 'libssh2-1', 'libssh2-1-dev', 'libsodium23', 'cpufrequtils', 'mcrypt'),
'debian13' => array('iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'perl', 'gawk', 'socat', 'libxml2-dev', 'libxslt1-dev', 'libonig5', 'libonig-dev', 'zlib1g-dev', 'libssl-dev', 'pkg-config', 'autoconf', 'automake', 'alsa-utils', 'v4l-utils', 'e2fsprogs', 'certbot', 'iptables-persistent', 'libssh2-1', 'libssh2-1-dev', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libgeoip1', 'geoip-bin', 'libsodium23', 'cpufrequtils', 'mcrypt', 'libogg0', 'libnuma1'),
'ubuntu20' => array('iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'ca-certificates', 'libcurl3-gnutls', 'libcurl4-gnutls-dev', 'libxml2-dev', 'libxslt1-dev', 'libonig5', 'libonig-dev', 'libjpeg-dev', 'libpng-dev', 'zlib1g-dev', 'alsa-utils', 'v4l-utils', 'e2fsprogs', 'iptables-persistent', 'certbot', 'python3-certbot', 'libssh2-1', 'libssh2-1-dev', 'libsodium23', 'cpufrequtils', 'mcrypt', 'libogg0', 'libnuma1'),
'ubuntu22' => array('iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'libcurl3-gnutls', 'libgeoip-dev', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'ca-certificates', 'libxml2-dev', 'libonig5', 'zlib1g-dev', 'alsa-utils', 'v4l-utils', 'certbot', 'python3-certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libogg0', 'libnuma1', 'libssh2-1', 'libssh2-1-dev', 'libsodium23', 'cpufrequtils', 'mcrypt'),
'ubuntu24' => array('iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4t64', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'perl', 'gawk', 'socat', 'libxml2-dev', 'libxslt1-dev', 'libonig5', 'libonig-dev', 'zlib1g-dev', 'libssl-dev', 'pkg-config', 'autoconf', 'automake', 'alsa-utils', 'v4l-utils', 'e2fsprogs', 'certbot', 'python3-certbot', 'ufw', 'libssh2-1t64', 'libssh2-1-dev', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libgeoip1t64', 'geoip-bin', 'libsodium23', 'cpufrequtils', 'mcrypt', 'libogg0', 'libnuma1'),
'redhat' => array('epel-release', 'wget', 'sysstat', 'alsa-utils', 'v4l-utils', 'libcurl-devel', 'geoip-devel', 'libxslt-devel', 'oniguruma-devel', 'e2fsprogs', 'libjpeg-turbo-devel', 'libpng-devel', 'harfbuzz-devel', 'fribidi-devel', 'libogg', 'xz', 'zip', 'unzip', 'libssh2-devel', 'cronie', 'certbot', 'iptables-services', 'GeoIP-update', 'git', 'curl', 'libsodium', 'numactl', 'kernel-tools'),
);
$rLists = [
'debian' => ['iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'libgeoip-dev', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'sysstat', 'alsa-utils', 'v4l-utils', 'certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libogg0', 'libnuma1', 'xz-utils', 'zip', 'unzip', 'libssh2-1', 'libsodium23', 'cpufrequtils', 'mcrypt', 'cron', 'git', 'curl'],
'debian11' => ['iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'libgeoip-dev', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'alsa-utils', 'v4l-utils', 'certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libogg0', 'libnuma1', 'libssh2-1', 'libssh2-1-dev', 'libsodium23', 'cpufrequtils', 'mcrypt'],
'debian13' => ['iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'perl', 'gawk', 'socat', 'libxml2-dev', 'libxslt1-dev', 'libonig5', 'libonig-dev', 'zlib1g-dev', 'libssl-dev', 'pkg-config', 'autoconf', 'automake', 'alsa-utils', 'v4l-utils', 'e2fsprogs', 'certbot', 'iptables-persistent', 'libssh2-1', 'libssh2-1-dev', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libgeoip1', 'geoip-bin', 'libsodium23', 'cpufrequtils', 'mcrypt', 'libogg0', 'libnuma1'],
'ubuntu20' => ['iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'ca-certificates', 'libcurl3-gnutls', 'libcurl4-gnutls-dev', 'libxml2-dev', 'libxslt1-dev', 'libonig5', 'libonig-dev', 'libjpeg-dev', 'libpng-dev', 'zlib1g-dev', 'alsa-utils', 'v4l-utils', 'e2fsprogs', 'iptables-persistent', 'certbot', 'python3-certbot', 'libssh2-1', 'libssh2-1-dev', 'libsodium23', 'cpufrequtils', 'mcrypt', 'libogg0', 'libnuma1'],
'ubuntu22' => ['iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4', 'libcurl3-gnutls', 'libgeoip-dev', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'ca-certificates', 'libxml2-dev', 'libonig5', 'zlib1g-dev', 'alsa-utils', 'v4l-utils', 'certbot', 'python3-certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libogg0', 'libnuma1', 'libssh2-1', 'libssh2-1-dev', 'libsodium23', 'cpufrequtils', 'mcrypt'],
'ubuntu24' => ['iproute2', 'net-tools', 'dirmngr', 'gpg-agent', 'software-properties-common', 'libcurl4t64', 'wget', 'curl', 'unzip', 'zip', 'xz-utils', 'cron', 'git', 'sysstat', 'perl', 'gawk', 'socat', 'libxml2-dev', 'libxslt1-dev', 'libonig5', 'libonig-dev', 'zlib1g-dev', 'libssl-dev', 'pkg-config', 'autoconf', 'automake', 'alsa-utils', 'v4l-utils', 'e2fsprogs', 'certbot', 'python3-certbot', 'ufw', 'libssh2-1t64', 'libssh2-1-dev', 'libjpeg-dev', 'libpng-dev', 'libharfbuzz-dev', 'libfribidi-dev', 'libgeoip1t64', 'geoip-bin', 'libsodium23', 'cpufrequtils', 'mcrypt', 'libogg0', 'libnuma1'],
'redhat' => ['epel-release', 'wget', 'sysstat', 'alsa-utils', 'v4l-utils', 'libcurl-devel', 'geoip-devel', 'libxslt-devel', 'oniguruma-devel', 'e2fsprogs', 'libjpeg-turbo-devel', 'libpng-devel', 'harfbuzz-devel', 'fribidi-devel', 'libogg', 'xz', 'zip', 'unzip', 'libssh2-devel', 'cronie', 'certbot', 'iptables-services', 'GeoIP-update', 'git', 'curl', 'libsodium', 'numactl', 'kernel-tools'],
];
$rKey = self::resolvePackageKey(strtolower(trim($rDistID)), explode('.', $rVersion)[0]);
return $rLists[$rKey] ?? $rLists['debian'];
@@ -27,26 +26,26 @@ class LbInstallFlow {
// Mirrors the MAIN installer's _PACKAGE_KEY_MAP: (dist_id, major_version) -> package list key.
private static function resolvePackageKey(string $rDistID, string $rMajor): string {
if (in_array($rDistID, array('rocky', 'almalinux', 'rhel', 'centos', 'redhat', 'fedora'), true)) {
if (in_array($rDistID, ['rocky', 'almalinux', 'rhel', 'centos', 'redhat', 'fedora'], true)) {
return 'redhat';
}
$rMap = array(
'ubuntu' => array('18' => 'ubuntu20', '20' => 'ubuntu20', '22' => 'ubuntu22', '24' => 'ubuntu24'),
'debian' => array('11' => 'debian11', '12' => 'debian', '13' => 'debian13'),
);
$rMap = [
'ubuntu' => ['18' => 'ubuntu20', '20' => 'ubuntu20', '22' => 'ubuntu22', '24' => 'ubuntu24'],
'debian' => ['11' => 'debian11', '12' => 'debian', '13' => 'debian13'],
];
return $rMap[$rDistID][$rMajor] ?? 'debian';
}
public static function resolveUpdateData(GitHubReleases $gitRelease): array {
$rUpdateData = $gitRelease->getUpdateFile("lb", XC_VM_VERSION);
return array(
return [
'url' => $rUpdateData['url'],
'md5' => $rUpdateData['md5'],
);
];
}
public static function writeInstallMetadata(string $rInstallDir, int $rServerID, string $rUsername, string $rPassword, int $rPort): void {
file_put_contents($rInstallDir . $rServerID . '.json', json_encode(array('root_username' => $rUsername, 'root_password' => $rPassword, 'ssh_port' => $rPort)));
file_put_contents($rInstallDir . $rServerID . '.json', json_encode(['root_username' => $rUsername, 'root_password' => $rPassword, 'ssh_port' => $rPort]));
}
public static function installArchive($rConn, callable $rRunSSH, string $rInstallFiles, string $rHash, int $rServerID, $db): bool {
@@ -152,13 +151,13 @@ class LbInstallFlow {
// Pack config.enc targeted at the node's install_id. Credentials are
// read from MAIN's config.enc inside the extension, never exposed here.
$rBlob = \XC_VM::config_pack($rInstallId, array(
$rBlob = \XC_VM::config_pack($rInstallId, [
'hostname' => $rServers[SERVER_ID]['server_ip'],
'database' => 'xc_vm',
'port' => intval(ConfigReader::get('port')),
'server_id' => $rServerID,
'is_lb' => 1,
));
]);
if (empty($rBlob)) {
$db->query('UPDATE `servers` SET `status` = 4 WHERE `id` = ?;', $rServerID);
echo "Failed to pack node configuration! Exiting\n";
@@ -264,14 +263,14 @@ class LbInstallFlow {
*/
private static function latestReleaseTagViaRedirect(string $rOwner, string $rRepo): string {
$rCurl = curl_init('https://github.com/' . $rOwner . '/' . $rRepo . '/releases/latest');
curl_setopt_array($rCurl, array(
curl_setopt_array($rCurl, [
CURLOPT_NOBODY => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 20,
CURLOPT_USERAGENT => 'XC_VM',
));
]);
$rHeaders = (string) curl_exec($rCurl);
$rLocation = (string) curl_getinfo($rCurl, CURLINFO_REDIRECT_URL);
curl_close($rCurl);
@@ -391,7 +390,7 @@ class LbInstallFlow {
call_user_func($rRunSSH, $rConn, 'sudo chmod 0750 ' . MAIN_HOME . 'bin/nginx_rtmp/sbin/nginx_rtmp');
$rVersionFile = BIN_PATH . 'bin_version.json';
$rVersionData = array(
$rVersionData = [
'owner' => GIT_OWNER,
'repository' => GIT_REPO_BIN,
'release' => $rTag,
@@ -399,7 +398,7 @@ class LbInstallFlow {
'distribution' => $rDistID,
'distribution_version' => $rVersion,
'updated_at_utc' => gmdate('Y-m-d\TH:i:s\Z'),
);
];
$rVersionJson = json_encode($rVersionData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($rVersionJson !== false) {
$rEncodedVersion = base64_encode($rVersionJson);
+48 -29
View File
@@ -20,7 +20,6 @@ use XcVm\Streaming\Fanout\IngestFeeder;
*/
class LlodCommand implements CommandInterface {
public function getName(): string {
return 'llod';
}
@@ -56,16 +55,36 @@ class LlodCommand implements CommandInterface {
echo "Stream arguments count: " . count($rStreamArguments) . "\n";
echo "====================\n\n";
if (!defined('MAIN_HOME')) define('MAIN_HOME', '/home/xc_vm/');
if (!defined('STREAMS_PATH')) define('STREAMS_PATH', MAIN_HOME . 'content/streams/');
if (!defined('CACHE_TMP_PATH')) define('CACHE_TMP_PATH', MAIN_HOME . 'tmp/cache/');
if (!defined('CONS_TMP_PATH')) define('CONS_TMP_PATH', MAIN_HOME . 'tmp/opened_cons/');
if (!defined('FFMPEG')) define('FFMPEG', FfmpegPaths::cpu() ?: FFMPEG_BIN_40);
if (!defined('FFPROBE')) define('FFPROBE', FfmpegPaths::probe() ?: FFPROBE_BIN_40);
if (!defined('PACKET_SIZE')) define('PACKET_SIZE', 188);
if (!defined('BUFFER_SIZE')) define('BUFFER_SIZE', 12032);
if (!defined('TIMEOUT')) define('TIMEOUT', 20);
if (!defined('SEGMENT_DURATION')) define('SEGMENT_DURATION', 4);
if (!defined('MAIN_HOME')) {
define('MAIN_HOME', '/home/xc_vm/');
}
if (!defined('STREAMS_PATH')) {
define('STREAMS_PATH', MAIN_HOME . 'content/streams/');
}
if (!defined('CACHE_TMP_PATH')) {
define('CACHE_TMP_PATH', MAIN_HOME . 'tmp/cache/');
}
if (!defined('CONS_TMP_PATH')) {
define('CONS_TMP_PATH', MAIN_HOME . 'tmp/opened_cons/');
}
if (!defined('FFMPEG')) {
define('FFMPEG', FfmpegPaths::cpu() ?: FFMPEG_BIN_40);
}
if (!defined('FFPROBE')) {
define('FFPROBE', FfmpegPaths::probe() ?: FFPROBE_BIN_40);
}
if (!defined('PACKET_SIZE')) {
define('PACKET_SIZE', 188);
}
if (!defined('BUFFER_SIZE')) {
define('BUFFER_SIZE', 12032);
}
if (!defined('TIMEOUT')) {
define('TIMEOUT', 20);
}
if (!defined('SEGMENT_DURATION')) {
define('SEGMENT_DURATION', 4);
}
if (!file_exists(CACHE_TMP_PATH . 'settings')) {
echo "Settings not cached!\n";
@@ -78,7 +97,7 @@ class LlodCommand implements CommandInterface {
$rFP = null;
$rSegmentFile = null;
$rSegmentStatus = array();
$rSegmentStatus = [];
register_shutdown_function(function () use (&$rFP, &$rSegmentFile) {
if (is_resource($rSegmentFile)) {
@@ -177,7 +196,7 @@ class LlodCommand implements CommandInterface {
$segment = 0;
$segmentOpen = false;
$segmentStart = microtime(true);
$rSegmentDurations = array();
$rSegmentDurations = [];
$lastData = time();
$firstDataAt = microtime(true);
@@ -316,7 +335,7 @@ class LlodCommand implements CommandInterface {
* @param string|null $pmtPacket Raw 188-byte PMT packet, or null if not seen yet.
* @return resource|false The open file handle, or false on failure.
*/
private function openSegment($rStreamID, $segment, $patPacket, $pmtPacket) {
private function openSegment(int|string $rStreamID, int $segment, ?string $patPacket, ?string $pmtPacket) {
$file = fopen(STREAMS_PATH . $rStreamID . "_{$segment}.ts", 'wb');
if (!$file) {
return false;
@@ -337,7 +356,7 @@ class LlodCommand implements CommandInterface {
* @param string $pkt A 188-byte TS packet.
* @return array{pid:int,pusi:bool,random_access:bool,payload_offset:int}
*/
private function parseTsHeader($pkt) {
private function parseTsHeader(string $pkt) {
$b1 = ord($pkt[1]);
$b2 = ord($pkt[2]);
$b3 = ord($pkt[3]);
@@ -377,7 +396,7 @@ class LlodCommand implements CommandInterface {
* @param int $payloadOffset Offset of the payload within the packet.
* @return int|null program_map_PID, or null if not resolvable in this packet.
*/
private function parsePat($pkt, $payloadOffset) {
private function parsePat(string $pkt, int $payloadOffset) {
if ($payloadOffset >= PACKET_SIZE) {
return null;
}
@@ -408,7 +427,7 @@ class LlodCommand implements CommandInterface {
* @param int $payloadOffset Offset of the payload within the packet.
* @return int|null Video/PCR PID, or null if not resolvable in this packet.
*/
private function parsePmt($pkt, $payloadOffset) {
private function parsePmt(string $pkt, int $payloadOffset) {
if ($payloadOffset >= PACKET_SIZE) {
return null;
}
@@ -450,7 +469,7 @@ class LlodCommand implements CommandInterface {
* @param mixed $rRequestPrebuffer The request_prebuffer setting.
* @return resource
*/
private function sourceContext($rURL, $rStreamArguments, $rRequestPrebuffer) {
private function sourceContext(string $rURL, array $rStreamArguments, mixed $rRequestPrebuffer) {
$rArg = static function (string $rKey) use ($rStreamArguments): string {
// `??` already maps a missing or null value to '': an empty value
// (after trimming) falls back to the argument's default.
@@ -461,7 +480,7 @@ class LlodCommand implements CommandInterface {
return $rValue;
};
$rHeaders = array();
$rHeaders = [];
foreach (preg_split('/\r\n|\r|\n/', $rArg('headers')) as $rLine) {
if (trim($rLine) !== '' && strpos($rLine, ':') !== false) {
$rHeaders[] = trim($rLine);
@@ -474,10 +493,10 @@ class LlodCommand implements CommandInterface {
$rHeaders[] = 'X-XC_VM-Prebuffer: 1';
}
$rHTTP = array(
$rHTTP = [
'timeout' => TIMEOUT,
'user_agent' => ($rArg('user_agent') !== '' ? $rArg('user_agent') : 'Mozilla/5.0'),
);
];
if (count($rHeaders) > 0) {
$rHTTP['header'] = implode("\r\n", $rHeaders);
}
@@ -487,10 +506,10 @@ class LlodCommand implements CommandInterface {
$rHTTP['request_fulluri'] = true;
}
return stream_context_create(array(
return stream_context_create([
'http' => $rHTTP,
'ssl' => array('verify_peer' => false, 'verify_peer_name' => false),
));
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
]);
}
/**
@@ -534,7 +553,7 @@ class LlodCommand implements CommandInterface {
$rMetadata = stream_get_meta_data($rFP);
echo "Stream metadata obtained\n";
$rHeaders = array();
$rHeaders = [];
if (!empty($rMetadata['wrapper_data']) && is_array($rMetadata['wrapper_data'])) {
foreach ($rMetadata['wrapper_data'] as $rLine) {
@@ -605,12 +624,12 @@ class LlodCommand implements CommandInterface {
return false;
}
private function deleteOldSegments($rStreamID, $rKeep, $rThreshold, &$rSegmentStatus, &$rSegmentDurations = array()): array {
private function deleteOldSegments($rStreamID, $rKeep, $rThreshold, &$rSegmentStatus, &$rSegmentDurations = []): array {
echo "Stream ID: $rStreamID\n";
echo "Keep segments: $rKeep\n";
echo "Delete threshold: $rThreshold\n";
$rReturn = array();
$rReturn = [];
if (empty($rSegmentStatus)) {
return $rReturn;
@@ -650,7 +669,7 @@ class LlodCommand implements CommandInterface {
return $rReturn;
}
private function updateSegments($rStreamID, $segments, $rSegmentDurations = array(), $rSegTime = SEGMENT_DURATION): void {
private function updateSegments($rStreamID, $segments, $rSegmentDurations = [], $rSegTime = SEGMENT_DURATION): void {
if (empty($segments)) {
return;
}
@@ -705,7 +724,7 @@ class LlodCommand implements CommandInterface {
*/
private function checkRunning($rStreamID): void {
echo "Checking for existing process for stream $rStreamID\n";
$rTerms = array('LLOD[' . intval($rStreamID) . ']', 'console.php llod ' . intval($rStreamID) . ' ');
$rTerms = ['LLOD[' . intval($rStreamID) . ']', 'console.php llod ' . intval($rStreamID) . ' '];
foreach (ProcessManager::findProcessPIDs($rTerms) as $rPID) {
echo "Killing existing LLOD process PID: $rPID\n";
@posix_kill($rPID, 9);
+55 -24
View File
@@ -19,7 +19,6 @@ use XcVm\Streaming\Fanout\IngestFeeder;
*/
class LoopbackCommand implements CommandInterface {
public function getName(): string {
return 'loopback';
}
@@ -44,29 +43,59 @@ class LoopbackCommand implements CommandInterface {
$rStreamID = intval($rArgs[0]);
$rServerID = intval($rArgs[1]);
if (!defined('MAIN_HOME')) define('MAIN_HOME', '/home/xc_vm/');
if (!defined('STREAMS_PATH')) define('STREAMS_PATH', MAIN_HOME . 'content/streams/');
if (!defined('FFMPEG')) define('FFMPEG', FfmpegPaths::cpu() ?: FFMPEG_BIN_40);
if (!defined('FFPROBE')) define('FFPROBE', FfmpegPaths::probe() ?: FFPROBE_BIN_40);
if (!defined('CACHE_TMP_PATH')) define('CACHE_TMP_PATH', MAIN_HOME . 'tmp/cache/');
if (!defined('CONFIG_PATH')) define('CONFIG_PATH', MAIN_HOME . 'config/');
if (!defined('MAIN_HOME')) {
define('MAIN_HOME', '/home/xc_vm/');
}
if (!defined('STREAMS_PATH')) {
define('STREAMS_PATH', MAIN_HOME . 'content/streams/');
}
if (!defined('FFMPEG')) {
define('FFMPEG', FfmpegPaths::cpu() ?: FFMPEG_BIN_40);
}
if (!defined('FFPROBE')) {
define('FFPROBE', FfmpegPaths::probe() ?: FFPROBE_BIN_40);
}
if (!defined('CACHE_TMP_PATH')) {
define('CACHE_TMP_PATH', MAIN_HOME . 'tmp/cache/');
}
if (!defined('CONFIG_PATH')) {
define('CONFIG_PATH', MAIN_HOME . 'config/');
}
// PAT_HEADER restored to the real PAT header bytes (0xB0 0x0D) derived from the stream.
// The value was corrupted (byte 0xB0 → space/U+FFFD) by a non-binary-safe editor.
if (!defined('PAT_HEADER')) define('PAT_HEADER', "\xB0\x0D");
if (!defined('KEYFRAME_HEADER')) define('KEYFRAME_HEADER', "\x07P");
if (!defined('PACKET_SIZE')) define('PACKET_SIZE', 188);
if (!defined('BUFFER_SIZE')) define('BUFFER_SIZE', 12032);
if (!defined('PAT_PERIOD')) define('PAT_PERIOD', 2);
if (!defined('PAT_HEADER')) {
define('PAT_HEADER', "\xB0\x0D");
}
if (!defined('KEYFRAME_HEADER')) {
define('KEYFRAME_HEADER', "\x07P");
}
if (!defined('PACKET_SIZE')) {
define('PACKET_SIZE', 188);
}
if (!defined('BUFFER_SIZE')) {
define('BUFFER_SIZE', 12032);
}
if (!defined('PAT_PERIOD')) {
define('PAT_PERIOD', 2);
}
// Minimum VIDEO duration per segment (90 kHz ticks). ~1.5s guarantees one cut per 2s GOP
// and prevents sub-GOP cuts that were producing 8 KB .ts files. Measured in PTS (video time),
// so it stays correct even when the source bursts at 400+ Mbps.
if (!defined('MIN_SEG_PTS')) define('MIN_SEG_PTS', 135000);
if (!defined('MIN_SEG_PTS')) {
define('MIN_SEG_PTS', 135000);
}
// Segment size safety cap. If the source stops delivering keyframes with an advancing PCR
// (e.g. re-serving the same chunk to a consumer), we still rotate on the next keyframe to
// avoid writing a giant .ts that fills the disk. ~16 MB ≈ ~20s, well above a normal segment (~800 KB).
if (!defined('MAX_SEG_BYTES')) define('MAX_SEG_BYTES', 16777216);
if (!defined('TIMEOUT')) define('TIMEOUT', 20);
if (!defined('TIMEOUT_READ')) define('TIMEOUT_READ', 1);
if (!defined('MAX_SEG_BYTES')) {
define('MAX_SEG_BYTES', 16777216);
}
if (!defined('TIMEOUT')) {
define('TIMEOUT', 20);
}
if (!defined('TIMEOUT_READ')) {
define('TIMEOUT_READ', 1);
}
if (!file_exists(CACHE_TMP_PATH . 'settings')) {
echo "Settings not cached!\n";
@@ -77,7 +106,9 @@ class LoopbackCommand implements CommandInterface {
return 0;
}
if (!defined('SERVER_ID')) define('SERVER_ID', intval(ConfigReader::get('server_id')));
if (!defined('SERVER_ID')) {
define('SERVER_ID', intval(ConfigReader::get('server_id')));
}
$this->checkRunning($rStreamID);
// Single-instance lock per stream. The monitor/watchdog (StreamProcess::startLoopback) can
@@ -92,8 +123,8 @@ class LoopbackCommand implements CommandInterface {
$rFP = null;
$rSegmentFile = null;
$rSegmentDuration = array();
$rSegmentStatus = array();
$rSegmentDuration = [];
$rSegmentStatus = [];
$rLastPTS = null;
$rCurPTS = null;
$rSegStartPTS = null;
@@ -143,7 +174,7 @@ class LoopbackCommand implements CommandInterface {
$rFeeder->connect();
$rExcessBuffer = $rPrebuffer = $rBuffer = $rPacket = '';
$rPATHeaders = array();
$rPATHeaders = [];
$rNewSegment = $rPAT = false;
$rFirstWrite = true;
$rLastPacket = time();
@@ -214,7 +245,7 @@ class LoopbackCommand implements CommandInterface {
if ($rSync == 71) {
if (substr($rPacket, 6, 2) == PAT_HEADER) {
$rPAT = true;
$rPATHeaders = array();
$rPATHeaders = [];
} else {
$rAdaptationField = $rHeader >> 4 & 3;
if (($rAdaptationField & 2) === 2) {
@@ -246,14 +277,14 @@ class LoopbackCommand implements CommandInterface {
$rPrebuffer = implode('', $rPATHeaders);
$rNewSegment = true;
$rPAT = false;
$rPATHeaders = array();
$rPATHeaders = [];
$rLastPTS = $rSegStartPTS;
$rCurPTS = $rKfPTS;
$rSegStartPTS = $rKfPTS;
} else {
// Still within the current segment duration: consume the PAT/keyframe pair without rotating.
$rPAT = false;
$rPATHeaders = array();
$rPATHeaders = [];
}
}
}
@@ -335,7 +366,7 @@ class LoopbackCommand implements CommandInterface {
}
private function deleteOldSegments($rStreamID, $rKeep, $rThreshold, &$rSegmentStatus): array {
$rReturn = array();
$rReturn = [];
$rCurrentSegment = max(array_keys($rSegmentStatus));
foreach ($rSegmentStatus as $rSegmentID => $rStatus) {
if ($rStatus) {
-1
View File
@@ -15,7 +15,6 @@ use XcVm\Cli\CommandInterface;
*/
class MigrateCommand implements CommandInterface {
public function getName(): string {
return 'migrate';
}
+30 -31
View File
@@ -22,40 +22,39 @@ use XcVm\Core\Module\ModuleManager;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class ModuleDeleteCommand implements CommandInterface {
public function getName(): string {
return 'module:delete';
}
public function getName(): string {
return 'module:delete';
}
public function getDescription(): string {
return 'Delete a module on a load balancer (files only)';
}
public function getDescription(): string {
return 'Delete a module on a load balancer (files only)';
}
public function execute(array $rArgs): int {
register_shutdown_function(function () {
global $db;
if (is_object($db)) {
$db->close_mysql();
}
});
public function execute(array $rArgs): int {
register_shutdown_function(function () {
global $db;
if (is_object($db)) {
$db->close_mysql();
}
});
$rArg = trim((string) ($rArgs[0] ?? ''), "'\" ");
$rJson = $rArg !== '' ? base64_decode($rArg, true) : false;
$rData = $rJson !== false ? json_decode($rJson, true) : null;
$rName = is_array($rData) ? (string) ($rData['name'] ?? '') : '';
$rArg = trim((string) ($rArgs[0] ?? ''), "'\" ");
$rJson = $rArg !== '' ? base64_decode($rArg, true) : false;
$rData = $rJson !== false ? json_decode($rJson, true) : null;
$rName = is_array($rData) ? (string) ($rData['name'] ?? '') : '';
if ($rName === '') {
echo "module:delete: missing module name.\n";
return 1;
}
if ($rName === '') {
echo "module:delete: missing module name.\n";
return 1;
}
try {
(new ModuleManager())->deleteModuleFilesOnly($rName);
echo "module:delete: '{$rName}' removed.\n";
return 0;
} catch (\Throwable $e) {
echo "module:delete failed for '{$rName}': " . $e->getMessage() . "\n";
return 1;
}
}
try {
(new ModuleManager())->deleteModuleFilesOnly($rName);
echo "module:delete: '{$rName}' removed.\n";
return 0;
} catch (\Throwable $e) {
echo "module:delete failed for '{$rName}': " . $e->getMessage() . "\n";
return 1;
}
}
}
+124 -125
View File
@@ -30,146 +30,145 @@ use XcVm\Domain\Server\ServerRepository;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class ModuleInstallCommand implements CommandInterface {
public function getName(): string {
return 'module:install';
}
public function getName(): string {
return 'module:install';
}
public function getDescription(): string {
return 'Install a module distributed from MAIN (load balancer, files only)';
}
public function getDescription(): string {
return 'Install a module distributed from MAIN (load balancer, files only)';
}
public function execute(array $rArgs): int {
register_shutdown_function(function () {
global $db;
if (is_object($db)) {
$db->close_mysql();
}
});
public function execute(array $rArgs): int {
register_shutdown_function(function () {
global $db;
if (is_object($db)) {
$db->close_mysql();
}
});
$rPayload = $this->decodePayload($rArgs[0] ?? '');
if ($rPayload === null) {
echo "Invalid module:install payload.\n";
return 1;
}
$rPayload = $this->decodePayload($rArgs[0] ?? '');
if ($rPayload === null) {
echo "Invalid module:install payload.\n";
return 1;
}
$rName = (string) ($rPayload['name'] ?? '');
$rVersion = (string) ($rPayload['version'] ?? '');
$rSource = ($rPayload['source'] ?? 'platform') === 'local' ? 'local' : 'platform';
$rName = (string) ($rPayload['name'] ?? '');
$rVersion = (string) ($rPayload['version'] ?? '');
$rSource = ($rPayload['source'] ?? 'platform') === 'local' ? 'local' : 'platform';
if ($rName === '') {
echo "module:install: missing module name.\n";
return 1;
}
if ($rName === '') {
echo "module:install: missing module name.\n";
return 1;
}
$rManager = new ModuleManager();
$rManager = new ModuleManager();
try {
if ($rSource === 'platform') {
$rApiKey = (string) (SettingsManager::get('platform_api_key') ?? '');
if ($rApiKey === '') {
echo "module:install: platform_api_key is not set in settings.\n";
return 1;
}
echo "Installing store module '{$rName}' v{$rVersion} from platform...\n";
$rManager->deployFromPlatformFilesOnly($rName, $rVersion, $rApiKey);
} else {
echo "Installing custom module '{$rName}' v{$rVersion} from MAIN...\n";
$rArchive = $this->fetchArchiveFromMain($rManager->archivePathFor($rName, $rVersion));
try {
$rManager->deployFromArchiveFilesOnly($rArchive);
} finally {
@unlink($rArchive);
}
}
try {
if ($rSource === 'platform') {
$rApiKey = (string) (SettingsManager::get('platform_api_key') ?? '');
if ($rApiKey === '') {
echo "module:install: platform_api_key is not set in settings.\n";
return 1;
}
echo "Installing store module '{$rName}' v{$rVersion} from platform...\n";
$rManager->deployFromPlatformFilesOnly($rName, $rVersion, $rApiKey);
} else {
echo "Installing custom module '{$rName}' v{$rVersion} from MAIN...\n";
$rArchive = $this->fetchArchiveFromMain($rManager->archivePathFor($rName, $rVersion));
try {
$rManager->deployFromArchiveFilesOnly($rArchive);
} finally {
@unlink($rArchive);
}
}
$this->fixOwnership($rName);
echo "module:install: '{$rName}' installed.\n";
return 0;
} catch (\Throwable $e) {
echo "module:install failed for '{$rName}': " . $e->getMessage() . "\n";
return 1;
}
}
$this->fixOwnership($rName);
echo "module:install: '{$rName}' installed.\n";
return 0;
} catch (\Throwable $e) {
echo "module:install failed for '{$rName}': " . $e->getMessage() . "\n";
return 1;
}
}
/** Decode + validate the base64 JSON payload. */
private function decodePayload(string $rArg): ?array {
$rArg = trim($rArg, "'\" ");
if ($rArg === '') {
return null;
}
$rJson = base64_decode($rArg, true);
if ($rJson === false) {
return null;
}
$rData = json_decode($rJson, true);
return is_array($rData) ? $rData : null;
}
/** Decode + validate the base64 JSON payload. */
private function decodePayload(string $rArg): ?array {
$rArg = trim($rArg, "'\" ");
if ($rArg === '') {
return null;
}
$rJson = base64_decode($rArg, true);
if ($rJson === false) {
return null;
}
$rData = json_decode($rJson, true);
return is_array($rData) ? $rData : null;
}
/**
* Pull a custom module archive from MAIN over the internal system API
* (action=getFile). Returns the path to a downloaded temp file.
*
* @throws \RuntimeException If MAIN cannot be located or the download fails.
*/
private function fetchArchiveFromMain(string $rArchivePath): string {
$rMain = null;
foreach (ServerRepository::getAll() as $rServer) {
if (!empty($rServer['is_main'])) {
$rMain = $rServer;
break;
}
}
/**
* Pull a custom module archive from MAIN over the internal system API
* (action=getFile). Returns the path to a downloaded temp file.
*
* @throws \RuntimeException If MAIN cannot be located or the download fails.
*/
private function fetchArchiveFromMain(string $rArchivePath): string {
$rMain = null;
foreach (ServerRepository::getAll() as $rServer) {
if (!empty($rServer['is_main'])) {
$rMain = $rServer;
break;
}
}
if (!$rMain || empty($rMain['server_ip'])) {
throw new \RuntimeException('Could not locate the MAIN server to fetch the module archive.');
}
if (!$rMain || empty($rMain['server_ip'])) {
throw new \RuntimeException('Could not locate the MAIN server to fetch the module archive.');
}
$rPass = (string) (SettingsManager::get('live_streaming_pass') ?? '');
$rUrl = 'http://' . $rMain['server_ip'] . ':' . intval($rMain['http_broadcast_port'])
. '/api?password=' . urlencode($rPass)
. '&action=getFile&filename=' . urlencode($rArchivePath);
$rPass = (string) (SettingsManager::get('live_streaming_pass') ?? '');
$rUrl = 'http://' . $rMain['server_ip'] . ':' . intval($rMain['http_broadcast_port'])
. '/api?password=' . urlencode($rPass)
. '&action=getFile&filename=' . urlencode($rArchivePath);
$rTmp = rtrim(sys_get_temp_dir(), '/') . '/xc_lbmod_' . bin2hex(random_bytes(8)) . '.zip';
$rFp = @fopen($rTmp, 'wb');
if (!$rFp) {
throw new \RuntimeException('Unable to create temporary archive file.');
}
$rTmp = rtrim(sys_get_temp_dir(), '/') . '/xc_lbmod_' . bin2hex(random_bytes(8)) . '.zip';
$rFp = @fopen($rTmp, 'wb');
if (!$rFp) {
throw new \RuntimeException('Unable to create temporary archive file.');
}
$rCh = curl_init();
curl_setopt($rCh, CURLOPT_URL, $rUrl);
curl_setopt($rCh, CURLOPT_FILE, $rFp);
curl_setopt($rCh, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($rCh, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($rCh, CURLOPT_TIMEOUT, 120);
$rOk = curl_exec($rCh);
$rCode = curl_getinfo($rCh, CURLINFO_RESPONSE_CODE);
curl_close($rCh);
fclose($rFp);
$rCh = curl_init();
curl_setopt($rCh, CURLOPT_URL, $rUrl);
curl_setopt($rCh, CURLOPT_FILE, $rFp);
curl_setopt($rCh, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($rCh, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($rCh, CURLOPT_TIMEOUT, 120);
$rOk = curl_exec($rCh);
$rCode = curl_getinfo($rCh, CURLINFO_RESPONSE_CODE);
curl_close($rCh);
fclose($rFp);
// serveFile() answers with a JSON {"result":false,...} body (HTTP 200)
// when the file is missing/invalid — detect that by checking the zip
// magic bytes instead of trusting the status code alone.
$rMagic = @file_get_contents($rTmp, false, null, 0, 2);
if (!$rOk || $rCode < 200 || $rCode >= 300 || $rMagic !== 'PK') {
@unlink($rTmp);
throw new \RuntimeException("Failed to download module archive from MAIN (HTTP {$rCode}).");
}
// serveFile() answers with a JSON {"result":false,...} body (HTTP 200)
// when the file is missing/invalid — detect that by checking the zip
// magic bytes instead of trusting the status code alone.
$rMagic = @file_get_contents($rTmp, false, null, 0, 2);
if (!$rOk || $rCode < 200 || $rCode >= 300 || $rMagic !== 'PK') {
@unlink($rTmp);
throw new \RuntimeException("Failed to download module archive from MAIN (HTTP {$rCode}).");
}
return $rTmp;
}
return $rTmp;
}
/**
* Hand the freshly installed module directory back to the panel user, since
* this command runs as root from the signals cron.
*/
private function fixOwnership(string $rName): void {
if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $rName)) {
return;
}
$rDir = (defined('MAIN_HOME') ? MAIN_HOME : '') . 'Modules/' . $rName;
if (is_dir($rDir)) {
shell_exec('sudo chown -R xc_vm:xc_vm ' . escapeshellarg($rDir) . ' 2>/dev/null');
}
}
/**
* Hand the freshly installed module directory back to the panel user, since
* this command runs as root from the signals cron.
*/
private function fixOwnership(string $rName): void {
if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $rName)) {
return;
}
$rDir = (defined('MAIN_HOME') ? MAIN_HOME : '') . 'Modules/' . $rName;
if (is_dir($rDir)) {
shell_exec('sudo chown -R xc_vm:xc_vm ' . escapeshellarg($rDir) . ' 2>/dev/null');
}
}
}
+11 -12
View File
@@ -34,7 +34,6 @@ use XcVm\Streaming\Fanout\FanoutClient;
*/
class MonitorCommand implements CommandInterface {
/** {@inheritDoc} */
public function getName(): string {
return 'monitor';
@@ -103,7 +102,7 @@ class MonitorCommand implements CommandInterface {
$rDelayPID = $rStreamInfo['delay_pid'];
$rParentID = $rStreamInfo['parent_id'];
$rStreamProbe = false;
$rSources = array();
$rSources = [];
$rSegmentTime = intval(SettingsManager::get('seg_time'));
$rPrioritySwitch = false;
$rMaxFails = 0;
@@ -349,7 +348,7 @@ class MonitorCommand implements CommandInterface {
if ($rStreamInfo['parent_id']) {
$rForceSource = (!is_null(ServerRepository::getAll()[SERVER_ID]['private_url_ip']) && !is_null(ServerRepository::getAll()[$rStreamInfo['parent_id']]['private_url_ip']) ? ServerRepository::getAll()[$rStreamInfo['parent_id']]['private_url_ip'] : ServerRepository::getAll()[$rStreamInfo['parent_id']]['public_url_ip']) . 'admin/live?stream=' . intval($rStreamID) . '&password=' . urlencode(SettingsManager::get('live_streaming_pass')) . '&extension=ts';
}
$rData = StreamProcess::startLLOD($rStreamID, $rStreamInfo, $rStreamInfo['parent_id'] ? array() : $rStreamArguments, $rForceSource);
$rData = StreamProcess::startLLOD($rStreamID, $rStreamInfo, $rStreamInfo['parent_id'] ? [] : $rStreamArguments, $rForceSource);
}
} elseif ($rStreamInfo['type'] == 3) {
if ((0 < $rPID) && !$rStreamInfo['parent_id'] && (0 < $rStreamInfo['stream_started'])) {
@@ -512,7 +511,7 @@ class MonitorCommand implements CommandInterface {
* @param mixed $rThreshold fps_threshold percentage (1..100), or empty.
* @return bool
*/
public static function isFpsBelowThreshold($rFps, $rBaseline, $rThreshold): bool {
public static function isFpsBelowThreshold(float $rFps, float $rBaseline, mixed $rThreshold): bool {
$rPercent = min(100, max(1, intval($rThreshold) ?: 90));
return 0 < $rBaseline && $rFps < $rBaseline * $rPercent / 100;
}
@@ -526,7 +525,7 @@ class MonitorCommand implements CommandInterface {
* @param mixed $rCurrentSource The source in use.
* @return array
*/
public static function higherPrioritySources(array $rSources, $rCurrentSource): array {
public static function higherPrioritySources(array $rSources, mixed $rCurrentSource): array {
$rKey = array_search($rCurrentSource, $rSources);
if (!is_numeric($rKey)) {
return array_values($rSources); // current is not in the list (e.g. it was edited): all are candidates
@@ -541,7 +540,7 @@ class MonitorCommand implements CommandInterface {
* @param mixed $rRate Raw avg_frame_rate / r_frame_rate value.
* @return float Frames per second; 0.0 for empty/zero/malformed input.
*/
private static function parseFrameRate($rRate): float {
private static function parseFrameRate(mixed $rRate): float {
$rRate = (string) $rRate;
if (strpos($rRate, '/') !== false) {
list($rNum, $rDen) = array_map('floatval', explode('/', $rRate));
@@ -558,7 +557,7 @@ class MonitorCommand implements CommandInterface {
* @param int|null $rNow Timestamp to test against (defaults to now).
* @return bool
*/
private static function isAutoRestartDue($rAutoRestart, $rNow = null): bool {
private static function isAutoRestartDue(mixed $rAutoRestart, ?int $rNow = null): bool {
if (empty($rAutoRestart['days']) || empty($rAutoRestart['at'])) {
return false;
}
@@ -578,7 +577,7 @@ class MonitorCommand implements CommandInterface {
* @param mixed $rAllowHevc player_allow_hevc setting.
* @return array{0:int,1:?string,2:?string,3:mixed} [compatible, audio, video, resolution]
*/
private static function resolveStreamCodecMeta($rStreamInfoJson, $rAllowHevc): array {
private static function resolveStreamCodecMeta(mixed $rStreamInfoJson, mixed $rAllowHevc): array {
$rCompatible = 0;
$rAudioCodec = $rVideoCodec = $rResolution = null;
if ($rStreamInfoJson) {
@@ -590,10 +589,10 @@ class MonitorCommand implements CommandInterface {
$rResolution = isset($rStreamJSON['codecs']['video']['height']) ? $rStreamJSON['codecs']['video']['height'] : null;
}
if ($rResolution) {
$rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution);
$rResolution = StreamSorter::getNearest([240, 360, 480, 576, 720, 1080, 1440, 2160], $rResolution);
}
}
return array($rCompatible, $rAudioCodec, $rVideoCodec, $rResolution);
return [$rCompatible, $rAudioCodec, $rVideoCodec, $rResolution];
}
/**
@@ -606,7 +605,7 @@ class MonitorCommand implements CommandInterface {
* @param mixed $rSegmentTime Current segment time.
* @return array{0:mixed,1:mixed} [clamped probe, updated segment time]
*/
private static function persistSegmentDuration($rProbe, $rStreamID, $rSegmentTime): array {
private static function persistSegmentDuration(mixed $rProbe, mixed $rStreamID, mixed $rSegmentTime): array {
if (10 < intval($rProbe['of_duration'])) {
$rProbe['of_duration'] = 10;
}
@@ -614,7 +613,7 @@ class MonitorCommand implements CommandInterface {
if ($rSegmentTime < intval($rProbe['of_duration'])) {
$rSegmentTime = intval($rProbe['of_duration']);
}
return array($rProbe, $rSegmentTime);
return [$rProbe, $rSegmentTime];
}
/**
+13 -8
View File
@@ -22,7 +22,6 @@ use XcVm\Streaming\Fanout\FanoutClient;
*/
class OndemandCommand implements CommandInterface {
public function getName(): string {
return 'ondemand';
}
@@ -51,7 +50,7 @@ class OndemandCommand implements CommandInterface {
// Kill any OTHER running ondemand instance (dedupe). findProcessPIDs()
// skips our own PID and matches both the retitled process and the raw
// command line (covers the window before a sibling sets its title).
foreach (ProcessManager::findProcessPIDs(array('XC_VM[Ondemand]', 'console.php ondemand')) as $rOtherPID) {
foreach (ProcessManager::findProcessPIDs(['XC_VM[Ondemand]', 'console.php ondemand']) as $rOtherPID) {
@posix_kill($rOtherPID, 9);
}
@@ -111,15 +110,17 @@ class OndemandCommand implements CommandInterface {
}
foreach ($rRows as $rRow) {
if ($rRow['online_clients'] > 0 || $rRow['attached'] > 0)
if ($rRow['online_clients'] > 0 || $rRow['attached'] > 0) {
continue;
}
$rStreamID = $rRow['stream_id'];
$pidFile = STREAMS_PATH . $rStreamID . '_.pid';
$monitorFile = STREAMS_PATH . $rStreamID . '_.monitor';
if (!file_exists($pidFile))
if (!file_exists($pidFile)) {
continue;
}
$rPID = (int) @file_get_contents($pidFile);
$rMonitorPID = file_exists($monitorFile) ? (int) @file_get_contents($monitorFile) : 0;
@@ -129,8 +130,9 @@ class OndemandCommand implements CommandInterface {
if (file_exists($queueFile)) {
$queue = @igbinary_unserialize(@file_get_contents($queueFile)) ?: [];
foreach ($queue as $pid) {
if (ProcessManager::isRunning($pid, 'php-fpm'))
if (ProcessManager::isRunning($pid, 'php-fpm')) {
$rQueue++;
}
}
}
@@ -150,10 +152,12 @@ class OndemandCommand implements CommandInterface {
FanoutClient::release($rStreamID);
FanoutClient::unregister($rStreamID);
if ($rMonitorPID > 0)
if ($rMonitorPID > 0) {
@posix_kill($rMonitorPID, 9);
if ($rPID > 0)
}
if ($rPID > 0) {
@posix_kill($rPID, 9);
}
@shell_exec('rm -f ' . STREAMS_PATH . $rStreamID . '_*');
@unlink($queueFile);
@@ -169,8 +173,9 @@ class OndemandCommand implements CommandInterface {
usleep(800000);
}
if (is_object($db))
if (is_object($db)) {
$db->close_mysql();
}
shell_exec('(sleep 2; ' . PHP_BIN . ' ' . MAIN_HOME . 'console.php ondemand) > /dev/null 2>&1 &');
return 0;
+2 -3
View File
@@ -3,9 +3,8 @@
namespace XcVm\Cli\Commands;
class ProxyInstallFlow {
public static function getPackages(): array {
return array('iproute2', 'net-tools', 'libcurl4', 'libcurl3-gnutls', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'sysstat', 'mcrypt', 'python3', 'certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libssh2-1', 'xz-utils', 'zip', 'unzip', 'cron');
return ['iproute2', 'net-tools', 'libcurl4', 'libcurl3-gnutls', 'libxslt1-dev', 'libonig-dev', 'e2fsprogs', 'wget', 'sysstat', 'mcrypt', 'python3', 'certbot', 'iptables-persistent', 'libjpeg-dev', 'libpng-dev', 'libssh2-1', 'xz-utils', 'zip', 'unzip', 'cron'];
}
public static function getInstallFile(): string {
@@ -13,7 +12,7 @@ class ProxyInstallFlow {
}
public static function writeInstallMetadata(string $rInstallDir, int $rServerID, string $rUsername, string $rPassword, int $rPort, int $rHTTPPort, int $rHTTPSPort, array $rParentIDs): void {
file_put_contents($rInstallDir . $rServerID . '.json', json_encode(array('root_username' => $rUsername, 'root_password' => $rPassword, 'ssh_port' => $rPort, 'http_broadcast_port' => $rHTTPPort, 'https_broadcast_port' => $rHTTPSPort, 'parent_id' => $rParentIDs)));
file_put_contents($rInstallDir . $rServerID . '.json', json_encode(['root_username' => $rUsername, 'root_password' => $rPassword, 'ssh_port' => $rPort, 'http_broadcast_port' => $rHTTPPort, 'https_broadcast_port' => $rHTTPSPort, 'parent_id' => $rParentIDs]));
}
public static function installArchive($rConn, callable $rSendFileSSH, callable $rRunSSH, string $rInstallDir, string $rInstallFile, int $rServerID, $db): bool {
+2 -2
View File
@@ -58,7 +58,7 @@ class QueueCommand implements CommandInterface {
// ── Movie queue ──────────────────────────────────────
if ($db->query("SELECT `id`, `pid` FROM `queue` WHERE `server_id` = ? AND `pid` IS NOT NULL AND `type` = 'movie' ORDER BY `added` ASC;", SERVER_ID)) {
$rDelete = $rInProgress = array();
$rDelete = $rInProgress = [];
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
if ($rRow['pid'] && (ProcessManager::isRunning($rRow['pid'], 'ffmpeg') || ProcessManager::isRunning($rRow['pid'], PHP_BIN))) {
@@ -86,7 +86,7 @@ class QueueCommand implements CommandInterface {
// ── Channel queue ────────────────────────────────
if ($db->query("SELECT `id`, `pid` FROM `queue` WHERE `server_id` = ? AND `pid` IS NOT NULL AND `type` = 'channel' ORDER BY `added` ASC;", SERVER_ID)) {
$rInProgress = array();
$rInProgress = [];
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
if ($rRow['pid'] && ProcessManager::isRunning($rRow['pid'], PHP_BIN)) {
+8 -9
View File
@@ -20,7 +20,6 @@ use XcVm\Streaming\Codec\FfmpegPaths;
*/
class RecordCommand implements CommandInterface {
use DatabaseAware;
public function getName(): string {
@@ -170,7 +169,7 @@ class RecordCommand implements CommandInterface {
$rImportArray['target_container'] = 'mp4';
$rImportArray['stream_display_name'] = $recordingData['title'];
$rImportArray['year'] = date('Y');
$rImportArray['movie_properties'] = array('kinopoisk_url' => null, 'tmdb_id' => null, 'name' => $recordingData['title'], 'o_name' => $recordingData['title'], 'cover_big' => $recordingData['stream_icon'], 'movie_image' => $recordingData['stream_icon'], 'release_date' => date('Y-m-d', $recordingData['start']), 'episode_run_time' => intval($rSeconds / 60), 'youtube_trailer' => null, 'director' => '', 'actors' => '', 'cast' => '', 'description' => trim($recordingData['description']), 'plot' => trim($recordingData['description']), 'age' => '', 'mpaa_rating' => '', 'rating_count_kinopoisk' => 0, 'country' => '', 'genre' => '', 'backdrop_path' => array(), 'duration_secs' => $rSeconds, 'duration' => sprintf('%02d:%02d:%02d', $rSeconds / 3600, ($rSeconds / 60) % 60, $rSeconds % 60), 'video' => array(), 'audio' => array(), 'bitrate' => 0, 'rating' => 0);
$rImportArray['movie_properties'] = ['kinopoisk_url' => null, 'tmdb_id' => null, 'name' => $recordingData['title'], 'o_name' => $recordingData['title'], 'cover_big' => $recordingData['stream_icon'], 'movie_image' => $recordingData['stream_icon'], 'release_date' => date('Y-m-d', $recordingData['start']), 'episode_run_time' => intval($rSeconds / 60), 'youtube_trailer' => null, 'director' => '', 'actors' => '', 'cast' => '', 'description' => trim($recordingData['description']), 'plot' => trim($recordingData['description']), 'age' => '', 'mpaa_rating' => '', 'rating_count_kinopoisk' => 0, 'country' => '', 'genre' => '', 'backdrop_path' => [], 'duration_secs' => $rSeconds, 'duration' => sprintf('%02d:%02d:%02d', $rSeconds / 3600, ($rSeconds / 60) % 60, $rSeconds % 60), 'video' => [], 'audio' => [], 'bitrate' => 0, 'rating' => 0];
$rImportArray['rating'] = 0;
$rImportArray['read_native'] = 0;
$rImportArray['movie_symlink'] = 0;
@@ -200,7 +199,7 @@ class RecordCommand implements CommandInterface {
foreach (json_decode($recordingData['bouquets'], true) as $rBouquet) {
$this->addToBouquet($rBouquet, $rInsertID);
}
$db->query('UPDATE `streams` SET `stream_source` = ? WHERE `id` = ?;', json_encode(array(VOD_PATH . $rInsertID . '.mp4')), $rInsertID);
$db->query('UPDATE `streams` SET `stream_source` = ? WHERE `id` = ?;', json_encode([VOD_PATH . $rInsertID . '.mp4']), $rInsertID);
$db->query('INSERT INTO `streams_servers`(`stream_id`, `server_id`, `parent_id`, `pid`, `to_analyze`) VALUES(?, ?, NULL, 1, 1);', $rInsertID, SERVER_ID);
$db->query('UPDATE `recordings` SET `status` = 2, `created_id` = ? WHERE `id` = ?;', $rInsertID, $recordingID);
}
@@ -278,7 +277,7 @@ class RecordCommand implements CommandInterface {
}
private function prepareArray($rArray): array {
$UpdateData = $rColumns = $rPlaceholder = $rData = array();
$UpdateData = $rColumns = $rPlaceholder = $rData = [];
foreach (array_keys($rArray) as $rKey) {
$rColumns[] = '`' . $this->preparecolumn($rKey) . '`';
$UpdateData[] = '`' . $this->preparecolumn($rKey) . '` = ?';
@@ -290,12 +289,12 @@ class RecordCommand implements CommandInterface {
$rPlaceholder[] = '?';
$rData[] = $rValue;
}
return array('placeholder' => implode(',', $rPlaceholder), 'columns' => implode(',', $rColumns), 'data' => $rData, 'update' => implode(',', $UpdateData));
return ['placeholder' => implode(',', $rPlaceholder), 'columns' => implode(',', $rColumns), 'data' => $rData, 'update' => implode(',', $UpdateData)];
}
private function verifyPostTable($rTable, $rData = array(), $rOnlyExisting = false): array {
private function verifyPostTable($rTable, $rData = [], $rOnlyExisting = false): array {
$db = self::db();
$rReturn = array();
$rReturn = [];
$db->query('SELECT `column_name`, `column_default`, `is_nullable`, `data_type` FROM `information_schema`.`columns` WHERE `table_schema` = (SELECT DATABASE()) AND `table_name` = ? ORDER BY `ordinal_position`;', $rTable);
foreach ($db->get_rows() as $rRow) {
if ($rRow['column_default'] == 'NULL') {
@@ -303,7 +302,7 @@ class RecordCommand implements CommandInterface {
}
$rForceDefault = false;
if (!($rRow['is_nullable'] != 'NO' || $rRow['column_default'])) {
if (in_array($rRow['data_type'], array('int', 'float', 'tinyint', 'double', 'decimal', 'smallint', 'mediumint', 'bigint', 'bit'))) {
if (in_array($rRow['data_type'], ['int', 'float', 'tinyint', 'double', 'decimal', 'smallint', 'mediumint', 'bigint', 'bit'])) {
$rRow['column_default'] = 0;
} else {
$rRow['column_default'] = '';
@@ -332,7 +331,7 @@ class RecordCommand implements CommandInterface {
$rPID = intval(file_get_contents(ARCHIVE_PATH . $recordingID . '_.record'));
}
if (empty($rPID)) {
$rPIDs = array();
$rPIDs = [];
exec("ps -ef | grep 'Record\\[" . intval($recordingID) . "\\]' | grep -v grep | awk '{print \$2}'", $rPIDs);
foreach ($rPIDs as $rKillPID) {
$rKillPID = intval(trim($rKillPID));
+4 -4
View File
@@ -123,7 +123,7 @@ class ScannerCommand implements CommandInterface {
$rProcessed = true;
}
if (!$rProcessed) {
$rStreamArguments[] = array('value' => 'X-XC_VM-Detect:1', 'argument_key' => 'headers', 'argument_cat' => 'fetch', 'argument_wprotocol' => 'http', 'argument_type' => 'text', 'argument_cmd' => "-headers '%s" . "\r\n" . "'");
$rStreamArguments[] = ['value' => 'X-XC_VM-Detect:1', 'argument_key' => 'headers', 'argument_cat' => 'fetch', 'argument_wprotocol' => 'http', 'argument_type' => 'text', 'argument_cmd' => "-headers '%s" . "\r\n" . "'"];
}
}
@@ -137,7 +137,7 @@ class ScannerCommand implements CommandInterface {
$rProcessed = true;
}
if (!$rProcessed) {
$rStreamArguments[] = array('value' => 'X-XC_VM-Prebuffer:1', 'argument_key' => 'headers', 'argument_cat' => 'fetch', 'argument_wprotocol' => 'http', 'argument_type' => 'text', 'argument_cmd' => "-headers '%s" . "\r\n" . "'");
$rStreamArguments[] = ['value' => 'X-XC_VM-Prebuffer:1', 'argument_key' => 'headers', 'argument_cat' => 'fetch', 'argument_wprotocol' => 'http', 'argument_type' => 'text', 'argument_cmd' => "-headers '%s" . "\r\n" . "'"];
}
}
@@ -156,7 +156,7 @@ class ScannerCommand implements CommandInterface {
}
$rTime = round(microtime(true) * 1000);
$rFFProbeOutput = json_decode(shell_exec(str_replace(array('{FETCH_OPTIONS}', '{STREAM_SOURCE}'), array($rFetchOptions, escapeshellarg($rStreamSource)), $rFFProbee)), true);
$rFFProbeOutput = json_decode(shell_exec(str_replace(['{FETCH_OPTIONS}', '{STREAM_SOURCE}'], [$rFetchOptions, escapeshellarg($rStreamSource)], $rFFProbee)), true);
$rTimeTaken = round(microtime(true) * 1000) - $rTime;
if (file_exists(STREAMS_TMP_PATH . $rRow['id'] . '._errors') && 0 < filesize(STREAMS_TMP_PATH . $rRow['id'] . '._errors')) {
@@ -192,7 +192,7 @@ class ScannerCommand implements CommandInterface {
$rFPS = intval($rFPS / 1000);
}
if ($rResolution) {
$rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution);
$rResolution = StreamSorter::getNearest([240, 360, 480, 576, 720, 1080, 1440, 2160], $rResolution);
}
$rStatus = 1;
} else {
+12 -12
View File
@@ -93,7 +93,7 @@ class ServerDiagnoseCommand implements CommandInterface {
echo "Diagnosing node #{$rServerID} FROM the main — " . ($rServer['server_name'] ?? '(no name)') . " @ {$rIP}:{$rPort}\n";
echo str_repeat('-', 64) . "\n";
$rProblems = array();
$rProblems = [];
$this->heartbeatSection($rServer, $rNow, $rProblems);
@@ -132,7 +132,7 @@ class ServerDiagnoseCommand implements CommandInterface {
echo "Self-diagnosis on node #" . SERVER_ID . "" . ($rMe['server_name'] ?? '(no name)') . " (type " . ($rMe['server_type'] ?? '?') . ")\n";
echo str_repeat('-', 64) . "\n";
$rProblems = array();
$rProblems = [];
// 1. How the main currently sees me (my own row in the shared DB).
$this->heartbeatSection($rMe, $rNow, $rProblems);
@@ -259,8 +259,8 @@ class ServerDiagnoseCommand implements CommandInterface {
private function signalSection(int $rServerID, int $rNow, array &$rProblems): void {
$db = self::db();
$db->query('SELECT COUNT(*) AS `c`, MIN(`time`) AS `oldest` FROM `signals` WHERE `server_id` = ?;', $rServerID);
$rRows = $db->get_rows() ?: array();
$rSig = $rRows[0] ?? array();
$rRows = $db->get_rows() ?: [];
$rSig = $rRows[0] ?? [];
$rBacklog = intval($rSig['c'] ?? 0);
$rOldest = $rBacklog > 0 ? ($rNow - intval($rSig['oldest'])) : 0;
$rOk = ($rBacklog === 0) || ($rOldest < self::SIGNAL_STUCK_AFTER);
@@ -295,7 +295,7 @@ class ServerDiagnoseCommand implements CommandInterface {
}
private function icmpPing(string $rIP): bool {
$rOut = array();
$rOut = [];
$rCode = 1;
exec('ping -c1 -W2 ' . escapeshellarg($rIP) . ' 2>/dev/null', $rOut, $rCode);
return $rCode === 0;
@@ -316,22 +316,22 @@ class ServerDiagnoseCommand implements CommandInterface {
/** @return array{0:int,1:string} [http_code, curl_error]; code 0 = no response. */
private function httpApi(string $rURL): array {
$ch = curl_init($rURL);
curl_setopt_array($ch, array(
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 6,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,
));
]);
curl_exec($ch);
$rCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$rErr = curl_error($ch);
curl_close($ch);
return array($rCode, $rErr);
return [$rCode, $rErr];
}
private function svcActive(string $rName): bool {
$rOut = array();
$rOut = [];
$rCode = 1;
exec('systemctl is-active ' . escapeshellarg($rName) . ' 2>/dev/null', $rOut, $rCode);
return trim(implode('', $rOut)) === 'active';
@@ -339,7 +339,7 @@ class ServerDiagnoseCommand implements CommandInterface {
/** @return true|false|string true=blocked, false=not, string=could not check. */
private function iptablesBlocks(string $rIP) {
$rOut = array();
$rOut = [];
$rCode = 1;
exec('sudo -n iptables -nL INPUT 2>/dev/null', $rOut, $rCode);
if ($rCode !== 0) {
@@ -356,7 +356,7 @@ class ServerDiagnoseCommand implements CommandInterface {
private function crontabHas(string $rNeedle): bool {
// Panel crons live in the xc_vm USER's crontab (LegacyInitializer::generateCron),
// not root's. `-u xc_vm` needs root; fall back to the caller's own crontab.
$rOut = array();
$rOut = [];
$rCode = 1;
exec('crontab -u xc_vm -l 2>/dev/null', $rOut, $rCode);
if ($rCode !== 0) {
@@ -370,7 +370,7 @@ class ServerDiagnoseCommand implements CommandInterface {
/** Whether a process whose command line matches the ERE $rPattern is running. */
private function processRunning(string $rPattern): bool {
$rOut = array();
$rOut = [];
$rCode = 1;
exec('pgrep -f ' . escapeshellarg($rPattern) . ' 2>/dev/null', $rOut, $rCode);
return $rCode === 0 && count($rOut) > 0;
+6 -7
View File
@@ -25,7 +25,6 @@ use XcVm\Domain\Server\ServerRepository;
*/
class ServerInstallCommand implements CommandInterface {
public function getName(): string {
return 'server:install';
}
@@ -54,7 +53,7 @@ class ServerInstallCommand implements CommandInterface {
return 0;
}
shell_exec("kill -9 `ps -ef | grep 'XC_VM Install\\[" . $rServerID . "\\]' | grep -v grep | awk '{print \$2}'`;" );
shell_exec("kill -9 `ps -ef | grep 'XC_VM Install\\[" . $rServerID . "\\]' | grep -v grep | awk '{print \$2}'`;");
set_time_limit(0);
cli_set_process_title('XC_VM Install[' . $rServerID . ']');
register_shutdown_function(function () use ($db) {
@@ -73,7 +72,7 @@ class ServerInstallCommand implements CommandInterface {
$rHTTPSPort = (empty($rArgs[6]) ? 443 : intval($rArgs[6]));
$rUpdateSysctl = (empty($rArgs[7]) ? 0 : intval($rArgs[7]));
$rPrivateIP = (empty($rArgs[8]) ? 0 : intval($rArgs[8]));
$rParentIDs = (empty($rArgs[9]) ? array() : json_decode($rArgs[9], true));
$rParentIDs = (empty($rArgs[9]) ? [] : json_decode($rArgs[9], true));
$rSysCtl = '# XC_VM' . PHP_EOL . PHP_EOL . 'net.ipv4.tcp_congestion_control = bbr' . PHP_EOL . 'net.core.default_qdisc = fq' . PHP_EOL . 'net.ipv4.tcp_rmem = 8192 87380 134217728' . PHP_EOL . 'net.ipv4.udp_rmem_min = 16384' . PHP_EOL . 'net.core.rmem_default = 262144' . PHP_EOL . 'net.core.rmem_max = 268435456' . PHP_EOL . 'net.ipv4.tcp_wmem = 8192 65536 134217728' . PHP_EOL . 'net.ipv4.udp_wmem_min = 16384' . PHP_EOL . 'net.core.wmem_default = 262144' . PHP_EOL . 'net.core.wmem_max = 268435456' . PHP_EOL . 'net.core.somaxconn = 1000000' . PHP_EOL . 'net.core.netdev_max_backlog = 250000' . PHP_EOL . 'net.core.optmem_max = 65535' . PHP_EOL . 'net.ipv4.tcp_max_tw_buckets = 1440000' . PHP_EOL . 'net.ipv4.tcp_max_orphans = 16384' . PHP_EOL . 'net.ipv4.ip_local_port_range = 2000 65000' . PHP_EOL . 'net.ipv4.tcp_no_metrics_save = 1' . PHP_EOL . 'net.ipv4.tcp_slow_start_after_idle = 0' . PHP_EOL . 'net.ipv4.tcp_fin_timeout = 15' . PHP_EOL . 'net.ipv4.tcp_keepalive_time = 300' . PHP_EOL . 'net.ipv4.tcp_keepalive_probes = 5' . PHP_EOL . 'net.ipv4.tcp_keepalive_intvl = 15' . PHP_EOL . 'fs.file-max=20970800' . PHP_EOL . 'fs.nr_open=20970800' . PHP_EOL . 'fs.aio-max-nr=20970800' . PHP_EOL . 'net.ipv4.tcp_timestamps = 1' . PHP_EOL . 'net.ipv4.tcp_window_scaling = 1' . PHP_EOL . 'net.ipv4.tcp_mtu_probing = 1' . PHP_EOL . 'net.ipv4.route.flush = 1' . PHP_EOL . 'net.ipv6.route.flush = 1';
$rInstallDir = BIN_PATH . 'install/';
@@ -188,7 +187,7 @@ class ServerInstallCommand implements CommandInterface {
ProxyInstallFlow::runStartup($rConn, $rRunSSH);
}
if (in_array($rType, array(1, 2))) {
if (in_array($rType, [1, 2])) {
$db->query('UPDATE `servers` SET `status` = 1, `http_broadcast_port` = ?, `https_broadcast_port` = ?, `total_services` = ? WHERE `id` = ?;', $rHTTPPort, $rHTTPSPort, $rServices, $rServerID);
} else {
$db->query('UPDATE `servers` SET `status` = 1 WHERE `id` = ?;', $rServerID);
@@ -228,7 +227,7 @@ class ServerInstallCommand implements CommandInterface {
}
private function prepareInstallRoot($rConn, callable $rRunSSH, int $rType): void {
if (!in_array($rType, array(1, 2))) {
if (!in_array($rType, [1, 2])) {
return;
}
@@ -315,6 +314,6 @@ class ServerInstallCommand implements CommandInterface {
$rError = ssh2_fetch_stream($rStream, SSH2_STREAM_STDERR);
stream_set_blocking($rError, true);
stream_set_blocking($rStream, true);
return array('output' => stream_get_contents($rStream), 'error' => stream_get_contents($rError));
return ['output' => stream_get_contents($rStream), 'error' => stream_get_contents($rError)];
}
}
}
-1
View File
@@ -21,7 +21,6 @@ use XcVm\Cli\CommandInterface;
*/
class ServiceCommand implements CommandInterface {
public function getName(): string {
return 'service';
}
+4 -4
View File
@@ -80,7 +80,7 @@ class SignalsCommand implements CommandInterface {
// ── Kill-сигналы из БД ──────────────────────────────
if ($db->query('SELECT `signal_id`, `pid`, `rtmp` FROM `signals` WHERE `server_id` = ? AND `pid` IS NOT NULL ORDER BY `signal_id` ASC LIMIT 100', SERVER_ID)) {
if ($db->num_rows() > 0) {
$rIDs = array();
$rIDs = [];
foreach ($db->get_rows() as $rRow) {
$rIDs[] = $rRow['signal_id'];
$rPID = $rRow['pid'];
@@ -100,7 +100,7 @@ class SignalsCommand implements CommandInterface {
// ── Cache-сигналы из БД ─────────────────────────
if ($db->query('SELECT `signal_id`, `custom_data` FROM `signals` WHERE `server_id` = ? AND `cache` = 1 ORDER BY `signal_id` ASC LIMIT 1000;', SERVER_ID)) {
if ($db->num_rows() > 0) {
$rUpdatedStreams = $rUpdatedLines = $rIDs = array();
$rUpdatedStreams = $rUpdatedLines = $rIDs = [];
foreach ($db->get_rows() as $rRow) {
$rCustomData = json_decode($rRow['custom_data'], true);
$rIDs[] = $rRow['signal_id'];
@@ -163,13 +163,13 @@ class SignalsCommand implements CommandInterface {
// ── Redis kill-сигналы ──────────────────────
if (SettingsManager::get('redis_handler')) {
$rSignals = array();
$rSignals = [];
foreach (RedisManager::instance()->sMembers('SIGNALS#' . SERVER_ID) as $rKey) {
$rSignals[] = $rKey;
}
if (count($rSignals) > 0) {
$rSignalData = RedisManager::instance()->mGet($rSignals);
$rIDs = array();
$rIDs = [];
foreach ($rSignalData as $rData) {
$rRow = igbinary_unserialize($rData);
$rIDs[] = $rRow['key'];
+3 -3
View File
@@ -95,7 +95,7 @@ class StartupCommand implements CommandInterface {
}
private function installRootCrontab(): void {
$rCrons = array();
$rCrons = [];
$rCrons[] = '* * * * * ' . PHP_BIN . ' ' . MAIN_HOME . 'console.php cron:root_signals # XC_VM';
if (file_exists(MAIN_HOME . 'Cli/CronJobs/RootMysqlCronJob.php')) {
$rCrons[] = '* * * * * ' . PHP_BIN . ' ' . MAIN_HOME . 'console.php cron:root_mysql # XC_VM';
@@ -112,13 +112,13 @@ class StartupCommand implements CommandInterface {
}
$rWrite = false;
$rOutput = array();
$rOutput = [];
exec('sudo crontab -l', $rOutput);
// Удаляем старые записи XC_VM: путь v1.x.x (crons/root_) и любые
// строки с нашим маркером — включая старый '# \XC_VM' от прошлой
// миграции — чтобы при апгрейде не появлялись дубликаты.
$rFiltered = array();
$rFiltered = [];
foreach ($rOutput as $rLine) {
if (strpos($rLine, MAIN_HOME . 'crons/root_') !== false
|| strpos($rLine, '# XC_VM') !== false
+9 -10
View File
@@ -28,7 +28,6 @@ use XcVm\Infrastructure\Database\DatabaseFactory;
*/
class StatusCommand implements CommandInterface {
use DatabaseAware;
public function getName(): string {
@@ -149,14 +148,14 @@ class StatusCommand implements CommandInterface {
private function getServers(): array {
$db = self::db();
$db->query('SELECT * FROM `servers`');
$rServers = array();
$rOnlineStatus = array(1);
$rServers = [];
$rOnlineStatus = [1];
foreach ($db->get_rows() as $rRow) {
if (empty($rRow['domain_name'])) {
$rURL = escapeshellcmd($rRow['server_ip']);
} else {
$rURL = str_replace(array('http://', '/', 'https://'), '', escapeshellcmd(explode(',', $rRow['domain_name'])[0]));
$rURL = str_replace(['http://', '/', 'https://'], '', escapeshellcmd(explode(',', $rRow['domain_name'])[0]));
}
$rProtocol = ($rRow['enable_https'] == 1) ? 'https' : 'http';
@@ -186,7 +185,7 @@ class StatusCommand implements CommandInterface {
$rReload = true;
}
foreach (array('http', 'https') as $rType) {
foreach (['http', 'https'] as $rType) {
$rPortConfig = file_get_contents(MAIN_HOME . 'bin/nginx/conf/ports/' . $rType . '.conf');
if (stripos($rPortConfig, ' reuseport') !== false) {
@@ -199,7 +198,7 @@ class StatusCommand implements CommandInterface {
}
private function installRootCrontab(): void {
$rCrons = array();
$rCrons = [];
$rCrons[] = '* * * * * ' . PHP_BIN . ' ' . MAIN_HOME . 'console.php cron:root_signals # XC_VM';
if (file_exists(MAIN_HOME . 'Cli/CronJobs/RootMysqlCronJob.php')) {
@@ -211,12 +210,12 @@ class StatusCommand implements CommandInterface {
}
$rWrite = false;
$rOutput = array();
$rOutput = [];
exec('sudo crontab -l', $rOutput);
// Удаляем старые строки с нашим маркером (включая '# \XC_VM' от
// прошлой миграции), чтобы при апгрейде не появлялись дубликаты.
$rFiltered = array();
$rFiltered = [];
foreach ($rOutput as $rLine) {
if (strpos($rLine, '# XC_VM') !== false || strpos($rLine, '# \XC_VM') !== false) {
$rWrite = true;
@@ -285,8 +284,8 @@ class StatusCommand implements CommandInterface {
private function broadcastUpdateBinaries(array $rServers): void {
$db = self::db();
foreach ($rServers as $rServerID => $rServerArray) {
$db->query('DELETE FROM `signals` WHERE `custom_data` = ?;', json_encode(array('action' => 'update_binaries')));
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', $rServerID, time(), json_encode(array('action' => 'update_binaries')));
$db->query('DELETE FROM `signals` WHERE `custom_data` = ?;', json_encode(['action' => 'update_binaries']));
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', $rServerID, time(), json_encode(['action' => 'update_binaries']));
}
}
-1
View File
@@ -18,7 +18,6 @@ use XcVm\Streaming\Codec\FfmpegPaths;
*/
class ThumbnailCommand implements CommandInterface {
public function getName(): string {
return 'thumbnail';
}
+22 -23
View File
@@ -24,7 +24,6 @@ use XcVm\Infrastructure\Database\DatabaseAware;
*/
class ToolsCommand implements CommandInterface {
use DatabaseAware;
public function getName(): string {
@@ -43,8 +42,8 @@ class ToolsCommand implements CommandInterface {
$rMethod = (!empty($rArgs[0]) ? $rArgs[0] : null);
$rUser = posix_getpwuid(posix_geteuid())['name'];
$rRootMethods = array('rescue', 'recaptcha', 'access', 'ports', 'migration', 'user', 'mysql', 'database', 'flush');
$rUserMethods = array('images', 'duplicates', 'bouquets');
$rRootMethods = ['rescue', 'recaptcha', 'access', 'ports', 'migration', 'user', 'mysql', 'database', 'flush'];
$rUserMethods = ['images', 'duplicates', 'bouquets'];
// No or unknown subcommand → show the full help to any user (root or xc_vm)
if ($rMethod === null || (!in_array($rMethod, $rRootMethods, true) && !in_array($rMethod, $rUserMethods, true))) {
@@ -257,21 +256,21 @@ class ToolsCommand implements CommandInterface {
private function processPorts(array $rServers): int {
echo "Generating port configuration...\n\n";
$rConfig = array(
$rConfig = [
'http' => array_unique(array_merge(
array($rServers[SERVER_ID]['http_broadcast_port']),
(explode(',', $rServers[SERVER_ID]['http_ports_add']) ?: array())
[$rServers[SERVER_ID]['http_broadcast_port']],
(explode(',', $rServers[SERVER_ID]['http_ports_add']) ?: [])
)),
'https' => array_unique(array_merge(
array($rServers[SERVER_ID]['https_broadcast_port']),
(explode(',', $rServers[SERVER_ID]['https_ports_add']) ?: array())
[$rServers[SERVER_ID]['https_broadcast_port']],
(explode(',', $rServers[SERVER_ID]['https_ports_add']) ?: [])
)),
'rtmp' => $rServers[SERVER_ID]['rtmp_port'],
);
];
foreach ($rConfig as $rKey => $rPorts) {
if ($rKey === 'http') {
$rListen = array();
$rListen = [];
foreach ($rPorts as $rPort) {
if (is_numeric($rPort) && 80 <= $rPort && $rPort <= 65535) {
$rListen[] = 'listen ' . intval($rPort) . ';';
@@ -280,7 +279,7 @@ class ToolsCommand implements CommandInterface {
file_put_contents(MAIN_HOME . 'bin/nginx/conf/ports/http.conf', implode(' ', $rListen));
file_put_contents(MAIN_HOME . 'bin/nginx_rtmp/conf/live.conf', 'on_play http://127.0.0.1:' . intval($rPorts[0]) . '/stream/rtmp; on_publish http://127.0.0.1:' . intval($rPorts[0]) . '/stream/rtmp; on_play_done http://127.0.0.1:' . intval($rPorts[0]) . '/stream/rtmp;');
} elseif ($rKey === 'https') {
$rListen = array();
$rListen = [];
foreach ($rPorts as $rPort) {
if (is_numeric($rPort) && 80 <= $rPort && $rPort <= 65535) {
$rListen[] = 'listen ' . intval($rPort) . ' ssl;';
@@ -309,13 +308,13 @@ class ToolsCommand implements CommandInterface {
private function processImages(): void {
$db = self::db();
$rImages = array();
$rImages = [];
$db->query('SELECT COUNT(*) AS `count` FROM `streams`;');
$rCount = $db->get_row()['count'];
if ($rCount > 0) {
$rSteps = range(0, $rCount, 1000);
if (!$rSteps) {
$rSteps = array(0);
$rSteps = [0];
}
foreach ($rSteps as $rStep) {
try {
@@ -346,7 +345,7 @@ class ToolsCommand implements CommandInterface {
if ($rCount > 0) {
$rSteps = range(0, $rCount, 1000);
if (!$rSteps) {
$rSteps = array(0);
$rSteps = [0];
}
foreach ($rSteps as $rStep) {
try {
@@ -389,7 +388,7 @@ class ToolsCommand implements CommandInterface {
private function processDuplicates(): void {
$db = self::db();
$rGroups = $rStreamIDs = array();
$rGroups = $rStreamIDs = [];
$db->query('SELECT `a`.`id`, `a`.`stream_source` FROM `streams` `a` INNER JOIN (SELECT `stream_source`, COUNT(*) `totalCount` FROM `streams` WHERE `type` IN (2,5) GROUP BY `stream_source`) `b` ON `a`.`stream_source` = `b`.`stream_source` WHERE `b`.`totalCount` > 1;');
foreach ($db->get_rows() as $rRow) {
$rGroups[md5($rRow['stream_source'])][] = $rRow['id'];
@@ -409,7 +408,7 @@ class ToolsCommand implements CommandInterface {
private function processBouquets(): void {
$db = self::db();
$rStreamIDs = array(array(), array());
$rStreamIDs = [[], []];
$db->query('SELECT `id` FROM `streams`;');
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
@@ -425,23 +424,23 @@ class ToolsCommand implements CommandInterface {
$db->query('SELECT * FROM `bouquets` ORDER BY `bouquet_order` ASC;');
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rBouquet) {
$UpdateData = array(array(), array(), array(), array());
foreach ((json_decode($rBouquet['bouquet_channels'], true) ?: array()) as $rID) {
$UpdateData = [[], [], [], []];
foreach ((json_decode($rBouquet['bouquet_channels'], true) ?: []) as $rID) {
if (0 < intval($rID) && in_array(intval($rID), $rStreamIDs[0])) {
$UpdateData[0][] = intval($rID);
}
}
foreach ((json_decode($rBouquet['bouquet_movies'], true) ?: array()) as $rID) {
foreach ((json_decode($rBouquet['bouquet_movies'], true) ?: []) as $rID) {
if (0 < intval($rID) && in_array(intval($rID), $rStreamIDs[0])) {
$UpdateData[1][] = intval($rID);
}
}
foreach ((json_decode($rBouquet['bouquet_radios'], true) ?: array()) as $rID) {
foreach ((json_decode($rBouquet['bouquet_radios'], true) ?: []) as $rID) {
if (0 < intval($rID) && in_array(intval($rID), $rStreamIDs[0])) {
$UpdateData[2][] = intval($rID);
}
}
foreach ((json_decode($rBouquet['bouquet_series'], true) ?: array()) as $rID) {
foreach ((json_decode($rBouquet['bouquet_series'], true) ?: []) as $rID) {
if (0 < intval($rID) && in_array(intval($rID), $rStreamIDs[1])) {
$UpdateData[3][] = intval($rID);
}
@@ -468,9 +467,9 @@ class ToolsCommand implements CommandInterface {
$db->query('SELECT `server_id` FROM `streams_servers` WHERE `stream_id` IN (' . implode(',', $rIDs) . ');');
$db->query('DELETE FROM `streams_servers` WHERE `stream_id` IN (' . implode(',', $rIDs) . ');');
$db->query('DELETE FROM `streams_servers` WHERE `parent_id` IS NOT NULL AND `parent_id` > 0 AND `parent_id` NOT IN (SELECT `id` FROM `servers` WHERE `server_type` = 0);');
$db->query('INSERT INTO `signals`(`server_id`, `cache`, `time`, `custom_data`) VALUES(?, 1, ?, ?);', SERVER_ID, time(), json_encode(array('type' => 'update_streams', 'id' => $rIDs)));
$db->query('INSERT INTO `signals`(`server_id`, `cache`, `time`, `custom_data`) VALUES(?, 1, ?, ?);', SERVER_ID, time(), json_encode(['type' => 'update_streams', 'id' => $rIDs]));
foreach (array_keys(ServerRepository::getAll()) as $rServerID) {
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`, `cache`) VALUES(?, ?, ?, 1);', $rServerID, time(), json_encode(array('type' => 'delete_vods', 'id' => $rIDs)));
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`, `cache`) VALUES(?, ?, ?, 1);', $rServerID, time(), json_encode(['type' => 'delete_vods', 'id' => $rIDs]));
}
return true;
}
+5 -4
View File
@@ -22,7 +22,6 @@ use XcVm\Domain\Server\ServerRepository;
*/
class UpdateCommand implements CommandInterface {
public function getName(): string {
return 'update';
}
@@ -282,7 +281,7 @@ class UpdateCommand implements CommandInterface {
UpdateLogger::info('Broadcasting update signal to LB servers');
foreach (ServerRepository::getAll() as $rServer) {
if (($rServer['enabled'] && $rServer['status'] == 1 && time() - $rServer['last_check_ago'] <= 180) || !$rServer['is_main']) {
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', $rServer['id'], time(), json_encode(array('action' => 'update')));
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', $rServer['id'], time(), json_encode(['action' => 'update']));
}
}
}
@@ -291,7 +290,7 @@ class UpdateCommand implements CommandInterface {
$db->query('UPDATE `settings` SET `update_data` = NULL;');
UpdateLogger::info('Server status set to 1 (online), version=' . XC_VM_VERSION);
foreach (array('http', 'https') as $rType) {
foreach (['http', 'https'] as $rType) {
$rPortConfig = file_get_contents(MAIN_HOME . 'bin/nginx/conf/ports/' . $rType . '.conf');
if (stripos($rPortConfig, ' reuseport') !== false) {
file_put_contents(MAIN_HOME . 'bin/nginx/conf/ports/' . $rType . '.conf', str_replace(' reuseport', '', $rPortConfig));
@@ -338,7 +337,9 @@ class UpdateCommand implements CommandInterface {
private function downloadFile($url, $targetPath): bool {
$rData = @fopen($url, 'rb');
if (!$rData) return false;
if (!$rData) {
return false;
}
$rOutput = fopen($targetPath, 'wb');
stream_copy_to_stream($rData, $rOutput);
fclose($rData);
+6 -6
View File
@@ -113,13 +113,13 @@ class WatchdogCommand implements CommandInterface {
$rInfoA = explode(' ', preg_replace('!cpu +!', '', $rPrevStat[0]));
$rInfoB = explode(' ', preg_replace('!cpu +!', '', $rStat[0]));
$rPrevStat = $rStat;
$rDiff = array();
$rDiff = [];
$rDiff['user'] = intval($rInfoB[0]) - intval($rInfoA[0]);
$rDiff['nice'] = intval($rInfoB[1]) - intval($rInfoA[1]);
$rDiff['sys'] = intval($rInfoB[2]) - intval($rInfoA[2]);
$rDiff['idle'] = intval($rInfoB[3]) - intval($rInfoA[3]);
$rTotal = array_sum($rDiff);
$rCPU = array();
$rCPU = [];
foreach ($rDiff as $x => $y) {
$rCPU[$x] = round($y / $rTotal * 100, 2);
}
@@ -134,8 +134,8 @@ class WatchdogCommand implements CommandInterface {
$rStats['fanout'] = FanoutClient::status();
// ── PHP PIDs ─────────────────────────────────────────
$rPHPPIDs = array();
foreach (glob(MAIN_HOME . 'bin/php/sockets/*.pid') ?: array() as $rPidFile) {
$rPHPPIDs = [];
foreach (glob(MAIN_HOME . 'bin/php/sockets/*.pid') ?: [] as $rPidFile) {
$rPid = trim(@file_get_contents($rPidFile) ?: '');
if (is_numeric($rPid) && 0 < intval($rPid)) {
$rPHPPIDs[] = intval($rPid);
@@ -165,11 +165,11 @@ class WatchdogCommand implements CommandInterface {
foreach (array_keys($rServers) as $rServerID) {
if ($rServers[$rServerID]['server_online']) {
$rMulti->zCard('SERVER#' . $rServerID);
$rMulti->zRangeByScore('SERVER_LINES#' . $rServerID, '-inf', '+inf', array('withscores' => true));
$rMulti->zRangeByScore('SERVER_LINES#' . $rServerID, '-inf', '+inf', ['withscores' => true]);
}
}
$rResults = $rMulti->exec();
$rTotalUsers = array();
$rTotalUsers = [];
$i = 0;
foreach (array_keys($rServers) as $rServerID) {
if ($rServers[$rServerID]['server_online']) {
-1
View File
@@ -38,7 +38,6 @@ use XcVm\Core\Updates\UpdateChannels;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class XcvmCoreCommand implements CommandInterface {
/** Branch of the binaries repo that carries the committed extension tree. */
private const BIN_BRANCH = 'main';
+74 -74
View File
@@ -17,96 +17,96 @@ use XcVm\Infrastructure\Database\DatabaseAware;
*/
class ActivityCronJob implements CommandInterface {
use DatabaseAware;
use CronTrait;
use DatabaseAware;
use CronTrait;
public function getName(): string {
return 'cron:activity';
}
public function getName(): string {
return 'cron:activity';
}
public function getDescription(): string {
return 'Cron: import user activity logs into DB';
}
public function getDescription(): string {
return 'Cron: import user activity logs into DB';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Activity]');
$this->loadCron();
$this->initCron('XC_VM[Activity]');
$this->loadCron();
return 0;
}
return 0;
}
private function loadCron(): void {
$db = self::db();
private function loadCron(): void {
$db = self::db();
$rLogFile = LOGS_TMP_PATH . 'activity';
$rUpdateQuery = $rQuery = '';
$rUpdates = array();
$rCount = 0;
$rLogFile = LOGS_TMP_PATH . 'activity';
$rUpdateQuery = $rQuery = '';
$rUpdates = [];
$rCount = 0;
if (!file_exists($rLogFile)) {
return;
}
if (!file_exists($rLogFile)) {
return;
}
list($rQuery, $rUpdates, $rCount) = $this->parseLog($rLogFile);
unlink($rLogFile);
list($rQuery, $rUpdates, $rCount) = $this->parseLog($rLogFile);
unlink($rLogFile);
if (0 >= $rCount) {
return;
}
if (0 >= $rCount) {
return;
}
$rQuery = rtrim($rQuery, ',');
if (empty($rQuery)) {
return;
}
$rQuery = rtrim($rQuery, ',');
if (empty($rQuery)) {
return;
}
if (!$db->query('INSERT INTO `lines_activity` (`server_id`,`proxy_id`,`user_id`,`isp`,`external_device`,`stream_id`,`date_start`,`user_agent`,`user_ip`,`date_end`,`container`,`geoip_country_code`,`divergence`,`hmac_id`,`hmac_identifier`) VALUES ' . $rQuery)) {
return;
}
if (!$db->query('INSERT INTO `lines_activity` (`server_id`,`proxy_id`,`user_id`,`isp`,`external_device`,`stream_id`,`date_start`,`user_agent`,`user_ip`,`date_end`,`container`,`geoip_country_code`,`divergence`,`hmac_id`,`hmac_identifier`) VALUES ' . $rQuery)) {
return;
}
$rFirstID = $db->last_insert_id();
$i = 0;
while ($i < $rCount) {
$rUpdateQuery .= '(' . $rUpdates[$i][0] . ',' . $db->escape($rUpdates[$i][1]) . ',' . ($rFirstID + $i) . ',' . $db->escape($rUpdates[$i][2]) . '),';
$i++;
}
$rFirstID = $db->last_insert_id();
$i = 0;
while ($i < $rCount) {
$rUpdateQuery .= '(' . $rUpdates[$i][0] . ',' . $db->escape($rUpdates[$i][1]) . ',' . ($rFirstID + $i) . ',' . $db->escape($rUpdates[$i][2]) . '),';
$i++;
}
$rUpdateQuery = rtrim($rUpdateQuery, ',');
if (!empty($rUpdateQuery)) {
$db->query('INSERT INTO `lines`(`id`,`last_ip`,`last_activity`,`last_activity_array`) VALUES ' . $rUpdateQuery . ' ON DUPLICATE KEY UPDATE `id`=VALUES(`id`), `last_ip`=VALUES(`last_ip`), `last_activity`=VALUES(`last_activity`), `last_activity_array`=VALUES(`last_activity_array`);');
}
}
$rUpdateQuery = rtrim($rUpdateQuery, ',');
if (!empty($rUpdateQuery)) {
$db->query('INSERT INTO `lines`(`id`,`last_ip`,`last_activity`,`last_activity_array`) VALUES ' . $rUpdateQuery . ' ON DUPLICATE KEY UPDATE `id`=VALUES(`id`), `last_ip`=VALUES(`last_ip`), `last_activity`=VALUES(`last_activity`), `last_activity_array`=VALUES(`last_activity_array`);');
}
}
private function parseLog(string $rFile): array {
$db = self::db();
$rQuery = '';
$rUpdates = array();
$rCount = 0;
private function parseLog(string $rFile): array {
$db = self::db();
$rQuery = '';
$rUpdates = [];
$rCount = 0;
if (!file_exists($rFile)) {
return array($rQuery, $rUpdates, $rCount);
}
if (!file_exists($rFile)) {
return [$rQuery, $rUpdates, $rCount];
}
$rFP = fopen($rFile, 'r');
while (!feof($rFP)) {
$rLine = trim(fgets($rFP));
if (!empty($rLine)) {
$rLine = json_decode(base64_decode($rLine), true);
if (!($rLine['server_id'] && $rLine['user_id'] && $rLine['stream_id'] && $rLine['user_ip'])) {
break;
}
$rUpdates[] = array($rLine['user_id'], $rLine['user_ip'], json_encode(array('date_end' => $rLine['date_end'], 'stream_id' => $rLine['stream_id'])));
$rLine = array_map(array($db, 'escape'), $rLine);
$rQuery .= '(' . $rLine['server_id'] . ',' . $rLine['proxy_id'] . ',' . $rLine['user_id'] . ',' . $rLine['isp'] . ',' . $rLine['external_device'] . ',' . $rLine['stream_id'] . ',' . $rLine['date_start'] . ',' . $rLine['user_agent'] . ',' . $rLine['user_ip'] . ',' . $rLine['date_end'] . ',' . $rLine['container'] . ',' . $rLine['geoip_country_code'] . ',' . $rLine['divergence'] . ',' . $rLine['hmac_id'] . ',' . $rLine['hmac_identifier'] . '),';
$rCount++;
break;
}
}
fclose($rFP);
$rFP = fopen($rFile, 'r');
while (!feof($rFP)) {
$rLine = trim(fgets($rFP));
if (!empty($rLine)) {
$rLine = json_decode(base64_decode($rLine), true);
if (!($rLine['server_id'] && $rLine['user_id'] && $rLine['stream_id'] && $rLine['user_ip'])) {
break;
}
$rUpdates[] = [$rLine['user_id'], $rLine['user_ip'], json_encode(['date_end' => $rLine['date_end'], 'stream_id' => $rLine['stream_id']])];
$rLine = array_map([$db, 'escape'], $rLine);
$rQuery .= '(' . $rLine['server_id'] . ',' . $rLine['proxy_id'] . ',' . $rLine['user_id'] . ',' . $rLine['isp'] . ',' . $rLine['external_device'] . ',' . $rLine['stream_id'] . ',' . $rLine['date_start'] . ',' . $rLine['user_agent'] . ',' . $rLine['user_ip'] . ',' . $rLine['date_end'] . ',' . $rLine['container'] . ',' . $rLine['geoip_country_code'] . ',' . $rLine['divergence'] . ',' . $rLine['hmac_id'] . ',' . $rLine['hmac_identifier'] . '),';
$rCount++;
break;
}
}
fclose($rFP);
return array($rQuery, $rUpdates, $rCount);
}
return [$rQuery, $rUpdates, $rCount];
}
}
+92 -92
View File
@@ -19,113 +19,113 @@ use XcVm\Domain\Server\ServerRepository;
*/
class BackupsCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:backups';
}
public function getName(): string {
return 'cron:backups';
}
public function getDescription(): string {
return 'Cron: automatic database backup (local + Dropbox)';
}
public function getDescription(): string {
return 'Cron: automatic database backup (local + Dropbox)';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(32757);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(32757);
if (!ServerRepository::getAll()[SERVER_ID]['is_main']) {
echo 'Please run on main server.' . "\n";
return 1;
}
if (!ServerRepository::getAll()[SERVER_ID]['is_main']) {
echo 'Please run on main server.' . "\n";
return 1;
}
$this->setProcessTitle('XC_VM[Backups]');
$this->acquireCronLock();
$this->setProcessTitle('XC_VM[Backups]');
$this->acquireCronLock();
global $db;
global $db;
$rForce = false;
if (!empty($rArgs[0]) && intval($rArgs[0]) == 1) {
$rForce = true;
}
$rForce = false;
if (!empty($rArgs[0]) && intval($rArgs[0]) == 1) {
$rForce = true;
}
$rBackups = SettingsManager::get('automatic_backups');
$rLastBackup = intval(SettingsManager::get('last_backup'));
$rPeriod = array('hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2419200);
$rBackups = SettingsManager::get('automatic_backups');
$rLastBackup = intval(SettingsManager::get('last_backup'));
$rPeriod = ['hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2419200];
if (!$rForce) {
$rPID = getmypid();
if (file_exists('/proc/' . SettingsManager::get('backups_pid')) && 0 < strlen(SettingsManager::get('backups_pid'))) {
return 0;
}
$db->query('UPDATE `settings` SET `backups_pid` = ?;', $rPID);
}
if (!$rForce) {
$rPID = getmypid();
if (file_exists('/proc/' . SettingsManager::get('backups_pid')) && 0 < strlen(SettingsManager::get('backups_pid'))) {
return 0;
}
$db->query('UPDATE `settings` SET `backups_pid` = ?;', $rPID);
}
if (isset($rBackups) && $rBackups != 'off' || $rForce) {
if ($rLastBackup + $rPeriod[$rBackups] <= time() || $rForce) {
if (!$rForce) {
$db->query('UPDATE `settings` SET `last_backup` = ?;', time());
}
$db->close_mysql();
$rFilename = MAIN_HOME . 'backups/backup_' . date('Y-m-d_H:i:s') . '.sql';
if (isset($rBackups) && $rBackups != 'off' || $rForce) {
if ($rLastBackup + $rPeriod[$rBackups] <= time() || $rForce) {
if (!$rForce) {
$db->query('UPDATE `settings` SET `last_backup` = ?;', time());
}
$db->close_mysql();
$rFilename = MAIN_HOME . 'backups/backup_' . date('Y-m-d_H:i:s') . '.sql';
BackupService::create($rFilename);
BackupService::create($rFilename);
if (0 < filesize($rFilename)) {
if (SettingsManager::get('dropbox_remote')) {
file_put_contents($rFilename . '.uploading', time());
$rResponse = BackupService::uploadRemote(basename($rFilename), $rFilename);
if (!isset($rResponse->error)) {
$rResponse = json_decode(json_encode($rResponse, JSON_UNESCAPED_UNICODE), true);
if (!(isset($rResponse['size']) && intval($rResponse['size']) == filesize($rFilename))) {
$rError = 'Failed to upload';
file_put_contents($rFilename . '.error', $rError);
}
} else {
try {
$rError = json_decode(explode(', in apiCall', $rResponse->error->getMessage())[0], true)['error_summary'];
} catch (\exception $e) {
$rError = 'Unknown error';
}
file_put_contents($rFilename . '.error', $rError);
}
unlink($rFilename . '.uploading');
}
} else {
unlink($rFilename);
}
}
}
if (0 < filesize($rFilename)) {
if (SettingsManager::get('dropbox_remote')) {
file_put_contents($rFilename . '.uploading', time());
$rResponse = BackupService::uploadRemote(basename($rFilename), $rFilename);
if (!isset($rResponse->error)) {
$rResponse = json_decode(json_encode($rResponse, JSON_UNESCAPED_UNICODE), true);
if (!(isset($rResponse['size']) && intval($rResponse['size']) == filesize($rFilename))) {
$rError = 'Failed to upload';
file_put_contents($rFilename . '.error', $rError);
}
} else {
try {
$rError = json_decode(explode(', in apiCall', $rResponse->error->getMessage())[0], true)['error_summary'];
} catch (\exception $e) {
$rError = 'Unknown error';
}
file_put_contents($rFilename . '.error', $rError);
}
unlink($rFilename . '.uploading');
}
} else {
unlink($rFilename);
}
}
}
$rBackups = BackupService::getLocal();
if (intval(SettingsManager::get('backups_to_keep')) < count($rBackups) && 0 < intval(SettingsManager::get('backups_to_keep'))) {
$rDelete = array_slice($rBackups, 0, count($rBackups) - intval(SettingsManager::get('backups_to_keep')));
foreach ($rDelete as $rItem) {
if (file_exists(MAIN_HOME . 'backups/' . $rItem['filename'])) {
unlink(MAIN_HOME . 'backups/' . $rItem['filename']);
}
}
}
$rBackups = BackupService::getLocal();
if (intval(SettingsManager::get('backups_to_keep')) < count($rBackups) && 0 < intval(SettingsManager::get('backups_to_keep'))) {
$rDelete = array_slice($rBackups, 0, count($rBackups) - intval(SettingsManager::get('backups_to_keep')));
foreach ($rDelete as $rItem) {
if (file_exists(MAIN_HOME . 'backups/' . $rItem['filename'])) {
unlink(MAIN_HOME . 'backups/' . $rItem['filename']);
}
}
}
if (SettingsManager::get('dropbox_remote')) {
$rRemoteBackups = BackupService::getRemote();
if (intval(SettingsManager::get('dropbox_keep')) < count($rRemoteBackups) && 0 < intval(SettingsManager::get('dropbox_keep'))) {
$rDelete = array_slice($rRemoteBackups, 0, count($rRemoteBackups) - intval(SettingsManager::get('dropbox_keep')));
foreach ($rDelete as $rItem) {
try {
BackupService::deleteRemote($rItem['path']);
} catch (\exception $e) {
}
}
}
}
if (SettingsManager::get('dropbox_remote')) {
$rRemoteBackups = BackupService::getRemote();
if (intval(SettingsManager::get('dropbox_keep')) < count($rRemoteBackups) && 0 < intval(SettingsManager::get('dropbox_keep'))) {
$rDelete = array_slice($rRemoteBackups, 0, count($rRemoteBackups) - intval(SettingsManager::get('dropbox_keep')));
foreach ($rDelete as $rItem) {
try {
BackupService::deleteRemote($rItem['path']);
} catch (\exception $e) {
}
}
}
}
@unlink($this->rIdentifier);
@unlink($this->rIdentifier);
return 0;
}
return 0;
}
}
+247 -239
View File
@@ -24,279 +24,287 @@ use XcVm\Infrastructure\Database\DatabaseAware;
*/
class CacheCronJob implements CommandInterface {
use DatabaseAware;
use CronTrait;
use DatabaseAware;
use CronTrait;
public function getName(): string {
return 'cron:cache';
}
public function getName(): string {
return 'cron:cache';
}
public function getDescription(): string {
return 'Cron: generate file cache (settings, bouquets, servers, blocked, categories, etc.)';
}
public function getDescription(): string {
return 'Cron: generate file cache (settings, bouquets, servers, blocked, categories, etc.)';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
ini_set('memory_limit', -1);
ini_set('memory_limit', -1);
$rStartup = false;
if (!empty($rArgs[0])) {
$rStartup = true;
}
$rStartup = false;
if (!empty($rArgs[0])) {
$rStartup = true;
}
$this->setProcessTitle('XC_VM[Cache Builder]');
$this->acquireCronLock();
$this->setProcessTitle('XC_VM[Cache Builder]');
$this->acquireCronLock();
$this->loadCron($rStartup);
$this->loadCron($rStartup);
return 0;
}
return 0;
}
private function loadCron(bool $rStartup): void {
$db = self::db();
if (!defined('CACHE_TMP_PATH')) {
exit();
}
private function loadCron(bool $rStartup): void {
$db = self::db();
if (!defined('CACHE_TMP_PATH')) {
exit();
}
// Atomic tmp+rename writes: a direct file_put_contents cannot replace a
// cache file left behind by a root-context run (Permission denied spam),
// while rename() only needs a writable directory.
$rCache = new FileCache(CACHE_TMP_PATH);
// Atomic tmp+rename writes: a direct file_put_contents cannot replace a
// cache file left behind by a root-context run (Permission denied spam),
// while rename() only needs a writable directory.
$rCache = new FileCache(CACHE_TMP_PATH);
if ($rStartup && file_exists(CACHE_TMP_PATH . 'settings')) {
echo 'Checking cache readability...' . "\n";
$rSerialize = igbinary_unserialize(file_get_contents(CACHE_TMP_PATH . 'settings'));
if (!(is_array($rSerialize) && isset($rSerialize['server_name']))) {
echo 'Clearing cache...' . "\n\n";
foreach (array(STREAMS_TMP_PATH, LINES_TMP_PATH, SERIES_TMP_PATH) as $rTmpPath) {
foreach (scandir($rTmpPath) as $rFile) {
unlink($rTmpPath . $rFile);
}
}
// No sudo: this runs as xc_vm (service boot chowns tmp/ first),
// and xc_vm has no sudoers entry — sudo here would fail silently.
exec('rm -rf ' . TMP_PATH . '*');
exec('rm -rf ' . SIGNALS_PATH . '*');
}
}
if ($rStartup && file_exists(CACHE_TMP_PATH . 'settings')) {
echo 'Checking cache readability...' . "\n";
$rSerialize = igbinary_unserialize(file_get_contents(CACHE_TMP_PATH . 'settings'));
if (!(is_array($rSerialize) && isset($rSerialize['server_name']))) {
echo 'Clearing cache...' . "\n\n";
foreach ([STREAMS_TMP_PATH, LINES_TMP_PATH, SERIES_TMP_PATH] as $rTmpPath) {
foreach (scandir($rTmpPath) as $rFile) {
unlink($rTmpPath . $rFile);
}
}
// No sudo: this runs as xc_vm (service boot chowns tmp/ first),
// and xc_vm has no sudoers entry — sudo here would fail silently.
exec('rm -rf ' . TMP_PATH . '*');
exec('rm -rf ' . SIGNALS_PATH . '*');
}
}
foreach (array(EPG_PATH, VOD_PATH, ARCHIVE_PATH, CREATED_PATH, DELAY_PATH, VIDEO_PATH, PLAYLIST_PATH, CONS_TMP_PATH, CRONS_TMP_PATH, PLAYER_TMP_PATH, CACHE_TMP_PATH, DIVERGENCE_TMP_PATH, FLOOD_TMP_PATH, MINISTRA_TMP_PATH, SIGNALS_TMP_PATH, LOGS_TMP_PATH, WATCH_TMP_PATH, CIDR_TMP_PATH, STREAMS_TMP_PATH, LINES_TMP_PATH, SERIES_TMP_PATH) as $rPath) {
if (!file_exists($rPath)) {
@mkdir($rPath, 0755, true);
if (is_dir($rPath) && function_exists('posix_getpwnam')) {
$rUser = posix_getpwnam('xc_vm');
if ($rUser) {
chown($rPath, $rUser['uid']);
chgrp($rPath, $rUser['gid']);
}
}
}
}
foreach ([EPG_PATH, VOD_PATH, ARCHIVE_PATH, CREATED_PATH, DELAY_PATH, VIDEO_PATH, PLAYLIST_PATH, CONS_TMP_PATH, CRONS_TMP_PATH, PLAYER_TMP_PATH, CACHE_TMP_PATH, DIVERGENCE_TMP_PATH, FLOOD_TMP_PATH, MINISTRA_TMP_PATH, SIGNALS_TMP_PATH, LOGS_TMP_PATH, WATCH_TMP_PATH, CIDR_TMP_PATH, STREAMS_TMP_PATH, LINES_TMP_PATH, SERIES_TMP_PATH] as $rPath) {
if (!file_exists($rPath)) {
@mkdir($rPath, 0755, true);
if (is_dir($rPath) && function_exists('posix_getpwnam')) {
$rUser = posix_getpwnam('xc_vm');
if ($rUser) {
chown($rPath, $rUser['uid']);
chgrp($rPath, $rUser['gid']);
}
}
}
}
FileCache::setCache('settings', SettingsRepository::getAll(true));
FileCache::setCache('bouquets', BouquetService::getAll(true));
$rServers = ServerRepository::getAll(true);
unset($rServers['php_pids']);
FileCache::setCache('servers', $rServers);
FileCache::setCache('proxy_servers', BlocklistService::getProxyIPs(true));
FileCache::setCache('blocked_servers', BlocklistService::getBlockedServers(true));
FileCache::setCache('blocked_isp', BlocklistService::getBlockedISP(true));
FileCache::setCache('blocked_ua', BlocklistService::getBlockedUA(true));
FileCache::setCache('blocked_ips', BlocklistService::getBlockedIPs(true));
FileCache::setCache('allowed_ips', ServerRepository::getAllowedIPs(true));
FileCache::setCache('categories', CategoryService::getFromDatabase(null, true));
FileCache::setCache('settings', SettingsRepository::getAll(true));
FileCache::setCache('bouquets', BouquetService::getAll(true));
$rServers = ServerRepository::getAll(true);
unset($rServers['php_pids']);
FileCache::setCache('servers', $rServers);
FileCache::setCache('proxy_servers', BlocklistService::getProxyIPs(true));
FileCache::setCache('blocked_servers', BlocklistService::getBlockedServers(true));
FileCache::setCache('blocked_isp', BlocklistService::getBlockedISP(true));
FileCache::setCache('blocked_ua', BlocklistService::getBlockedUA(true));
FileCache::setCache('blocked_ips', BlocklistService::getBlockedIPs(true));
FileCache::setCache('allowed_ips', ServerRepository::getAllowedIPs(true));
FileCache::setCache('categories', CategoryService::getFromDatabase(null, true));
$rAllServers = ServerRepository::getAll();
if (!isset($rAllServers[SERVER_ID]) || !$rAllServers[SERVER_ID]['is_main']) {
return;
}
$rAllServers = ServerRepository::getAll();
if (!isset($rAllServers[SERVER_ID]) || !$rAllServers[SERVER_ID]['is_main']) {
return;
}
// Skip expensive heavy cache rebuild for 5 minutes; lightweight caches above still refresh every run.
$rHeavyMarker = CACHE_TMP_PATH . 'heavy_cache_built';
if (!$rStartup && file_exists($rHeavyMarker) && (time() - filemtime($rHeavyMarker)) < 300) {
return;
}
// Skip expensive heavy cache rebuild for 5 minutes; lightweight caches above still refresh every run.
$rHeavyMarker = CACHE_TMP_PATH . 'heavy_cache_built';
if (!$rStartup && file_exists($rHeavyMarker) && (time() - filemtime($rHeavyMarker)) < 300) {
return;
}
$rOutputFormats = array();
$db->query('SELECT `access_output_id`, `output_key` FROM `output_formats`;');
foreach ($db->get_rows() as $rRow) {
$rOutputFormats[] = $rRow;
}
$rCache->set('output_formats', $rOutputFormats);
$rOutputFormats = [];
$db->query('SELECT `access_output_id`, `output_key` FROM `output_formats`;');
foreach ($db->get_rows() as $rRow) {
$rOutputFormats[] = $rRow;
}
$rCache->set('output_formats', $rOutputFormats);
$rHMACKeys = array();
$db->query('SELECT `id`, `key` FROM `hmac_keys` WHERE `enabled` = 1;');
foreach ($db->get_rows() as $rRow) {
$rHMACKeys[] = $rRow;
}
$rCache->set('hmac_keys', $rHMACKeys);
$rHMACKeys = [];
$db->query('SELECT `id`, `key` FROM `hmac_keys` WHERE `enabled` = 1;');
foreach ($db->get_rows() as $rRow) {
$rHMACKeys[] = $rRow;
}
$rCache->set('hmac_keys', $rHMACKeys);
$rRTMPIPs = array();
$db->query('SELECT `ip`, `password`, `push`, `pull` FROM `rtmp_ips`');
foreach ($db->get_rows() as $rRow) {
$rRTMPIPs[gethostbyname($rRow['ip'])] = array('password' => $rRow['password'], 'push' => boolval($rRow['push']), 'pull' => boolval($rRow['pull']));
}
$rCache->set('rtmp_ips', $rRTMPIPs);
$rRTMPIPs = [];
$db->query('SELECT `ip`, `password`, `push`, `pull` FROM `rtmp_ips`');
foreach ($db->get_rows() as $rRow) {
$rRTMPIPs[gethostbyname($rRow['ip'])] = ['password' => $rRow['password'], 'push' => boolval($rRow['push']), 'pull' => boolval($rRow['pull'])];
}
$rCache->set('rtmp_ips', $rRTMPIPs);
if (file_exists(BIN_PATH . 'maxmind/cidr.db')) {
exec('ls ' . CIDR_TMP_PATH . ' | wc -l', $rOutput);
if (intval($rOutput[0]) == 0) {
$rDatabase = json_decode(file_get_contents(BIN_PATH . 'maxmind/cidr.db'), true);
foreach ($rDatabase as $rASN => $rData) {
file_put_contents(CIDR_TMP_PATH . $rASN, json_encode($rData));
}
}
}
if (file_exists(BIN_PATH . 'maxmind/cidr.db')) {
exec('ls ' . CIDR_TMP_PATH . ' | wc -l', $rOutput);
if (intval($rOutput[0]) == 0) {
$rDatabase = json_decode(file_get_contents(BIN_PATH . 'maxmind/cidr.db'), true);
foreach ($rDatabase as $rASN => $rData) {
file_put_contents(CIDR_TMP_PATH . $rASN, json_encode($rData));
}
}
}
$rChannelOrder = array();
if (SettingsManager::get('channel_number_type') == 'manual') {
$db->query('SELECT `id`, `order` FROM `streams` ORDER BY `order` ASC;');
foreach ($db->get_rows() as $rRow) {
$rChannelOrder[] = intval($rRow['id']);
}
}
$rChannelOrder = [];
if (SettingsManager::get('channel_number_type') == 'manual') {
$db->query('SELECT `id`, `order` FROM `streams` ORDER BY `order` ASC;');
foreach ($db->get_rows() as $rRow) {
$rChannelOrder[] = intval($rRow['id']);
}
}
$rCategoryMap = array();
$rBouquetMap = array();
$rStreamIDs = array('channels' => array(), 'radios' => array(), 'movies' => array(), 'episodes' => array(), 'series' => array());
$rCategoryMap = [];
$rBouquetMap = [];
$rStreamIDs = ['channels' => [], 'radios' => [], 'movies' => [], 'episodes' => [], 'series' => []];
$db->query('SELECT *, IF(`bouquet_order` > 0, `bouquet_order`, 999) AS `order` FROM `bouquets` ORDER BY `order` ASC;');
foreach ($db->get_rows(true, 'id') as $rID => $rChannels) {
$rAllowedCategories = array();
$db->query('SELECT *, IF(`bouquet_order` > 0, `bouquet_order`, 999) AS `order` FROM `bouquets` ORDER BY `order` ASC;');
foreach ($db->get_rows(true, 'id') as $rID => $rChannels) {
$rAllowedCategories = [];
foreach ((json_decode($rChannels['bouquet_channels'], true) ?: array()) as $rStreamID) {
if (!(0 >= intval($rStreamID) || in_array($rStreamID, $rStreamIDs['channels']))) {
$rStreamIDs['channels'][] = $rStreamID;
}
if (!isset($rBouquetMap[intval($rStreamID)])) {
$rBouquetMap[intval($rStreamID)] = array();
}
$rBouquetMap[intval($rStreamID)][] = $rID;
}
foreach ((json_decode($rChannels['bouquet_channels'], true) ?: []) as $rStreamID) {
if (!(0 >= intval($rStreamID) || in_array($rStreamID, $rStreamIDs['channels']))) {
$rStreamIDs['channels'][] = $rStreamID;
}
if (!isset($rBouquetMap[intval($rStreamID)])) {
$rBouquetMap[intval($rStreamID)] = [];
}
$rBouquetMap[intval($rStreamID)][] = $rID;
}
foreach ((json_decode($rChannels['bouquet_radios'], true) ?: array()) as $rStreamID) {
if (!(0 >= intval($rStreamID) || in_array($rStreamID, $rStreamIDs['radios']))) {
$rStreamIDs['radios'][] = $rStreamID;
}
if (!isset($rBouquetMap[intval($rStreamID)])) {
$rBouquetMap[intval($rStreamID)] = array();
}
$rBouquetMap[intval($rStreamID)][] = $rID;
}
foreach ((json_decode($rChannels['bouquet_radios'], true) ?: []) as $rStreamID) {
if (!(0 >= intval($rStreamID) || in_array($rStreamID, $rStreamIDs['radios']))) {
$rStreamIDs['radios'][] = $rStreamID;
}
if (!isset($rBouquetMap[intval($rStreamID)])) {
$rBouquetMap[intval($rStreamID)] = [];
}
$rBouquetMap[intval($rStreamID)][] = $rID;
}
foreach ((json_decode($rChannels['bouquet_movies'], true) ?: array()) as $rStreamID) {
if (!(0 >= intval($rStreamID) || in_array($rStreamID, $rStreamIDs['movies']))) {
$rStreamIDs['movies'][] = $rStreamID;
}
if (!isset($rBouquetMap[intval($rStreamID)])) {
$rBouquetMap[intval($rStreamID)] = array();
}
$rBouquetMap[intval($rStreamID)][] = $rID;
}
foreach ((json_decode($rChannels['bouquet_movies'], true) ?: []) as $rStreamID) {
if (!(0 >= intval($rStreamID) || in_array($rStreamID, $rStreamIDs['movies']))) {
$rStreamIDs['movies'][] = $rStreamID;
}
if (!isset($rBouquetMap[intval($rStreamID)])) {
$rBouquetMap[intval($rStreamID)] = [];
}
$rBouquetMap[intval($rStreamID)][] = $rID;
}
foreach ((json_decode($rChannels['bouquet_series'], true) ?: array()) as $rSeriesID) {
if (!(0 >= intval($rSeriesID) || in_array($rSeriesID, $rStreamIDs['series']))) {
$db->query('SELECT `stream_id` FROM `streams_episodes` WHERE `series_id` = ? ORDER BY `season_num` ASC, `episode_num` ASC;', $rSeriesID);
foreach ($db->get_rows() as $rEpisode) {
if (0 < intval($rEpisode['stream_id'])) {
$rStreamIDs['episodes'][] = $rEpisode['stream_id'];
}
if (!isset($rBouquetMap[intval($rEpisode['stream_id'])])) {
$rBouquetMap[intval($rEpisode['stream_id'])] = array();
}
$rBouquetMap[intval($rEpisode['stream_id'])][] = $rID;
}
}
}
foreach ((json_decode($rChannels['bouquet_series'], true) ?: []) as $rSeriesID) {
if (!(0 >= intval($rSeriesID) || in_array($rSeriesID, $rStreamIDs['series']))) {
$db->query('SELECT `stream_id` FROM `streams_episodes` WHERE `series_id` = ? ORDER BY `season_num` ASC, `episode_num` ASC;', $rSeriesID);
foreach ($db->get_rows() as $rEpisode) {
if (0 < intval($rEpisode['stream_id'])) {
$rStreamIDs['episodes'][] = $rEpisode['stream_id'];
}
if (!isset($rBouquetMap[intval($rEpisode['stream_id'])])) {
$rBouquetMap[intval($rEpisode['stream_id'])] = [];
}
$rBouquetMap[intval($rEpisode['stream_id'])][] = $rID;
}
}
}
$rAllChannels = array_map('intval', array_unique(array_merge((json_decode($rChannels['bouquet_channels'], true) ?: array()), (json_decode($rChannels['bouquet_radios'], true) ?: array()), (json_decode($rChannels['bouquet_movies'], true) ?: array()))));
$rAllSeries = array_map('intval', array_unique((json_decode($rChannels['bouquet_series'], true) ?: array())));
$rAllChannels = array_map('intval', array_unique(array_merge((json_decode($rChannels['bouquet_channels'], true) ?: []), (json_decode($rChannels['bouquet_radios'], true) ?: []), (json_decode($rChannels['bouquet_movies'], true) ?: []))));
$rAllSeries = array_map('intval', array_unique((json_decode($rChannels['bouquet_series'], true) ?: [])));
if (count($rAllChannels) > 0) {
$db->query('SELECT DISTINCT(`category_id`) AS `category_id` FROM `streams` WHERE `id` IN (' . implode(',', $rAllChannels) . ');');
foreach ($db->get_rows() as $rRow) {
$rAllowedCategories = array_merge($rAllowedCategories, (json_decode($rRow['category_id'], true) ?: array()));
}
}
if (count($rAllChannels) > 0) {
$db->query('SELECT DISTINCT(`category_id`) AS `category_id` FROM `streams` WHERE `id` IN (' . implode(',', $rAllChannels) . ');');
foreach ($db->get_rows() as $rRow) {
$rAllowedCategories = array_merge($rAllowedCategories, (json_decode($rRow['category_id'], true) ?: []));
}
}
if (count($rAllSeries) > 0) {
$db->query('SELECT DISTINCT(`category_id`) AS `category_id` FROM `streams_series` WHERE `id` IN (' . implode(',', $rAllSeries) . ');');
foreach ($db->get_rows() as $rRow) {
$rAllowedCategories = array_merge($rAllowedCategories, (json_decode($rRow['category_id'], true) ?: array()));
}
}
if (count($rAllSeries) > 0) {
$db->query('SELECT DISTINCT(`category_id`) AS `category_id` FROM `streams_series` WHERE `id` IN (' . implode(',', $rAllSeries) . ');');
foreach ($db->get_rows() as $rRow) {
$rAllowedCategories = array_merge($rAllowedCategories, (json_decode($rRow['category_id'], true) ?: []));
}
}
$rCategoryMap[$rID] = array_unique($rAllowedCategories);
}
$rCategoryMap[$rID] = array_unique($rAllowedCategories);
}
if (SettingsManager::get('channel_number_type') != 'manual') {
foreach (array('channels', 'radios', 'movies', 'episodes') as $rKey) {
if (0 < count($rStreamIDs[$rKey])) {
$rWhere = 'AND `id` NOT IN (' . implode(',', array_map('intval', $rStreamIDs[$rKey])) . ')';
} else {
$rWhere = '';
}
switch ($rKey) {
case 'channels': $rType = array(1, 3); break;
case 'radios': $rType = array(4); break;
case 'movies': $rType = array(2); break;
case 'episodes': $rType = array(5); break;
}
if (count($rType) > 0) {
$db->query('SELECT `id` FROM `streams` WHERE `type` IN (' . implode(',', $rType) . ') ' . $rWhere . ' ORDER BY `order` ASC;');
foreach ($db->get_rows() as $rRow) {
$rStreamIDs[$rKey][] = $rRow['id'];
}
}
}
if (SettingsManager::get('channel_number_type') != 'manual') {
foreach (['channels', 'radios', 'movies', 'episodes'] as $rKey) {
if (0 < count($rStreamIDs[$rKey])) {
$rWhere = 'AND `id` NOT IN (' . implode(',', array_map('intval', $rStreamIDs[$rKey])) . ')';
} else {
$rWhere = '';
}
switch ($rKey) {
case 'channels':
$rType = [1, 3];
break;
case 'radios':
$rType = [4];
break;
case 'movies':
$rType = [2];
break;
case 'episodes':
$rType = [5];
break;
}
if (count($rType) > 0) {
$db->query('SELECT `id` FROM `streams` WHERE `type` IN (' . implode(',', $rType) . ') ' . $rWhere . ' ORDER BY `order` ASC;');
foreach ($db->get_rows() as $rRow) {
$rStreamIDs[$rKey][] = $rRow['id'];
}
}
}
if (SettingsManager::get('vod_sort_newest')) {
$rStreamIDs['movies'] = array();
$rStreamIDs['episodes'] = array();
$db->query('SELECT `type`, `id` FROM `streams` WHERE `type` IN (2,5) ORDER BY `added` DESC, `id` DESC;');
foreach ($db->get_rows() as $rRow) {
$rStreamIDs[array(2 => 'movies', 5 => 'episodes')[$rRow['type']]][] = $rRow['id'];
}
$rSeriesOrder = array();
$db->query('SELECT `id`, (SELECT MAX(`streams`.`added`) FROM `streams_episodes` LEFT JOIN `streams` ON `streams`.`id` = `streams_episodes`.`stream_id` WHERE `streams_episodes`.`series_id` = `streams_series`.`id`) AS `last_modified_stream` FROM `streams_series` ORDER BY `last_modified_stream` DESC, `last_modified` DESC, `id` DESC;');
foreach ($db->get_rows() as $rRow) {
$rSeriesOrder[] = intval($rRow['id']);
}
$rCache->set('series_order', $rSeriesOrder);
}
if (SettingsManager::get('vod_sort_newest')) {
$rStreamIDs['movies'] = [];
$rStreamIDs['episodes'] = [];
$db->query('SELECT `type`, `id` FROM `streams` WHERE `type` IN (2,5) ORDER BY `added` DESC, `id` DESC;');
foreach ($db->get_rows() as $rRow) {
$rStreamIDs[[2 => 'movies', 5 => 'episodes'][$rRow['type']]][] = $rRow['id'];
}
$rSeriesOrder = [];
$db->query('SELECT `id`, (SELECT MAX(`streams`.`added`) FROM `streams_episodes` LEFT JOIN `streams` ON `streams`.`id` = `streams_episodes`.`stream_id` WHERE `streams_episodes`.`series_id` = `streams_series`.`id`) AS `last_modified_stream` FROM `streams_series` ORDER BY `last_modified_stream` DESC, `last_modified` DESC, `id` DESC;');
foreach ($db->get_rows() as $rRow) {
$rSeriesOrder[] = intval($rRow['id']);
}
$rCache->set('series_order', $rSeriesOrder);
}
foreach (array('channels', 'radios', 'movies', 'episodes') as $rKey) {
foreach ($rStreamIDs[$rKey] as $rStreamID) {
$rChannelOrder[] = intval($rStreamID);
}
}
$rChannelOrder = array_unique($rChannelOrder);
}
foreach (['channels', 'radios', 'movies', 'episodes'] as $rKey) {
foreach ($rStreamIDs[$rKey] as $rStreamID) {
$rChannelOrder[] = intval($rStreamID);
}
}
$rChannelOrder = array_unique($rChannelOrder);
}
$rCategoryChannels = array();
$db->query('SELECT `id`, `category_id` FROM `streams`;');
if ($db->dbh && $db->result) {
if ($db->result->rowCount() > 0) {
foreach ($db->result->fetchAll(\PDO::FETCH_ASSOC) as $rStreamInfo) {
$rCategoryChannels[$rStreamInfo['id']] = json_decode($rStreamInfo['category_id'] ?? '[]', true);
}
}
}
$rCategoryChannels = [];
$db->query('SELECT `id`, `category_id` FROM `streams`;');
if ($db->dbh && $db->result) {
if ($db->result->rowCount() > 0) {
foreach ($db->result->fetchAll(\PDO::FETCH_ASSOC) as $rStreamInfo) {
$rCategoryChannels[$rStreamInfo['id']] = json_decode($rStreamInfo['category_id'] ?? '[]', true);
}
}
}
$rResellerDomains = array();
$db->query('SELECT `reseller_dns` FROM `users` WHERE `status` = 1 AND `reseller_dns` IS NOT NULL;');
foreach ($db->get_rows() as $rRow) {
$rResellerDomains[] = strtolower($rRow['reseller_dns']);
}
$rResellerDomains = [];
$db->query('SELECT `reseller_dns` FROM `users` WHERE `status` = 1 AND `reseller_dns` IS NOT NULL;');
foreach ($db->get_rows() as $rRow) {
$rResellerDomains[] = strtolower($rRow['reseller_dns']);
}
$rCache->set('reseller_domains', $rResellerDomains);
$rCache->set('channel_order', $rChannelOrder);
$rCache->set('bouquet_map', $rBouquetMap);
$rCache->set('category_map', $rCategoryMap);
(new FileCache(STREAMS_TMP_PATH))->set('channels_categories', $rCategoryChannels);
@touch($rHeavyMarker);
}
$rCache->set('reseller_domains', $rResellerDomains);
$rCache->set('channel_order', $rChannelOrder);
$rCache->set('bouquet_map', $rBouquetMap);
$rCache->set('category_map', $rCategoryMap);
(new FileCache(STREAMS_TMP_PATH))->set('channels_categories', $rCategoryChannels);
@touch($rHeavyMarker);
}
}
File diff suppressed because it is too large Load Diff
+69 -69
View File
@@ -19,80 +19,80 @@ use XcVm\Infrastructure\Database\DatabaseAware;
*/
class CertbotCronJob implements CommandInterface {
use DatabaseAware;
use CronTrait;
use DatabaseAware;
use CronTrait;
public function getName(): string {
return 'cron:certbot';
}
public function getName(): string {
return 'cron:certbot';
}
public function getDescription(): string {
return 'Cron: check/renew SSL certificates via certbot';
}
public function getDescription(): string {
return 'Cron: check/renew SSL certificates via certbot';
}
public function execute(array $rArgs): int {
$this->registerShutdown();
$rCheck = !empty($rArgs[0]);
$this->loadCron($rCheck);
return 0;
}
public function execute(array $rArgs): int {
$this->registerShutdown();
$rCheck = !empty($rArgs[0]);
$this->loadCron($rCheck);
return 0;
}
private function loadCron(bool $rCheck): void {
$db = self::db();
$rCertInfo = null;
private function loadCron(bool $rCheck): void {
$db = self::db();
$rCertInfo = null;
if (!$rCheck) {
if (!PHP_ERRORS) {
DiagnosticsService::submitPanelLogs();
}
$rCertInfo = DiagnosticsService::getCertificateInfo();
if (ServerRepository::getAll()[SERVER_ID]['enable_https'] && $rCertInfo) {
if ($rCertInfo['expiration'] - time() < 604800) {
echo 'Certificate due for renewal.' . "\n";
$rData = array('action' => 'certbot_generate', 'domain' => array());
foreach (explode(',', ServerRepository::getAll()[SERVER_ID]['domain_name']) as $rDomain) {
if (!filter_var($rDomain, FILTER_VALIDATE_IP)) {
$rData['domain'][] = $rDomain;
}
}
if (count($rData['domain']) > 0) {
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', SERVER_ID, time(), json_encode($rData));
}
} else {
echo 'Certificate valid, not due for renewal.' . "\n";
}
}
}
if (!$rCheck) {
if (!PHP_ERRORS) {
DiagnosticsService::submitPanelLogs();
}
$rCertInfo = DiagnosticsService::getCertificateInfo();
if (ServerRepository::getAll()[SERVER_ID]['enable_https'] && $rCertInfo) {
if ($rCertInfo['expiration'] - time() < 604800) {
echo 'Certificate due for renewal.' . "\n";
$rData = ['action' => 'certbot_generate', 'domain' => []];
foreach (explode(',', ServerRepository::getAll()[SERVER_ID]['domain_name']) as $rDomain) {
if (!filter_var($rDomain, FILTER_VALIDATE_IP)) {
$rData['domain'][] = $rDomain;
}
}
if (count($rData['domain']) > 0) {
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', SERVER_ID, time(), json_encode($rData));
}
} else {
echo 'Certificate valid, not due for renewal.' . "\n";
}
}
}
$db->query('SELECT `certbot_ssl` FROM `servers` WHERE `id` = ?;', SERVER_ID);
$rDBCertInfo = json_decode($db->get_row()['certbot_ssl'], true);
$rLines = explode("\n", file_get_contents(MAIN_HOME . 'bin/nginx/conf/ssl.conf'));
$db->query('SELECT `certbot_ssl` FROM `servers` WHERE `id` = ?;', SERVER_ID);
$rDBCertInfo = json_decode($db->get_row()['certbot_ssl'], true);
$rLines = explode("\n", file_get_contents(MAIN_HOME . 'bin/nginx/conf/ssl.conf'));
foreach ($rLines as $rLine) {
if (explode(' ', $rLine)[0] == 'ssl_certificate') {
list($rCertificate) = explode(';', explode(' ', $rLine)[1]);
if ($rCertificate != 'server.crt') {
$rCertInfoFile = DiagnosticsService::getCertificateInfo($rCertificate);
if ($rCertInfoFile && ($rCertInfo === null || $rCertInfo['serial'] != $rCertInfoFile['serial'] || !ServerRepository::getAll()[SERVER_ID]['certbot_ssl'] || $rDBCertInfo['serial'] != $rCertInfoFile['serial'])) {
$db->query('UPDATE `servers` SET `certbot_ssl` = ? WHERE `id` = ?;', json_encode($rCertInfoFile), SERVER_ID);
echo 'Updated ssl configuration in database' . "\n";
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', SERVER_ID, time(), json_encode(array('action' => 'reload_nginx')));
}
} else {
if (ServerRepository::getAll()[SERVER_ID]['certbot_ssl']) {
$rCertInfo = json_decode(ServerRepository::getAll()[SERVER_ID]['certbot_ssl'], true);
if (file_exists($rCertInfo['path'] . '/fullchain.pem')) {
$rCertificate = $rCertInfo['path'] . '/fullchain.pem';
$rChain = $rCertInfo['path'] . '/chain.pem';
$rPrivateKey = $rCertInfo['path'] . '/privkey.pem';
$rSSLConfig = 'ssl_certificate ' . $rCertificate . ';' . "\n" . 'ssl_certificate_key ' . $rPrivateKey . ';' . "\n" . 'ssl_trusted_certificate ' . $rChain . ';' . "\n" . 'ssl_protocols TLSv1.2 TLSv1.3;' . "\n" . 'ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;' . "\n" . 'ssl_prefer_server_ciphers off;' . "\n" . 'ssl_ecdh_curve auto;' . "\n" . 'ssl_session_timeout 10m;' . "\n" . 'ssl_session_cache shared:MozSSL:10m;' . "\n" . 'ssl_session_tickets off;';
file_put_contents(BIN_PATH . 'nginx/conf/ssl.conf', $rSSLConfig);
echo 'Fixed ssl configuration file' . "\n";
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', SERVER_ID, time(), json_encode(array('action' => 'reload_nginx')));
}
}
}
}
}
}
foreach ($rLines as $rLine) {
if (explode(' ', $rLine)[0] == 'ssl_certificate') {
list($rCertificate) = explode(';', explode(' ', $rLine)[1]);
if ($rCertificate != 'server.crt') {
$rCertInfoFile = DiagnosticsService::getCertificateInfo($rCertificate);
if ($rCertInfoFile && ($rCertInfo === null || $rCertInfo['serial'] != $rCertInfoFile['serial'] || !ServerRepository::getAll()[SERVER_ID]['certbot_ssl'] || $rDBCertInfo['serial'] != $rCertInfoFile['serial'])) {
$db->query('UPDATE `servers` SET `certbot_ssl` = ? WHERE `id` = ?;', json_encode($rCertInfoFile), SERVER_ID);
echo 'Updated ssl configuration in database' . "\n";
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', SERVER_ID, time(), json_encode(['action' => 'reload_nginx']));
}
} else {
if (ServerRepository::getAll()[SERVER_ID]['certbot_ssl']) {
$rCertInfo = json_decode(ServerRepository::getAll()[SERVER_ID]['certbot_ssl'], true);
if (file_exists($rCertInfo['path'] . '/fullchain.pem')) {
$rCertificate = $rCertInfo['path'] . '/fullchain.pem';
$rChain = $rCertInfo['path'] . '/chain.pem';
$rPrivateKey = $rCertInfo['path'] . '/privkey.pem';
$rSSLConfig = 'ssl_certificate ' . $rCertificate . ';' . "\n" . 'ssl_certificate_key ' . $rPrivateKey . ';' . "\n" . 'ssl_trusted_certificate ' . $rChain . ';' . "\n" . 'ssl_protocols TLSv1.2 TLSv1.3;' . "\n" . 'ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;' . "\n" . 'ssl_prefer_server_ciphers off;' . "\n" . 'ssl_ecdh_curve auto;' . "\n" . 'ssl_session_timeout 10m;' . "\n" . 'ssl_session_cache shared:MozSSL:10m;' . "\n" . 'ssl_session_tickets off;';
file_put_contents(BIN_PATH . 'nginx/conf/ssl.conf', $rSSLConfig);
echo 'Fixed ssl configuration file' . "\n";
$db->query('INSERT INTO `signals`(`server_id`, `time`, `custom_data`) VALUES(?, ?, ?);', SERVER_ID, time(), json_encode(['action' => 'reload_nginx']));
}
}
}
}
}
}
}
+181 -181
View File
@@ -22,196 +22,196 @@ use XcVm\Streaming\Codec\FFprobeRunner;
*/
class CleanupCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:cleanup';
}
public function getName(): string {
return 'cron:cleanup';
}
public function getDescription(): string {
return 'Cron: cleanup streams, archives, VOD, rotate tables';
}
public function getDescription(): string {
return 'Cron: cleanup streams, archives, VOD, rotate tables';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Cleanup]');
$this->initCron('XC_VM[Cleanup]');
$rTimeout = 3600;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
$rTimeout = 3600;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
$this->loadCron();
$this->loadCron();
return 0;
}
return 0;
}
private function loadCron(): void {
global $db;
private function loadCron(): void {
global $db;
if (intval(SettingsManager::get('cleanup')) == 1) {
$rStreams = array();
$db->query('SELECT `id` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams`.`type` IN (1,3,4) AND `streams_servers`.`server_id` = ?;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rStreams[] = intval($rRow['id']);
}
foreach (glob(STREAMS_PATH . '*') as $rFilename) {
$rID = intval(rtrim(explode('.', basename($rFilename))[0], '_')) . "\n";
if (0 < $rID && !in_array($rID, $rStreams)) {
echo 'Deleting: ' . $rFilename . "\n";
unlink($rFilename);
}
}
$rArchive = array();
$db->query('SELECT `id`, `tv_archive_duration` FROM `streams` WHERE `type` = 1 AND `tv_archive_server_id` = ? AND `tv_archive_duration` > 0;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rArchive[intval($rRow['id'])] = $rRow['tv_archive_duration'];
}
date_default_timezone_set('UTC');
foreach (glob(ARCHIVE_PATH . '*') as $rStreamID) {
$rID = intval(basename($rStreamID));
if (0 < $rID && is_dir(ARCHIVE_PATH . $rID)) {
if (!isset($rArchive[$rID])) {
echo 'Deleting: ' . $rStreamID . "\n";
exec('rm -rf ' . $rStreamID);
} else {
$rDuration = $rArchive[$rID];
$rDeleteBefore = time() - $rDuration * 86400 + 3600;
foreach (glob(ARCHIVE_PATH . $rID . '/*') as $rArchiveFile) {
list($rDate, $rTime) = explode(':', explode('.', basename($rArchiveFile))[0]);
list($rHour, $rMinute) = explode('-', $rTime);
$rFileTime = strtotime($rDate . ' ' . $rHour . ':' . $rMinute . ':00');
if ($rFileTime < $rDeleteBefore) {
echo 'Deleting: ' . $rArchiveFile . "\n";
unlink($rArchiveFile);
}
}
}
}
}
$rCreated = array();
$db->query('SELECT `id` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams`.`type` = 3 AND `streams_servers`.`server_id` = ?;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rCreated[] = intval($rRow['id']);
}
foreach (glob(CREATED_PATH . '*') as $rFilename) {
$rID = intval(rtrim(explode('.', basename($rFilename))[0], '_')) . "\n";
if (0 < $rID && !in_array($rID, $rCreated)) {
echo 'Deleting: ' . $rFilename . "\n";
unlink($rFilename);
}
}
}
if (intval(SettingsManager::get('cleanup')) == 1) {
$rStreams = [];
$db->query('SELECT `id` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams`.`type` IN (1,3,4) AND `streams_servers`.`server_id` = ?;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rStreams[] = intval($rRow['id']);
}
foreach (glob(STREAMS_PATH . '*') as $rFilename) {
$rID = intval(rtrim(explode('.', basename($rFilename))[0], '_')) . "\n";
if (0 < $rID && !in_array($rID, $rStreams)) {
echo 'Deleting: ' . $rFilename . "\n";
unlink($rFilename);
}
}
$rArchive = [];
$db->query('SELECT `id`, `tv_archive_duration` FROM `streams` WHERE `type` = 1 AND `tv_archive_server_id` = ? AND `tv_archive_duration` > 0;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rArchive[intval($rRow['id'])] = $rRow['tv_archive_duration'];
}
date_default_timezone_set('UTC');
foreach (glob(ARCHIVE_PATH . '*') as $rStreamID) {
$rID = intval(basename($rStreamID));
if (0 < $rID && is_dir(ARCHIVE_PATH . $rID)) {
if (!isset($rArchive[$rID])) {
echo 'Deleting: ' . $rStreamID . "\n";
exec('rm -rf ' . $rStreamID);
} else {
$rDuration = $rArchive[$rID];
$rDeleteBefore = time() - $rDuration * 86400 + 3600;
foreach (glob(ARCHIVE_PATH . $rID . '/*') as $rArchiveFile) {
list($rDate, $rTime) = explode(':', explode('.', basename($rArchiveFile))[0]);
list($rHour, $rMinute) = explode('-', $rTime);
$rFileTime = strtotime($rDate . ' ' . $rHour . ':' . $rMinute . ':00');
if ($rFileTime < $rDeleteBefore) {
echo 'Deleting: ' . $rArchiveFile . "\n";
unlink($rArchiveFile);
}
}
}
}
}
$rCreated = [];
$db->query('SELECT `id` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams`.`type` = 3 AND `streams_servers`.`server_id` = ?;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rCreated[] = intval($rRow['id']);
}
foreach (glob(CREATED_PATH . '*') as $rFilename) {
$rID = intval(rtrim(explode('.', basename($rFilename))[0], '_')) . "\n";
if (0 < $rID && !in_array($rID, $rCreated)) {
echo 'Deleting: ' . $rFilename . "\n";
unlink($rFilename);
}
}
}
if (intval(SettingsManager::get('check_vod')) == 1) {
$db->query('SELECT `server_stream_id`, `id`, `target_container`, `movie_properties`, `stream_status` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `server_id` = ? AND `type` IN (2,5) AND `streams`.`direct_source` = 0 AND `streams_servers`.`pid` > 0;', SERVER_ID);
if ($db->num_rows() > 0) {
$rRows = $db->get_rows();
foreach ($rRows as $rRow) {
$rMoviePath = VOD_PATH . $rRow['id'] . '.' . $rRow['target_container'];
if ($rRow['stream_status'] == 0) {
if (!file_exists($rMoviePath)) {
echo 'BAD MOVIE' . "\n";
$db->query('UPDATE `streams_servers` SET `stream_status` = 1 WHERE `server_stream_id` = ?', $rRow['server_stream_id']);
StreamProcess::updateStream($rRow['id']);
}
} elseif ($rRow['stream_status'] == 1) {
if (file_exists($rMoviePath) && ($rFFProbee = FFprobeRunner::probeStream($rMoviePath))) {
$rDuration = (isset($rFFProbee['duration']) ? $rFFProbee['duration'] : 0);
sscanf($rDuration, '%d:%d:%d', $rHours, $rMinutes, $rSeconds);
$rSeconds = (isset($rSeconds) ? $rHours * 3600 + $rMinutes * 60 + $rSeconds : $rHours * 60 + $rMinutes);
$rSize = filesize($rMoviePath);
$rBitrate = round(($rSize * 0.008) / $rSeconds);
$rMovieProperties = json_decode($rRow['movie_properties'], true);
if (!is_array($rMovieProperties)) {
$rMovieProperties = array();
}
if (!(isset($rMovieProperties['duration_secs']) && $rSeconds == $rMovieProperties['duration_secs'])) {
$rMovieProperties['duration_secs'] = $rSeconds;
$rMovieProperties['duration'] = $rDuration;
}
if (!(isset($rMovieProperties['video']) && $rFFProbee['codecs']['video']['codec_name'] == $rMovieProperties['video'])) {
$rMovieProperties['video'] = $rFFProbee['codecs']['video'];
}
if (!(isset($rMovieProperties['audio']) && $rFFProbee['codecs']['audio']['codec_name'] == $rMovieProperties['audio'])) {
$rMovieProperties['audio'] = $rFFProbee['codecs']['audio'];
}
if (SettingsManager::get('extract_subtitles')) {
if (!(isset($rMovieProperties['subtitle']) && $rFFProbee['codecs']['subtitle']['codec_name'] == $rMovieProperties['subtitle'])) {
$rMovieProperties['subtitle'] = $rFFProbee['codecs']['subtitle'];
}
}
if (!(isset($rMovieProperties['bitrate']) && $rBitrate == $rMovieProperties['bitrate'])) {
if (0 < $rBitrate) {
$rMovieProperties['bitrate'] = $rBitrate;
} else {
$rBitrate = $rMovieProperties['bitrate'];
}
}
if (isset($rFFProbee['codecs']['subtitle']) && SettingsManager::get('extract_subtitles')) {
$i = 0;
foreach ($rFFProbee['codecs']['subtitle'] as $rSubtitle) {
FFmpegCommand::extractSubtitle($rRow['stream_id'], $rMoviePath, $i);
$i++;
}
}
$rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbee, SettingsManager::get('player_allow_hevc')));
$rAudioCodec = ($rFFProbee['codecs']['audio']['codec_name'] ?: null);
$rVideoCodec = ($rFFProbee['codecs']['video']['codec_name'] ?: null);
$rResolution = ($rFFProbee['codecs']['video']['height'] ?: null);
if ($rResolution) {
$rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution);
}
$db->query('UPDATE `streams` SET `movie_properties` = ? WHERE `id` = ?', json_encode($rMovieProperties, JSON_UNESCAPED_UNICODE), $rRow['id']);
$db->query('UPDATE `streams_servers` SET `bitrate` = ?,`to_analyze` = 0,`stream_status` = 0,`stream_info` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ?, `compatible` = ? WHERE `server_stream_id` = ?', $rBitrate, json_encode($rFFProbee, JSON_UNESCAPED_UNICODE), $rAudioCodec, $rVideoCodec, $rResolution, $rCompatible, $rRow['server_stream_id']);
StreamProcess::updateStream($rRow['id']);
echo 'VALID MOVIE' . "\n";
}
}
}
}
$db->query("SELECT `id`, `stream_display_name`, `server_stream_id` FROM `streams` t1 INNER JOIN `streams_servers` t3 ON t3.stream_id = t1.id LEFT JOIN `profiles` t2 ON t2.profile_id = t1.transcode_profile_id WHERE t1.type = 3 AND t3.server_id = ? AND JSON_CONTAINS(t3.cchannel_rsources, t1.stream_source) AND JSON_CONTAINS(t1.stream_source, t3.cchannel_rsources) AND t3.pids_create_channel = '[]';", SERVER_ID);
if ($db->num_rows() > 0) {
$rStreams = $db->get_rows();
foreach ($rStreams as $rStream) {
echo "\n\n" . '[*] Checking Channel ' . $rStream['stream_display_name'] . "\n";
if (file_exists(CREATED_PATH . $rStream['id'] . '_.list')) {
$rList = explode("\n", file_get_contents(CREATED_PATH . $rStream['id'] . '_.list'));
$rExisting = glob(CREATED_PATH . $rStream['id'] . '*.*');
$rFailure = false;
$rActualFiles = array();
foreach ($rList as $rItem) {
$rFilename = trim(explode("'", explode("'", $rItem)[1])[0]);
if (0 < strlen($rFilename)) {
if (in_array($rFilename, $rExisting)) {
$rActualFiles[] = $rFilename;
} else {
$rFailure = true;
}
}
}
if ($rFailure) {
echo 'BAD CHANNEL' . "\n";
$db->query('UPDATE `streams_servers` SET `cchannel_rsources` = ? WHERE `server_stream_id` = ?;', json_encode($rActualFiles, JSON_UNESCAPED_UNICODE), $rStream['server_stream_id']);
StreamProcess::updateStream($rStream['id']);
}
} else {
echo 'BAD CHANNEL' . "\n";
$db->query("UPDATE `streams_servers` SET `cchannel_rsources` = '[]' WHERE `server_stream_id` = ?;", $rStream['server_stream_id']);
StreamProcess::updateStream($rStream['id']);
}
}
}
}
if (intval(SettingsManager::get('check_vod')) == 1) {
$db->query('SELECT `server_stream_id`, `id`, `target_container`, `movie_properties`, `stream_status` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `server_id` = ? AND `type` IN (2,5) AND `streams`.`direct_source` = 0 AND `streams_servers`.`pid` > 0;', SERVER_ID);
if ($db->num_rows() > 0) {
$rRows = $db->get_rows();
foreach ($rRows as $rRow) {
$rMoviePath = VOD_PATH . $rRow['id'] . '.' . $rRow['target_container'];
if ($rRow['stream_status'] == 0) {
if (!file_exists($rMoviePath)) {
echo 'BAD MOVIE' . "\n";
$db->query('UPDATE `streams_servers` SET `stream_status` = 1 WHERE `server_stream_id` = ?', $rRow['server_stream_id']);
StreamProcess::updateStream($rRow['id']);
}
} elseif ($rRow['stream_status'] == 1) {
if (file_exists($rMoviePath) && ($rFFProbee = FFprobeRunner::probeStream($rMoviePath))) {
$rDuration = (isset($rFFProbee['duration']) ? $rFFProbee['duration'] : 0);
sscanf($rDuration, '%d:%d:%d', $rHours, $rMinutes, $rSeconds);
$rSeconds = (isset($rSeconds) ? $rHours * 3600 + $rMinutes * 60 + $rSeconds : $rHours * 60 + $rMinutes);
$rSize = filesize($rMoviePath);
$rBitrate = round(($rSize * 0.008) / $rSeconds);
$rMovieProperties = json_decode($rRow['movie_properties'], true);
if (!is_array($rMovieProperties)) {
$rMovieProperties = [];
}
if (!(isset($rMovieProperties['duration_secs']) && $rSeconds == $rMovieProperties['duration_secs'])) {
$rMovieProperties['duration_secs'] = $rSeconds;
$rMovieProperties['duration'] = $rDuration;
}
if (!(isset($rMovieProperties['video']) && $rFFProbee['codecs']['video']['codec_name'] == $rMovieProperties['video'])) {
$rMovieProperties['video'] = $rFFProbee['codecs']['video'];
}
if (!(isset($rMovieProperties['audio']) && $rFFProbee['codecs']['audio']['codec_name'] == $rMovieProperties['audio'])) {
$rMovieProperties['audio'] = $rFFProbee['codecs']['audio'];
}
if (SettingsManager::get('extract_subtitles')) {
if (!(isset($rMovieProperties['subtitle']) && $rFFProbee['codecs']['subtitle']['codec_name'] == $rMovieProperties['subtitle'])) {
$rMovieProperties['subtitle'] = $rFFProbee['codecs']['subtitle'];
}
}
if (!(isset($rMovieProperties['bitrate']) && $rBitrate == $rMovieProperties['bitrate'])) {
if (0 < $rBitrate) {
$rMovieProperties['bitrate'] = $rBitrate;
} else {
$rBitrate = $rMovieProperties['bitrate'];
}
}
if (isset($rFFProbee['codecs']['subtitle']) && SettingsManager::get('extract_subtitles')) {
$i = 0;
foreach ($rFFProbee['codecs']['subtitle'] as $rSubtitle) {
FFmpegCommand::extractSubtitle($rRow['stream_id'], $rMoviePath, $i);
$i++;
}
}
$rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbee, SettingsManager::get('player_allow_hevc')));
$rAudioCodec = ($rFFProbee['codecs']['audio']['codec_name'] ?: null);
$rVideoCodec = ($rFFProbee['codecs']['video']['codec_name'] ?: null);
$rResolution = ($rFFProbee['codecs']['video']['height'] ?: null);
if ($rResolution) {
$rResolution = StreamSorter::getNearest([240, 360, 480, 576, 720, 1080, 1440, 2160], $rResolution);
}
$db->query('UPDATE `streams` SET `movie_properties` = ? WHERE `id` = ?', json_encode($rMovieProperties, JSON_UNESCAPED_UNICODE), $rRow['id']);
$db->query('UPDATE `streams_servers` SET `bitrate` = ?,`to_analyze` = 0,`stream_status` = 0,`stream_info` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ?, `compatible` = ? WHERE `server_stream_id` = ?', $rBitrate, json_encode($rFFProbee, JSON_UNESCAPED_UNICODE), $rAudioCodec, $rVideoCodec, $rResolution, $rCompatible, $rRow['server_stream_id']);
StreamProcess::updateStream($rRow['id']);
echo 'VALID MOVIE' . "\n";
}
}
}
}
$db->query("SELECT `id`, `stream_display_name`, `server_stream_id` FROM `streams` t1 INNER JOIN `streams_servers` t3 ON t3.stream_id = t1.id LEFT JOIN `profiles` t2 ON t2.profile_id = t1.transcode_profile_id WHERE t1.type = 3 AND t3.server_id = ? AND JSON_CONTAINS(t3.cchannel_rsources, t1.stream_source) AND JSON_CONTAINS(t1.stream_source, t3.cchannel_rsources) AND t3.pids_create_channel = '[]';", SERVER_ID);
if ($db->num_rows() > 0) {
$rStreams = $db->get_rows();
foreach ($rStreams as $rStream) {
echo "\n\n" . '[*] Checking Channel ' . $rStream['stream_display_name'] . "\n";
if (file_exists(CREATED_PATH . $rStream['id'] . '_.list')) {
$rList = explode("\n", file_get_contents(CREATED_PATH . $rStream['id'] . '_.list'));
$rExisting = glob(CREATED_PATH . $rStream['id'] . '*.*');
$rFailure = false;
$rActualFiles = [];
foreach ($rList as $rItem) {
$rFilename = trim(explode("'", explode("'", $rItem)[1])[0]);
if (0 < strlen($rFilename)) {
if (in_array($rFilename, $rExisting)) {
$rActualFiles[] = $rFilename;
} else {
$rFailure = true;
}
}
}
if ($rFailure) {
echo 'BAD CHANNEL' . "\n";
$db->query('UPDATE `streams_servers` SET `cchannel_rsources` = ? WHERE `server_stream_id` = ?;', json_encode($rActualFiles, JSON_UNESCAPED_UNICODE), $rStream['server_stream_id']);
StreamProcess::updateStream($rStream['id']);
}
} else {
echo 'BAD CHANNEL' . "\n";
$db->query("UPDATE `streams_servers` SET `cchannel_rsources` = '[]' WHERE `server_stream_id` = ?;", $rStream['server_stream_id']);
StreamProcess::updateStream($rStream['id']);
}
}
}
}
$rTables = array('lines_activity' => array('keep_activity', 'date_end'), 'lines_logs' => array('keep_client', 'date'), 'login_logs' => array('keep_login', 'date'), 'streams_errors' => array('keep_errors', 'date'), 'streams_logs' => array('keep_restarts', 'date'), 'ondemand_check' => array('on_demand_scan_keep', 'date'));
foreach ($rTables as $rTable => $rArray) {
if (SettingsManager::getAll()[$rArray[0]] && 0 < SettingsManager::getAll()[$rArray[0]]) {
$rDeleteBefore = time() - intval(SettingsManager::getAll()[$rArray[0]]);
$db->query('DELETE FROM `' . $rTable . '` WHERE `' . $rArray[1] . '` < ?;', $rDeleteBefore);
}
}
}
$rTables = ['lines_activity' => ['keep_activity', 'date_end'], 'lines_logs' => ['keep_client', 'date'], 'login_logs' => ['keep_login', 'date'], 'streams_errors' => ['keep_errors', 'date'], 'streams_logs' => ['keep_restarts', 'date'], 'ondemand_check' => ['on_demand_scan_keep', 'date']];
foreach ($rTables as $rTable => $rArray) {
if (SettingsManager::getAll()[$rArray[0]] && 0 < SettingsManager::getAll()[$rArray[0]]) {
$rDeleteBefore = time() - intval(SettingsManager::getAll()[$rArray[0]]);
$db->query('DELETE FROM `' . $rTable . '` WHERE `' . $rArray[1] . '` < ?;', $rDeleteBefore);
}
}
}
}
+276 -270
View File
@@ -19,348 +19,354 @@ use XcVm\Domain\Epg\EPG;
*/
class EpgCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:epg';
}
public function getName(): string {
return 'cron:epg';
}
public function getDescription(): string {
return 'Cron: import EPG data, generate XMLTV, per-stream cache';
}
public function getDescription(): string {
return 'Cron: import EPG data, generate XMLTV, per-stream cache';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$rEPGID = null;
if (!empty($rArgs[0])) {
$rEPGID = intval($rArgs[0]);
}
$rEPGID = null;
if (!empty($rArgs[0])) {
$rEPGID = intval($rArgs[0]);
}
$this->printLog("=== XC_VM[EPG] Process started ===");
$this->printLog("Mode: " . ($rEPGID ? "Single EPG ID: $rEPGID" : "Full update"));
$this->printLog("=== XC_VM[EPG] Process started ===");
$this->printLog("Mode: " . ($rEPGID ? "Single EPG ID: $rEPGID" : "Full update"));
set_time_limit(0);
ini_set('memory_limit', -1);
set_time_limit(0);
ini_set('memory_limit', -1);
shell_exec('kill -9 `ps -ef | grep \'XC_VM\\[EPG\\]\' | grep -v grep | awk \'{print $2}\'`;');
cli_set_process_title('XC_VM[EPG]');
shell_exec('kill -9 `ps -ef | grep \'XC_VM\\[EPG\\]\' | grep -v grep | awk \'{print $2}\'`;');
cli_set_process_title('XC_VM[EPG]');
global $db;
global $db;
if (SettingsManager::get('force_epg_timezone')) {
date_default_timezone_set('UTC');
$this->printLog("[SYSTEM] Forced timezone to UTC");
}
if (SettingsManager::get('force_epg_timezone')) {
date_default_timezone_set('UTC');
$this->printLog("[SYSTEM] Forced timezone to UTC");
}
$this->printLog("[EPG] Clearing old channel mappings...");
if ($rEPGID) {
$db->query('DELETE FROM `epg_channels` WHERE `epg_id` = ?;', $rEPGID);
$db->query('SELECT * FROM `epg` WHERE `id` = ?;', $rEPGID);
} else {
$db->query('TRUNCATE `epg_channels`;');
$db->query('SELECT * FROM `epg`;');
}
$this->printLog("[EPG] Clearing old channel mappings...");
if ($rEPGID) {
$db->query('DELETE FROM `epg_channels` WHERE `epg_id` = ?;', $rEPGID);
$db->query('SELECT * FROM `epg` WHERE `id` = ?;', $rEPGID);
} else {
$db->query('TRUNCATE `epg_channels`;');
$db->query('SELECT * FROM `epg`;');
}
$epgSources = $db->get_rows();
$this->printLog("[EPG] Found " . count($epgSources) . " EPG sources to process");
$epgSources = $db->get_rows();
$this->printLog("[EPG] Found " . count($epgSources) . " EPG sources to process");
foreach ($epgSources as $rRow) {
$this->printLog("[EPG] Processing source ID: {$rRow['id']} | File: {$rRow['epg_file']}");
$rEPG = new EPG($rRow['epg_file']);
foreach ($epgSources as $rRow) {
$this->printLog("[EPG] Processing source ID: {$rRow['id']} | File: {$rRow['epg_file']}");
$rEPG = new EPG($rRow['epg_file']);
if ($rEPG->rValid) {
$rData = $rEPG->getData();
if ($rEPG->rValid) {
$rData = $rEPG->getData();
$this->reconnectDb();
$this->reconnectDb();
$db->query('UPDATE `epg` SET `data` = ?, `last_updated` = ? WHERE `id` = ?', json_encode($rData, JSON_UNESCAPED_UNICODE), time(), $rRow['id']);
$db->query('UPDATE `epg` SET `data` = ?, `last_updated` = ? WHERE `id` = ?', json_encode($rData, JSON_UNESCAPED_UNICODE), time(), $rRow['id']);
$this->printLog("[EPG] Updated metadata for EPG ID {$rRow['id']}, found " . count($rData) . " channels");
$this->printLog("[EPG] Updated metadata for EPG ID {$rRow['id']}, found " . count($rData) . " channels");
foreach ($rData as $rID => $rArray) {
$db->query('INSERT INTO `epg_channels`(`epg_id`, `channel_id`, `name`, `langs`) VALUES(?, ?, ?, ?);', $rRow['id'], $rID, $rArray['display_name'], json_encode($rArray['langs']));
}
} else {
$this->printLog("[EPG] Failed to load EPG source ID {$rRow['id']}");
}
}
foreach ($rData as $rID => $rArray) {
$db->query('INSERT INTO `epg_channels`(`epg_id`, `channel_id`, `name`, `langs`) VALUES(?, ?, ?, ?);', $rRow['id'], $rID, $rArray['display_name'], json_encode($rArray['langs']));
}
} else {
$this->printLog("[EPG] Failed to load EPG source ID {$rRow['id']}");
}
}
$this->printLog("[EPG] Starting full programme data import...");
$this->printLog("[EPG] Starting full programme data import...");
if ($rEPGID) {
$db->query('SELECT DISTINCT(t1.`epg_id`), t2.* FROM `streams` t1 INNER JOIN `epg` t2 ON t2.id = t1.epg_id WHERE t1.`epg_id` IS NOT NULL AND t2.id = ?;', $rEPGID);
} else {
$db->query('SELECT DISTINCT(t1.`epg_id`), t2.* FROM `streams` t1 INNER JOIN `epg` t2 ON t2.id = t1.epg_id WHERE t1.`epg_id` IS NOT NULL;');
}
if ($rEPGID) {
$db->query('SELECT DISTINCT(t1.`epg_id`), t2.* FROM `streams` t1 INNER JOIN `epg` t2 ON t2.id = t1.epg_id WHERE t1.`epg_id` IS NOT NULL AND t2.id = ?;', $rEPGID);
} else {
$db->query('SELECT DISTINCT(t1.`epg_id`), t2.* FROM `streams` t1 INNER JOIN `epg` t2 ON t2.id = t1.epg_id WHERE t1.`epg_id` IS NOT NULL;');
}
foreach ($db->get_rows() as $rData) {
$this->printLog("[EPG] === Processing EPG ID: {$rData['epg_id']} ===");
foreach ($db->get_rows() as $rData) {
$this->printLog("[EPG] === Processing EPG ID: {$rData['epg_id']} ===");
if ($rData['days_keep'] == 0) {
$this->printLog("[EPG] Clearing all existing data for EPG ID {$rData['epg_id']}");
$db->query('DELETE FROM `epg_data` WHERE `epg_id` = ?', $rData['epg_id']);
}
if ($rData['days_keep'] == 0) {
$this->printLog("[EPG] Clearing all existing data for EPG ID {$rData['epg_id']}");
$db->query('DELETE FROM `epg_data` WHERE `epg_id` = ?', $rData['epg_id']);
}
$rEPG = new EPG($rData['epg_file'], true);
if ($rEPG->rValid) {
$db->query('SELECT t1.`channel_id`, t1.`epg_lang`, t1.`epg_offset`, last_row.start
$rEPG = new EPG($rData['epg_file'], true);
if ($rEPG->rValid) {
$db->query('SELECT t1.`channel_id`, t1.`epg_lang`, t1.`epg_offset`, last_row.start
FROM `streams` t1
LEFT JOIN (SELECT channel_id, MAX(`start`) as start FROM epg_data WHERE epg_id = ? GROUP BY channel_id) last_row
ON last_row.channel_id = t1.channel_id
WHERE `epg_id` = ?;', $rData['epg_id'], $rData['epg_id']);
$channelMap = $db->get_rows(true, 'channel_id');
$channelMap = $db->get_rows(true, 'channel_id');
$batches = $rEPG->parseEPG($rData['epg_id'], $channelMap, intval($rData['offset']) ?: 0);
$batches = $rEPG->parseEPG($rData['epg_id'], $channelMap, intval($rData['offset']) ?: 0);
$this->reconnectDb();
$this->reconnectDb();
if ($batches) {
$totalInserted = 0;
foreach ($batches as $insertBatch) {
if (!empty($insertBatch)) {
$db->simple_query('INSERT INTO `epg_data` (`epg_id`,`channel_id`,`start`,`end`,`lang`,`title`,`description`) VALUES ' . $insertBatch);
$totalInserted += substr_count($insertBatch, '),(') + 1;
}
}
$this->printLog("[EPG] Inserted $totalInserted programmes for EPG ID {$rData['epg_id']}");
} else {
$this->printLog("[EPG] No new programmes found for EPG ID {$rData['epg_id']}");
}
if ($batches) {
$totalInserted = 0;
foreach ($batches as $insertBatch) {
if (!empty($insertBatch)) {
$db->simple_query('INSERT INTO `epg_data` (`epg_id`,`channel_id`,`start`,`end`,`lang`,`title`,`description`) VALUES ' . $insertBatch);
$totalInserted += substr_count($insertBatch, '),(') + 1;
}
}
$this->printLog("[EPG] Inserted $totalInserted programmes for EPG ID {$rData['epg_id']}");
} else {
$this->printLog("[EPG] No new programmes found for EPG ID {$rData['epg_id']}");
}
$db->query('UPDATE `epg` SET `last_updated` = ? WHERE `id` = ?', time(), $rData['epg_id']);
} else {
$this->printLog("[EPG] Failed to parse EPG file for ID {$rData['epg_id']}");
}
$db->query('UPDATE `epg` SET `last_updated` = ? WHERE `id` = ?', time(), $rData['epg_id']);
} else {
$this->printLog("[EPG] Failed to parse EPG file for ID {$rData['epg_id']}");
}
if ($rData['days_keep'] > 0) {
$cleanupTime = strtotime('-' . (int)$rData['days_keep'] . ' days');
if ($cleanupTime !== false) {
$db->query('DELETE FROM `epg_data` WHERE `epg_id` = ? AND `start` < ?', $rData['epg_id'], $cleanupTime);
echo "[EPG] Cleaned up old data (older than {$rData['days_keep']} days)\n";
} else {
echo "[EPG] Invalid days_keep value, skipping cleanup\n";
}
}
}
if ($rData['days_keep'] > 0) {
$cleanupTime = strtotime('-' . (int) $rData['days_keep'] . ' days');
if ($cleanupTime !== false) {
$db->query('DELETE FROM `epg_data` WHERE `epg_id` = ? AND `start` < ?', $rData['epg_id'], $cleanupTime);
echo "[EPG] Cleaned up old data (older than {$rData['days_keep']} days)\n";
} else {
echo "[EPG] Invalid days_keep value, skipping cleanup\n";
}
}
}
$this->printLog("[EPG] Removing duplicate EPG entries...");
$db->query('DELETE n1 FROM `epg_data` n1, `epg_data` n2 WHERE n1.id < n2.id AND n1.epg_id = n2.epg_id AND n1.channel_id = n2.channel_id AND n1.start = n2.start;');
$this->printLog("[EPG] Removing duplicate EPG entries...");
$db->query('DELETE n1 FROM `epg_data` n1, `epg_data` n2 WHERE n1.id < n2.id AND n1.epg_id = n2.epg_id AND n1.channel_id = n2.channel_id AND n1.start = n2.start;');
$this->printLog("[EPG] Cleaning temporary XML files...");
shell_exec('rm -f ' . TMP_PATH . '*.xml');
$this->printLog("[EPG] Cleaning temporary XML files...");
shell_exec('rm -f ' . TMP_PATH . '*.xml');
// Marks the start of recording files. Everything that is (re)recorded in this
// execution will have mtime >= $runStart; cleanup at the end only removes what doesn't
// was played in this round (orphans of old executions). Without this, the
// old "time () - 10" erased the caches written at the beginning of the loop
// (which takes well over 10s), leaving most channels without EPG.
$runStart = time();
// Marks the start of recording files. Everything that is (re)recorded in this
// execution will have mtime >= $runStart; cleanup at the end only removes what doesn't
// was played in this round (orphans of old executions). Without this, the
// old "time () - 10" erased the caches written at the beginning of the loop
// (which takes well over 10s), leaving most channels without EPG.
$runStart = time();
$this->printLog("[XMLTV] Starting XMLTV generation...");
$ApiDependencyIdentifier = $this->getBouquetGroups();
$this->printLog("[XMLTV] Starting XMLTV generation...");
$ApiDependencyIdentifier = $this->getBouquetGroups();
$totalBouquets = count($ApiDependencyIdentifier);
$this->printLog("[XMLTV] Generating XMLTV for $totalBouquets bouquet(s)");
$totalBouquets = count($ApiDependencyIdentifier);
$this->printLog("[XMLTV] Generating XMLTV for $totalBouquets bouquet(s)");
foreach ($ApiDependencyIdentifier as $rBouquet => $BatchProcessId) {
if (!(strlen($rBouquet) > 0 && (count($BatchProcessId['streams']) > 0 || $rBouquet == 'all'))) {
continue;
}
foreach ($ApiDependencyIdentifier as $rBouquet => $BatchProcessId) {
if (!(strlen($rBouquet) > 0 && (count($BatchProcessId['streams']) > 0 || $rBouquet == 'all'))) {
continue;
}
$this->printLog("[XMLTV] Generating EPG for bouquet: " . ($rBouquet === 'all' ? 'ALL' : $rBouquet));
$this->printLog("[XMLTV] Generating EPG for bouquet: " . ($rBouquet === 'all' ? 'ALL' : $rBouquet));
$rOutput = '';
$rServerName = htmlspecialchars(SettingsManager::get('server_name'), ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rOutput .= '<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE tv SYSTEM "xmltv.dtd">' . "\n";
$rOutput .= '<tv generator-info-name="' . $rServerName . '">' . "\n";
$rOutput = '';
$rServerName = htmlspecialchars(SettingsManager::get('server_name'), ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rOutput .= '<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE tv SYSTEM "xmltv.dtd">' . "\n";
$rOutput .= '<tv generator-info-name="' . $rServerName . '">' . "\n";
if ($rBouquet == 'all') {
$db->query('SELECT `stream_display_name`,`stream_icon`,`channel_id`,`epg_id`,`tv_archive_duration` FROM `streams` WHERE `epg_id` IS NOT NULL AND `channel_id` IS NOT NULL;');
} else {
$db->query('SELECT `stream_display_name`,`stream_icon`,`channel_id`,`epg_id`,`tv_archive_duration` FROM `streams` WHERE `epg_id` IS NOT NULL AND `channel_id` IS NOT NULL AND `id` IN (' . implode(',', array_map('intval', $BatchProcessId['streams'])) . ');');
}
if ($rBouquet == 'all') {
$db->query('SELECT `stream_display_name`,`stream_icon`,`channel_id`,`epg_id`,`tv_archive_duration` FROM `streams` WHERE `epg_id` IS NOT NULL AND `channel_id` IS NOT NULL;');
} else {
$db->query('SELECT `stream_display_name`,`stream_icon`,`channel_id`,`epg_id`,`tv_archive_duration` FROM `streams` WHERE `epg_id` IS NOT NULL AND `channel_id` IS NOT NULL AND `id` IN (' . implode(',', array_map('intval', $BatchProcessId['streams'])) . ');');
}
$channels = $db->get_rows();
$channelCount = count($channels);
$this->printLog("[XMLTV] Found $channelCount channels in this bouquet");
$channels = $db->get_rows();
$channelCount = count($channels);
$this->printLog("[XMLTV] Found $channelCount channels in this bouquet");
$fa4629d757fa3640 = [];
$hasArchive = 0;
$fa4629d757fa3640 = [];
$hasArchive = 0;
foreach ($channels as $rRow) {
if ($rRow['tv_archive_duration'] > 0) $hasArchive++;
foreach ($channels as $rRow) {
if ($rRow['tv_archive_duration'] > 0) {
$hasArchive++;
}
$displayName = htmlspecialchars($rRow['stream_display_name'], ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$icon = htmlspecialchars(ImageUtils::validateURL($rRow['stream_icon']), ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$channelID = htmlspecialchars($rRow['channel_id'], ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$displayName = htmlspecialchars($rRow['stream_display_name'], ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$icon = htmlspecialchars(ImageUtils::validateURL($rRow['stream_icon']), ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$channelID = htmlspecialchars($rRow['channel_id'], ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rOutput .= "\t<channel id=\"$channelID\">";
$rOutput .= "\t\t<display-name>$displayName</display-name>";
if (!empty($rRow['stream_icon'])) {
$rOutput .= "\t\t<icon src=\"$icon\" />";
}
$rOutput .= "\t</channel>";
$rOutput .= "\t<channel id=\"$channelID\">";
$rOutput .= "\t\t<display-name>$displayName</display-name>";
if (!empty($rRow['stream_icon'])) {
$rOutput .= "\t\t<icon src=\"$icon\" />";
}
$rOutput .= "\t</channel>";
$fa4629d757fa3640[] = $rRow['epg_id'];
}
$fa4629d757fa3640[] = $rRow['epg_id'];
}
$fa4629d757fa3640 = array_unique($fa4629d757fa3640);
$fa4629d757fa3640 = array_unique($fa4629d757fa3640);
if (count($fa4629d757fa3640) > 0) {
if ($hasArchive > 0) {
$this->printLog("[XMLTV] Archive channels detected ($hasArchive), including all historical programmes");
$db->query('SELECT * FROM `epg_data` WHERE `epg_id` IN (' . implode(',', array_map('intval', $fa4629d757fa3640)) . ');');
} else {
$this->printLog("[XMLTV] No archive channels, filtering only current/future programmes");
$db->query('SELECT * FROM `epg_data` WHERE `epg_id` IN (' . implode(',', array_map('intval', $fa4629d757fa3640)) . ') AND `end` >= UNIX_TIMESTAMP();');
}
if (count($fa4629d757fa3640) > 0) {
if ($hasArchive > 0) {
$this->printLog("[XMLTV] Archive channels detected ($hasArchive), including all historical programmes");
$db->query('SELECT * FROM `epg_data` WHERE `epg_id` IN (' . implode(',', array_map('intval', $fa4629d757fa3640)) . ');');
} else {
$this->printLog("[XMLTV] No archive channels, filtering only current/future programmes");
$db->query('SELECT * FROM `epg_data` WHERE `epg_id` IN (' . implode(',', array_map('intval', $fa4629d757fa3640)) . ') AND `end` >= UNIX_TIMESTAMP();');
}
$programmes = $db->get_rows();
$progCount = count($programmes);
$this->printLog("[XMLTV] Adding $progCount programmes to XML");
$programmes = $db->get_rows();
$progCount = count($programmes);
$this->printLog("[XMLTV] Adding $progCount programmes to XML");
$seen = [];
foreach ($programmes as $rRow) {
$key = $rRow['channel_id'] . '|' . $rRow['start'];
if (isset($seen[$key])) continue;
$seen[$key] = true;
$seen = [];
foreach ($programmes as $rRow) {
$key = $rRow['channel_id'] . '|' . $rRow['start'];
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$rTitle = htmlspecialchars($rRow['title'] ?? '', ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rDescription = htmlspecialchars($rRow['description'] ?? '', ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rChannelID = htmlspecialchars($rRow['channel_id'], ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rStart = date('YmdHis', $rRow['start']) . ' ' . str_replace(':', '', date('P', $rRow['start']));
$rEnd = date('YmdHis', $rRow['end']) . ' ' . str_replace(':', '', date('P', $rRow['end']));
$rTitle = htmlspecialchars($rRow['title'] ?? '', ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rDescription = htmlspecialchars($rRow['description'] ?? '', ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rChannelID = htmlspecialchars($rRow['channel_id'], ENT_XML1 | ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
$rStart = date('YmdHis', $rRow['start']) . ' ' . str_replace(':', '', date('P', $rRow['start']));
$rEnd = date('YmdHis', $rRow['end']) . ' ' . str_replace(':', '', date('P', $rRow['end']));
$rOutput .= "\t<programme start=\"$rStart\" stop=\"$rEnd\" channel=\"$rChannelID\">";
$rOutput .= "\t\t<title>$rTitle</title>";
$rOutput .= "\t\t<desc>$rDescription</desc>";
$rOutput .= "\t</programme>";
}
}
$rOutput .= "\t<programme start=\"$rStart\" stop=\"$rEnd\" channel=\"$rChannelID\">";
$rOutput .= "\t\t<title>$rTitle</title>";
$rOutput .= "\t\t<desc>$rDescription</desc>";
$rOutput .= "\t</programme>";
}
}
$rOutput .= '</tv>';
$fileName = ($rBouquet == 'all' ? 'all' : md5($rBouquet));
$xmlPath = EPG_PATH . 'epg_' . $fileName . '.xml';
$gzPath = EPG_PATH . 'epg_' . $fileName . '.xml.gz';
$rOutput .= '</tv>';
$fileName = ($rBouquet == 'all' ? 'all' : md5($rBouquet));
$xmlPath = EPG_PATH . 'epg_' . $fileName . '.xml';
$gzPath = EPG_PATH . 'epg_' . $fileName . '.xml.gz';
file_put_contents($xmlPath, $rOutput);
$gz = gzopen($gzPath, 'w9');
gzwrite($gz, $rOutput);
gzclose($gz);
file_put_contents($xmlPath, $rOutput);
$gz = gzopen($gzPath, 'w9');
gzwrite($gz, $rOutput);
gzclose($gz);
$this->printLog("[XMLTV] Saved epg_$fileName.xml.gz (" . number_format(strlen($rOutput)) . " bytes)");
}
$this->printLog("[XMLTV] Saved epg_$fileName.xml.gz (" . number_format(strlen($rOutput)) . " bytes)");
}
$this->printLog("[CACHE] Building per-stream EPG cache...");
$db->query('SELECT `id`, `epg_id`, `channel_id` FROM `streams` WHERE `type` = 1 AND `epg_id` IS NOT NULL AND `channel_id` IS NOT NULL;');
$streams = $db->get_rows();
$this->printLog("[CACHE] Caching EPG for " . count($streams) . " live streams");
$this->printLog("[CACHE] Building per-stream EPG cache...");
$db->query('SELECT `id`, `epg_id`, `channel_id` FROM `streams` WHERE `type` = 1 AND `epg_id` IS NOT NULL AND `channel_id` IS NOT NULL;');
$streams = $db->get_rows();
$this->printLog("[CACHE] Caching EPG for " . count($streams) . " live streams");
foreach ($streams as $rRow) {
$rEPGData = [];
$seen = [];
foreach ($streams as $rRow) {
$rEPGData = [];
$seen = [];
$db->query('SELECT * FROM `epg_data` WHERE `epg_id` = ? AND `channel_id` = ? ORDER BY `start` ASC;', $rRow['epg_id'], $rRow['channel_id']);
foreach ($db->get_rows() as $prog) {
if (!in_array($prog['start'], $seen)) {
$seen[] = $prog['start'];
$rEPGData[] = $prog;
}
}
$db->query('SELECT * FROM `epg_data` WHERE `epg_id` = ? AND `channel_id` = ? ORDER BY `start` ASC;', $rRow['epg_id'], $rRow['channel_id']);
foreach ($db->get_rows() as $prog) {
if (!in_array($prog['start'], $seen)) {
$seen[] = $prog['start'];
$rEPGData[] = $prog;
}
}
if (count($rEPGData) > 0) {
file_put_contents(EPG_PATH . 'stream_' . $rRow['id'], igbinary_serialize($rEPGData));
}
}
if (count($rEPGData) > 0) {
file_put_contents(EPG_PATH . 'stream_' . $rRow['id'], igbinary_serialize($rEPGData));
}
}
$this->printLog("[CLEANUP] Removing orphan cache files...");
$deleted = 0;
clearstatcache();
foreach (scandir(EPG_PATH) as $rFile) {
if ($rFile === '.' || $rFile === '..') continue;
$fullPath = EPG_PATH . $rFile;
// Only removes files that were not rewritten in this run
// (mtime before the start of the Round = old execution orphan).
if (filemtime($fullPath) < $runStart) {
unlink($fullPath);
$deleted++;
}
}
$this->printLog("[CLEANUP] Deleted $deleted orphan cache files");
$this->printLog("[CLEANUP] Removing orphan cache files...");
$deleted = 0;
clearstatcache();
foreach (scandir(EPG_PATH) as $rFile) {
if ($rFile === '.' || $rFile === '..') {
continue;
}
$fullPath = EPG_PATH . $rFile;
// Only removes files that were not rewritten in this run
// (mtime before the start of the Round = old execution orphan).
if (filemtime($fullPath) < $runStart) {
unlink($fullPath);
$deleted++;
}
}
$this->printLog("[CLEANUP] Deleted $deleted orphan cache files");
$this->printLog("=== EPG processing completed successfully! ===");
$this->printLog("=== EPG processing completed successfully! ===");
return 0;
}
return 0;
}
private function printLog(string $message): void {
echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n";
}
private function printLog(string $message): void {
echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n";
}
private function reconnectDb(): void {
global $db;
if ($db->ping()) {
$this->printLog("[EPG] Database connection is alive.");
} else {
$this->printLog("[EPG] Database connection lost. Attempting to reconnect...");
$db->db_connect();
if ($db->ping()) {
$this->printLog("[EPG] Reconnected to the database successfully.");
} else {
$this->printLog("[EPG] Failed to reconnect to the database. Exiting.");
exit(1);
}
}
}
private function reconnectDb(): void {
global $db;
if ($db->ping()) {
$this->printLog("[EPG] Database connection is alive.");
} else {
$this->printLog("[EPG] Database connection lost. Attempting to reconnect...");
$db->db_connect();
if ($db->ping()) {
$this->printLog("[EPG] Reconnected to the database successfully.");
} else {
$this->printLog("[EPG] Failed to reconnect to the database. Exiting.");
exit(1);
}
}
}
private function getBouquetGroups(): array {
global $db;
$this->printLog("[XMLTV] Building bouquet groups...");
$db->query('SELECT DISTINCT(`bouquet`) AS `bouquet` FROM `lines`;');
$ApiDependencyIdentifier = [
'all' => [
'streams' => [],
'bouquets' => []
]
];
private function getBouquetGroups(): array {
global $db;
$this->printLog("[XMLTV] Building bouquet groups...");
$db->query('SELECT DISTINCT(`bouquet`) AS `bouquet` FROM `lines`;');
$ApiDependencyIdentifier = [
'all' => [
'streams' => [],
'bouquets' => []
]
];
foreach ($db->get_rows() as $rRow) {
$rBouquets = json_decode($rRow['bouquet'] ?? null, true);
foreach ($db->get_rows() as $rRow) {
$rBouquets = json_decode($rRow['bouquet'] ?? null, true);
if (!is_array($rBouquets) || empty($rBouquets)) {
$this->printLog("[XMLTV] Skipping invalid/empty bouquet value: " . var_export($rBouquets, true));
continue;
}
if (!is_array($rBouquets) || empty($rBouquets)) {
$this->printLog("[XMLTV] Skipping invalid/empty bouquet value: " . var_export($rBouquets, true));
continue;
}
sort($rBouquets);
$ApiDependencyIdentifier[implode('_', $rBouquets)] = [
'streams' => [],
'bouquets' => $rBouquets
];
}
$count = count($ApiDependencyIdentifier);
$this->printLog("[XMLTV] Found $count bouquet groups (including 'all')");
sort($rBouquets);
$ApiDependencyIdentifier[implode('_', $rBouquets)] = [
'streams' => [],
'bouquets' => $rBouquets
];
}
$count = count($ApiDependencyIdentifier);
$this->printLog("[XMLTV] Found $count bouquet groups (including 'all')");
foreach ($ApiDependencyIdentifier as $rGroup => $CacheFlushInterval) {
$FileReference = [];
foreach ($ApiDependencyIdentifier as $rGroup => $CacheFlushInterval) {
$FileReference = [];
foreach ($CacheFlushInterval['bouquets'] as $rBouquetID) {
$db->query('SELECT `bouquet_channels` FROM `bouquets` WHERE `id` = ?;', $rBouquetID);
foreach ($CacheFlushInterval['bouquets'] as $rBouquetID) {
$db->query('SELECT `bouquet_channels` FROM `bouquets` WHERE `id` = ?;', $rBouquetID);
foreach ($db->get_rows() as $rRow) {
$FileReference[] = $rBouquetID;
$ApiDependencyIdentifier[$rGroup]['streams'] = array_merge($ApiDependencyIdentifier[$rGroup]['streams'], json_decode($rRow['bouquet_channels'], true));
}
foreach ($db->get_rows() as $rRow) {
$FileReference[] = $rBouquetID;
$ApiDependencyIdentifier[$rGroup]['streams'] = array_merge($ApiDependencyIdentifier[$rGroup]['streams'], json_decode($rRow['bouquet_channels'], true));
}
$ApiDependencyIdentifier[$rGroup]['streams'] = array_unique($ApiDependencyIdentifier[$rGroup]['streams']);
}
$ApiDependencyIdentifier[$rGroup]['streams'] = array_unique($ApiDependencyIdentifier[$rGroup]['streams']);
}
$ApiDependencyIdentifier[$rGroup]['bouquets'] = $FileReference;
}
$ApiDependencyIdentifier[$rGroup]['bouquets'] = $FileReference;
}
return $ApiDependencyIdentifier;
}
return $ApiDependencyIdentifier;
}
}
+156 -152
View File
@@ -17,183 +17,187 @@ use XcVm\Core\Config\SettingsManager;
*/
class ErrorsCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:errors';
}
public function getName(): string {
return 'cron:errors';
}
public function getDescription(): string {
return 'Cron: collect stream and panel errors from logs';
}
public function getDescription(): string {
return 'Cron: collect stream and panel errors from logs';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Errors]');
$this->initCron('XC_VM[Errors]');
$rIgnoreErrors = array('the user-agent option is deprecated', 'last message repeated', 'deprecated', 'packets poorly interleaved', 'invalid timestamps', 'timescale not set', 'frame size not set', 'non-monotonous dts in output stream', 'invalid dts', 'no trailing crlf', 'failed to parse extradata', 'truncated', 'missing picture', 'non-existing pps', 'clipping', 'out of range', 'cannot use rename on non file protocol', 'end of file', 'stream ends prematurely');
$rIgnoreErrors = ['the user-agent option is deprecated', 'last message repeated', 'deprecated', 'packets poorly interleaved', 'invalid timestamps', 'timescale not set', 'frame size not set', 'non-monotonous dts in output stream', 'invalid dts', 'no trailing crlf', 'failed to parse extradata', 'truncated', 'missing picture', 'non-existing pps', 'clipping', 'out of range', 'cannot use rename on non file protocol', 'end of file', 'stream ends prematurely'];
$this->loadCron($rIgnoreErrors);
$this->loadCron($rIgnoreErrors);
return 0;
}
return 0;
}
private function sqlValue($value, bool $isNumeric = false): string {
global $db;
private function sqlValue($value, bool $isNumeric = false): string {
global $db;
if ($value === null || $value === '') {
return 'NULL';
}
if ($isNumeric) {
if (!is_numeric($value)) {
return 'NULL';
}
return (string) ((int) $value);
}
return $db->escape($value);
}
if ($value === null || $value === '') {
return 'NULL';
}
if ($isNumeric) {
if (!is_numeric($value)) {
return 'NULL';
}
return (string) ((int) $value);
}
return $db->escape($value);
}
private function parseLog(string $logFile): string {
global $db;
private function parseLog(string $logFile): string {
global $db;
if (!file_exists($logFile)) {
return '';
}
if (!file_exists($logFile)) {
return '';
}
$fp = fopen($logFile, 'r');
if (!$fp) {
return '';
}
$fp = fopen($logFile, 'r');
if (!$fp) {
return '';
}
$hashes = [];
$query = '';
$hashes = [];
$query = '';
while (!feof($fp)) {
$line = trim(fgets($fp));
if ($line === '') continue;
while (!feof($fp)) {
$line = trim(fgets($fp));
if ($line === '') {
continue;
}
$row = json_decode(base64_decode($line), true);
if (!is_array($row)) continue;
$row = json_decode(base64_decode($line), true);
if (!is_array($row)) {
continue;
}
// Поддержка обоих форматов: legacy (log_*) и текущий (message/extra)
$rLogMessage = (string) ($row['log_message'] ?? ($row['message'] ?? ''));
$rLogExtra = (string) ($row['log_extra'] ?? ($row['extra'] ?? ''));
$rLogType = (string) ($row['type'] ?? 'unknown');
$rLogLine = (int) ($row['line'] ?? 0);
$rLogTime = (int) ($row['time'] ?? time());
$rLogFile = (string) ($row['file'] ?? '');
$rLogEnv = (string) ($row['env'] ?? php_sapi_name());
// Panel version frozen when the error occurred (Logger::log). Empty for
// legacy records written before the field existed.
$rLogVersion = (string) ($row['version'] ?? '');
// Prefer the origin server stamped into the record (FileLogger); fall
// back to this node's SERVER_ID for legacy files written before the
// field existed. This is what lets a panel_logs row show LB vs MAIN.
$rLogServerID = (isset($row['server_id']) && is_numeric($row['server_id']))
? (int) $row['server_id']
: SERVER_ID;
// Поддержка обоих форматов: legacy (log_*) и текущий (message/extra)
$rLogMessage = (string) ($row['log_message'] ?? ($row['message'] ?? ''));
$rLogExtra = (string) ($row['log_extra'] ?? ($row['extra'] ?? ''));
$rLogType = (string) ($row['type'] ?? 'unknown');
$rLogLine = (int) ($row['line'] ?? 0);
$rLogTime = (int) ($row['time'] ?? time());
$rLogFile = (string) ($row['file'] ?? '');
$rLogEnv = (string) ($row['env'] ?? php_sapi_name());
// Panel version frozen when the error occurred (Logger::log). Empty for
// legacy records written before the field existed.
$rLogVersion = (string) ($row['version'] ?? '');
// Prefer the origin server stamped into the record (FileLogger); fall
// back to this node's SERVER_ID for legacy files written before the
// field existed. This is what lets a panel_logs row show LB vs MAIN.
$rLogServerID = (isset($row['server_id']) && is_numeric($row['server_id']))
? (int) $row['server_id']
: SERVER_ID;
if (
stripos($rLogMessage, 'server has gone away') !== false ||
stripos($rLogMessage, 'socket error on read socket') !== false ||
stripos($rLogMessage, 'connection lost') !== false
) {
continue;
}
if (
stripos($rLogMessage, 'server has gone away') !== false ||
stripos($rLogMessage, 'socket error on read socket') !== false ||
stripos($rLogMessage, 'connection lost') !== false
) {
continue;
}
// server_id is part of the key so the SAME error from LB and MAIN keeps
// two distinct panel_logs rows — INSERT IGNORE on `unique` would
// otherwise collapse them into one and lose the per-server attribution.
$hash = md5(
$rLogServerID .
$rLogType .
$rLogMessage .
$rLogExtra .
$rLogFile .
$rLogLine
);
// server_id is part of the key so the SAME error from LB and MAIN keeps
// two distinct panel_logs rows — INSERT IGNORE on `unique` would
// otherwise collapse them into one and lose the per-server attribution.
$hash = md5(
$rLogServerID .
$rLogType .
$rLogMessage .
$rLogExtra .
$rLogFile .
$rLogLine
);
if (isset($hashes[$hash])) {
continue;
}
$hashes[$hash] = true;
if (isset($hashes[$hash])) {
continue;
}
$hashes[$hash] = true;
$query .= sprintf(
"(%d,%s,%s,%s,%s,%s,%s,%s,%s,%s),",
$rLogServerID,
$this->sqlValue($rLogType),
$this->sqlValue($rLogMessage),
$this->sqlValue($rLogExtra),
$this->sqlValue($rLogLine, true),
$this->sqlValue($rLogTime, true),
$this->sqlValue($rLogFile),
$this->sqlValue($rLogEnv),
$this->sqlValue($rLogVersion),
$this->sqlValue($hash)
);
}
$query .= sprintf(
"(%d,%s,%s,%s,%s,%s,%s,%s,%s,%s),",
$rLogServerID,
$this->sqlValue($rLogType),
$this->sqlValue($rLogMessage),
$this->sqlValue($rLogExtra),
$this->sqlValue($rLogLine, true),
$this->sqlValue($rLogTime, true),
$this->sqlValue($rLogFile),
$this->sqlValue($rLogEnv),
$this->sqlValue($rLogVersion),
$this->sqlValue($hash)
);
}
fclose($fp);
fclose($fp);
return rtrim($query, ',');
}
return rtrim($query, ',');
}
private function inArray(array $needles, string $haystack): bool {
foreach ($needles as $needle) {
if (stristr($haystack, $needle)) {
return true;
}
}
return false;
}
private function inArray(array $needles, string $haystack): bool {
foreach ($needles as $needle) {
if (stristr($haystack, $needle)) {
return true;
}
}
return false;
}
private function loadCron(array $rIgnoreErrors): void {
global $db;
private function loadCron(array $rIgnoreErrors): void {
global $db;
$rQuery = '';
foreach (array(STREAMS_PATH) as $rPath) {
if ($rHandle = opendir($rPath)) {
while (false !== ($fileEntry = readdir($rHandle))) {
if ($fileEntry != '.' && $fileEntry != '..' && is_file($rPath . $fileEntry)) {
$rFile = $rPath . $fileEntry;
$rPathInfo = pathinfo($fileEntry);
$rStreamID = (int) ($rPathInfo['filename'] ?? 0);
$rExtension = $rPathInfo['extension'] ?? '';
if ($rExtension == 'errors' && 0 < $rStreamID) {
$rErrors = preg_split('/\r\n|\r|\n/', (string) file_get_contents($rFile));
foreach ($rErrors as $rError) {
$rError = trim((string) $rError);
if (!(empty($rError) || $this->inArray($rIgnoreErrors, $rError))) {
if (SettingsManager::get('stream_logs_save')) {
$rQuery .= '(' . $rStreamID . ',' . SERVER_ID . ',' . time() . ',' . $db->escape($rError) . '),';
}
}
}
unlink($rFile);
}
}
}
closedir($rHandle);
}
}
$rQuery = '';
foreach ([STREAMS_PATH] as $rPath) {
if ($rHandle = opendir($rPath)) {
while (false !== ($fileEntry = readdir($rHandle))) {
if ($fileEntry != '.' && $fileEntry != '..' && is_file($rPath . $fileEntry)) {
$rFile = $rPath . $fileEntry;
$rPathInfo = pathinfo($fileEntry);
$rStreamID = (int) ($rPathInfo['filename'] ?? 0);
$rExtension = $rPathInfo['extension'] ?? '';
if ($rExtension == 'errors' && 0 < $rStreamID) {
$rErrors = preg_split('/\r\n|\r|\n/', (string) file_get_contents($rFile));
foreach ($rErrors as $rError) {
$rError = trim((string) $rError);
if (!(empty($rError) || $this->inArray($rIgnoreErrors, $rError))) {
if (SettingsManager::get('stream_logs_save')) {
$rQuery .= '(' . $rStreamID . ',' . SERVER_ID . ',' . time() . ',' . $db->escape($rError) . '),';
}
}
}
unlink($rFile);
}
}
}
closedir($rHandle);
}
}
if (SettingsManager::get('stream_logs_save') && !empty($rQuery)) {
$rQuery = rtrim($rQuery, ',');
$db->query('INSERT INTO `streams_errors` (`stream_id`,`server_id`,`date`,`error`) VALUES ' . $rQuery . ';');
}
if (SettingsManager::get('stream_logs_save') && !empty($rQuery)) {
$rQuery = rtrim($rQuery, ',');
$db->query('INSERT INTO `streams_errors` (`stream_id`,`server_id`,`date`,`error`) VALUES ' . $rQuery . ';');
}
$rLog = LOGS_TMP_PATH . 'error_log.log';
if (file_exists($rLog)) {
$rQuery = $this->parseLog(LOGS_TMP_PATH . 'error_log.log');
if ($rQuery !== '') {
$rInserted = $db->query("INSERT IGNORE INTO panel_logs(server_id, type, log_message, log_extra, line, date, file, env, version, `unique`) VALUES {$rQuery};");
if ($rInserted) {
unlink($rLog);
}
}
}
}
$rLog = LOGS_TMP_PATH . 'error_log.log';
if (file_exists($rLog)) {
$rQuery = $this->parseLog(LOGS_TMP_PATH . 'error_log.log');
if ($rQuery !== '') {
$rInserted = $db->query("INSERT IGNORE INTO panel_logs(server_id, type, log_message, log_extra, line, date, file, env, version, `unique`) VALUES {$rQuery};");
if ($rInserted) {
unlink($rLog);
}
}
}
}
}
+44 -44
View File
@@ -17,57 +17,57 @@ use XcVm\Infrastructure\Database\DatabaseAware;
*/
class LinesLogsCronJob implements CommandInterface {
use DatabaseAware;
use CronTrait;
use DatabaseAware;
use CronTrait;
public function getName(): string {
return 'cron:lines_logs';
}
public function getName(): string {
return 'cron:lines_logs';
}
public function getDescription(): string {
return 'Cron: import client request logs into DB';
}
public function getDescription(): string {
return 'Cron: import client request logs into DB';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Lines Logs]');
$this->loadCron();
$this->initCron('XC_VM[Lines Logs]');
$this->loadCron();
return 0;
}
return 0;
}
private function loadCron(): void {
$db = self::db();
private function loadCron(): void {
$db = self::db();
$rLog = LOGS_TMP_PATH . 'client_request.log';
if (!file_exists($rLog)) {
return;
}
$rLog = LOGS_TMP_PATH . 'client_request.log';
if (!file_exists($rLog)) {
return;
}
$rQuery = rtrim($this->parseLog($rLog), ',');
if (!empty($rQuery)) {
$db->query('INSERT INTO `lines_logs` (`stream_id`,`user_id`,`client_status`,`query_string`,`user_agent`,`ip`,`extra_data`,`date`) VALUES ' . $rQuery . ';');
}
unlink($rLog);
}
$rQuery = rtrim($this->parseLog($rLog), ',');
if (!empty($rQuery)) {
$db->query('INSERT INTO `lines_logs` (`stream_id`,`user_id`,`client_status`,`query_string`,`user_agent`,`ip`,`extra_data`,`date`) VALUES ' . $rQuery . ';');
}
unlink($rLog);
}
private function parseLog(string $rLog): string {
$db = self::db();
$rQuery = '';
$rFP = fopen($rLog, 'r');
while (!feof($rFP)) {
$rLine = trim(fgets($rFP));
if (!empty($rLine)) {
$rLine = json_decode(base64_decode($rLine), true);
$rLine = array_map(array($db, 'escape'), $rLine);
$rQuery .= '(' . $rLine['stream_id'] . ',' . $rLine['user_id'] . ',' . $rLine['action'] . ',' . $rLine['query_string'] . ',' . $rLine['user_agent'] . ',' . $rLine['user_ip'] . ',' . $rLine['extra_data'] . ',' . $rLine['time'] . '),';
break;
}
}
fclose($rFP);
return $rQuery;
}
private function parseLog(string $rLog): string {
$db = self::db();
$rQuery = '';
$rFP = fopen($rLog, 'r');
while (!feof($rFP)) {
$rLine = trim(fgets($rFP));
if (!empty($rLine)) {
$rLine = json_decode(base64_decode($rLine), true);
$rLine = array_map([$db, 'escape'], $rLine);
$rQuery .= '(' . $rLine['stream_id'] . ',' . $rLine['user_id'] . ',' . $rLine['action'] . ',' . $rLine['query_string'] . ',' . $rLine['user_agent'] . ',' . $rLine['user_ip'] . ',' . $rLine['extra_data'] . ',' . $rLine['time'] . '),';
break;
}
}
fclose($rFP);
return $rQuery;
}
}
+48 -48
View File
@@ -25,63 +25,63 @@ use XcVm\Core\Module\ModuleManager;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class ModuleLicensesCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:module_licenses';
}
public function getName(): string {
return 'cron:module_licenses';
}
public function getDescription(): string {
return 'Cron: renew per-machine ionCube licenses for platform modules';
}
public function getDescription(): string {
return 'Cron: renew per-machine ionCube licenses for platform modules';
}
public function execute(array $rArgs): int {
$this->setProcessTitle('xc_vm: module licenses');
public function execute(array $rArgs): int {
$this->setProcessTitle('xc_vm: module licenses');
// Licensing only applies when the ionCube loader + extension are present.
if (!function_exists('ioncube_server_data') || !class_exists('XC_VM')) {
echo "ionCube loader/extension not present — nothing to do.\n";
return 0;
}
// Licensing only applies when the ionCube loader + extension are present.
if (!function_exists('ioncube_server_data') || !class_exists('XC_VM')) {
echo "ionCube loader/extension not present — nothing to do.\n";
return 0;
}
$apiKey = (string) (SettingsManager::get('platform_api_key') ?? '');
if ($apiKey === '') {
echo "No platform API key configured — skipping.\n";
return 0;
}
$apiKey = (string) (SettingsManager::get('platform_api_key') ?? '');
if ($apiKey === '') {
echo "No platform API key configured — skipping.\n";
return 0;
}
$manager = new ModuleManager(container: ServiceContainer::getInstance());
$manager = new ModuleManager(container: ServiceContainer::getInstance());
$platform = array_filter(
$manager->listModules(),
static fn ($m) => ($m['source'] ?? '') === 'platform'
);
if (empty($platform)) {
echo "No platform modules installed.\n";
return 0;
}
$platform = array_filter(
$manager->listModules(),
static fn ($m) => ($m['source'] ?? '') === 'platform'
);
if (empty($platform)) {
echo "No platform modules installed.\n";
return 0;
}
$renewed = 0;
$skipped = 0;
foreach ($platform as $m) {
$slug = (string) ($m['name'] ?? '');
$dir = (string) ($m['path'] ?? '');
$renewed = 0;
$skipped = 0;
foreach ($platform as $m) {
$slug = (string) ($m['name'] ?? '');
$dir = (string) ($m['path'] ?? '');
// Only renew modules that are actually licensed (carry a .lic).
if ($slug === '' || $dir === '' || empty(glob($dir . '/*.lic'))) {
continue;
}
// Only renew modules that are actually licensed (carry a .lic).
if ($slug === '' || $dir === '' || empty(glob($dir . '/*.lic'))) {
continue;
}
if ($manager->renewModuleLicense($slug, $apiKey)) {
echo "[OK] {$slug}: license renewed\n";
$renewed++;
} else {
echo "[WARN] {$slug}: not renewed (licensing off / not entitled / error)\n";
$skipped++;
}
}
if ($manager->renewModuleLicense($slug, $apiKey)) {
echo "[OK] {$slug}: license renewed\n";
$renewed++;
} else {
echo "[WARN] {$slug}: not renewed (licensing off / not entitled / error)\n";
$skipped++;
}
}
echo "Done: {$renewed} renewed, {$skipped} skipped.\n";
return 0;
}
echo "Done: {$renewed} renewed, {$skipped} skipped.\n";
return 0;
}
}
+37 -38
View File
@@ -29,49 +29,48 @@ use XcVm\Core\Module\ModuleUpdateChecker;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class ModuleUpdatesCronJob implements CommandInterface {
public function getName(): string {
return 'cron:module_updates';
}
public function getName(): string {
return 'cron:module_updates';
}
public function getDescription(): string {
return 'Cron: check module update availability from their declared sources';
}
public function getDescription(): string {
return 'Cron: check module update availability from their declared sources';
}
public function execute(array $rArgs): int {
register_shutdown_function(function () {
global $db;
if (is_object($db)) {
$db->close_mysql();
}
});
public function execute(array $rArgs): int {
register_shutdown_function(function () {
global $db;
if (is_object($db)) {
$db->close_mysql();
}
});
$rManager = new ModuleManager(container: ServiceContainer::getInstance());
$rChecker = new ModuleUpdateChecker();
$rManager = new ModuleManager(container: ServiceContainer::getInstance());
$rChecker = new ModuleUpdateChecker();
foreach ($rManager->listModules() as $rModule) {
// Only installed modules — the check compares against installed_version.
if (($rModule['installed_version'] ?? '') === '') {
continue;
}
foreach ($rManager->listModules() as $rModule) {
// Only installed modules — the check compares against installed_version.
if (($rModule['installed_version'] ?? '') === '') {
continue;
}
$rInstalled = (string) $rModule['installed_version'];
$rLatest = $rChecker->latestAvailable($rModule);
$rInstalled = (string) $rModule['installed_version'];
$rLatest = $rChecker->latestAvailable($rModule);
if ($rLatest !== null && version_compare($rLatest, $rInstalled, '>')) {
$rManager->recordAvailableVersion($rModule['name'], $rLatest);
echo '[UPDATE] ' . $rModule['name'] . ': ' . $rInstalled . ' -> ' . $rLatest . "\n";
} elseif ($rChecker->lastError() !== null) {
// Source unreachable (rate limit, network) — keep any previously
// recorded flag; clearing here would hide a real update until the
// next successful check.
echo '[SKIP] ' . $rModule['name'] . ': ' . $rChecker->lastError() . "\n";
} else {
// Nothing newer — clear any stale flag.
$rManager->recordAvailableVersion($rModule['name'], null);
}
}
if ($rLatest !== null && version_compare($rLatest, $rInstalled, '>')) {
$rManager->recordAvailableVersion($rModule['name'], $rLatest);
echo '[UPDATE] ' . $rModule['name'] . ': ' . $rInstalled . ' -> ' . $rLatest . "\n";
} elseif ($rChecker->lastError() !== null) {
// Source unreachable (rate limit, network) — keep any previously
// recorded flag; clearing here would hide a real update until the
// next successful check.
echo '[SKIP] ' . $rModule['name'] . ': ' . $rChecker->lastError() . "\n";
} else {
// Nothing newer — clear any stale flag.
$rManager->recordAvailableVersion($rModule['name'], null);
}
}
return 0;
}
return 0;
}
}
+155 -141
View File
@@ -17,168 +17,182 @@ use XcVm\Domain\Stream\StreamProcess;
*/
class ProvidersCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:providers';
}
public function getName(): string {
return 'cron:providers';
}
public function getDescription(): string {
return 'Cron: sync providers (channels, VOD, series)';
}
public function getDescription(): string {
return 'Cron: sync providers (channels, VOD, series)';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Providers]');
$this->initCron('XC_VM[Providers]');
$rTimeout = 300;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
$rTimeout = 300;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
$rProviderID = null;
if (!empty($rArgs[0])) {
$rProviderID = intval($rArgs[0]);
}
$rProviderID = null;
if (!empty($rArgs[0])) {
$rProviderID = intval($rArgs[0]);
}
$this->loadCron($rProviderID);
$this->loadCron($rProviderID);
return 0;
}
return 0;
}
private function readURL(string $rURL): ?array {
$rContext = stream_context_create(array('http' => array('timeout' => 30)));
return json_decode(@file_get_contents($rURL, false, $rContext) ?: 'null', true);
}
private function readURL(string $rURL): ?array {
$rContext = stream_context_create(['http' => ['timeout' => 30]]);
return json_decode(@file_get_contents($rURL, false, $rContext) ?: 'null', true);
}
private function loadCron(?int $rProviderID): void {
global $db;
private function loadCron(?int $rProviderID): void {
global $db;
if ($rProviderID) {
$db->query('SELECT `id`, `stream_display_name`, `title_sync` FROM `streams` WHERE `title_sync` LIKE ?;', $rProviderID . '_%');
} else {
$db->query('SELECT `id`, `stream_display_name`, `title_sync` FROM `streams` WHERE `title_sync` IS NOT NULL;');
}
if ($rProviderID) {
$db->query('SELECT `id`, `stream_display_name`, `title_sync` FROM `streams` WHERE `title_sync` LIKE ?;', $rProviderID . '_%');
} else {
$db->query('SELECT `id`, `stream_display_name`, `title_sync` FROM `streams` WHERE `title_sync` IS NOT NULL;');
}
$rSyncTitle = array();
foreach ($db->get_rows() as $rRow) {
list($rSyncID, $rSyncStream) = array_map('intval', explode('_', $rRow['title_sync']));
if (!isset($rSyncTitle[$rSyncID])) {
$rSyncTitle[$rSyncID] = array();
}
$rSyncTitle[$rSyncID][$rSyncStream] = array($rRow['id'], $rRow['stream_display_name']);
}
$rSyncTitle = [];
foreach ($db->get_rows() as $rRow) {
list($rSyncID, $rSyncStream) = array_map('intval', explode('_', $rRow['title_sync']));
if (!isset($rSyncTitle[$rSyncID])) {
$rSyncTitle[$rSyncID] = [];
}
$rSyncTitle[$rSyncID][$rSyncStream] = [$rRow['id'], $rRow['stream_display_name']];
}
if ($rProviderID) {
$db->query('SELECT * FROM `providers` WHERE `id` = ?;', $rProviderID);
} else {
$db->query('SELECT * FROM `providers` WHERE `enabled` = 1;');
}
if ($rProviderID) {
$db->query('SELECT * FROM `providers` WHERE `id` = ?;', $rProviderID);
} else {
$db->query('SELECT * FROM `providers` WHERE `enabled` = 1;');
}
foreach ($db->get_rows() as $rRow) {
$rArray = array();
$rURL = (($rRow['ssl'] ? 'https' : 'http')) . '://' . $rRow['ip'] . ':' . $rRow['port'] . '/';
if ($rRow['legacy']) {
$rURL .= 'player_api.php?username=' . $rRow['username'] . '&password=' . $rRow['password'];
} else {
$rURL .= 'player_api/' . $rRow['username'] . '/' . $rRow['password'] . '?connections=1';
}
foreach ($db->get_rows() as $rRow) {
$rArray = [];
$rURL = (($rRow['ssl'] ? 'https' : 'http')) . '://' . $rRow['ip'] . ':' . $rRow['port'] . '/';
if ($rRow['legacy']) {
$rURL .= 'player_api.php?username=' . $rRow['username'] . '&password=' . $rRow['password'];
} else {
$rURL .= 'player_api/' . $rRow['username'] . '/' . $rRow['password'] . '?connections=1';
}
$rInfo = $this->readURL($rURL);
if ($rInfo) {
$rStatus = 1;
$rUserInfo = $rInfo['user_info'] ?? [];
$rArray['max_connections'] = $rUserInfo['max_connections'] ?? null;
$rArray['active_connections'] = $rUserInfo['active_cons'] ?? 0;
$rArray['exp_date'] = $rUserInfo['exp_date'] ?? null;
} else {
$rStatus = 0;
$rArray['exp_date'] = ($rRow['exp_date'] ?: -1);
}
$rInfo = $this->readURL($rURL);
if ($rInfo) {
$rStatus = 1;
$rUserInfo = $rInfo['user_info'] ?? [];
$rArray['max_connections'] = $rUserInfo['max_connections'] ?? null;
$rArray['active_connections'] = $rUserInfo['active_cons'] ?? 0;
$rArray['exp_date'] = $rUserInfo['exp_date'] ?? null;
} else {
$rStatus = 0;
$rArray['exp_date'] = ($rRow['exp_date'] ?: -1);
}
$rCategories = array();
$rCategoriesURL = $rURL . '&action=get_live_categories';
$rLiveCategories = $this->readURL($rCategoriesURL);
if (!is_array($rLiveCategories)) $rLiveCategories = [];
foreach ($rLiveCategories as $rCategory) {
if (isset($rCategory['category_id'])) { $rCategories[$rCategory['category_id']] = $rCategory['category_name'] ?? ''; }
}
$rCategoriesURL = $rURL . '&action=get_vod_categories';
$rVodCategories = $this->readURL($rCategoriesURL);
if (!is_array($rVodCategories)) $rVodCategories = [];
foreach ($rVodCategories as $rCategory) {
if (isset($rCategory['category_id'])) { $rCategories[$rCategory['category_id']] = $rCategory['category_name'] ?? ''; }
}
$rCategories = [];
$rCategoriesURL = $rURL . '&action=get_live_categories';
$rLiveCategories = $this->readURL($rCategoriesURL);
if (!is_array($rLiveCategories)) {
$rLiveCategories = [];
}
foreach ($rLiveCategories as $rCategory) {
if (isset($rCategory['category_id'])) {
$rCategories[$rCategory['category_id']] = $rCategory['category_name'] ?? '';
}
}
$rCategoriesURL = $rURL . '&action=get_vod_categories';
$rVodCategories = $this->readURL($rCategoriesURL);
if (!is_array($rVodCategories)) {
$rVodCategories = [];
}
foreach ($rVodCategories as $rCategory) {
if (isset($rCategory['category_id'])) {
$rCategories[$rCategory['category_id']] = $rCategory['category_name'] ?? '';
}
}
$rStreamsURL = $rURL . '&action=get_live_streams';
$rStreams = $this->readURL($rStreamsURL);
if (!is_array($rStreams)) $rStreams = [];
$rArray['streams'] = count($rStreams);
$rStreamsURL = $rURL . '&action=get_live_streams';
$rStreams = $this->readURL($rStreamsURL);
if (!is_array($rStreams)) {
$rStreams = [];
}
$rArray['streams'] = count($rStreams);
$rVODURL = $rURL . '&action=get_vod_streams';
$rVOD = $this->readURL($rVODURL);
if (!is_array($rVOD)) $rVOD = [];
$rArray['movies'] = count($rVOD);
$rVODURL = $rURL . '&action=get_vod_streams';
$rVOD = $this->readURL($rVODURL);
if (!is_array($rVOD)) {
$rVOD = [];
}
$rArray['movies'] = count($rVOD);
$rSeriesURL = $rURL . '&action=get_series';
$rSeries = $this->readURL($rSeriesURL);
if (!is_array($rSeries)) $rSeries = [];
$rArray['series'] = count($rSeries);
$rSeriesURL = $rURL . '&action=get_series';
$rSeries = $this->readURL($rSeriesURL);
if (!is_array($rSeries)) {
$rSeries = [];
}
$rArray['series'] = count($rSeries);
$rLastChanged = time();
$db->query('UPDATE `providers` SET `data` = ?, `last_changed` = ?, `status` = ? WHERE `id` = ?;', json_encode($rArray), $rLastChanged, $rStatus, $rRow['id']);
$rLastChanged = time();
$db->query('UPDATE `providers` SET `data` = ?, `last_changed` = ?, `status` = ? WHERE `id` = ?;', json_encode($rArray), $rLastChanged, $rStatus, $rRow['id']);
$db->query('SELECT `type`, `stream_id`, `category_id`, `stream_display_name`, `stream_icon`, `channel_id` FROM `providers_streams` WHERE `provider_id` = ?;', $rRow['id']);
$rNewIDs = $rExistingIDs = array();
foreach ($db->get_rows() as $rStream) {
$rExistingIDs[$rStream['stream_id']] = md5($rStream['category_id'] . '_' . (($rStream['stream_display_name'] ?: '')) . '_' . (($rStream['stream_icon'] ?: '')) . '_' . (($rStream['channel_id'] ?: '')));
}
$db->query('SELECT `type`, `stream_id`, `category_id`, `stream_display_name`, `stream_icon`, `channel_id` FROM `providers_streams` WHERE `provider_id` = ?;', $rRow['id']);
$rNewIDs = $rExistingIDs = [];
foreach ($db->get_rows() as $rStream) {
$rExistingIDs[$rStream['stream_id']] = md5($rStream['category_id'] . '_' . (($rStream['stream_display_name'] ?: '')) . '_' . (($rStream['stream_icon'] ?: '')) . '_' . (($rStream['channel_id'] ?: '')));
}
$rTime = time();
foreach (array('live' => $rStreams, 'movie' => $rVOD) as $rType => $rSelection) {
foreach ($rSelection as $rStream) {
// External provider payloads may omit any of these keys.
$rStream += array('stream_id' => null, 'category_id' => '', 'name' => '', 'stream_icon' => '', 'epg_channel_id' => '', 'container_extension' => '');
if ($rStream['stream_id'] === null) {
continue;
}
$rNewIDs[] = $rStream['stream_id'];
$rCategoryIDs = (isset($rStream['category_ids']) ? (is_array($rStream['category_ids']) ? $rStream['category_ids'] : array()) : array($rStream['category_id']));
$rCategoryArray = array();
foreach ($rCategoryIDs as $rCategoryID) {
// The provider may reference a category id absent from the
// categories feed; default to '' instead of warning.
$rCategoryArray[] = $rCategories[$rCategoryID] ?? '';
}
$rCategoryIDs = '[' . implode(',', array_map('intval', $rCategoryIDs)) . ']';
if (isset($rExistingIDs[$rStream['stream_id']])) {
$rUUID = $rExistingIDs[$rStream['stream_id']];
if (md5($rCategoryIDs . '_' . (($rStream['name'] ?: '')) . '_' . (($rStream['stream_icon'] ?: '')) . '_' . ((($rType == 'live' ? $rStream['epg_channel_id'] : $rStream['container_extension']) ?: ''))) != $rUUID) {
$db->query('UPDATE `providers_streams` SET `category_id` = ?, `category_array` = ?, `stream_display_name` = ?, `stream_icon` = ?, `channel_id` = ?, `modified` = ? WHERE `provider_id` = ? AND `stream_id` = ?;', $rCategoryIDs, json_encode($rCategoryArray), $rStream['name'], $rStream['stream_icon'], ($rType == 'live' ? $rStream['epg_channel_id'] : $rStream['container_extension']), $rTime, $rRow['id'], $rStream['stream_id']);
}
} else {
$db->query('INSERT INTO `providers_streams`(`provider_id`, `type`, `stream_id`, `category_id`, `category_array`, `stream_display_name`, `stream_icon`, `channel_id`, `added`, `modified`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', $rRow['id'], $rType, $rStream['stream_id'], $rCategoryIDs, json_encode($rCategoryArray), $rStream['name'], $rStream['stream_icon'], ($rType == 'live' ? $rStream['epg_channel_id'] : $rStream['container_extension']), $rTime, $rTime);
}
if ($rType == 'live' && isset($rSyncTitle[$rRow['id']][$rStream['stream_id']])) {
if ($rStream['name'] != $rSyncTitle[$rRow['id']][$rStream['stream_id']][1]) {
$db->query('UPDATE `streams` SET `stream_display_name` = ? WHERE `id` = ?;', $rStream['name'], $rSyncTitle[$rRow['id']][$rStream['stream_id']][0]);
StreamProcess::updateStream($rSyncTitle[$rRow['id']][$rStream['stream_id']][0]);
}
}
}
}
$rTime = time();
foreach (['live' => $rStreams, 'movie' => $rVOD] as $rType => $rSelection) {
foreach ($rSelection as $rStream) {
// External provider payloads may omit any of these keys.
$rStream += ['stream_id' => null, 'category_id' => '', 'name' => '', 'stream_icon' => '', 'epg_channel_id' => '', 'container_extension' => ''];
if ($rStream['stream_id'] === null) {
continue;
}
$rNewIDs[] = $rStream['stream_id'];
$rCategoryIDs = (isset($rStream['category_ids']) ? (is_array($rStream['category_ids']) ? $rStream['category_ids'] : []) : [$rStream['category_id']]);
$rCategoryArray = [];
foreach ($rCategoryIDs as $rCategoryID) {
// The provider may reference a category id absent from the
// categories feed; default to '' instead of warning.
$rCategoryArray[] = $rCategories[$rCategoryID] ?? '';
}
$rCategoryIDs = '[' . implode(',', array_map('intval', $rCategoryIDs)) . ']';
if (isset($rExistingIDs[$rStream['stream_id']])) {
$rUUID = $rExistingIDs[$rStream['stream_id']];
if (md5($rCategoryIDs . '_' . (($rStream['name'] ?: '')) . '_' . (($rStream['stream_icon'] ?: '')) . '_' . ((($rType == 'live' ? $rStream['epg_channel_id'] : $rStream['container_extension']) ?: ''))) != $rUUID) {
$db->query('UPDATE `providers_streams` SET `category_id` = ?, `category_array` = ?, `stream_display_name` = ?, `stream_icon` = ?, `channel_id` = ?, `modified` = ? WHERE `provider_id` = ? AND `stream_id` = ?;', $rCategoryIDs, json_encode($rCategoryArray), $rStream['name'], $rStream['stream_icon'], ($rType == 'live' ? $rStream['epg_channel_id'] : $rStream['container_extension']), $rTime, $rRow['id'], $rStream['stream_id']);
}
} else {
$db->query('INSERT INTO `providers_streams`(`provider_id`, `type`, `stream_id`, `category_id`, `category_array`, `stream_display_name`, `stream_icon`, `channel_id`, `added`, `modified`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', $rRow['id'], $rType, $rStream['stream_id'], $rCategoryIDs, json_encode($rCategoryArray), $rStream['name'], $rStream['stream_icon'], ($rType == 'live' ? $rStream['epg_channel_id'] : $rStream['container_extension']), $rTime, $rTime);
}
if ($rType == 'live' && isset($rSyncTitle[$rRow['id']][$rStream['stream_id']])) {
if ($rStream['name'] != $rSyncTitle[$rRow['id']][$rStream['stream_id']][1]) {
$db->query('UPDATE `streams` SET `stream_display_name` = ? WHERE `id` = ?;', $rStream['name'], $rSyncTitle[$rRow['id']][$rStream['stream_id']][0]);
StreamProcess::updateStream($rSyncTitle[$rRow['id']][$rStream['stream_id']][0]);
}
}
}
}
$rDelete = array();
foreach (array_keys($rExistingIDs) as $rStreamID) {
if (!in_array($rStreamID, $rNewIDs)) {
$rDelete[] = $rStreamID;
}
}
if (count($rDelete) > 0) {
$db->query('DELETE FROM `providers_streams` WHERE `provider_id` = ? AND `stream_id` IN (' . implode(',', array_map('intval', $rDelete)) . ');', $rRow['id']);
}
}
}
$rDelete = [];
foreach (array_keys($rExistingIDs) as $rStreamID) {
if (!in_array($rStreamID, $rNewIDs)) {
$rDelete[] = $rStreamID;
}
}
if (count($rDelete) > 0) {
$db->query('DELETE FROM `providers_streams` WHERE `provider_id` = ? AND `stream_id` IN (' . implode(',', array_map('intval', $rDelete)) . ');', $rRow['id']);
}
}
}
}
+126 -126
View File
@@ -19,161 +19,161 @@ use XcVm\Domain\Server\ServerRepository;
*/
class RootMysqlCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:root_mysql';
}
public function getName(): string {
return 'cron:root_mysql';
}
public function getDescription(): string {
return 'Cron: monitor MariaDB, parse syslog, block bruteforce (root)';
}
public function getDescription(): string {
return 'Cron: monitor MariaDB, parse syslog, block bruteforce (root)';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsRoot()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsRoot()) {
return 1;
}
if (!$this->checkMariaDB()) {
return 1;
}
if (!$this->checkMariaDB()) {
return 1;
}
global $db;
global $db;
$this->setProcessTitle('XC_VM[MysqlErrors]');
$this->acquireCronLock();
$this->setProcessTitle('XC_VM[MysqlErrors]');
$this->acquireCronLock();
$rIgnoreErrors = array('innodb: page_cleaner', 'aborted connection', 'got an error reading communication packets', 'got packets out of order', 'got timeout reading communication packets');
$rIgnoreErrors = ['innodb: page_cleaner', 'aborted connection', 'got an error reading communication packets', 'got packets out of order', 'got timeout reading communication packets'];
if (SettingsManager::get('mysql_sleep_kill') > 0) {
$db->query("SELECT `id` FROM `INFORMATION_SCHEMA`.`PROCESSLIST` WHERE `COMMAND` = 'Sleep' AND `TIME` > ?;", intval(SettingsManager::get('mysql_sleep_kill')));
foreach ($db->get_rows() as $rRow) {
$db->query('KILL ?;', $rRow['id']);
}
}
if (SettingsManager::get('mysql_sleep_kill') > 0) {
$db->query("SELECT `id` FROM `INFORMATION_SCHEMA`.`PROCESSLIST` WHERE `COMMAND` = 'Sleep' AND `TIME` > ?;", intval(SettingsManager::get('mysql_sleep_kill')));
foreach ($db->get_rows() as $rRow) {
$db->query('KILL ?;', $rRow['id']);
}
}
$db->query('SELECT MAX(`date`) AS `date` FROM `mysql_syslog`;');
$rMaxTime = intval($db->get_row()['date']);
$db->query('SELECT MAX(`date`) AS `date` FROM `mysql_syslog`;');
$rMaxTime = intval($db->get_row()['date']);
$rMaxAttempts = 10;
$rAttempts = array();
$rMaxAttempts = 10;
$rAttempts = [];
$db->query("SELECT `mysql_syslog`.`ip`, COUNT(`mysql_syslog`.`id`) AS `count`, `blocked_ips`.`id` AS `block_id` FROM `mysql_syslog` LEFT JOIN `blocked_ips` ON `blocked_ips`.`ip` = `mysql_syslog`.`ip` WHERE `type` = 'AUTH' AND `mysql_syslog`.`date` > UNIX_TIMESTAMP() - 86400 GROUP BY `mysql_syslog`.`ip`;");
foreach ($db->get_rows() as $rRow) {
$rAttempts[$rRow['ip']] = $rRow['count'];
if ($rMaxAttempts < $rRow['count'] && !$rRow['block_id']) {
if (!in_array($rRow['ip'], ServerRepository::getAllowedIPs())) {
echo 'Blocking IP ' . $rRow['ip'] . "\n";
BlocklistService::blockIP(array('ip' => $rRow['ip'], 'notes' => 'MYSQL BRUTEFORCE ATTACK'));
}
}
}
// Fast-path: skip expensive syslog tail/grep when file size has not changed.
$rSyslogMarker = CRONS_TMP_PATH . 'mysql_syslog_size';
$rCurrentSize = @filesize('/var/log/syslog') ?: 0;
$rLastSize = file_exists($rSyslogMarker) ? intval(@file_get_contents($rSyslogMarker)) : -1;
$db->query("SELECT `mysql_syslog`.`ip`, COUNT(`mysql_syslog`.`id`) AS `count`, `blocked_ips`.`id` AS `block_id` FROM `mysql_syslog` LEFT JOIN `blocked_ips` ON `blocked_ips`.`ip` = `mysql_syslog`.`ip` WHERE `type` = 'AUTH' AND `mysql_syslog`.`date` > UNIX_TIMESTAMP() - 86400 GROUP BY `mysql_syslog`.`ip`;");
foreach ($db->get_rows() as $rRow) {
$rAttempts[$rRow['ip']] = $rRow['count'];
if ($rMaxAttempts < $rRow['count'] && !$rRow['block_id']) {
if (!in_array($rRow['ip'], ServerRepository::getAllowedIPs())) {
echo 'Blocking IP ' . $rRow['ip'] . "\n";
BlocklistService::blockIP(['ip' => $rRow['ip'], 'notes' => 'MYSQL BRUTEFORCE ATTACK']);
}
}
}
if ($rCurrentSize === $rLastSize) {
@unlink($this->rIdentifier);
return 0;
}
// Fast-path: skip expensive syslog tail/grep when file size has not changed.
$rSyslogMarker = CRONS_TMP_PATH . 'mysql_syslog_size';
$rCurrentSize = @filesize('/var/log/syslog') ?: 0;
$rLastSize = file_exists($rSyslogMarker) ? intval(@file_get_contents($rSyslogMarker)) : -1;
@file_put_contents($rSyslogMarker, $rCurrentSize);
exec('sudo tail -n 1000 /var/log/syslog | grep mysqld', $rOutput, $rRetVal);
foreach ($rOutput as $rError) {
$rMySQLDParts = explode('mysqld[', $rError, 2);
if (count($rMySQLDParts) < 2) {
continue;
}
if ($rCurrentSize === $rLastSize) {
@unlink($this->rIdentifier);
return 0;
}
$rErrorParts = explode(']:', $rMySQLDParts[1], 2);
if (count($rErrorParts) < 2) {
continue;
}
@file_put_contents($rSyslogMarker, $rCurrentSize);
exec('sudo tail -n 1000 /var/log/syslog | grep mysqld', $rOutput, $rRetVal);
foreach ($rOutput as $rError) {
$rMySQLDParts = explode('mysqld[', $rError, 2);
if (count($rMySQLDParts) < 2) {
continue;
}
$rStrip = trim($rErrorParts[1]);
$rTime = strtotime(substr($rStrip, 0, 19));
$rErrorParts = explode(']:', $rMySQLDParts[1], 2);
if (count($rErrorParts) < 2) {
continue;
}
if ($rMaxTime >= $rTime) {
continue;
}
$rStrip = trim($rErrorParts[1]);
$rTime = strtotime(substr($rStrip, 0, 19));
if (empty($rStrip) || $this->inArray($rIgnoreErrors, $rStrip)) {
continue;
}
if ($rMaxTime >= $rTime) {
continue;
}
$rNote = null;
$rType = null;
if (empty($rStrip) || $this->inArray($rIgnoreErrors, $rStrip)) {
continue;
}
if (stripos($rStrip, '[Note]') !== false) {
$rNote = trim(explode('[Note]', $rStrip)[1]);
$rType = 'NOTICE';
} elseif (stripos($rStrip, '[Warning]') !== false) {
$rNote = trim(explode('[Warning]', $rStrip)[1]);
$rType = 'WARNING';
} elseif (stripos($rStrip, '[Error]') !== false) {
$rNote = trim(explode('[Error]', $rStrip)[1]);
$rType = 'ERROR';
}
$rNote = null;
$rType = null;
if (!$rNote) {
continue;
}
if (stripos($rStrip, '[Note]') !== false) {
$rNote = trim(explode('[Note]', $rStrip)[1]);
$rType = 'NOTICE';
} elseif (stripos($rStrip, '[Warning]') !== false) {
$rNote = trim(explode('[Warning]', $rStrip)[1]);
$rType = 'WARNING';
} elseif (stripos($rStrip, '[Error]') !== false) {
$rNote = trim(explode('[Error]', $rStrip)[1]);
$rType = 'ERROR';
}
$rUsername = null;
$rHost = null;
$rDatabase = null;
if (!$rNote) {
continue;
}
if (stripos($rNote, 'access denied for user') !== false) {
$rUsername = trim(explode("'", explode("user '", $rNote)[1])[0]);
$rHost = trim(explode("'", explode("user '", $rNote)[1])[2]);
$rType = 'AUTH';
}
$rUsername = null;
$rHost = null;
$rDatabase = null;
if (stripos($rNote, 'user:') !== false) {
$rUsername = trim(explode("'", explode("user: '", $rNote)[1])[0]);
$rHost = trim(explode("'", explode("host: '", $rNote)[1])[0]);
$rDatabase = trim(explode("'", explode("db: '", $rNote)[1])[0]);
$rType = 'ABORTED';
}
if (stripos($rNote, 'access denied for user') !== false) {
$rUsername = trim(explode("'", explode("user '", $rNote)[1])[0]);
$rHost = trim(explode("'", explode("user '", $rNote)[1])[2]);
$rType = 'AUTH';
}
$db->query('INSERT INTO `mysql_syslog`(`type`,`error`,`username`,`ip`,`database`,`date`) VALUES(?,?,?,?,?,?)', $rType, $rNote, $rUsername, $rHost, $rDatabase, $rTime);
}
if (stripos($rNote, 'user:') !== false) {
$rUsername = trim(explode("'", explode("user: '", $rNote)[1])[0]);
$rHost = trim(explode("'", explode("host: '", $rNote)[1])[0]);
$rDatabase = trim(explode("'", explode("db: '", $rNote)[1])[0]);
$rType = 'ABORTED';
}
@unlink($this->rIdentifier);
$db->query('INSERT INTO `mysql_syslog`(`type`,`error`,`username`,`ip`,`database`,`date`) VALUES(?,?,?,?,?,?)', $rType, $rNote, $rUsername, $rHost, $rDatabase, $rTime);
}
return 0;
}
@unlink($this->rIdentifier);
private function inArray(array $needles, string $haystack): bool {
foreach ($needles as $needle) {
if (stristr($haystack, $needle)) {
return true;
}
}
return false;
}
return 0;
}
private function checkMariaDB(): bool {
exec('systemctl is-active mariadb 2>/dev/null', $out, $code);
$isActive = isset($out[0]) && trim($out[0]) === 'active';
private function inArray(array $needles, string $haystack): bool {
foreach ($needles as $needle) {
if (stristr($haystack, $needle)) {
return true;
}
}
return false;
}
if (!$isActive) {
echo "[MYSQL] MariaDB is DOWN, restarting...\n";
exec('systemctl restart mariadb 2>&1', $restartOut, $restartCode);
sleep(3);
private function checkMariaDB(): bool {
exec('systemctl is-active mariadb 2>/dev/null', $out, $code);
$isActive = isset($out[0]) && trim($out[0]) === 'active';
exec('systemctl is-active mariadb 2>/dev/null', $checkOut);
if (isset($checkOut[0]) && trim($checkOut[0]) === 'active') {
echo "[MYSQL] MariaDB successfully restarted\n";
return true;
} else {
echo "[MYSQL] FAILED to restart MariaDB\n";
return false;
}
}
if (!$isActive) {
echo "[MYSQL] MariaDB is DOWN, restarting...\n";
exec('systemctl restart mariadb 2>&1', $restartOut, $restartCode);
sleep(3);
return true;
}
exec('systemctl is-active mariadb 2>/dev/null', $checkOut);
if (isset($checkOut[0]) && trim($checkOut[0]) === 'active') {
echo "[MYSQL] MariaDB successfully restarted\n";
return true;
} else {
echo "[MYSQL] FAILED to restart MariaDB\n";
return false;
}
}
return true;
}
}
File diff suppressed because it is too large Load Diff
+44 -44
View File
@@ -19,60 +19,60 @@ use XcVm\Domain\Vod\SeriesService;
*/
class SeriesCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:series';
}
public function getName(): string {
return 'cron:series';
}
public function getDescription(): string {
return 'Cron: update series playlists and scan bouquets';
}
public function getDescription(): string {
return 'Cron: update series playlists and scan bouquets';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->setProcessTitle('XC_VM[Series]');
$this->acquireCronLock();
$this->setProcessTitle('XC_VM[Series]');
$this->acquireCronLock();
$this->loadCron();
$this->loadCron();
@unlink($this->rIdentifier);
@unlink($this->rIdentifier);
return 0;
}
return 0;
}
private function loadCron(): void {
global $db;
private function loadCron(): void {
global $db;
if (time() - SettingsManager::get('cc_time') < 3600) {
return;
}
if (time() - SettingsManager::get('cc_time') < 3600) {
return;
}
$db->query('UPDATE `settings` SET `cc_time` = ?;', time());
$db->query('SELECT `id`, `stream_display_name`, `series_no`, `stream_source` FROM `streams` WHERE `type` = 3 AND `series_no` <> 0;');
$db->query('UPDATE `settings` SET `cc_time` = ?;', time());
$db->query('SELECT `id`, `stream_display_name`, `series_no`, `stream_source` FROM `streams` WHERE `type` = 3 AND `series_no` <> 0;');
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
$rPlaylist = SeriesService::generatePlaylist(intval($rRow['series_no']));
if ($rPlaylist['success']) {
$rSourceArray = json_decode($rRow['stream_source'], true);
$UpdateSeries = false;
foreach ($rPlaylist['sources'] as $rSource) {
if (!in_array($rSource, $rSourceArray)) {
$UpdateSeries = true;
}
}
if ($UpdateSeries) {
$db->query('UPDATE `streams` SET `stream_source` = ? WHERE `id` = ?;', json_encode($rPlaylist['sources'], JSON_UNESCAPED_UNICODE), $rRow['id']);
echo 'Updated: ' . $rRow['stream_display_name'] . "\n";
}
}
}
}
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
$rPlaylist = SeriesService::generatePlaylist(intval($rRow['series_no']));
if ($rPlaylist['success']) {
$rSourceArray = json_decode($rRow['stream_source'], true);
$UpdateSeries = false;
foreach ($rPlaylist['sources'] as $rSource) {
if (!in_array($rSource, $rSourceArray)) {
$UpdateSeries = true;
}
}
if ($UpdateSeries) {
$db->query('UPDATE `streams` SET `stream_source` = ? WHERE `id` = ?;', json_encode($rPlaylist['sources'], JSON_UNESCAPED_UNICODE), $rRow['id']);
echo 'Updated: ' . $rRow['stream_display_name'] . "\n";
}
}
}
}
BouquetService::scan();
}
BouquetService::scan();
}
}
+170 -170
View File
@@ -21,204 +21,204 @@ use XcVm\Domain\Server\ServerRepository;
*/
class ServersCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:servers';
}
public function getName(): string {
return 'cron:servers';
}
public function getDescription(): string {
return 'Cron: monitor server, launch daemons, update statistics';
}
public function getDescription(): string {
return 'Cron: monitor server, launch daemons, update statistics';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Servers]');
$this->loadCron();
$this->initCron('XC_VM[Servers]');
$this->loadCron();
return 0;
}
return 0;
}
private function pingServer(string $rIP, $rPort): int {
$rStartTime = microtime(true);
$rSocket = @fsockopen($rIP, $rPort, $rErrNo, $rErrStr, 3);
$rStopTime = microtime(true);
if (!$rSocket) {
$rStatus = -1;
} else {
fclose($rSocket);
$rStatus = floor(($rStopTime - $rStartTime) * 1000);
}
return $rStatus;
}
private function pingServer(string $rIP, $rPort): int {
$rStartTime = microtime(true);
$rSocket = @fsockopen($rIP, $rPort, $rErrNo, $rErrStr, 3);
$rStopTime = microtime(true);
if (!$rSocket) {
$rStatus = -1;
} else {
fclose($rSocket);
$rStatus = floor(($rStopTime - $rStartTime) * 1000);
}
return $rStatus;
}
private function loadCron(): void {
global $db;
private function loadCron(): void {
global $db;
SettingsManager::set(SettingsRepository::getAll(true));
SettingsManager::set(SettingsRepository::getAll(true));
if (!ProcessManager::isNginxRunning()) {
echo 'XC_VM not running...' . "\n";
return;
}
if (!ProcessManager::isNginxRunning()) {
echo 'XC_VM not running...' . "\n";
return;
}
$rServers = ServerRepository::getAll(true);
$rServers = ServerRepository::getAll(true);
// The current server must be present in the map; if it isn't (row not
// yet inserted, or a transient load failure) every $rServers[SERVER_ID]
// access below would raise offset-on-null warnings and do nothing useful.
if (!isset($rServers[SERVER_ID])) {
echo 'Server ' . SERVER_ID . ' not found in servers list...' . "\n";
return;
}
// The current server must be present in the map; if it isn't (row not
// yet inserted, or a transient load failure) every $rServers[SERVER_ID]
// access below would raise offset-on-null warnings and do nothing useful.
if (!isset($rServers[SERVER_ID])) {
echo 'Server ' . SERVER_ID . ' not found in servers list...' . "\n";
return;
}
if ($rServers[SERVER_ID]['is_main'] && SettingsManager::get('redis_handler')) {
exec('pgrep -u xc_vm redis-server', $rRedis);
if (count($rRedis) == 0) {
echo 'Restarting Redis!' . "\n";
shell_exec(MAIN_HOME . 'bin/redis/redis-server ' . MAIN_HOME . '/bin/redis/redis.conf > /dev/null 2>/dev/null &');
}
}
if ($rServers[SERVER_ID]['is_main'] && SettingsManager::get('redis_handler')) {
exec('pgrep -u xc_vm redis-server', $rRedis);
if (count($rRedis) == 0) {
echo 'Restarting Redis!' . "\n";
shell_exec(MAIN_HOME . 'bin/redis/redis-server ' . MAIN_HOME . '/bin/redis/redis.conf > /dev/null 2>/dev/null &');
}
}
// Daemon liveness checks read /proc via ProcessManager: the old
// "ps | grep <word>" pipelines matched unrelated processes — e.g.
// ffmpeg's -thread_queue_size satisfied the "queue" check, so the
// encode queue daemon was never revived while any stream was up.
if (!ProcessManager::isAnyProcessRunning(array('XC_VM[Signals]', 'console.php signals'))) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php signals > /dev/null 2>/dev/null &');
}
// Daemon liveness checks read /proc via ProcessManager: the old
// "ps | grep <word>" pipelines matched unrelated processes — e.g.
// ffmpeg's -thread_queue_size satisfied the "queue" check, so the
// encode queue daemon was never revived while any stream was up.
if (!ProcessManager::isAnyProcessRunning(['XC_VM[Signals]', 'console.php signals'])) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php signals > /dev/null 2>/dev/null &');
}
if ($rServers[SERVER_ID]['is_main']) {
$rCachePIDs = ProcessManager::findProcessPIDs(array('XC_VM[CacheHandler]', 'console.php cache_handler'));
if (SettingsManager::get('enable_cache') && count($rCachePIDs) == 0) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php cache_handler > /dev/null 2>/dev/null &');
} elseif (!SettingsManager::get('enable_cache') && count($rCachePIDs) > 0) {
echo 'Killing Cache Handler' . "\n";
foreach ($rCachePIDs as $rPID) {
shell_exec('kill -9 ' . intval($rPID));
}
}
}
if ($rServers[SERVER_ID]['is_main']) {
$rCachePIDs = ProcessManager::findProcessPIDs(['XC_VM[CacheHandler]', 'console.php cache_handler']);
if (SettingsManager::get('enable_cache') && count($rCachePIDs) == 0) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php cache_handler > /dev/null 2>/dev/null &');
} elseif (!SettingsManager::get('enable_cache') && count($rCachePIDs) > 0) {
echo 'Killing Cache Handler' . "\n";
foreach ($rCachePIDs as $rPID) {
shell_exec('kill -9 ' . intval($rPID));
}
}
}
if (!ProcessManager::isAnyProcessRunning(array(BIN_PATH . 'network'))) {
shell_exec(BIN_PATH . 'network > /dev/null 2>/dev/null &');
}
if (!ProcessManager::isAnyProcessRunning([BIN_PATH . 'network'])) {
shell_exec(BIN_PATH . 'network > /dev/null 2>/dev/null &');
}
// A watchdog generation lives only a few seconds. If one is still
// present but far older it is wedged — typically blocked in poll()
// on a half-open MariaDB socket (CLOSE_WAIT) inside its DB ping — and
// will never refresh last_check_ago, so the panel marks the node
// offline while nginx/php/redis/streams are all fine. Kill the stale
// process and start a fresh generation; a plain presence check alone
// would keep trusting the wedged one forever.
$rWatchdogAlive = false;
foreach (ProcessManager::findProcessPIDs(array('XC_VM[Watchdog]', 'console.php watchdog')) as $rWatchdogPID) {
if (ProcessManager::getProcessAge($rWatchdogPID) > 90) {
ProcessManager::kill($rWatchdogPID);
continue;
}
$rWatchdogAlive = true;
}
if (!$rWatchdogAlive) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php watchdog > /dev/null 2>/dev/null &');
}
// A watchdog generation lives only a few seconds. If one is still
// present but far older it is wedged — typically blocked in poll()
// on a half-open MariaDB socket (CLOSE_WAIT) inside its DB ping — and
// will never refresh last_check_ago, so the panel marks the node
// offline while nginx/php/redis/streams are all fine. Kill the stale
// process and start a fresh generation; a plain presence check alone
// would keep trusting the wedged one forever.
$rWatchdogAlive = false;
foreach (ProcessManager::findProcessPIDs(['XC_VM[Watchdog]', 'console.php watchdog']) as $rWatchdogPID) {
if (ProcessManager::getProcessAge($rWatchdogPID) > 90) {
ProcessManager::kill($rWatchdogPID);
continue;
}
$rWatchdogAlive = true;
}
if (!$rWatchdogAlive) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php watchdog > /dev/null 2>/dev/null &');
}
if (!ProcessManager::isAnyProcessRunning(array('XC_VM[Queue]', 'console.php queue'))) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php queue > /dev/null 2>/dev/null &');
}
if (!ProcessManager::isAnyProcessRunning(['XC_VM[Queue]', 'console.php queue'])) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php queue > /dev/null 2>/dev/null &');
}
$rOnDemandPIDs = ProcessManager::findProcessPIDs(array('XC_VM[Ondemand]', 'console.php ondemand'));
if (SettingsManager::get('on_demand_instant_off') && count($rOnDemandPIDs) == 0) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php ondemand > /dev/null 2>/dev/null &');
} elseif (!SettingsManager::get('on_demand_instant_off') && count($rOnDemandPIDs) > 0) {
echo 'Killing On-Demand Instant-Off' . "\n";
foreach ($rOnDemandPIDs as $rPID) {
shell_exec('kill -9 ' . intval($rPID));
}
}
$rOnDemandPIDs = ProcessManager::findProcessPIDs(['XC_VM[Ondemand]', 'console.php ondemand']);
if (SettingsManager::get('on_demand_instant_off') && count($rOnDemandPIDs) == 0) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php ondemand > /dev/null 2>/dev/null &');
} elseif (!SettingsManager::get('on_demand_instant_off') && count($rOnDemandPIDs) > 0) {
echo 'Killing On-Demand Instant-Off' . "\n";
foreach ($rOnDemandPIDs as $rPID) {
shell_exec('kill -9 ' . intval($rPID));
}
}
$rScannerPIDs = ProcessManager::findProcessPIDs(array('XC_VM[Scanner]', 'console.php scanner'));
if (SettingsManager::get('on_demand_checker') && count($rScannerPIDs) == 0) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php scanner > /dev/null 2>/dev/null &');
} elseif (!SettingsManager::get('on_demand_checker') && count($rScannerPIDs) > 0) {
echo 'Killing On-Demand Scanner' . "\n";
foreach ($rScannerPIDs as $rPID) {
shell_exec('kill -9 ' . intval($rPID));
}
}
$rScannerPIDs = ProcessManager::findProcessPIDs(['XC_VM[Scanner]', 'console.php scanner']);
if (SettingsManager::get('on_demand_checker') && count($rScannerPIDs) == 0) {
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php scanner > /dev/null 2>/dev/null &');
} elseif (!SettingsManager::get('on_demand_checker') && count($rScannerPIDs) > 0) {
echo 'Killing On-Demand Scanner' . "\n";
foreach ($rScannerPIDs as $rPID) {
shell_exec('kill -9 ' . intval($rPID));
}
}
$rStats = SystemInfo::getStats();
$rWatchdog = json_decode($rServers[SERVER_ID]['watchdog_data'] ?? '', true);
$rCPUAverage = ($rWatchdog['cpu_average_array'] ?? []) ?: [];
if (count($rCPUAverage) > 0) {
$rStats['cpu'] = round(array_sum($rCPUAverage) / count($rCPUAverage), 2);
}
$rStats = SystemInfo::getStats();
$rWatchdog = json_decode($rServers[SERVER_ID]['watchdog_data'] ?? '', true);
$rCPUAverage = ($rWatchdog['cpu_average_array'] ?? []) ?: [];
if (count($rCPUAverage) > 0) {
$rStats['cpu'] = round(array_sum($rCPUAverage) / count($rCPUAverage), 2);
}
$rHardware = array('total_ram' => $rStats['total_mem'], 'total_used' => $rStats['total_mem_used'], 'cores' => $rStats['cpu_cores'], 'threads' => $rStats['cpu_cores'], 'kernel' => $rStats['kernel'], 'total_running_streams' => $rStats['total_running_streams'], 'cpu_name' => $rStats['cpu_name'], 'cpu_usage' => $rStats['cpu'], 'network_speed' => $rStats['network_speed'], 'bytes_sent' => $rStats['bytes_sent'], 'bytes_received' => $rStats['bytes_received']);
$rHardware = ['total_ram' => $rStats['total_mem'], 'total_used' => $rStats['total_mem_used'], 'cores' => $rStats['cpu_cores'], 'threads' => $rStats['cpu_cores'], 'kernel' => $rStats['kernel'], 'total_running_streams' => $rStats['total_running_streams'], 'cpu_name' => $rStats['cpu_name'], 'cpu_usage' => $rStats['cpu'], 'network_speed' => $rStats['network_speed'], 'bytes_sent' => $rStats['bytes_sent'], 'bytes_received' => $rStats['bytes_received']];
if (@fsockopen($rServers[SERVER_ID]['server_ip'], $rServers[SERVER_ID]['http_broadcast_port'], $rErrNo, $rErrStr, 3) || @fsockopen($rServers[SERVER_ID]['server_ip'], $rServers[SERVER_ID]['https_broadcast_port'], $rErrNo, $rErrStr, 3)) {
$rRemoteStatus = true;
} else {
$rRemoteStatus = false;
}
if (@fsockopen($rServers[SERVER_ID]['server_ip'], $rServers[SERVER_ID]['http_broadcast_port'], $rErrNo, $rErrStr, 3) || @fsockopen($rServers[SERVER_ID]['server_ip'], $rServers[SERVER_ID]['https_broadcast_port'], $rErrNo, $rErrStr, 3)) {
$rRemoteStatus = true;
} else {
$rRemoteStatus = false;
}
if (SettingsManager::get('redis_handler')) {
$rConnections = $rServers[SERVER_ID]['connections'];
$rUsers = $rServers[SERVER_ID]['users'];
$rAllUsers = 0;
foreach (array_keys($rServers) as $rServerID) {
if ($rServers[$rServerID]['server_online']) {
$rAllUsers += $rServers[$rServerID]['users'];
}
}
} else {
$db->query('SELECT COUNT(*) AS `count` FROM `lines_live` WHERE `server_id` = ? AND `hls_end` = 0;', SERVER_ID);
$rConnections = intval($db->get_row()['count']);
$db->query('SELECT `activity_id` FROM `lines_live` WHERE `server_id` = ? AND `hls_end` = 0 GROUP BY `user_id`;', SERVER_ID);
$rUsers = intval($db->num_rows());
$db->query('SELECT `activity_id` FROM `lines_live` WHERE `hls_end` = 0 GROUP BY `user_id`;');
$rAllUsers = intval($db->num_rows());
}
if (SettingsManager::get('redis_handler')) {
$rConnections = $rServers[SERVER_ID]['connections'];
$rUsers = $rServers[SERVER_ID]['users'];
$rAllUsers = 0;
foreach (array_keys($rServers) as $rServerID) {
if ($rServers[$rServerID]['server_online']) {
$rAllUsers += $rServers[$rServerID]['users'];
}
}
} else {
$db->query('SELECT COUNT(*) AS `count` FROM `lines_live` WHERE `server_id` = ? AND `hls_end` = 0;', SERVER_ID);
$rConnections = intval($db->get_row()['count']);
$db->query('SELECT `activity_id` FROM `lines_live` WHERE `server_id` = ? AND `hls_end` = 0 GROUP BY `user_id`;', SERVER_ID);
$rUsers = intval($db->num_rows());
$db->query('SELECT `activity_id` FROM `lines_live` WHERE `hls_end` = 0 GROUP BY `user_id`;');
$rAllUsers = intval($db->num_rows());
}
$db->query('SELECT COUNT(*) AS `count` FROM `streams_servers` LEFT JOIN `streams` ON `streams`.`id` = `streams_servers`.`stream_id` WHERE `server_id` = ? AND `pid` > 0 AND `type` = 1;', SERVER_ID);
$rStreams = intval($db->get_row()['count']);
$db->query('SELECT COUNT(*) AS `count` FROM `streams_servers` LEFT JOIN `streams` ON `streams`.`id` = `streams_servers`.`stream_id` WHERE `server_id` = ? AND `pid` > 0 AND `type` = 1;', SERVER_ID);
$rStreams = intval($db->get_row()['count']);
$rPing = 0;
if (!$rServers[SERVER_ID]['is_main']) {
$rMainID = null;
foreach ($rServers as $rServerID => $rServerArray) {
if ($rServerArray['is_main']) {
$rMainID = $rServerID;
break;
}
}
if ($rMainID) {
$rPing = $this->pingServer($rServers[$rMainID]['server_ip'], $rServers[$rMainID]['http_broadcast_port']);
}
}
$rPing = 0;
if (!$rServers[SERVER_ID]['is_main']) {
$rMainID = null;
foreach ($rServers as $rServerID => $rServerArray) {
if ($rServerArray['is_main']) {
$rMainID = $rServerID;
break;
}
}
if ($rMainID) {
$rPing = $this->pingServer($rServers[$rMainID]['server_ip'], $rServers[$rMainID]['http_broadcast_port']);
}
}
$rSysCtl = file_get_contents('/etc/sysctl.conf');
$rGovernors = array();
if (shell_exec('which cpufreq-info')) {
$rGovernors = array_filter(explode(' ', trim(shell_exec('cpufreq-info -g') ?? '')));
}
$rSysCtl = file_get_contents('/etc/sysctl.conf');
$rGovernors = [];
if (shell_exec('which cpufreq-info')) {
$rGovernors = array_filter(explode(' ', trim(shell_exec('cpufreq-info -g') ?? '')));
}
$rAddresses = array_values(array_unique(array_map('trim', explode("\n", shell_exec("ip -4 addr | grep -oP '(?<=inet\\s)\\d+(\\.\\d+){3}'")))));
$rAddresses = array_values(array_unique(array_map('trim', explode("\n", shell_exec("ip -4 addr | grep -oP '(?<=inet\\s)\\d+(\\.\\d+){3}'")))));
$db->query('INSERT INTO `servers_stats`(`server_id`, `connections`, `total_users`, `users`, `streams`, `cpu`, `cpu_cores`, `cpu_avg`, `total_mem`, `total_mem_free`, `total_mem_used`, `total_mem_used_percent`, `total_disk_space`, `uptime`, `total_running_streams`, `bytes_sent`, `bytes_received`, `bytes_sent_total`, `bytes_received_total`, `cpu_load_average`, `gpu_info`, `iostat_info`, `time`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP());', SERVER_ID, $rConnections, $rAllUsers, $rUsers, $rStreams, $rStats['cpu'], $rStats['cpu_cores'], $rStats['cpu_avg'], $rStats['total_mem'], $rStats['total_mem_free'], $rStats['total_mem_used'], $rStats['total_mem_used_percent'], $rStats['total_disk_space'], $rStats['uptime'], $rStats['total_running_streams'], $rStats['bytes_sent'], $rStats['bytes_received'], $rStats['bytes_sent_total'], $rStats['bytes_received_total'], $rStats['cpu_load_average'], json_encode($rStats['gpu_info'], JSON_UNESCAPED_UNICODE), json_encode($rStats['iostat_info'], JSON_UNESCAPED_UNICODE));
$db->query('INSERT INTO `servers_stats`(`server_id`, `connections`, `total_users`, `users`, `streams`, `cpu`, `cpu_cores`, `cpu_avg`, `total_mem`, `total_mem_free`, `total_mem_used`, `total_mem_used_percent`, `total_disk_space`, `uptime`, `total_running_streams`, `bytes_sent`, `bytes_received`, `bytes_sent_total`, `bytes_received_total`, `cpu_load_average`, `gpu_info`, `iostat_info`, `time`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP());', SERVER_ID, $rConnections, $rAllUsers, $rUsers, $rStreams, $rStats['cpu'], $rStats['cpu_cores'], $rStats['cpu_avg'], $rStats['total_mem'], $rStats['total_mem_free'], $rStats['total_mem_used'], $rStats['total_mem_used_percent'], $rStats['total_disk_space'], $rStats['uptime'], $rStats['total_running_streams'], $rStats['bytes_sent'], $rStats['bytes_received'], $rStats['bytes_sent_total'], $rStats['bytes_received_total'], $rStats['cpu_load_average'], json_encode($rStats['gpu_info'], JSON_UNESCAPED_UNICODE), json_encode($rStats['iostat_info'], JSON_UNESCAPED_UNICODE));
$db->query('UPDATE `servers` SET `remote_status` = ?, `xc_vm_version` = ?, `server_hardware` = ?,`whitelist_ips` = ?, `governors` = ?, `sysctl` = ?, `video_devices` = ?, `audio_devices` = ?, `gpu_info` = ?, `interfaces` = ?, `time_offset` = ' . intval(time()) . ' - UNIX_TIMESTAMP(), `ping` = ? WHERE `id` = ?', $rRemoteStatus, XC_VM_VERSION, json_encode($rHardware, JSON_UNESCAPED_UNICODE), json_encode($rAddresses, JSON_UNESCAPED_UNICODE), json_encode($rGovernors, JSON_UNESCAPED_UNICODE), $rSysCtl, json_encode($rStats['video_devices'], JSON_UNESCAPED_UNICODE), json_encode($rStats['audio_devices'], JSON_UNESCAPED_UNICODE), json_encode($rStats['gpu_info'], JSON_UNESCAPED_UNICODE), json_encode($rStats['interfaces'], JSON_UNESCAPED_UNICODE), $rPing, SERVER_ID);
$db->query('UPDATE `servers` SET `remote_status` = ?, `xc_vm_version` = ?, `server_hardware` = ?,`whitelist_ips` = ?, `governors` = ?, `sysctl` = ?, `video_devices` = ?, `audio_devices` = ?, `gpu_info` = ?, `interfaces` = ?, `time_offset` = ' . intval(time()) . ' - UNIX_TIMESTAMP(), `ping` = ? WHERE `id` = ?', $rRemoteStatus, XC_VM_VERSION, json_encode($rHardware, JSON_UNESCAPED_UNICODE), json_encode($rAddresses, JSON_UNESCAPED_UNICODE), json_encode($rGovernors, JSON_UNESCAPED_UNICODE), $rSysCtl, json_encode($rStats['video_devices'], JSON_UNESCAPED_UNICODE), json_encode($rStats['audio_devices'], JSON_UNESCAPED_UNICODE), json_encode($rStats['gpu_info'], JSON_UNESCAPED_UNICODE), json_encode($rStats['interfaces'], JSON_UNESCAPED_UNICODE), $rPing, SERVER_ID);
if ($rServers[SERVER_ID]['is_main']) {
foreach ($rServers as $rServerID => $rServerArray) {
if ($rServerArray['server_online'] != $rServerArray['last_status']) {
$db->query('UPDATE `servers` SET `last_status` = ? WHERE `id` = ?;', $rServerArray['server_online'], $rServerID);
}
}
$db->query('DELETE FROM `signals` WHERE `time` <= ?;', time() - 86400);
}
}
if ($rServers[SERVER_ID]['is_main']) {
foreach ($rServers as $rServerID => $rServerArray) {
if ($rServerArray['server_online'] != $rServerArray['last_status']) {
$db->query('UPDATE `servers` SET `last_status` = ? WHERE `id` = ?;', $rServerArray['server_online'], $rServerID);
}
}
$db->query('DELETE FROM `signals` WHERE `time` <= ?;', time() - 86400);
}
}
}
+54 -54
View File
@@ -17,73 +17,73 @@ use XcVm\Domain\Server\ServerRepository;
*/
class StatsCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:stats';
}
public function getName(): string {
return 'cron:stats';
}
public function getDescription(): string {
return 'Cron: recalculate stream statistics (rating, uptime, connections)';
}
public function getDescription(): string {
return 'Cron: recalculate stream statistics (rating, uptime, connections)';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Stats]');
$this->initCron('XC_VM[Stats]');
$rTimeout = 60;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
$rTimeout = 60;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
$this->loadCron();
$this->loadCron();
return 0;
}
return 0;
}
private function loadCron(): void {
global $db;
private function loadCron(): void {
global $db;
if (!ServerRepository::getAll()[SERVER_ID]['is_main']) {
return;
}
if (!ServerRepository::getAll()[SERVER_ID]['is_main']) {
return;
}
$rTime = time();
$rDates = array(
'today' => array($rTime - 86400, $rTime),
'week' => array($rTime - 604800, $rTime),
'month' => array($rTime - 2592000, $rTime),
'all' => array(0, $rTime),
);
$rTime = time();
$rDates = [
'today' => [$rTime - 86400, $rTime],
'week' => [$rTime - 604800, $rTime],
'month' => [$rTime - 2592000, $rTime],
'all' => [0, $rTime],
];
$db->query('TRUNCATE `streams_stats`;');
$db->query('TRUNCATE `streams_stats`;');
foreach ($rDates as $rType => $rDate) {
$rStats = array();
foreach ($rDates as $rType => $rDate) {
$rStats = [];
$db->query('SELECT `stream_id`, COUNT(*) AS `connections`, SUM(`date_end` - `date_start`) AS `time`, COUNT(DISTINCT(`user_id`)) AS `users` FROM `lines_activity` LEFT JOIN `streams` ON `streams`.`id` = `lines_activity`.`stream_id` WHERE `date_start` > ? AND `date_end` <= ? GROUP BY `stream_id`;', $rDate[0], $rDate[1]);
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
$rStats[$rRow['stream_id']] = array('rank' => 0, 'time' => intval($rRow['time']), 'connections' => $rRow['connections'], 'users' => $rRow['users']);
}
}
$db->query('SELECT `stream_id`, COUNT(*) AS `connections`, SUM(`date_end` - `date_start`) AS `time`, COUNT(DISTINCT(`user_id`)) AS `users` FROM `lines_activity` LEFT JOIN `streams` ON `streams`.`id` = `lines_activity`.`stream_id` WHERE `date_start` > ? AND `date_end` <= ? GROUP BY `stream_id`;', $rDate[0], $rDate[1]);
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
$rStats[$rRow['stream_id']] = ['rank' => 0, 'time' => intval($rRow['time']), 'connections' => $rRow['connections'], 'users' => $rRow['users']];
}
}
$db->query('SELECT `stream_id`, SUM(`date_end` - `date_start`) AS `time` FROM `lines_activity` LEFT JOIN `streams` ON `streams`.`id` = `lines_activity`.`stream_id` WHERE `date_start` > ? AND `date_end` <= ? GROUP BY `stream_id` ORDER BY `time` DESC, `stream_id` DESC;', $rDate[0], $rDate[1]);
if ($db->num_rows() > 0) {
$rRank = 1;
foreach ($db->get_rows() as $rRow) {
if (isset($rStats[$rRow['stream_id']])) {
$rStats[$rRow['stream_id']]['rank'] = $rRank;
$rRank++;
}
}
}
$db->query('SELECT `stream_id`, SUM(`date_end` - `date_start`) AS `time` FROM `lines_activity` LEFT JOIN `streams` ON `streams`.`id` = `lines_activity`.`stream_id` WHERE `date_start` > ? AND `date_end` <= ? GROUP BY `stream_id` ORDER BY `time` DESC, `stream_id` DESC;', $rDate[0], $rDate[1]);
if ($db->num_rows() > 0) {
$rRank = 1;
foreach ($db->get_rows() as $rRow) {
if (isset($rStats[$rRow['stream_id']])) {
$rStats[$rRow['stream_id']]['rank'] = $rRank;
$rRank++;
}
}
}
foreach ($rStats as $rStreamID => $rArray) {
$db->query('INSERT INTO `streams_stats`(`stream_id`, `rank`, `time`, `connections`, `users`, `type`) VALUES(?, ?, ?, ?, ?, ?);', $rStreamID, $rArray['rank'], $rArray['time'], $rArray['connections'], $rArray['users'], $rType);
}
}
}
foreach ($rStats as $rStreamID => $rArray) {
$db->query('INSERT INTO `streams_stats`(`stream_id`, `rank`, `time`, `connections`, `users`, `type`) VALUES(?, ?, ?, ?, ?, ?);', $rStreamID, $rArray['rank'], $rArray['time'], $rArray['connections'], $rArray['users'], $rType);
}
}
}
}
+395 -395
View File
@@ -26,429 +26,429 @@ use XcVm\Streaming\Fanout\FanoutClient;
*/
class StreamsCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:streams';
}
public function getName(): string {
return 'cron:streams';
}
public function getDescription(): string {
return 'Cron: check live streams, monitors, on-demand, rogue PIDs';
}
public function getDescription(): string {
return 'Cron: check live streams, monitors, on-demand, rogue PIDs';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Live Checker]');
$this->loadCron();
$this->initCron('XC_VM[Live Checker]');
$this->loadCron();
return 0;
}
return 0;
}
/**
* Whether handing a stream (whose watchdog is gone) to the fanout supervisor
* must restart its producer rather than adopt it.
*
* An ffmpeg producer's feed into the daemon is a tee slave that, once broken
* by a daemon restart, stays broken for the life of the process adopting it
* would leave the daemon's viewers on dead air until a stall restart. The
* native remuxer redials the ingest socket by itself, so it is adopted and
* the channel does not blink.
*/
private static function handOverNeedsRestart(array $rStream): int {
$rPID = file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.pid') ? intval(@file_get_contents(STREAMS_PATH . $rStream['stream_id'] . '_.pid')) : intval($rStream['pid']);
if (!ProcessManager::isStreamRunning($rPID, $rStream['stream_id'])) {
return 0; // nothing running: a plain start
}
$rExe = basename((string) @readlink('/proc/' . $rPID . '/exe'));
return (strpos($rExe, 'ffmpeg') === 0 && FanoutClient::daemonStreamMissing(intval($rStream['stream_id']))) ? 1 : 0;
}
/**
* Whether handing a stream (whose watchdog is gone) to the fanout supervisor
* must restart its producer rather than adopt it.
*
* An ffmpeg producer's feed into the daemon is a tee slave that, once broken
* by a daemon restart, stays broken for the life of the process adopting it
* would leave the daemon's viewers on dead air until a stall restart. The
* native remuxer redials the ingest socket by itself, so it is adopted and
* the channel does not blink.
*/
private static function handOverNeedsRestart(array $rStream): int {
$rPID = file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.pid') ? intval(@file_get_contents(STREAMS_PATH . $rStream['stream_id'] . '_.pid')) : intval($rStream['pid']);
if (!ProcessManager::isStreamRunning($rPID, $rStream['stream_id'])) {
return 0; // nothing running: a plain start
}
$rExe = basename((string) @readlink('/proc/' . $rPID . '/exe'));
return (strpos($rExe, 'ffmpeg') === 0 && FanoutClient::daemonStreamMissing(intval($rStream['stream_id']))) ? 1 : 0;
}
/**
* Fold the producer's CPU, memory and kind into the stream's progress JSON
* what the admin streams list shows per stream.
*
* Only this node can read its own /proc, so the reading is taken here and
* travels to the panel in the row the cron already writes. CPU is the
* difference against this pass's predecessor, so it is the average over the
* last minute rather than ffmpeg's lifetime average and where there is no
* usable predecessor (the producer's first pass, or a new pid after a
* restart) the lifetime average stands in, so the column never waits a pass
* to show a figure.
*
* The previous reading is kept beside the stream's files (`<id>_.usage`, on
* the streams tmpfs, removed with the rest of `<id>_*` when it stops) rather
* than in the database row: it is this node's bookkeeping, and a value that
* has to survive a round trip through a row other code also rewrites is one
* that sometimes does not.
*
* @param string $rProgressJson This pass's progress report.
* @param int $rStreamID The stream.
* @param int $rPID The producer's pid.
* @return string The JSON to store.
*/
private static function withResourceUsage(string $rProgressJson, int $rStreamID, int $rPID): string {
$rProgress = json_decode($rProgressJson, true);
if (!is_array($rProgress)) {
$rProgress = array();
}
// Keys an earlier version kept in the row for the CPU difference.
unset($rProgress['cpu_t'], $rProgress['cpu_at']);
/**
* Fold the producer's CPU, memory and kind into the stream's progress JSON
* what the admin streams list shows per stream.
*
* Only this node can read its own /proc, so the reading is taken here and
* travels to the panel in the row the cron already writes. CPU is the
* difference against this pass's predecessor, so it is the average over the
* last minute rather than ffmpeg's lifetime average and where there is no
* usable predecessor (the producer's first pass, or a new pid after a
* restart) the lifetime average stands in, so the column never waits a pass
* to show a figure.
*
* The previous reading is kept beside the stream's files (`<id>_.usage`, on
* the streams tmpfs, removed with the rest of `<id>_*` when it stops) rather
* than in the database row: it is this node's bookkeeping, and a value that
* has to survive a round trip through a row other code also rewrites is one
* that sometimes does not.
*
* @param string $rProgressJson This pass's progress report.
* @param int $rStreamID The stream.
* @param int $rPID The producer's pid.
* @return string The JSON to store.
*/
private static function withResourceUsage(string $rProgressJson, int $rStreamID, int $rPID): string {
$rProgress = json_decode($rProgressJson, true);
if (!is_array($rProgress)) {
$rProgress = [];
}
// Keys an earlier version kept in the row for the CPU difference.
unset($rProgress['cpu_t'], $rProgress['cpu_at']);
$rSamplePath = STREAMS_PATH . $rStreamID . '_.usage';
$rSample = ProcessManager::resourceSample($rPID);
if ($rSample === null) {
@unlink($rSamplePath);
unset($rProgress['cpu'], $rProgress['mem'], $rProgress['producer']);
return json_encode($rProgress);
}
$rSamplePath = STREAMS_PATH . $rStreamID . '_.usage';
$rSample = ProcessManager::resourceSample($rPID);
if ($rSample === null) {
@unlink($rSamplePath);
unset($rProgress['cpu'], $rProgress['mem'], $rProgress['producer']);
return json_encode($rProgress);
}
$rCPU = null;
$rPrevious = json_decode((string) @file_get_contents($rSamplePath), true);
if (is_array($rPrevious) && intval($rPrevious['pid'] ?? 0) === $rPID) {
$rCPU = ProcessManager::cpuPercent($rSample, $rPrevious);
}
if ($rCPU === null) {
$rCPU = ProcessManager::cpuPercentSinceStart($rSample);
}
@file_put_contents($rSamplePath, json_encode(array('pid' => $rPID, 'ticks' => $rSample['ticks'], 'at' => $rSample['at'])));
$rCPU = null;
$rPrevious = json_decode((string) @file_get_contents($rSamplePath), true);
if (is_array($rPrevious) && intval($rPrevious['pid'] ?? 0) === $rPID) {
$rCPU = ProcessManager::cpuPercent($rSample, $rPrevious);
}
if ($rCPU === null) {
$rCPU = ProcessManager::cpuPercentSinceStart($rSample);
}
@file_put_contents($rSamplePath, json_encode(['pid' => $rPID, 'ticks' => $rSample['ticks'], 'at' => $rSample['at']]));
$rProgress['cpu'] = $rCPU;
$rProgress['mem'] = $rSample['rss'];
$rProgress['producer'] = ProcessManager::producerKind($rPID);
$rProgress['cpu'] = $rCPU;
$rProgress['mem'] = $rSample['rss'];
$rProgress['producer'] = ProcessManager::producerKind($rPID);
return json_encode($rProgress);
}
return json_encode($rProgress);
}
private function loadCron(): void {
$rRedis = SettingsManager::getBool('redis_handler');
global $db;
private function loadCron(): void {
$rRedis = SettingsManager::getBool('redis_handler');
global $db;
if (!ProcessManager::isNginxRunning()) {
echo 'XC_VM not running...' . "\n";
}
if (!ProcessManager::isNginxRunning()) {
echo 'XC_VM not running...' . "\n";
}
if ($rRedis) {
RedisManager::ensureConnected();
}
if ($rRedis) {
RedisManager::ensureConnected();
}
$rActivePIDs = array();
$rStreamIDs = array();
$rActivePIDs = [];
$rStreamIDs = [];
// Bring streams_servers in step with the fanout supervisor first, so the
// pass below reads what is actually running. $rSupervised is the set it
// is supervising, or null when it cannot be asked — unknown, which the
// checks below treat as "not supervised" exactly as before this existed.
$rStates = FanoutClient::monitorStates();
$rSupervised = StreamProcess::reconcileSupervised($rStates);
$rSupervisedSet = array_flip($rSupervised ?? array());
// While the daemon takes hand-overs, streams still under a PHP monitor
// (started before supervision was on, or while the daemon was down) are
// moved to it — adopting their running encoder, so they do not restart.
$rMigrate = $rStates !== null && !empty($rStates['accepting']) && StreamProcess::supervisionEnabled();
// Bring streams_servers in step with the fanout supervisor first, so the
// pass below reads what is actually running. $rSupervised is the set it
// is supervising, or null when it cannot be asked — unknown, which the
// checks below treat as "not supervised" exactly as before this existed.
$rStates = FanoutClient::monitorStates();
$rSupervised = StreamProcess::reconcileSupervised($rStates);
$rSupervisedSet = array_flip($rSupervised ?? []);
// While the daemon takes hand-overs, streams still under a PHP monitor
// (started before supervision was on, or while the daemon was down) are
// moved to it — adopting their running encoder, so they do not restart.
$rMigrate = $rStates !== null && !empty($rStates['accepting']) && StreamProcess::supervisionEnabled();
if ($rRedis) {
$db->query('SELECT t2.stream_display_name, t2.delay_minutes, t1.stream_started, t1.stream_info, t2.fps_restart, t1.stream_status, t1.progress_info, t1.stream_id, t1.monitor_pid, t1.on_demand, t1.server_stream_id, t1.pid, servers_attached.attached, t2.vframes_server_id, t2.vframes_pid, t2.tv_archive_server_id, t2.tv_archive_pid FROM `streams_servers` t1 INNER JOIN `streams` t2 ON t2.id = t1.stream_id AND t2.direct_source = 0 INNER JOIN `streams_types` t3 ON t3.type_id = t2.type LEFT JOIN (SELECT `stream_id`, COUNT(*) AS `attached` FROM `streams_servers` WHERE `parent_id` = ? AND `pid` IS NOT NULL AND `pid` > 0 AND `monitor_pid` IS NOT NULL AND `monitor_pid` > 0 GROUP BY `stream_id`) AS `servers_attached` ON `servers_attached`.`stream_id` = t1.`stream_id` WHERE (t1.pid IS NOT NULL OR t1.stream_status <> 0 OR t1.to_analyze = 1) AND t1.server_id = ? AND t3.live = 1', SERVER_ID, SERVER_ID);
} else {
$db->query("SELECT t2.stream_display_name, t2.delay_minutes, t1.stream_started, t1.stream_info, t2.fps_restart, t1.stream_status, t1.progress_info, t1.stream_id, t1.monitor_pid, t1.on_demand, t1.server_stream_id, t1.pid, clients.online_clients, clients_hls.online_clients_hls, servers_attached.attached, t2.vframes_server_id, t2.vframes_pid, t2.tv_archive_server_id, t2.tv_archive_pid FROM `streams_servers` t1 INNER JOIN `streams` t2 ON t2.id = t1.stream_id AND t2.direct_source = 0 INNER JOIN `streams_types` t3 ON t3.type_id = t2.type LEFT JOIN (SELECT stream_id, COUNT(*) as online_clients FROM `lines_live` WHERE `server_id` = ? AND `hls_end` = 0 GROUP BY stream_id) AS clients ON clients.stream_id = t1.stream_id LEFT JOIN (SELECT `stream_id`, COUNT(*) AS `attached` FROM `streams_servers` WHERE `parent_id` = ? AND `pid` IS NOT NULL AND `pid` > 0 AND `monitor_pid` IS NOT NULL AND `monitor_pid` > 0 GROUP BY `stream_id`) AS `servers_attached` ON `servers_attached`.`stream_id` = t1.`stream_id` LEFT JOIN (SELECT stream_id, COUNT(*) as online_clients_hls FROM `lines_live` WHERE `server_id` = ? AND `container` = 'hls' AND `hls_end` = 0 GROUP BY stream_id) AS clients_hls ON clients_hls.stream_id = t1.stream_id WHERE (t1.pid IS NOT NULL OR t1.stream_status <> 0 OR t1.to_analyze = 1) AND t1.server_id = ? AND t3.live = 1", SERVER_ID, SERVER_ID, SERVER_ID, SERVER_ID);
}
if ($rRedis) {
$db->query('SELECT t2.stream_display_name, t2.delay_minutes, t1.stream_started, t1.stream_info, t2.fps_restart, t1.stream_status, t1.progress_info, t1.stream_id, t1.monitor_pid, t1.on_demand, t1.server_stream_id, t1.pid, servers_attached.attached, t2.vframes_server_id, t2.vframes_pid, t2.tv_archive_server_id, t2.tv_archive_pid FROM `streams_servers` t1 INNER JOIN `streams` t2 ON t2.id = t1.stream_id AND t2.direct_source = 0 INNER JOIN `streams_types` t3 ON t3.type_id = t2.type LEFT JOIN (SELECT `stream_id`, COUNT(*) AS `attached` FROM `streams_servers` WHERE `parent_id` = ? AND `pid` IS NOT NULL AND `pid` > 0 AND `monitor_pid` IS NOT NULL AND `monitor_pid` > 0 GROUP BY `stream_id`) AS `servers_attached` ON `servers_attached`.`stream_id` = t1.`stream_id` WHERE (t1.pid IS NOT NULL OR t1.stream_status <> 0 OR t1.to_analyze = 1) AND t1.server_id = ? AND t3.live = 1', SERVER_ID, SERVER_ID);
} else {
$db->query("SELECT t2.stream_display_name, t2.delay_minutes, t1.stream_started, t1.stream_info, t2.fps_restart, t1.stream_status, t1.progress_info, t1.stream_id, t1.monitor_pid, t1.on_demand, t1.server_stream_id, t1.pid, clients.online_clients, clients_hls.online_clients_hls, servers_attached.attached, t2.vframes_server_id, t2.vframes_pid, t2.tv_archive_server_id, t2.tv_archive_pid FROM `streams_servers` t1 INNER JOIN `streams` t2 ON t2.id = t1.stream_id AND t2.direct_source = 0 INNER JOIN `streams_types` t3 ON t3.type_id = t2.type LEFT JOIN (SELECT stream_id, COUNT(*) as online_clients FROM `lines_live` WHERE `server_id` = ? AND `hls_end` = 0 GROUP BY stream_id) AS clients ON clients.stream_id = t1.stream_id LEFT JOIN (SELECT `stream_id`, COUNT(*) AS `attached` FROM `streams_servers` WHERE `parent_id` = ? AND `pid` IS NOT NULL AND `pid` > 0 AND `monitor_pid` IS NOT NULL AND `monitor_pid` > 0 GROUP BY `stream_id`) AS `servers_attached` ON `servers_attached`.`stream_id` = t1.`stream_id` LEFT JOIN (SELECT stream_id, COUNT(*) as online_clients_hls FROM `lines_live` WHERE `server_id` = ? AND `container` = 'hls' AND `hls_end` = 0 GROUP BY stream_id) AS clients_hls ON clients_hls.stream_id = t1.stream_id WHERE (t1.pid IS NOT NULL OR t1.stream_status <> 0 OR t1.to_analyze = 1) AND t1.server_id = ? AND t3.live = 1", SERVER_ID, SERVER_ID, SERVER_ID, SERVER_ID);
}
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rStream) {
echo 'Stream ID: ' . $rStream['stream_id'] . "\n";
$rStreamIDs[] = $rStream['stream_id'];
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rStream) {
echo 'Stream ID: ' . $rStream['stream_id'] . "\n";
$rStreamIDs[] = $rStream['stream_id'];
$rIsSupervised = isset($rSupervisedSet[intval($rStream['stream_id'])]);
// superviseStream, not startMonitor: a stream the daemon will not
// take (delay, created channels) must keep the PHP monitor it has,
// not have a second one spawned beside it every pass.
if ($rMigrate && !$rIsSupervised && ProcessManager::isMonitorAlive($rStream['monitor_pid'], $rStream['stream_id'])) {
if (StreamProcess::superviseStream(intval($rStream['stream_id']), false)) {
echo 'Handed over to the fanout supervisor.' . "\n\n";
continue;
}
}
if ($rIsSupervised || ProcessManager::isMonitorAlive($rStream['monitor_pid'], $rStream['stream_id']) || $rStream['on_demand']) {
if ($rStream['on_demand'] == 1 && $rStream['attached'] == 0) {
if ($rRedis) {
$rCount = 0;
$rRedis = RedisManager::instance();
if ($rRedis) {
$rKeys = $rRedis->zRangeByScore('STREAM#' . $rStream['stream_id'], '-inf', '+inf');
if (count($rKeys) > 0) {
$rConnections = array_map('igbinary_unserialize', $rRedis->mGet($rKeys));
foreach ($rConnections as $rConnection) {
if ($rConnection && $rConnection['server_id'] == SERVER_ID) {
$rCount++;
}
}
}
}
$rStream['online_clients'] = $rCount;
}
$rIsSupervised = isset($rSupervisedSet[intval($rStream['stream_id'])]);
// superviseStream, not startMonitor: a stream the daemon will not
// take (delay, created channels) must keep the PHP monitor it has,
// not have a second one spawned beside it every pass.
if ($rMigrate && !$rIsSupervised && ProcessManager::isMonitorAlive($rStream['monitor_pid'], $rStream['stream_id'])) {
if (StreamProcess::superviseStream(intval($rStream['stream_id']), false)) {
echo 'Handed over to the fanout supervisor.' . "\n\n";
continue;
}
}
if ($rIsSupervised || ProcessManager::isMonitorAlive($rStream['monitor_pid'], $rStream['stream_id']) || $rStream['on_demand']) {
if ($rStream['on_demand'] == 1 && $rStream['attached'] == 0) {
if ($rRedis) {
$rCount = 0;
$rRedis = RedisManager::instance();
if ($rRedis) {
$rKeys = $rRedis->zRangeByScore('STREAM#' . $rStream['stream_id'], '-inf', '+inf');
if (count($rKeys) > 0) {
$rConnections = array_map('igbinary_unserialize', $rRedis->mGet($rKeys));
foreach ($rConnections as $rConnection) {
if ($rConnection && $rConnection['server_id'] == SERVER_ID) {
$rCount++;
}
}
}
}
$rStream['online_clients'] = $rCount;
}
$rAdminQueue = $rQueue = 0;
if (SettingsManager::getBool('on_demand_instant_off') && file_exists(SIGNALS_TMP_PATH . 'queue_' . intval($rStream['stream_id']))) {
foreach ((igbinary_unserialize(file_get_contents(SIGNALS_TMP_PATH . 'queue_' . intval($rStream['stream_id']))) ?: array()) as $rPID) {
if (ProcessManager::isRunning($rPID, 'php-fpm')) {
$rQueue++;
}
}
}
if (file_exists(SIGNALS_TMP_PATH . 'admin_' . intval($rStream['stream_id']))) {
if (time() - filemtime(SIGNALS_TMP_PATH . 'admin_' . intval($rStream['stream_id'])) <= 30) {
$rAdminQueue = 1;
} else {
unlink(SIGNALS_TMP_PATH . 'admin_' . intval($rStream['stream_id']));
}
}
if ($rQueue == 0 && $rAdminQueue == 0 && $rStream['online_clients'] == 0 && (file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.m3u8') || SettingsManager::getInt('on_demand_wait_time') < time() - intval($rStream['stream_started']) || $rStream['stream_status'] == 1)) {
echo 'Stop on-demand stream...' . "\n\n";
StreamProcess::stopStream($rStream['stream_id'], true);
// Stopped: nothing below applies (it would start a thumbnail
// and a TV archive worker for the stream just stopped).
continue;
}
}
$rAdminQueue = $rQueue = 0;
if (SettingsManager::getBool('on_demand_instant_off') && file_exists(SIGNALS_TMP_PATH . 'queue_' . intval($rStream['stream_id']))) {
foreach ((igbinary_unserialize(file_get_contents(SIGNALS_TMP_PATH . 'queue_' . intval($rStream['stream_id']))) ?: []) as $rPID) {
if (ProcessManager::isRunning($rPID, 'php-fpm')) {
$rQueue++;
}
}
}
if (file_exists(SIGNALS_TMP_PATH . 'admin_' . intval($rStream['stream_id']))) {
if (time() - filemtime(SIGNALS_TMP_PATH . 'admin_' . intval($rStream['stream_id'])) <= 30) {
$rAdminQueue = 1;
} else {
unlink(SIGNALS_TMP_PATH . 'admin_' . intval($rStream['stream_id']));
}
}
if ($rQueue == 0 && $rAdminQueue == 0 && $rStream['online_clients'] == 0 && (file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.m3u8') || SettingsManager::getInt('on_demand_wait_time') < time() - intval($rStream['stream_started']) || $rStream['stream_status'] == 1)) {
echo 'Stop on-demand stream...' . "\n\n";
StreamProcess::stopStream($rStream['stream_id'], true);
// Stopped: nothing below applies (it would start a thumbnail
// and a TV archive worker for the stream just stopped).
continue;
}
}
if ($rStream['vframes_server_id'] == SERVER_ID && !ProcessManager::isNamedProcessRunning($rStream['vframes_pid'], 'Thumbnail', $rStream['stream_id'])) {
echo 'Start Thumbnail...' . "\n";
StreamProcess::startThumbnail($rStream['stream_id']);
}
if ($rStream['tv_archive_server_id'] == SERVER_ID && !ProcessManager::isNamedProcessRunning($rStream['tv_archive_pid'], 'TVArchive', $rStream['stream_id'])) {
echo 'Start TV Archive...' . "\n";
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php archive ' . intval($rStream['stream_id']) . ' >/dev/null 2>/dev/null & echo $!');
}
if ($rStream['vframes_server_id'] == SERVER_ID && !ProcessManager::isNamedProcessRunning($rStream['vframes_pid'], 'Thumbnail', $rStream['stream_id'])) {
echo 'Start Thumbnail...' . "\n";
StreamProcess::startThumbnail($rStream['stream_id']);
}
if ($rStream['tv_archive_server_id'] == SERVER_ID && !ProcessManager::isNamedProcessRunning($rStream['tv_archive_pid'], 'TVArchive', $rStream['stream_id'])) {
echo 'Start TV Archive...' . "\n";
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php archive ' . intval($rStream['stream_id']) . ' >/dev/null 2>/dev/null & echo $!');
}
foreach (glob(STREAMS_PATH . $rStream['stream_id'] . '_*.ts.enc') as $rFile) {
if (!file_exists(rtrim($rFile, '.enc'))) {
unlink($rFile);
}
}
foreach (glob(STREAMS_PATH . $rStream['stream_id'] . '_*.ts.enc') as $rFile) {
if (!file_exists(rtrim($rFile, '.enc'))) {
unlink($rFile);
}
}
if (file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.pid')) {
$rPID = intval(file_get_contents(STREAMS_PATH . $rStream['stream_id'] . '_.pid'));
} else {
$rPID = intval(shell_exec("ps aux | grep -v grep | grep '/" . intval($rStream['stream_id']) . "_.m3u8' | awk '{print \$2}'"));
}
$rActivePIDs[] = intval($rPID);
if (file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.pid')) {
$rPID = intval(file_get_contents(STREAMS_PATH . $rStream['stream_id'] . '_.pid'));
} else {
$rPID = intval(shell_exec("ps aux | grep -v grep | grep '/" . intval($rStream['stream_id']) . "_.m3u8' | awk '{print \$2}'"));
}
$rActivePIDs[] = intval($rPID);
$rPlaylist = STREAMS_PATH . $rStream['stream_id'] . '_.m3u8';
if (ProcessManager::isStreamRunning($rPID, $rStream['stream_id']) && file_exists($rPlaylist)) {
// Re-feed after a daemon restart (ADR 0003, C-ops). This
// stream's ffmpeg is running and teeing into the daemon,
// but if the daemon restarted it wiped its in-memory
// registry and `onfail=ignore` silently dropped the tee
// slave — the daemon no longer knows the stream
// (control /streams/<id> → 404) and delivery has quietly
// fallen back to legacy. Restart it so buildLive
// re-registers the ingest and the daemon serves it again.
// Guard: only a reachable-daemon 404 (daemonStreamMissing),
// so a stopped daemon leaves legacy alone; throttled by a
// stamp so a stream whose ingest keeps failing is not
// restart-looped every cron tick. Proxy streams have no
// local ffmpeg, so they never reach this running branch.
// Only an ffmpeg producer needs the restart: a broken tee slave
// stays broken. The PHP relays (LLOD, loopback) and a delayed
// stream's DelayCommand re-register and redial the daemon by
// themselves (IngestFeeder), and restarting a delayed stream
// would throw its buffer away.
$rSelfFeeding = intval($rStream['delay_minutes'] ?? 0) > 0 || ProcessManager::producerKind($rPID) === 'php';
if (!$rSelfFeeding && FanoutClient::daemonStreamMissing($rStream['stream_id'])) {
// The stamp lives outside STREAMS_PATH: the restart below
// runs `rm -f <id>_*` there, which deleted a `<id>_.refeed`
// stamp — and with it the 120 s throttle it was meant to be.
$rRefeedStamp = SIGNALS_TMP_PATH . 'refeed_' . intval($rStream['stream_id']);
if (!file_exists($rRefeedStamp) || time() - filemtime($rRefeedStamp) > 120) {
echo 'Daemon lost stream ' . $rStream['stream_id'] . ' (restarted) — re-feeding...' . "\n\n";
touch($rRefeedStamp);
StreamProcess::startMonitor($rStream['stream_id'], 1);
continue;
}
}
$rPlaylist = STREAMS_PATH . $rStream['stream_id'] . '_.m3u8';
if (ProcessManager::isStreamRunning($rPID, $rStream['stream_id']) && file_exists($rPlaylist)) {
// Re-feed after a daemon restart (ADR 0003, C-ops). This
// stream's ffmpeg is running and teeing into the daemon,
// but if the daemon restarted it wiped its in-memory
// registry and `onfail=ignore` silently dropped the tee
// slave — the daemon no longer knows the stream
// (control /streams/<id> → 404) and delivery has quietly
// fallen back to legacy. Restart it so buildLive
// re-registers the ingest and the daemon serves it again.
// Guard: only a reachable-daemon 404 (daemonStreamMissing),
// so a stopped daemon leaves legacy alone; throttled by a
// stamp so a stream whose ingest keeps failing is not
// restart-looped every cron tick. Proxy streams have no
// local ffmpeg, so they never reach this running branch.
// Only an ffmpeg producer needs the restart: a broken tee slave
// stays broken. The PHP relays (LLOD, loopback) and a delayed
// stream's DelayCommand re-register and redial the daemon by
// themselves (IngestFeeder), and restarting a delayed stream
// would throw its buffer away.
$rSelfFeeding = intval($rStream['delay_minutes'] ?? 0) > 0 || ProcessManager::producerKind($rPID) === 'php';
if (!$rSelfFeeding && FanoutClient::daemonStreamMissing($rStream['stream_id'])) {
// The stamp lives outside STREAMS_PATH: the restart below
// runs `rm -f <id>_*` there, which deleted a `<id>_.refeed`
// stamp — and with it the 120 s throttle it was meant to be.
$rRefeedStamp = SIGNALS_TMP_PATH . 'refeed_' . intval($rStream['stream_id']);
if (!file_exists($rRefeedStamp) || time() - filemtime($rRefeedStamp) > 120) {
echo 'Daemon lost stream ' . $rStream['stream_id'] . ' (restarted) — re-feeding...' . "\n\n";
touch($rRefeedStamp);
StreamProcess::startMonitor($rStream['stream_id'], 1);
continue;
}
}
echo 'Update Stream Information...' . "\n";
$rBitrate = StreamUtils::getStreamBitrate('live', STREAMS_PATH . $rStream['stream_id'] . '_.m3u8');
$rProgressPath = STREAMS_PATH . $rStream['stream_id'] . '_.progress';
if (file_exists($rProgressPath)) {
// ffmpeg appends key=value progress reports to this file
// for the stream's whole life. Read only the tail, keep
// the last COMPLETE report block (terminated by a
// "progress=" line) and re-encode it as the JSON the rest
// of the panel expects. Then truncate so the file stays
// tiny on disk (ffmpeg keeps its write offset, so the
// hole left behind is sparse).
$rTail = '';
$rFp = fopen($rProgressPath, 'rb');
if ($rFp !== false) {
fseek($rFp, 0, SEEK_END);
if (ftell($rFp) > 16384) {
fseek($rFp, -16384, SEEK_END);
} else {
rewind($rFp);
}
$rTail = stream_get_contents($rFp);
fclose($rFp);
}
$rReport = array();
$rCurrentReport = array();
foreach (explode("\n", (string) $rTail) as $rLine) {
$rLine = trim($rLine);
if ($rLine === '') {
continue;
}
$rKV = explode('=', $rLine, 2);
if (count($rKV) !== 2) {
continue;
}
$rReportKey = trim($rKV[0]);
$rCurrentReport[$rReportKey] = trim($rKV[1]);
if ($rReportKey === 'progress') {
$rReport = $rCurrentReport;
$rCurrentReport = array();
}
}
$rProgress = $rReport ? json_encode($rReport) : ($rStream['progress_info'] ?: json_encode(array()));
file_put_contents($rProgressPath, '');
if ($rStream['fps_restart']) {
file_put_contents(STREAMS_PATH . $rStream['stream_id'] . '_.progress_check', $rProgress);
}
} else {
$rProgress = $rStream['progress_info'];
}
$rProgress = self::withResourceUsage((string) $rProgress, intval($rStream['stream_id']), $rPID);
// A supervised stream's codecs, resolution and bitrate come
// from the daemon, measured off the bytes (reconcileSupervised
// wrote them above); recomputing them here from a stream_info
// that nothing probes any more would blank them. Only the
// producer's progress report is this pass's to record.
if ($rIsSupervised) {
if ($rProgress !== $rStream['progress_info']) {
$db->query('UPDATE `streams_servers` SET `progress_info` = ? WHERE `server_stream_id` = ?', $rProgress, $rStream['server_stream_id']);
}
echo "\n";
continue;
}
if (file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.stream_info')) {
$rStreamInfo = file_get_contents(STREAMS_PATH . $rStream['stream_id'] . '_.stream_info');
unlink(STREAMS_PATH . $rStream['stream_id'] . '_.stream_info');
} else {
$rStreamInfo = $rStream['stream_info'];
}
$rCompatible = 0;
$rAudioCodec = $rVideoCodec = $rResolution = null;
if ($rStreamInfo) {
$rStreamJSON = json_decode($rStreamInfo, true);
$rCompatible = intval(DiagnosticsService::checkCompatibility($rStreamJSON, SettingsManager::getBool('player_allow_hevc')));
if (is_array($rStreamJSON) && isset($rStreamJSON['codecs']) && is_array($rStreamJSON['codecs'])) {
$rAudioCodec = isset($rStreamJSON['codecs']['audio']['codec_name']) ? $rStreamJSON['codecs']['audio']['codec_name'] : null;
$rVideoCodec = isset($rStreamJSON['codecs']['video']['codec_name']) ? $rStreamJSON['codecs']['video']['codec_name'] : null;
$rResolution = isset($rStreamJSON['codecs']['video']['height']) ? $rStreamJSON['codecs']['video']['height'] : null;
}
if ($rResolution) {
$rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution);
}
}
if ($rStream['pid'] != $rPID) {
$db->query('UPDATE `streams_servers` SET `pid` = ?, `progress_info` = ?, `stream_info` = ?, `compatible` = ?, `bitrate` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ? WHERE `server_stream_id` = ?', $rPID, $rProgress, $rStreamInfo, $rCompatible, $rBitrate, $rAudioCodec, $rVideoCodec, $rResolution, $rStream['server_stream_id']);
} else {
$db->query('UPDATE `streams_servers` SET `progress_info` = ?, `stream_info` = ?, `compatible` = ?, `bitrate` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ? WHERE `server_stream_id` = ?', $rProgress, $rStreamInfo, $rCompatible, $rBitrate, $rAudioCodec, $rVideoCodec, $rResolution, $rStream['server_stream_id']);
}
}
echo "\n";
} else {
echo 'Start monitor...' . "\n\n";
if (StreamProcess::startMonitor($rStream['stream_id'], self::handOverNeedsRestart($rStream)) === StreamProcess::MONITOR_PHP) {
usleep(50000); // stagger PHP monitor spawns
}
}
}
}
echo 'Update Stream Information...' . "\n";
$rBitrate = StreamUtils::getStreamBitrate('live', STREAMS_PATH . $rStream['stream_id'] . '_.m3u8');
$rProgressPath = STREAMS_PATH . $rStream['stream_id'] . '_.progress';
if (file_exists($rProgressPath)) {
// ffmpeg appends key=value progress reports to this file
// for the stream's whole life. Read only the tail, keep
// the last COMPLETE report block (terminated by a
// "progress=" line) and re-encode it as the JSON the rest
// of the panel expects. Then truncate so the file stays
// tiny on disk (ffmpeg keeps its write offset, so the
// hole left behind is sparse).
$rTail = '';
$rFp = fopen($rProgressPath, 'rb');
if ($rFp !== false) {
fseek($rFp, 0, SEEK_END);
if (ftell($rFp) > 16384) {
fseek($rFp, -16384, SEEK_END);
} else {
rewind($rFp);
}
$rTail = stream_get_contents($rFp);
fclose($rFp);
}
$rReport = [];
$rCurrentReport = [];
foreach (explode("\n", (string) $rTail) as $rLine) {
$rLine = trim($rLine);
if ($rLine === '') {
continue;
}
$rKV = explode('=', $rLine, 2);
if (count($rKV) !== 2) {
continue;
}
$rReportKey = trim($rKV[0]);
$rCurrentReport[$rReportKey] = trim($rKV[1]);
if ($rReportKey === 'progress') {
$rReport = $rCurrentReport;
$rCurrentReport = [];
}
}
$rProgress = $rReport ? json_encode($rReport) : ($rStream['progress_info'] ?: json_encode([]));
file_put_contents($rProgressPath, '');
if ($rStream['fps_restart']) {
file_put_contents(STREAMS_PATH . $rStream['stream_id'] . '_.progress_check', $rProgress);
}
} else {
$rProgress = $rStream['progress_info'];
}
$rProgress = self::withResourceUsage((string) $rProgress, intval($rStream['stream_id']), $rPID);
// A supervised stream's codecs, resolution and bitrate come
// from the daemon, measured off the bytes (reconcileSupervised
// wrote them above); recomputing them here from a stream_info
// that nothing probes any more would blank them. Only the
// producer's progress report is this pass's to record.
if ($rIsSupervised) {
if ($rProgress !== $rStream['progress_info']) {
$db->query('UPDATE `streams_servers` SET `progress_info` = ? WHERE `server_stream_id` = ?', $rProgress, $rStream['server_stream_id']);
}
echo "\n";
continue;
}
if (file_exists(STREAMS_PATH . $rStream['stream_id'] . '_.stream_info')) {
$rStreamInfo = file_get_contents(STREAMS_PATH . $rStream['stream_id'] . '_.stream_info');
unlink(STREAMS_PATH . $rStream['stream_id'] . '_.stream_info');
} else {
$rStreamInfo = $rStream['stream_info'];
}
$rCompatible = 0;
$rAudioCodec = $rVideoCodec = $rResolution = null;
if ($rStreamInfo) {
$rStreamJSON = json_decode($rStreamInfo, true);
$rCompatible = intval(DiagnosticsService::checkCompatibility($rStreamJSON, SettingsManager::getBool('player_allow_hevc')));
if (is_array($rStreamJSON) && isset($rStreamJSON['codecs']) && is_array($rStreamJSON['codecs'])) {
$rAudioCodec = isset($rStreamJSON['codecs']['audio']['codec_name']) ? $rStreamJSON['codecs']['audio']['codec_name'] : null;
$rVideoCodec = isset($rStreamJSON['codecs']['video']['codec_name']) ? $rStreamJSON['codecs']['video']['codec_name'] : null;
$rResolution = isset($rStreamJSON['codecs']['video']['height']) ? $rStreamJSON['codecs']['video']['height'] : null;
}
if ($rResolution) {
$rResolution = StreamSorter::getNearest([240, 360, 480, 576, 720, 1080, 1440, 2160], $rResolution);
}
}
if ($rStream['pid'] != $rPID) {
$db->query('UPDATE `streams_servers` SET `pid` = ?, `progress_info` = ?, `stream_info` = ?, `compatible` = ?, `bitrate` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ? WHERE `server_stream_id` = ?', $rPID, $rProgress, $rStreamInfo, $rCompatible, $rBitrate, $rAudioCodec, $rVideoCodec, $rResolution, $rStream['server_stream_id']);
} else {
$db->query('UPDATE `streams_servers` SET `progress_info` = ?, `stream_info` = ?, `compatible` = ?, `bitrate` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ? WHERE `server_stream_id` = ?', $rProgress, $rStreamInfo, $rCompatible, $rBitrate, $rAudioCodec, $rVideoCodec, $rResolution, $rStream['server_stream_id']);
}
}
echo "\n";
} else {
echo 'Start monitor...' . "\n\n";
if (StreamProcess::startMonitor($rStream['stream_id'], self::handOverNeedsRestart($rStream)) === StreamProcess::MONITOR_PHP) {
usleep(50000); // stagger PHP monitor spawns
}
}
}
}
$db->query('SELECT `streams`.`id` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams`.`direct_source` = 1 AND `streams`.`direct_proxy` = 1 AND `streams_servers`.`server_id` = ? AND `streams_servers`.`pid` > 0;', SERVER_ID);
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rStream) {
if (file_exists(STREAMS_PATH . $rStream['id'] . '.analyse')) {
$rFFProbeOutput = FFprobeRunner::probeStream(STREAMS_PATH . $rStream['id'] . '.analyse');
// Defaults: the UPDATE below runs even when probing fails or
// the output has no codec info, so these must always be set.
$rBitrate = $rCompatible = $rAudioCodec = $rVideoCodec = $rResolution = null;
if ($rFFProbeOutput) {
$rBitrate = $rFFProbeOutput['bitrate'] / 1024;
$rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbeOutput, SettingsManager::getBool('player_allow_hevc')));
if (is_array($rFFProbeOutput) && isset($rFFProbeOutput['codecs']) && is_array($rFFProbeOutput['codecs'])) {
$rAudioCodec = isset($rFFProbeOutput['codecs']['audio']['codec_name']) ? $rFFProbeOutput['codecs']['audio']['codec_name'] : null;
$rVideoCodec = isset($rFFProbeOutput['codecs']['video']['codec_name']) ? $rFFProbeOutput['codecs']['video']['codec_name'] : null;
$rResolution = isset($rFFProbeOutput['codecs']['video']['height']) ? $rFFProbeOutput['codecs']['video']['height'] : null;
}
if ($rResolution) {
$rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution);
}
}
echo 'Stream ID: ' . $rStream['id'] . "\n";
echo 'Update Stream Information...' . "\n";
$db->query('UPDATE `streams_servers` SET `bitrate` = ?, `stream_info` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ?, `compatible` = ? WHERE `stream_id` = ? AND `server_id` = ?', $rBitrate, json_encode($rFFProbeOutput), $rAudioCodec, $rVideoCodec, $rResolution, $rCompatible, $rStream['id'], SERVER_ID);
}
$db->query('SELECT `streams`.`id` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams`.`direct_source` = 1 AND `streams`.`direct_proxy` = 1 AND `streams_servers`.`server_id` = ? AND `streams_servers`.`pid` > 0;', SERVER_ID);
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rStream) {
if (file_exists(STREAMS_PATH . $rStream['id'] . '.analyse')) {
$rFFProbeOutput = FFprobeRunner::probeStream(STREAMS_PATH . $rStream['id'] . '.analyse');
// Defaults: the UPDATE below runs even when probing fails or
// the output has no codec info, so these must always be set.
$rBitrate = $rCompatible = $rAudioCodec = $rVideoCodec = $rResolution = null;
if ($rFFProbeOutput) {
$rBitrate = $rFFProbeOutput['bitrate'] / 1024;
$rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbeOutput, SettingsManager::getBool('player_allow_hevc')));
if (is_array($rFFProbeOutput) && isset($rFFProbeOutput['codecs']) && is_array($rFFProbeOutput['codecs'])) {
$rAudioCodec = isset($rFFProbeOutput['codecs']['audio']['codec_name']) ? $rFFProbeOutput['codecs']['audio']['codec_name'] : null;
$rVideoCodec = isset($rFFProbeOutput['codecs']['video']['codec_name']) ? $rFFProbeOutput['codecs']['video']['codec_name'] : null;
$rResolution = isset($rFFProbeOutput['codecs']['video']['height']) ? $rFFProbeOutput['codecs']['video']['height'] : null;
}
if ($rResolution) {
$rResolution = StreamSorter::getNearest([240, 360, 480, 576, 720, 1080, 1440, 2160], $rResolution);
}
}
echo 'Stream ID: ' . $rStream['id'] . "\n";
echo 'Update Stream Information...' . "\n";
$db->query('UPDATE `streams_servers` SET `bitrate` = ?, `stream_info` = ?, `audio_codec` = ?, `video_codec` = ?, `resolution` = ?, `compatible` = ? WHERE `stream_id` = ? AND `server_id` = ?', $rBitrate, json_encode($rFFProbeOutput), $rAudioCodec, $rVideoCodec, $rResolution, $rCompatible, $rStream['id'], SERVER_ID);
}
$rUUIDs = array();
$rConnections = ConnectionTracker::getConnections(SERVER_ID, null, $rStream['id']);
foreach ($rConnections as $rItems) {
foreach ($rItems as $rItem) {
$rUUIDs[] = $rItem['uuid'];
}
}
$rUUIDs = [];
$rConnections = ConnectionTracker::getConnections(SERVER_ID, null, $rStream['id']);
foreach ($rConnections as $rItems) {
foreach ($rItems as $rItem) {
$rUUIDs[] = $rItem['uuid'];
}
}
$rConDir = CONS_TMP_PATH . $rStream['id'] . '/';
// The per-stream connection dir only exists once a client connects,
// so its absence is normal — guard with is_dir() to avoid a bogus
// opendir() warning being logged every cron tick on idle streams.
if (is_dir($rConDir) && ($rHandle = opendir($rConDir))) {
while (false !== ($rFilename = readdir($rHandle))) {
if ($rFilename != '.' && $rFilename != '..') {
if (!in_array($rFilename, $rUUIDs)) {
unlink(CONS_TMP_PATH . $rStream['id'] . '/' . $rFilename);
}
}
}
closedir($rHandle);
}
}
}
$rConDir = CONS_TMP_PATH . $rStream['id'] . '/';
// The per-stream connection dir only exists once a client connects,
// so its absence is normal — guard with is_dir() to avoid a bogus
// opendir() warning being logged every cron tick on idle streams.
if (is_dir($rConDir) && ($rHandle = opendir($rConDir))) {
while (false !== ($rFilename = readdir($rHandle))) {
if ($rFilename != '.' && $rFilename != '..') {
if (!in_array($rFilename, $rUUIDs)) {
unlink(CONS_TMP_PATH . $rStream['id'] . '/' . $rFilename);
}
}
}
closedir($rHandle);
}
}
}
$db->query('SELECT `stream_id` FROM `streams_servers` WHERE `on_demand` = 1 AND `server_id` = ?;', SERVER_ID);
$rOnDemandIDs = array_keys($db->get_rows(true, 'stream_id'));
$rProcesses = shell_exec('ps aux | grep XC_VM');
if (preg_match_all('/XC_VM\\[(.*)\\]/', $rProcesses, $rMatches)) {
$rRemove = array_diff($rMatches[1], $rStreamIDs);
$rRemove = array_diff($rRemove, $rOnDemandIDs);
foreach ($rRemove as $rStreamID) {
if (is_numeric($rStreamID)) {
echo 'Kill Stream ID: ' . $rStreamID . "\n";
shell_exec("kill -9 `ps -ef | grep '/" . intval($rStreamID) . '_.m3u8\\|XC_VM\\[' . intval($rStreamID) . "\\]' | grep -v grep | awk '{print \$2}'`;");
shell_exec('rm -f ' . STREAMS_PATH . intval($rStreamID) . '_*');
}
}
}
$db->query('SELECT `stream_id` FROM `streams_servers` WHERE `on_demand` = 1 AND `server_id` = ?;', SERVER_ID);
$rOnDemandIDs = array_keys($db->get_rows(true, 'stream_id'));
$rProcesses = shell_exec('ps aux | grep XC_VM');
if (preg_match_all('/XC_VM\\[(.*)\\]/', $rProcesses, $rMatches)) {
$rRemove = array_diff($rMatches[1], $rStreamIDs);
$rRemove = array_diff($rRemove, $rOnDemandIDs);
foreach ($rRemove as $rStreamID) {
if (is_numeric($rStreamID)) {
echo 'Kill Stream ID: ' . $rStreamID . "\n";
shell_exec("kill -9 `ps -ef | grep '/" . intval($rStreamID) . '_.m3u8\\|XC_VM\\[' . intval($rStreamID) . "\\]' | grep -v grep | awk '{print \$2}'`;");
shell_exec('rm -f ' . STREAMS_PATH . intval($rStreamID) . '_*');
}
}
}
if (SettingsManager::getBool('kill_rogue_ffmpeg')) {
// The supervisor restarts producers on its own schedule: a pid read from
// _.pid at the top of this pass may already have been replaced, and the
// replacement is not rogue. Ask the daemon for what it runs NOW.
$rStates = FanoutClient::monitorStates();
foreach (($rStates['streams'] ?? array()) as $rState) {
if (intval($rState['pid'] ?? 0) > 0) {
$rActivePIDs[] = intval($rState['pid']);
}
}
exec("ps aux | grep -v grep | grep '/*_.m3u8' | awk '{print \$2}'", $rRoguePIDs);
foreach ($rRoguePIDs as $rPID) {
if (is_numeric($rPID) && intval($rPID) > 0 && !in_array($rPID, $rActivePIDs)) {
echo 'Kill Roque PID: ' . $rPID . "\n";
shell_exec('kill -9 ' . $rPID . ';');
}
}
}
}
if (SettingsManager::getBool('kill_rogue_ffmpeg')) {
// The supervisor restarts producers on its own schedule: a pid read from
// _.pid at the top of this pass may already have been replaced, and the
// replacement is not rogue. Ask the daemon for what it runs NOW.
$rStates = FanoutClient::monitorStates();
foreach (($rStates['streams'] ?? []) as $rState) {
if (intval($rState['pid'] ?? 0) > 0) {
$rActivePIDs[] = intval($rState['pid']);
}
}
exec("ps aux | grep -v grep | grep '/*_.m3u8' | awk '{print \$2}'", $rRoguePIDs);
foreach ($rRoguePIDs as $rPID) {
if (is_numeric($rPID) && intval($rPID) > 0 && !in_array($rPID, $rActivePIDs)) {
echo 'Kill Roque PID: ' . $rPID . "\n";
shell_exec('kill -9 ' . $rPID . ';');
}
}
}
}
}
+43 -43
View File
@@ -16,58 +16,58 @@ use XcVm\Cli\CronTrait;
*/
class StreamsLogsCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:streams_logs';
}
public function getName(): string {
return 'cron:streams_logs';
}
public function getDescription(): string {
return 'Cron: import stream logs into DB';
}
public function getDescription(): string {
return 'Cron: import stream logs into DB';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Stream Logs]');
$this->initCron('XC_VM[Stream Logs]');
global $db;
global $db;
$rLog = LOGS_TMP_PATH . 'stream_log.log';
if (!file_exists($rLog)) {
return 0;
}
$rLog = LOGS_TMP_PATH . 'stream_log.log';
if (!file_exists($rLog)) {
return 0;
}
$rQuery = rtrim($this->parseLog($rLog), ',');
if (!empty($rQuery)) {
$db->query('INSERT INTO `streams_logs` (`stream_id`,`server_id`,`action`,`source`,`date`) VALUES ' . $rQuery . ';');
}
unlink($rLog);
$rQuery = rtrim($this->parseLog($rLog), ',');
if (!empty($rQuery)) {
$db->query('INSERT INTO `streams_logs` (`stream_id`,`server_id`,`action`,`source`,`date`) VALUES ' . $rQuery . ';');
}
unlink($rLog);
return 0;
}
return 0;
}
private function parseLog(string $rLog): string {
$rQuery = '';
if (!file_exists($rLog)) {
return $rQuery;
}
private function parseLog(string $rLog): string {
$rQuery = '';
if (!file_exists($rLog)) {
return $rQuery;
}
$rFP = fopen($rLog, 'r');
while (!feof($rFP)) {
$rLine = trim(fgets($rFP));
if (!empty($rLine)) {
$rLine = json_decode(base64_decode($rLine), true);
if (!$rLine['stream_id']) {
continue;
}
$rQuery .= '(' . intval($rLine['stream_id']) . ',' . SERVER_ID . ",'" . addslashes($rLine['action']) . "','" . addslashes($rLine['source']) . "','" . addslashes($rLine['time']) . "'),";
}
}
fclose($rFP);
$rFP = fopen($rLog, 'r');
while (!feof($rFP)) {
$rLine = trim(fgets($rFP));
if (!empty($rLine)) {
$rLine = json_decode(base64_decode($rLine), true);
if (!$rLine['stream_id']) {
continue;
}
$rQuery .= '(' . intval($rLine['stream_id']) . ',' . SERVER_ID . ",'" . addslashes($rLine['action']) . "','" . addslashes($rLine['source']) . "','" . addslashes($rLine['time']) . "'),";
}
}
fclose($rFP);
return $rQuery;
}
return $rQuery;
}
}
+19 -19
View File
@@ -18,31 +18,31 @@ use XcVm\Infrastructure\Tmdb\TmdbApiService;
*/
class TmdbCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:tmdb';
}
public function getName(): string {
return 'cron:tmdb';
}
public function getDescription(): string {
return 'Cron: update TMDB data (series, movies)';
}
public function getDescription(): string {
return 'Cron: update TMDB data (series, movies)';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
TmdbApiService::requireLibrary();
TmdbApiService::requireLibrary();
$this->initCron('XC_VM[TMDB]');
$this->initCron('XC_VM[TMDB]');
$rTimeout = 3600;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
$rTimeout = 3600;
set_time_limit($rTimeout);
ini_set('max_execution_time', $rTimeout);
TmdbCron::run();
TmdbCron::run();
return 0;
}
return 0;
}
}
+16 -16
View File
@@ -18,27 +18,27 @@ use XcVm\Infrastructure\Tmdb\TmdbApiService;
*/
class TmdbPopularCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:tmdb_popular';
}
public function getName(): string {
return 'cron:tmdb_popular';
}
public function getDescription(): string {
return 'Cron: update popular TMDB movies';
}
public function getDescription(): string {
return 'Cron: update popular TMDB movies';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[Popular]');
$this->initCron('XC_VM[Popular]');
TmdbApiService::requireLibrary();
TmdbApiService::requireLibrary();
TmdbPopularCron::run();
TmdbPopularCron::run();
return 0;
}
return 0;
}
}
+49 -49
View File
@@ -17,63 +17,63 @@ use XcVm\Core\Config\SettingsManager;
*/
class TmpCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:tmp';
}
public function getName(): string {
return 'cron:tmp';
}
public function getDescription(): string {
return 'Cron: cleanup temporary files and stale playlists';
}
public function getDescription(): string {
return 'Cron: cleanup temporary files and stale playlists';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
global $db;
$db->close_mysql();
global $db;
$db->close_mysql();
$this->setProcessTitle('XC_VM[TMP]');
$this->acquireCronLock();
$this->setProcessTitle('XC_VM[TMP]');
$this->acquireCronLock();
$rTmpPaths = array(
TMP_PATH, CRONS_TMP_PATH, DIVERGENCE_TMP_PATH,
FLOOD_TMP_PATH, MINISTRA_TMP_PATH, SIGNALS_TMP_PATH, LOGS_TMP_PATH
);
$rTmpPaths = [
TMP_PATH, CRONS_TMP_PATH, DIVERGENCE_TMP_PATH,
FLOOD_TMP_PATH, MINISTRA_TMP_PATH, SIGNALS_TMP_PATH, LOGS_TMP_PATH
];
foreach ($rTmpPaths as $rTmpPath) {
if (!is_dir($rTmpPath)) {
@mkdir($rTmpPath, 0775, true);
continue;
}
foreach (scandir($rTmpPath) as $rFile) {
$fullPath = $rTmpPath . '/' . $rFile;
if ($rFile === '.' || $rFile === '..') {
continue;
}
if (is_file($fullPath) && time() - filemtime($fullPath) >= 600 && stripos($rFile, 'ministra_') === false) {
unlink($fullPath);
}
}
}
foreach ($rTmpPaths as $rTmpPath) {
if (!is_dir($rTmpPath)) {
@mkdir($rTmpPath, 0775, true);
continue;
}
foreach (scandir($rTmpPath) as $rFile) {
$fullPath = $rTmpPath . '/' . $rFile;
if ($rFile === '.' || $rFile === '..') {
continue;
}
if (is_file($fullPath) && time() - filemtime($fullPath) >= 600 && stripos($rFile, 'ministra_') === false) {
unlink($fullPath);
}
}
}
foreach (scandir(PLAYLIST_PATH) as $rFile) {
$fullPath = rtrim(PLAYLIST_PATH, '/') . '/' . $rFile;
if ($rFile === '.' || $rFile === '..') {
continue;
}
if (is_file($fullPath)) {
if (SettingsManager::get('cache_playlists') <= time() - filemtime($fullPath)) {
unlink($fullPath);
}
}
}
foreach (scandir(PLAYLIST_PATH) as $rFile) {
$fullPath = rtrim(PLAYLIST_PATH, '/') . '/' . $rFile;
if ($rFile === '.' || $rFile === '..') {
continue;
}
if (is_file($fullPath)) {
if (SettingsManager::get('cache_playlists') <= time() - filemtime($fullPath)) {
unlink($fullPath);
}
}
}
clearstatcache();
@unlink($this->rIdentifier);
clearstatcache();
@unlink($this->rIdentifier);
return 0;
}
return 0;
}
}
+53 -53
View File
@@ -19,68 +19,68 @@ use XcVm\Core\Updates\UpdateChannels;
*/
class UpdateCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:update';
}
public function getName(): string {
return 'cron:update';
}
public function getDescription(): string {
return 'Cron: check for XC_VM updates';
}
public function getDescription(): string {
return 'Cron: check for XC_VM updates';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
if (!$this->isRunning()) {
return 1;
}
if (!$this->isRunning()) {
return 1;
}
global $db, $gitRelease;
global $db, $gitRelease;
if (!$gitRelease) {
if (defined('GIT_OWNER') && defined('GIT_REPO_MAIN')) {
$gitRelease = new GitHubReleases(GIT_OWNER, GIT_REPO_MAIN, UpdateChannels::main());
}
}
if (!$gitRelease) {
if (defined('GIT_OWNER') && defined('GIT_REPO_MAIN')) {
$gitRelease = new GitHubReleases(GIT_OWNER, GIT_REPO_MAIN, UpdateChannels::main());
}
}
if (!$gitRelease) {
FileLogger::log('cron', 'GitRelease service not initialized', 'cron:update');
return 1;
}
if (!$gitRelease) {
FileLogger::log('cron', 'GitRelease service not initialized', 'cron:update');
return 1;
}
$rUpdate = $gitRelease->getUpdate(XC_VM_VERSION);
$rUpdate = $gitRelease->getUpdate(XC_VM_VERSION);
if (is_array($rUpdate) && $rUpdate['version'] && (0 < version_compare($rUpdate['version'], XC_VM_VERSION) || version_compare($rUpdate['version'], XC_VM_VERSION) == 0)) {
echo 'Update is available!' . "\n";
$updatedChanges = array();
foreach (array_reverse($rUpdate['changelog']) as $rItem) {
if (!($rItem['version'] == XC_VM_VERSION)) {
$updatedChanges[] = $rItem;
} else {
break;
}
}
$rUpdate['changelog'] = $updatedChanges;
$db->query('UPDATE `settings` SET `update_data` = ?;', json_encode($rUpdate));
} else {
$db->query('UPDATE `settings` SET `update_data` = NULL;');
}
if (is_array($rUpdate) && $rUpdate['version'] && (0 < version_compare($rUpdate['version'], XC_VM_VERSION) || version_compare($rUpdate['version'], XC_VM_VERSION) == 0)) {
echo 'Update is available!' . "\n";
$updatedChanges = [];
foreach (array_reverse($rUpdate['changelog']) as $rItem) {
if (!($rItem['version'] == XC_VM_VERSION)) {
$updatedChanges[] = $rItem;
} else {
break;
}
}
$rUpdate['changelog'] = $updatedChanges;
$db->query('UPDATE `settings` SET `update_data` = ?;', json_encode($rUpdate));
} else {
$db->query('UPDATE `settings` SET `update_data` = NULL;');
}
return 0;
}
return 0;
}
private function isRunning(): bool {
$rNginx = 0;
exec('ps -fp $(pgrep -u xc_vm)', $rOutput, $rReturnVar);
foreach ($rOutput as $rProcess) {
$rSplit = explode(' ', preg_replace('!\\s+!', ' ', trim($rProcess)));
if ($rSplit[8] == 'nginx:' && $rSplit[9] == 'master') {
$rNginx++;
}
}
return 0 < $rNginx;
}
private function isRunning(): bool {
$rNginx = 0;
exec('ps -fp $(pgrep -u xc_vm)', $rOutput, $rReturnVar);
foreach ($rOutput as $rProcess) {
$rSplit = explode(' ', preg_replace('!\\s+!', ' ', trim($rProcess)));
if ($rSplit[8] == 'nginx:' && $rSplit[9] == 'master') {
$rNginx++;
}
}
return 0 < $rNginx;
}
}
File diff suppressed because it is too large Load Diff
+161 -161
View File
@@ -23,178 +23,178 @@ use XcVm\Streaming\Health\ProcessChecker;
*/
class VodCronJob implements CommandInterface {
use CronTrait;
use CronTrait;
public function getName(): string {
return 'cron:vod';
}
public function getName(): string {
return 'cron:vod';
}
public function getDescription(): string {
return 'Cron: check VOD/channels, start recordings, analyze media';
}
public function getDescription(): string {
return 'Cron: check VOD/channels, start recordings, analyze media';
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
public function execute(array $rArgs): int {
if (!$this->assertRunAsXcVm()) {
return 1;
}
$this->initCron('XC_VM[VOD]');
$this->loadCron();
$this->initCron('XC_VM[VOD]');
$this->loadCron();
return 0;
}
return 0;
}
private function loadCron(): void {
global $db;
private function loadCron(): void {
global $db;
$db->query('SELECT * FROM `streams` t1 INNER JOIN `streams_servers` t3 ON t3.stream_id = t1.id LEFT JOIN `profiles` t2 ON t2.profile_id = t1.transcode_profile_id WHERE t1.type = 3 AND t3.server_id = ? AND t3.parent_id IS NULL;', SERVER_ID);
if ($db->num_rows() > 0) {
$rStreams = $db->get_rows();
foreach ($rStreams as $rStream) {
echo "\n\n" . '[*] Checking Stream ' . $rStream['stream_display_name'] . "\n";
$rCreateFile = CREATED_PATH . $rStream['id'] . '_.create';
$rPID = is_file($rCreateFile) ? intval(file_get_contents($rCreateFile)) : 0;
if ($rPID && ProcessChecker::checkPID($rPID, 'XC_VMCreate[' . intval($rStream['id']) . ']')) {
echo "\t" . 'Build Is Still Going!' . "\n";
} else {
$rSourcesLeft = array_diff(json_decode($rStream['stream_source'], true), json_decode($rStream['cchannel_rsources'], true));
if (count($rSourcesLeft) > 0) {
echo "\t" . 'Needs Updating!' . "\n";
StreamProcess::queueChannel($rStream['id']);
} else {
if (file_exists(CREATED_PATH . $rStream['id'] . '_.info')) {
$rCCInfo = file_get_contents(CREATED_PATH . $rStream['id'] . '_.info');
$db->query('UPDATE `streams_servers` SET `cc_info` = ? WHERE `server_id` = ? AND `stream_id` = ?;', $rCCInfo, SERVER_ID, $rStream['id']);
unlink(CREATED_PATH . $rStream['id'] . '_.info');
}
echo "\t" . 'Build Finished' . "\n";
}
}
}
}
$db->query('SELECT * FROM `streams` t1 INNER JOIN `streams_servers` t3 ON t3.stream_id = t1.id LEFT JOIN `profiles` t2 ON t2.profile_id = t1.transcode_profile_id WHERE t1.type = 3 AND t3.server_id = ? AND t3.parent_id IS NULL;', SERVER_ID);
if ($db->num_rows() > 0) {
$rStreams = $db->get_rows();
foreach ($rStreams as $rStream) {
echo "\n\n" . '[*] Checking Stream ' . $rStream['stream_display_name'] . "\n";
$rCreateFile = CREATED_PATH . $rStream['id'] . '_.create';
$rPID = is_file($rCreateFile) ? intval(file_get_contents($rCreateFile)) : 0;
if ($rPID && ProcessChecker::checkPID($rPID, 'XC_VMCreate[' . intval($rStream['id']) . ']')) {
echo "\t" . 'Build Is Still Going!' . "\n";
} else {
$rSourcesLeft = array_diff(json_decode($rStream['stream_source'], true), json_decode($rStream['cchannel_rsources'], true));
if (count($rSourcesLeft) > 0) {
echo "\t" . 'Needs Updating!' . "\n";
StreamProcess::queueChannel($rStream['id']);
} else {
if (file_exists(CREATED_PATH . $rStream['id'] . '_.info')) {
$rCCInfo = file_get_contents(CREATED_PATH . $rStream['id'] . '_.info');
$db->query('UPDATE `streams_servers` SET `cc_info` = ? WHERE `server_id` = ? AND `stream_id` = ?;', $rCCInfo, SERVER_ID, $rStream['id']);
unlink(CREATED_PATH . $rStream['id'] . '_.info');
}
echo "\t" . 'Build Finished' . "\n";
}
}
}
}
$db->query('SELECT `id` FROM `recordings` WHERE `status` NOT IN (1,2) AND `source_id` = ? AND ((`start` <= UNIX_TIMESTAMP() AND `end` > UNIX_TIMESTAMP()) OR (`archive` = 1));', SERVER_ID);
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
echo 'Start recording ID: ' . intval($rRow['id']) . "\n";
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php record ' . intval($rRow['id']) . ' > /dev/null 2>/dev/null &');
}
}
$db->query('SELECT `id` FROM `recordings` WHERE `status` NOT IN (1,2) AND `source_id` = ? AND ((`start` <= UNIX_TIMESTAMP() AND `end` > UNIX_TIMESTAMP()) OR (`archive` = 1));', SERVER_ID);
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
echo 'Start recording ID: ' . intval($rRow['id']) . "\n";
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php record ' . intval($rRow['id']) . ' > /dev/null 2>/dev/null &');
}
}
exec("ps ax | grep 'ffmpeg' | awk '{print \$1}'", $rPIDs);
exec("ps ax | grep 'ffmpeg' | awk '{print \$1}'", $rPIDs);
$db->query('SELECT COUNT(*) AS `count` FROM `streams_servers` WHERE `to_analyze` = 1 AND `server_id` = ?', SERVER_ID);
$rCount = $db->get_row()['count'];
$db->query('SELECT COUNT(*) AS `count` FROM `streams_servers` WHERE `to_analyze` = 1 AND `server_id` = ?', SERVER_ID);
$rCount = $db->get_row()['count'];
if ($rCount > 0) {
if ($rCount <= 1000) {
$rSteps = [0, $rCount];
} else {
$rSteps = range(0, $rCount, 1000);
}
if (!$rSteps) {
$rSteps = array(0);
}
if ($rCount > 0) {
if ($rCount <= 1000) {
$rSteps = [0, $rCount];
} else {
$rSteps = range(0, $rCount, 1000);
}
if (!$rSteps) {
$rSteps = [0];
}
foreach ($rSteps as $rStep) {
$db->query('SELECT t1.*,t2.* FROM `streams_servers` t1 INNER JOIN `streams` t2 ON t2.id = t1.stream_id AND t2.direct_source = 0 INNER JOIN `streams_types` t3 ON t3.type_id = t2.type AND t3.live = 0 WHERE t1.to_analyze = 1 AND t1.server_id = ? LIMIT ' . $rStep . ', 1000', SERVER_ID);
if ($db->num_rows() <= 0) {
continue;
}
foreach ($rSteps as $rStep) {
$db->query('SELECT t1.*,t2.* FROM `streams_servers` t1 INNER JOIN `streams` t2 ON t2.id = t1.stream_id AND t2.direct_source = 0 INNER JOIN `streams_types` t3 ON t3.type_id = t2.type AND t3.live = 0 WHERE t1.to_analyze = 1 AND t1.server_id = ? LIMIT ' . $rStep . ', 1000', SERVER_ID);
if ($db->num_rows() <= 0) {
continue;
}
$rRows = $db->get_rows();
foreach ($rRows as $rRow) {
echo '[*] Checking Movie ' . $rRow['stream_display_name'] . ' ' . "\t\t" . '---> ';
if (in_array($rRow['pid'], $rPIDs)) {
echo 'ENCODING...' . "\n";
} else {
$rMoviePath = VOD_PATH . intval($rRow['stream_id']) . '.' . escapeshellcmd($rRow['target_container']);
if ($rFFProbee = FFprobeRunner::probeStream($rMoviePath)) {
if (!isset($rFFProbee['codecs']['video']) || !is_array($rFFProbee['codecs']['video'])) {
// ffprobe opened the file but found no usable video stream
// (e.g. a truncated/placeholder upload): parseFFProbe returns
// '' for the missing codec, and the VALID branch below treats
// it as an array ($rFFProbee['codecs']['video']['codec_name']),
// which is a TypeError on PHP 8 that aborts the whole analyzer
// run and leaves every remaining movie stuck in `to_analyze = 1`
// (yellow) forever. Treat such a file as broken instead.
$db->query('UPDATE `streams_servers` SET `to_analyze` = 0,`stream_status` = 1 WHERE `server_stream_id` = ?', $rRow['server_stream_id']);
echo 'BROKEN (no video stream)' . "\n";
StreamProcess::updateStream($rRow['stream_id']);
continue;
}
// ffprobe (especially over network/rclone mounts) can still
// return a partial result for an odd file — e.g. a video stream
// but no audio, where parseFFProbe stores '' (a string) instead
// of an array. The VALID branch dereferences these as arrays
// ($rFFProbee['codecs']['audio']['codec_name']), a TypeError on
// PHP 8 that aborts the whole run. Normalise to arrays.
foreach (array('video', 'audio') as $rCodecKind) {
if (!is_array($rFFProbee['codecs'][$rCodecKind] ?? null)) {
$rFFProbee['codecs'][$rCodecKind] = array();
}
}
$rDuration = (isset($rFFProbee['duration']) ? $rFFProbee['duration'] : 0);
sscanf($rDuration, '%d:%d:%d', $rHours, $rMinutes, $rSeconds);
$rSeconds = (isset($rSeconds) ? $rHours * 3600 + $rMinutes * 60 + $rSeconds : $rHours * 60 + $rMinutes);
$rSize = filesize($rMoviePath);
// Guard against a zero/unknown duration (ffprobe reports
// 'N/A' for truncated or duration-less files): dividing by
// it throws DivisionByZeroError on PHP 8, which aborts the
// whole analyzer run and leaves every remaining movie stuck
// in `to_analyze = 1` (yellow) forever.
$rBitrate = ($rSeconds > 0 ? round(($rSize * 0.008) / $rSeconds) : 0);
$rMovieProperties = json_decode($rRow['movie_properties'], true);
if (!is_array($rMovieProperties)) {
$rMovieProperties = array();
}
if (!(isset($rMovieProperties['duration_secs']) && $rSeconds == $rMovieProperties['duration_secs'])) {
$rMovieProperties['duration_secs'] = $rSeconds;
$rMovieProperties['duration'] = $rDuration;
}
if (!(isset($rMovieProperties['video']) && $rFFProbee['codecs']['video']['codec_name'] == $rMovieProperties['video'])) {
$rMovieProperties['video'] = $rFFProbee['codecs']['video'];
}
if (!(isset($rMovieProperties['audio']) && $rFFProbee['codecs']['audio']['codec_name'] == $rMovieProperties['audio'])) {
$rMovieProperties['audio'] = $rFFProbee['codecs']['audio'];
}
if (SettingsManager::get('extract_subtitles')) {
if (!(isset($rMovieProperties['subtitle']) && $rFFProbee['codecs']['subtitle']['codec_name'] == $rMovieProperties['subtitle'])) {
$rMovieProperties['subtitle'] = $rFFProbee['codecs']['subtitle'];
}
}
if (!(isset($rMovieProperties['bitrate']) && $rBitrate == $rMovieProperties['bitrate'])) {
if (0 < $rBitrate) {
$rMovieProperties['bitrate'] = $rBitrate;
} else {
$rBitrate = $rMovieProperties['bitrate'];
}
}
if (isset($rFFProbee['codecs']['subtitle']) && SettingsManager::get('extract_subtitles')) {
$i = 0;
foreach ($rFFProbee['codecs']['subtitle'] as $rSubtitle) {
FFmpegCommand::extractSubtitle($rRow['stream_id'], $rMoviePath, $i);
$i++;
}
}
$rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbee, SettingsManager::get('player_allow_hevc')));
$rAudioCodec = ($rFFProbee['codecs']['audio']['codec_name'] ?: null);
$rVideoCodec = ($rFFProbee['codecs']['video']['codec_name'] ?: null);
$rResolution = ($rFFProbee['codecs']['video']['height'] ?: null);
if ($rResolution) {
$rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution);
}
$db->query('UPDATE `streams` SET `movie_properties` = ? WHERE `id` = ?', json_encode($rMovieProperties, JSON_UNESCAPED_UNICODE), $rRow['stream_id']);
$db->query('UPDATE `streams_servers` SET `bitrate` = ?,`to_analyze` = 0,`stream_status` = 0,`stream_info` = ?,`audio_codec` = ?,`video_codec` = ?,`resolution` = ?,`compatible` = ? WHERE `server_stream_id` = ?', $rBitrate, json_encode($rFFProbee, JSON_UNESCAPED_UNICODE), $rAudioCodec, $rVideoCodec, $rResolution, $rCompatible, $rRow['server_stream_id']);
echo 'VALID' . "\n";
} else {
$db->query('UPDATE `streams_servers` SET `to_analyze` = 0,`stream_status` = 1 WHERE `server_stream_id` = ?', $rRow['server_stream_id']);
echo 'BROKEN' . "\n";
}
StreamProcess::updateStream($rRow['stream_id']);
}
}
}
}
}
$rRows = $db->get_rows();
foreach ($rRows as $rRow) {
echo '[*] Checking Movie ' . $rRow['stream_display_name'] . ' ' . "\t\t" . '---> ';
if (in_array($rRow['pid'], $rPIDs)) {
echo 'ENCODING...' . "\n";
} else {
$rMoviePath = VOD_PATH . intval($rRow['stream_id']) . '.' . escapeshellcmd($rRow['target_container']);
if ($rFFProbee = FFprobeRunner::probeStream($rMoviePath)) {
if (!isset($rFFProbee['codecs']['video']) || !is_array($rFFProbee['codecs']['video'])) {
// ffprobe opened the file but found no usable video stream
// (e.g. a truncated/placeholder upload): parseFFProbe returns
// '' for the missing codec, and the VALID branch below treats
// it as an array ($rFFProbee['codecs']['video']['codec_name']),
// which is a TypeError on PHP 8 that aborts the whole analyzer
// run and leaves every remaining movie stuck in `to_analyze = 1`
// (yellow) forever. Treat such a file as broken instead.
$db->query('UPDATE `streams_servers` SET `to_analyze` = 0,`stream_status` = 1 WHERE `server_stream_id` = ?', $rRow['server_stream_id']);
echo 'BROKEN (no video stream)' . "\n";
StreamProcess::updateStream($rRow['stream_id']);
continue;
}
// ffprobe (especially over network/rclone mounts) can still
// return a partial result for an odd file — e.g. a video stream
// but no audio, where parseFFProbe stores '' (a string) instead
// of an array. The VALID branch dereferences these as arrays
// ($rFFProbee['codecs']['audio']['codec_name']), a TypeError on
// PHP 8 that aborts the whole run. Normalise to arrays.
foreach (['video', 'audio'] as $rCodecKind) {
if (!is_array($rFFProbee['codecs'][$rCodecKind] ?? null)) {
$rFFProbee['codecs'][$rCodecKind] = [];
}
}
$rDuration = (isset($rFFProbee['duration']) ? $rFFProbee['duration'] : 0);
sscanf($rDuration, '%d:%d:%d', $rHours, $rMinutes, $rSeconds);
$rSeconds = (isset($rSeconds) ? $rHours * 3600 + $rMinutes * 60 + $rSeconds : $rHours * 60 + $rMinutes);
$rSize = filesize($rMoviePath);
// Guard against a zero/unknown duration (ffprobe reports
// 'N/A' for truncated or duration-less files): dividing by
// it throws DivisionByZeroError on PHP 8, which aborts the
// whole analyzer run and leaves every remaining movie stuck
// in `to_analyze = 1` (yellow) forever.
$rBitrate = ($rSeconds > 0 ? round(($rSize * 0.008) / $rSeconds) : 0);
$rMovieProperties = json_decode($rRow['movie_properties'], true);
if (!is_array($rMovieProperties)) {
$rMovieProperties = [];
}
if (!(isset($rMovieProperties['duration_secs']) && $rSeconds == $rMovieProperties['duration_secs'])) {
$rMovieProperties['duration_secs'] = $rSeconds;
$rMovieProperties['duration'] = $rDuration;
}
if (!(isset($rMovieProperties['video']) && $rFFProbee['codecs']['video']['codec_name'] == $rMovieProperties['video'])) {
$rMovieProperties['video'] = $rFFProbee['codecs']['video'];
}
if (!(isset($rMovieProperties['audio']) && $rFFProbee['codecs']['audio']['codec_name'] == $rMovieProperties['audio'])) {
$rMovieProperties['audio'] = $rFFProbee['codecs']['audio'];
}
if (SettingsManager::get('extract_subtitles')) {
if (!(isset($rMovieProperties['subtitle']) && $rFFProbee['codecs']['subtitle']['codec_name'] == $rMovieProperties['subtitle'])) {
$rMovieProperties['subtitle'] = $rFFProbee['codecs']['subtitle'];
}
}
if (!(isset($rMovieProperties['bitrate']) && $rBitrate == $rMovieProperties['bitrate'])) {
if (0 < $rBitrate) {
$rMovieProperties['bitrate'] = $rBitrate;
} else {
$rBitrate = $rMovieProperties['bitrate'];
}
}
if (isset($rFFProbee['codecs']['subtitle']) && SettingsManager::get('extract_subtitles')) {
$i = 0;
foreach ($rFFProbee['codecs']['subtitle'] as $rSubtitle) {
FFmpegCommand::extractSubtitle($rRow['stream_id'], $rMoviePath, $i);
$i++;
}
}
$rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbee, SettingsManager::get('player_allow_hevc')));
$rAudioCodec = ($rFFProbee['codecs']['audio']['codec_name'] ?: null);
$rVideoCodec = ($rFFProbee['codecs']['video']['codec_name'] ?: null);
$rResolution = ($rFFProbee['codecs']['video']['height'] ?: null);
if ($rResolution) {
$rResolution = StreamSorter::getNearest([240, 360, 480, 576, 720, 1080, 1440, 2160], $rResolution);
}
$db->query('UPDATE `streams` SET `movie_properties` = ? WHERE `id` = ?', json_encode($rMovieProperties, JSON_UNESCAPED_UNICODE), $rRow['stream_id']);
$db->query('UPDATE `streams_servers` SET `bitrate` = ?,`to_analyze` = 0,`stream_status` = 0,`stream_info` = ?,`audio_codec` = ?,`video_codec` = ?,`resolution` = ?,`compatible` = ? WHERE `server_stream_id` = ?', $rBitrate, json_encode($rFFProbee, JSON_UNESCAPED_UNICODE), $rAudioCodec, $rVideoCodec, $rResolution, $rCompatible, $rRow['server_stream_id']);
echo 'VALID' . "\n";
} else {
$db->query('UPDATE `streams_servers` SET `to_analyze` = 0,`stream_status` = 1 WHERE `server_stream_id` = ?', $rRow['server_stream_id']);
echo 'BROKEN' . "\n";
}
StreamProcess::updateStream($rRow['stream_id']);
}
}
}
}
}
}
+64 -65
View File
@@ -20,75 +20,74 @@ use XcVm\Core\Util\Encryption;
*/
trait CronTrait {
/** @var string|null Путь к lock-файлу */
protected $rIdentifier;
/** @var string|null Путь к lock-файлу */
protected $rIdentifier;
/**
* Проверить что процесс запущен от пользователя xc_vm.
*/
protected function assertRunAsXcVm(): bool {
if ((posix_getpwuid(posix_geteuid())['name'] ?? null) !== 'xc_vm') {
echo "Please run as XC_VM!\n";
return false;
}
return true;
}
/**
* Проверить что процесс запущен от пользователя xc_vm.
*/
protected function assertRunAsXcVm(): bool {
if ((posix_getpwuid(posix_geteuid())['name'] ?? null) !== 'xc_vm') {
echo "Please run as XC_VM!\n";
return false;
}
return true;
}
/**
* Проверить что процесс запущен от root.
*/
protected function assertRunAsRoot(): bool {
if ((posix_getpwuid(posix_geteuid())['name'] ?? null) !== 'root') {
echo "Please run as root!\n";
return false;
}
return true;
}
/**
* Проверить что процесс запущен от root.
*/
protected function assertRunAsRoot(): bool {
if ((posix_getpwuid(posix_geteuid())['name'] ?? null) !== 'root') {
echo "Please run as root!\n";
return false;
}
return true;
}
/**
* Установить заголовок процесса + time limit.
*/
protected function setProcessTitle(string $rTitle): void {
set_time_limit(0);
cli_set_process_title($rTitle);
}
/**
* Установить заголовок процесса + time limit.
*/
protected function setProcessTitle(string $rTitle): void {
set_time_limit(0);
cli_set_process_title($rTitle);
}
/**
* Получить cron lock (уникальный файл в CRONS_TMP_PATH).
* Если lock уже занят выходит с кодом 0.
*/
protected function acquireCronLock(): void {
$this->rIdentifier = CRONS_TMP_PATH . md5(
Encryption::generateUniqueCode(SettingsManager::get('live_streaming_pass')) . static::class
);
ProcessManager::acquireCronLock($this->rIdentifier);
}
/**
* Получить cron lock (уникальный файл в CRONS_TMP_PATH).
* Если lock уже занят выходит с кодом 0.
*/
protected function acquireCronLock(): void {
$this->rIdentifier = CRONS_TMP_PATH . md5(
Encryption::generateUniqueCode(SettingsManager::get('live_streaming_pass')) . static::class
);
ProcessManager::acquireCronLock($this->rIdentifier);
}
/**
* Зарегистрировать shutdown handler: закрытие БД + удаление lock.
*/
protected function registerShutdown(): void {
$rIdentifier = &$this->rIdentifier;
register_shutdown_function(static function () use (&$rIdentifier) {
global $db;
if (isset($db) && is_object($db)) {
$db->close_mysql();
}
if (!empty($rIdentifier) && file_exists($rIdentifier)) {
@unlink($rIdentifier);
}
});
}
/**
* Зарегистрировать shutdown handler: закрытие БД + удаление lock.
*/
protected function registerShutdown(): void {
$rIdentifier = &$this->rIdentifier;
register_shutdown_function(static function () use (&$rIdentifier) {
global $db;
if (isset($db) && is_object($db)) {
$db->close_mysql();
}
if (!empty($rIdentifier) && file_exists($rIdentifier)) {
@unlink($rIdentifier);
}
});
}
/**
* Стандартная инициализация cron-задачи.
*
* @param string $rTitle Заголовок процесса (например 'XC_VM[Activity]')
*/
protected function initCron(string $rTitle): void {
$this->registerShutdown();
$this->setProcessTitle($rTitle);
$this->acquireCronLock();
}
/**
* Стандартная инициализация cron-задачи.
*
* @param string $rTitle Заголовок процесса (например 'XC_VM[Activity]')
*/
protected function initCron(string $rTitle): void {
$this->registerShutdown();
$this->setProcessTitle($rTitle);
$this->acquireCronLock();
}
}
-1
View File
@@ -21,7 +21,6 @@ use XcVm\Infrastructure\Redis\RedisManager;
*/
trait DaemonTrait {
/** @var string MD5 файла команды при запуске */
protected $rDaemonMD5;
+1537 -1537
View File
File diff suppressed because it is too large Load Diff
+25 -25
View File
@@ -23,9 +23,9 @@ class AuthRepository {
* @param int|null $rType Access-code type to filter by, or null for all.
* @return array Rows keyed by access-code id.
*/
public static function getAllCodes($rType = null) {
public static function getAllCodes(?int $rType = null) {
global $db;
$rReturn = array();
$rReturn = [];
if (!is_null($rType)) {
$db->query('SELECT * FROM `access_codes` WHERE `type` = ? ORDER BY `id` ASC;', $rType);
@@ -48,8 +48,8 @@ class AuthRepository {
* @param string $rMainHome Panel home path (trailing slash).
* @return string[] Access-code names (config filenames without extension, excluding 'default').
*/
public static function getActiveCodes($rMainHome) {
$rCodes = array();
public static function getActiveCodes(string $rMainHome) {
$rCodes = [];
$rFiles = scandir($rMainHome . 'bin/nginx/conf/codes/');
foreach ($rFiles as $rFile) {
@@ -72,7 +72,7 @@ class AuthRepository {
public static function getWebPlayerCode(): ?string {
foreach (self::getAllCodes(6) as $code) {
if (!empty($code['enabled'])) {
return (string)$code['code'];
return (string) $code['code'];
}
}
return null;
@@ -86,7 +86,7 @@ class AuthRepository {
public static function getActiveCodePortalCode(): ?string {
foreach (self::getAllCodes(7) as $code) {
if (!empty($code['enabled'])) {
return (string)$code['code'];
return (string) $code['code'];
}
}
return null;
@@ -109,7 +109,7 @@ class AuthRepository {
foreach (self::getAllCodes() as $rCode) {
if ($rCode['enabled']) {
$rWhitelist = array();
$rWhitelist = [];
foreach ((array) json_decode($rCode['whitelist'], true) as $rIP) {
if (filter_var($rIP, FILTER_VALIDATE_IP)) {
@@ -128,22 +128,22 @@ class AuthRepository {
$rAliasMap = [0 => 'Public/Views/admin', 1 => 'reseller', 2 => 'Ministra', 3 => 'includes/api/admin', 4 => 'includes/api/reseller', 5 => 'Ministra/new', 6 => 'Public/assets/player', 7 => 'Public/Views/portal'];
$rBurstMap = [0 => 500, 1 => 50, 2 => 50, 3 => 1000, 4 => 1000, 5 => 50, 6 => 500, 7 => 500];
$rType = $rTypeMap[(int)$rCode['type']] ?? 'admin';
$rAlias = $rAliasMap[(int)$rCode['type']] ?? 'Public/Views/admin';
$rBurst = $rBurstMap[(int)$rCode['type']] ?? 500;
$rCurrentTemplate = in_array($rType, array('ministra', 'ministra/new')) ? $rMinistraTemplate : $rTemplate;
$rType = $rTypeMap[(int) $rCode['type']] ?? 'admin';
$rAlias = $rAliasMap[(int) $rCode['type']] ?? 'Public/Views/admin';
$rBurst = $rBurstMap[(int) $rCode['type']] ?? 500;
$rCurrentTemplate = in_array($rType, ['ministra', 'ministra/new']) ? $rMinistraTemplate : $rTemplate;
if (in_array($rType, array('ministra', 'ministra/new')) || strlen($rCode['code']) >= 4) {
file_put_contents($rMainHome . 'bin/nginx/conf/codes/' . $rCode['code'] . '.conf', str_replace(array('#WHITELIST#', '#CODE#', '#TYPE#', '#BURST#', '#ALIAS#'), array(implode(' ', $rWhitelist), (string) $rCode['code'], $rType, (string) $rBurst, $rAlias), $rCurrentTemplate));
if (in_array($rType, ['ministra', 'ministra/new']) || strlen($rCode['code']) >= 4) {
file_put_contents($rMainHome . 'bin/nginx/conf/codes/' . $rCode['code'] . '.conf', str_replace(['#WHITELIST#', '#CODE#', '#TYPE#', '#BURST#', '#ALIAS#'], [implode(' ', $rWhitelist), (string) $rCode['code'], $rType, (string) $rBurst, $rAlias], $rCurrentTemplate));
} else {
file_put_contents($rMainHome . 'bin/nginx/conf/codes/' . $rCode['code'] . '.conf', str_replace(array('#WHITELIST#', '#CODE#', '#TYPE#', '#BURST#', '#ALIAS#'), array(implode(' ', $rWhitelist), $rCode['code'] . '/', $rType . '/', (string) $rBurst, $rAlias . '/'), $rCurrentTemplate));
file_put_contents($rMainHome . 'bin/nginx/conf/codes/' . $rCode['code'] . '.conf', str_replace(['#WHITELIST#', '#CODE#', '#TYPE#', '#BURST#', '#ALIAS#'], [implode(' ', $rWhitelist), $rCode['code'] . '/', $rType . '/', (string) $rBurst, $rAlias . '/'], $rCurrentTemplate));
}
}
}
if (count(self::getActiveCodes($rMainHome)) == 0) {
if (!file_exists($rMainHome . 'bin/nginx/conf/codes/default.conf')) {
file_put_contents($rMainHome . 'bin/nginx/conf/codes/default.conf', str_replace(array('alias ', '#WHITELIST#', '#CODE#', '#TYPE#', '#ALIAS#'), array('root ', '', '', 'admin', 'Public/Views/admin'), $rTemplate));
file_put_contents($rMainHome . 'bin/nginx/conf/codes/default.conf', str_replace(['alias ', '#WHITELIST#', '#CODE#', '#TYPE#', '#ALIAS#'], ['root ', '', '', 'admin', 'Public/Views/admin'], $rTemplate));
}
} else {
if (file_exists($rMainHome . 'bin/nginx/conf/codes/default.conf')) {
@@ -151,7 +151,7 @@ class AuthRepository {
}
}
ApiClient::systemRequest($rServerId, array('action' => 'reload_nginx'));
ApiClient::systemRequest($rServerId, ['action' => 'reload_nginx']);
}
/**
@@ -163,7 +163,7 @@ class AuthRepository {
* @param bool $rInfo When true, return the full access-code DB row instead of the code string.
* @return string|array|null Code string, or the DB row when $rInfo is true (null if not found).
*/
public static function getCurrentCode($rInfo = false) {
public static function getCurrentCode(bool $rInfo = false) {
global $db;
// Front Controller передаёт XC_CODE через fastcgi_param.
// Без FC — определяем из PHP_SELF (legacy поведение).
@@ -189,7 +189,7 @@ class AuthRepository {
*/
public static function getAllHMAC() {
global $db;
$rReturn = array();
$rReturn = [];
$db->query('SELECT * FROM `hmac_keys` ORDER BY `id` ASC;');
if ($db->num_rows() > 0) {
@@ -207,7 +207,7 @@ class AuthRepository {
* @param int $rID HMAC key id.
* @return array|null The key row, or null if not found.
*/
public static function getHMACById($rID) {
public static function getHMACById(int $rID) {
global $db;
$db->query('SELECT * FROM `hmac_keys` WHERE `id` = ?;', $rID);
if ($db->num_rows() == 1) {
@@ -228,7 +228,7 @@ class AuthRepository {
* @param int $rID User group id.
* @return array The group permissions row, or [] if not found.
*/
public static function getPermissions($rID) {
public static function getPermissions(int $rID) {
global $db;
$db->query('SELECT * FROM `users_groups` WHERE `group_id` = ?;', $rID);
@@ -257,9 +257,9 @@ class AuthRepository {
* @param bool $rUsers When true, include sub-users and report maps.
* @return array Effective permissions (create flags, stream/series/category ids, users, reports).
*/
public static function getGroupPermissions($rUserID, $rStreams = true, $rUsers = true) {
public static function getGroupPermissions(int $rUserID, bool $rStreams = true, bool $rUsers = true) {
global $db;
$rReturn = array('create_line' => false, 'create_mag' => false, 'create_enigma' => false, 'stream_ids' => array(), 'series_ids' => array(), 'category_ids' => array(), 'users' => array(), 'direct_reports' => array(), 'all_reports' => array(), 'report_map' => array());
$rReturn = ['create_line' => false, 'create_mag' => false, 'create_enigma' => false, 'stream_ids' => [], 'series_ids' => [], 'category_ids' => [], 'users' => [], 'direct_reports' => [], 'all_reports' => [], 'report_map' => []];
$rUser = UserRepository::getRegisteredUserById($rUserID);
if (!$rUser) {
@@ -315,7 +315,7 @@ class AuthRepository {
* @param int $rID Access-code id.
* @return array|null The access-code row, or null if not found.
*/
public static function getCodeById($rID) {
public static function getCodeById(int $rID) {
global $db;
$db->query('SELECT * FROM `access_codes` WHERE `id` = ?;', $rID);
@@ -332,7 +332,7 @@ class AuthRepository {
* @param int $rID Access-code id.
* @return bool True on deletion, false if the code does not exist.
*/
public static function deleteCode($rID) {
public static function deleteCode(int $rID) {
global $db;
$db->query('SELECT `id` FROM `access_codes` WHERE `id` = ?;', $rID);
@@ -352,7 +352,7 @@ class AuthRepository {
* @param int $rID HMAC key id.
* @return bool True on deletion, false if the key does not exist.
*/
public static function deleteHMAC($rID) {
public static function deleteHMAC(int $rID) {
global $db;
$db->query('SELECT `id` FROM `hmac_keys` WHERE `id` = ?;', $rID);
+22 -22
View File
@@ -31,7 +31,7 @@ class AuthService {
* @param array $rData Submitted form data (includes `edit` id when updating).
* @return array ['status' => STATUS_* constant, 'data' => payload].
*/
public static function processCode($rData) {
public static function processCode(array $rData) {
global $db;
if (isset($rData['edit'])) {
$rArray = AdminHelpers::overwriteData(AuthRepository::getCodeById($rData['edit']), $rData);
@@ -49,7 +49,7 @@ class AuthService {
}
if (isset($rData['groups'])) {
$rArray['groups'] = array();
$rArray['groups'] = [];
foreach ($rData['groups'] as $rGroupID) {
$rArray['groups'][] = intval($rGroupID);
}
@@ -57,7 +57,7 @@ class AuthService {
$rArray['groups'] = is_string($rArray['groups'] ?? null) ? (json_decode($rArray['groups'], true) ?: []) : [];
}
if (in_array($rData['type'], array(0, 1, 3, 4))) {
if (in_array($rData['type'], [0, 1, 3, 4])) {
$rArray['groups'] = '[' . implode(',', array_map('intval', $rArray['groups'])) . ']';
} else {
$rArray['groups'] = '[]';
@@ -67,20 +67,20 @@ class AuthService {
$rArray['whitelist'] = '[]';
}
if (in_array((int)$rData['type'], [6, 7], true)) {
if (in_array((int) $rData['type'], [6, 7], true)) {
if (strlen($rData['code']) < 3) {
return array('status' => STATUS_CODE_LENGTH, 'data' => $rData);
return ['status' => STATUS_CODE_LENGTH, 'data' => $rData];
}
} elseif ($rData['type'] != 2 && strlen($rData['code']) < 8) {
return array('status' => STATUS_CODE_LENGTH, 'data' => $rData);
return ['status' => STATUS_CODE_LENGTH, 'data' => $rData];
}
if ($rData['type'] == 2 && empty($rData['code'])) {
return array('status' => STATUS_INVALID_CODE, 'data' => $rData);
return ['status' => STATUS_INVALID_CODE, 'data' => $rData];
}
if (in_array($rData['code'], array('admin', 'stream', 'images', 'player_api', 'player', 'playlist', 'epg', 'live', 'movie', 'series', 'status', 'nginx_status', 'get', 'panel_api', 'xmltv', 'probe', 'thumb', 'timeshift', 'auth', 'vauth', 'tsauth', 'hls', 'play', 'key', 'api', 'c'))) {
return array('status' => STATUS_RESERVED_CODE, 'data' => $rData);
if (in_array($rData['code'], ['admin', 'stream', 'images', 'player_api', 'player', 'playlist', 'epg', 'live', 'movie', 'series', 'status', 'nginx_status', 'get', 'panel_api', 'xmltv', 'probe', 'thumb', 'timeshift', 'auth', 'vauth', 'tsauth', 'hls', 'play', 'key', 'api', 'c'])) {
return ['status' => STATUS_RESERVED_CODE, 'data' => $rData];
}
if (isset($rData['edit'])) {
@@ -90,7 +90,7 @@ class AuthService {
}
if (0 < $db->num_rows()) {
return array('status' => STATUS_EXISTS_CODE, 'data' => $rData);
return ['status' => STATUS_EXISTS_CODE, 'data' => $rData];
}
$rPrepare = QueryHelper::prepareArray($rArray);
@@ -99,10 +99,10 @@ class AuthService {
if ($db->query($rQuery, ...$rPrepare['data'])) {
$rInsertID = $db->last_insert_id();
AuthRepository::updateCodes();
return array('status' => STATUS_SUCCESS, 'data' => array('insert_id' => $rInsertID, 'orig_code' => $rOrigCode, 'new_code' => $rData['code']));
return ['status' => STATUS_SUCCESS, 'data' => ['insert_id' => $rInsertID, 'orig_code' => $rOrigCode, 'new_code' => $rData['code']]];
}
return array('status' => STATUS_FAILURE, 'data' => $rData);
return ['status' => STATUS_FAILURE, 'data' => $rData];
}
// ──────────────────────────────────────────────
@@ -118,7 +118,7 @@ class AuthService {
* @param array $rData Submitted form data (includes `edit` id when updating).
* @return array ['status' => STATUS_* constant, 'data' => payload or insert_id].
*/
public static function processHMAC($rData) {
public static function processHMAC(array $rData) {
global $db, $rSettings;
if (isset($rData['edit'])) {
$rArray = AdminHelpers::overwriteData(AuthRepository::getHMACById($rData['edit']), $rData);
@@ -134,24 +134,24 @@ class AuthService {
}
if ($rData['keygen'] != 'HMAC KEY HIDDEN' && strlen($rData['keygen']) != 32) {
return array('status' => STATUS_NO_KEY, 'data' => $rData);
return ['status' => STATUS_NO_KEY, 'data' => $rData];
}
if (strlen($rData['notes']) == 0) {
return array('status' => STATUS_NO_DESCRIPTION, 'data' => $rData);
return ['status' => STATUS_NO_DESCRIPTION, 'data' => $rData];
}
if (isset($rData['edit'])) {
if ($rData['keygen'] != 'HMAC KEY HIDDEN') {
$db->query('SELECT `id` FROM `hmac_keys` WHERE `key` = ? AND `id` <> ?;', Encryption::encrypt($rData['keygen'], $rSettings['live_streaming_pass'], OPENSSL_EXTRA), $rData['edit']);
if (0 < $db->num_rows()) {
return array('status' => STATUS_EXISTS_HMAC, 'data' => $rData);
return ['status' => STATUS_EXISTS_HMAC, 'data' => $rData];
}
}
} else {
$db->query('SELECT `id` FROM `hmac_keys` WHERE `key` = ?;', Encryption::encrypt($rData['keygen'], $rSettings['live_streaming_pass'], OPENSSL_EXTRA));
if (0 < $db->num_rows()) {
return array('status' => STATUS_EXISTS_HMAC, 'data' => $rData);
return ['status' => STATUS_EXISTS_HMAC, 'data' => $rData];
}
}
@@ -164,10 +164,10 @@ class AuthService {
if ($db->query($rQuery, ...$rPrepare['data'])) {
$rInsertID = $db->last_insert_id();
return array('status' => STATUS_SUCCESS, 'data' => array('insert_id' => $rInsertID));
return ['status' => STATUS_SUCCESS, 'data' => ['insert_id' => $rInsertID]];
}
return array('status' => STATUS_FAILURE, 'data' => $rData);
return ['status' => STATUS_FAILURE, 'data' => $rData];
}
/**
@@ -184,7 +184,7 @@ class AuthService {
* query string can make: null, an array).
* @return bool
*/
public static function secretMatches($rKnown, $rGiven): bool {
public static function secretMatches(mixed $rKnown, mixed $rGiven): bool {
if (!is_scalar($rKnown) || !is_string($rGiven)) {
return false;
}
@@ -212,7 +212,7 @@ class AuthService {
* @param int $rMaxConnections Max-connections component.
* @return int|null Matching HMAC key id, or null if no key matches.
*/
public static function validateHMAC($rHMAC, $rExpiry, $rStreamID, $rExtension, $rIP = '', $rMACIP = '', $rIdentifier = '', $rMaxConnections = 0) {
public static function validateHMAC(string $rHMAC, int|string $rExpiry, int|string $rStreamID, string $rExtension, string $rIP = '', string $rMACIP = '', string $rIdentifier = '', int $rMaxConnections = 0) {
global $db, $rSettings;
$rCached = $rSettings['enable_cache'];
if (0 < strlen($rIP) && 0 < strlen($rMACIP) && $rIP != $rMACIP) {
@@ -223,7 +223,7 @@ class AuthService {
if ($rCached) {
$rKeys = igbinary_unserialize(file_get_contents(CACHE_TMP_PATH . 'hmac_keys'));
} else {
$rKeys = array();
$rKeys = [];
$db->query('SELECT `id`, `key` FROM `hmac_keys` WHERE `enabled` = 1;');
foreach ($db->get_rows() as $rKey) {
$rKeys[] = $rKey;
+15 -16
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Auth;
@@ -37,7 +36,7 @@ class Authenticator {
return false;
}
$rPost = http_build_query(array('secret' => $rSecret, 'response' => $rToken));
$rPost = http_build_query(['secret' => $rSecret, 'response' => $rToken]);
$rUrl = 'https://www.google.com/recaptcha/api/siteverify';
$rRaw = false;
$rErr = '';
@@ -93,7 +92,7 @@ class Authenticator {
global $db, $rSettings;
if (!empty($rSettings['recaptcha_enable']) && !$rBypassRecaptcha) {
if (!self::verifyRecaptcha($rData['g-recaptcha-response'] ?? '')) {
return array('status' => STATUS_INVALID_CAPTCHA);
return ['status' => STATUS_INVALID_CAPTCHA];
}
}
@@ -105,7 +104,7 @@ class Authenticator {
// Always recorded, whatever save_login_logs says: these rows are what
// the login flood limit counts (loginFloodExceeded).
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('ADMIN', ?, 0, ?, ?, ?);", $rAccessCode['id'] ?? null, 'INVALID_LOGIN', $rIP, time());
return array('status' => STATUS_FAILURE);
return ['status' => STATUS_FAILURE];
}
$db->query('SELECT COUNT(*) AS `count` FROM `access_codes`;');
@@ -119,7 +118,7 @@ class Authenticator {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('ADMIN', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'INVALID_CODE', $rIP, time());
}
return array('status' => STATUS_INVALID_CODE);
return ['status' => STATUS_INVALID_CODE];
}
$rPermissions = AuthRepository::getPermissions($rUserInfo['member_group_id']);
@@ -127,7 +126,7 @@ class Authenticator {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('ADMIN', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'NOT_ADMIN', $rIP, time());
}
return array('status' => STATUS_NOT_ADMIN);
return ['status' => STATUS_NOT_ADMIN];
}
if ($rUserInfo['status'] == 1) {
@@ -143,17 +142,17 @@ class Authenticator {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('ADMIN', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'SUCCESS', $rIP, time());
}
return array('status' => STATUS_SUCCESS);
return ['status' => STATUS_SUCCESS];
}
if (!$rUserInfo['status']) {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('ADMIN', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'DISABLED', $rIP, time());
}
return array('status' => STATUS_DISABLED);
return ['status' => STATUS_DISABLED];
}
return array('status' => STATUS_FAILURE);
return ['status' => STATUS_FAILURE];
}
/**
@@ -166,7 +165,7 @@ class Authenticator {
global $db, $rSettings;
if (!empty($rSettings['recaptcha_enable'])) {
if (!self::verifyRecaptcha($rData['g-recaptcha-response'] ?? '')) {
return array('status' => STATUS_INVALID_CAPTCHA);
return ['status' => STATUS_INVALID_CAPTCHA];
}
}
@@ -177,14 +176,14 @@ class Authenticator {
if (!isset($rUserInfo)) {
// Always recorded: the login flood limit counts these (see login()).
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('RESELLER', ?, 0, ?, ?, ?);", $rAccessCode['id'] ?? null, 'INVALID_LOGIN', $rIP, time());
return array('status' => STATUS_FAILURE);
return ['status' => STATUS_FAILURE];
}
if (!(in_array($rUserInfo['member_group_id'], ($rAccessCode && isset($rAccessCode['groups'])) ? (json_decode($rAccessCode['groups'], true) ?: []) : []) || count(AuthRepository::getActiveCodes(MAIN_HOME)) == 0)) {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('RESELLER', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'INVALID_CODE', $rIP, time());
}
return array('status' => STATUS_INVALID_CODE);
return ['status' => STATUS_INVALID_CODE];
}
$rPermissions = AuthRepository::getPermissions($rUserInfo['member_group_id']);
@@ -192,7 +191,7 @@ class Authenticator {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('RESELLER', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'NOT_ADMIN', $rIP, time());
}
return array('status' => STATUS_NOT_RESELLER);
return ['status' => STATUS_NOT_RESELLER];
}
if ($rUserInfo['status'] == 1) {
@@ -208,17 +207,17 @@ class Authenticator {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('RESELLER', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'SUCCESS', $rIP, time());
}
return array('status' => STATUS_SUCCESS);
return ['status' => STATUS_SUCCESS];
}
if (!$rUserInfo['status']) {
if (!empty($rSettings['save_login_logs'])) {
$db->query("INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) VALUES('RESELLER', ?, ?, ?, ?, ?);", $rAccessCode['id'], $rUserInfo['id'], 'DISABLED', $rIP, time());
}
return array('status' => STATUS_DISABLED);
return ['status' => STATUS_DISABLED];
}
return array('status' => STATUS_FAILURE);
return ['status' => STATUS_FAILURE];
}
/**
+3 -4
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Auth;
@@ -48,7 +47,7 @@ class Authorization {
}
if ($rType == 'user') {
$rReports = array_map('intval', array_merge(array($rUserInfo['id']), $rPermissions['all_reports']));
$rReports = array_map('intval', array_merge([$rUserInfo['id']], $rPermissions['all_reports']));
if (0 < count($rReports)) {
$db->query('SELECT `id` FROM `users` WHERE `id` = ? AND (`owner_id` IN (' . implode(',', $rReports) . ') OR `id` = ?);', $rID, $rUserInfo['id']);
@@ -59,7 +58,7 @@ class Authorization {
}
if ($rType == 'line') {
$rReports = array_map('intval', array_merge(array($rUserInfo['id']), $rPermissions['all_reports']));
$rReports = array_map('intval', array_merge([$rUserInfo['id']], $rPermissions['all_reports']));
if (0 < count($rReports)) {
$db->query('SELECT `id` FROM `lines` WHERE `id` = ? AND `member_id` IN (' . implode(',', $rReports) . ');', $rID);
return 0 < $db->num_rows();
@@ -72,7 +71,7 @@ class Authorization {
}
if (0 < count($rPermissions['advanced']) && $rUserInfo['member_group_id'] != 1) {
return in_array($rID, ($rPermissions['advanced'] ?: array()));
return in_array($rID, ($rPermissions['advanced'] ?: []));
}
return true;
+267 -270
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Auth;
@@ -25,306 +24,304 @@ use XcVm\Infrastructure\Signal\SignalQueue;
*/
class BruteforceGuard {
/**
* Resolve panel settings, preferring SettingsManager over the legacy global.
*
* @return array Settings array, or [] when none are available.
*/
private static function getSettings(): array {
if (!empty(SettingsManager::getAll())) {
return SettingsManager::getAll();
}
if (!empty($GLOBALS['rSettings'])) {
return $GLOBALS['rSettings'];
}
return array();
}
/**
* Resolve panel settings, preferring SettingsManager over the legacy global.
*
* @return array Settings array, or [] when none are available.
*/
private static function getSettings(): array {
if (!empty(SettingsManager::getAll())) {
return SettingsManager::getAll();
}
if (!empty($GLOBALS['rSettings'])) {
return $GLOBALS['rSettings'];
}
return [];
}
/**
* Resolve the user's IP address.
*
* @return string
*/
private static function getUserIP(): string {
if (class_exists(NetworkUtils::class, false)) {
return NetworkUtils::getUserIP();
}
return $_SERVER['REMOTE_ADDR'] ?? '';
}
/**
* Resolve the user's IP address.
*
* @return string
*/
private static function getUserIP(): string {
if (class_exists(NetworkUtils::class, false)) {
return NetworkUtils::getUserIP();
}
return $_SERVER['REMOTE_ADDR'] ?? '';
}
/**
* Get the allowed IPs list.
*
* @return array
*/
private static function getAllowedIPs(): array {
if (class_exists(ServerRepository::class, false)) {
return ServerRepository::getAllowedIPs();
}
if (isset($GLOBALS['rAllowedIPs'])) {
return $GLOBALS['rAllowedIPs'];
}
return array();
}
/**
* Get the allowed IPs list.
*
* @return array
*/
private static function getAllowedIPs(): array {
if (class_exists(ServerRepository::class, false)) {
return ServerRepository::getAllowedIPs();
}
if (isset($GLOBALS['rAllowedIPs'])) {
return $GLOBALS['rAllowedIPs'];
}
return [];
}
/**
* Get the blocked IPs list.
*
* @return array
*/
private static function getBlockedIPs(): array {
if (class_exists(BlocklistService::class, false)) {
return BlocklistService::getBlockedIPs();
}
if (isset($GLOBALS['rBlockedIPs'])) {
return $GLOBALS['rBlockedIPs'];
}
return array();
}
/**
* Get the blocked IPs list.
*
* @return array
*/
private static function getBlockedIPs(): array {
if (class_exists(BlocklistService::class, false)) {
return BlocklistService::getBlockedIPs();
}
if (isset($GLOBALS['rBlockedIPs'])) {
return $GLOBALS['rBlockedIPs'];
}
return [];
}
/**
* Get database instance.
*
* @return object|null
*/
private static function getDB(): ?object {
if (class_exists(DatabaseFactory::class, false) && DatabaseFactory::get() !== null) {
return DatabaseFactory::get();
}
global $db;
if (is_object($db)) {
return $db;
}
return null;
}
/**
* Get database instance.
*
* @return object|null
*/
private static function getDB(): ?object {
if (class_exists(DatabaseFactory::class, false) && DatabaseFactory::get() !== null) {
return DatabaseFactory::get();
}
global $db;
if (is_object($db)) {
return $db;
}
return null;
}
/**
* Block an IP: insert into DB (or signal if in cached/streaming mode).
*
* @param string $ip
* @param string $reason
* @param bool $useCachedMode Use signal-based blocking
*/
private static function blockIP(string $ip, string $reason, bool $useCachedMode = false): void {
if ($useCachedMode && !empty($GLOBALS['rCached'])) {
$signalKey = (stripos($reason, 'BRUTEFORCE') !== false ? 'bruteforce_attack' : 'flood_attack');
SignalQueue::push($signalKey . '/' . $ip, 1);
} else {
$db = self::getDB();
if ($db) {
$db->query('INSERT INTO `blocked_ips` (`ip`,`notes`,`date`) VALUES(?,?,?)', $ip, $reason, time());
}
// Force-refresh blocked IPs cache
if (class_exists(BlocklistService::class, false)) {
BlocklistService::getBlockedIPs(true);
}
}
touch(FLOOD_TMP_PATH . 'block_' . $ip);
}
/**
* Block an IP: insert into DB (or signal if in cached/streaming mode).
*
* @param bool $useCachedMode Use signal-based blocking
*/
private static function blockIP(string $ip, string $reason, bool $useCachedMode = false): void {
if ($useCachedMode && !empty($GLOBALS['rCached'])) {
$signalKey = (stripos($reason, 'BRUTEFORCE') !== false ? 'bruteforce_attack' : 'flood_attack');
SignalQueue::push($signalKey . '/' . $ip, 1);
} else {
$db = self::getDB();
if ($db) {
$db->query('INSERT INTO `blocked_ips` (`ip`,`notes`,`date`) VALUES(?,?,?)', $ip, $reason, time());
}
// Force-refresh blocked IPs cache
if (class_exists(BlocklistService::class, false)) {
BlocklistService::getBlockedIPs(true);
}
}
touch(FLOOD_TMP_PATH . 'block_' . $ip);
}
/**
* Check for flood attacks (too many requests per time window).
*
* @param string|null $ip IP address (auto-detected if null)
* @param bool $useCachedMode Use signal-based blocking for streaming context
*/
public static function checkFlood(?string $ip = null, bool $useCachedMode = false): void {
$settings = self::getSettings();
if (empty($settings['flood_limit']) || $settings['flood_limit'] == 0) {
return;
}
/**
* Check for flood attacks (too many requests per time window).
*
* @param string|null $ip IP address (auto-detected if null)
* @param bool $useCachedMode Use signal-based blocking for streaming context
*/
public static function checkFlood(?string $ip = null, bool $useCachedMode = false): void {
$settings = self::getSettings();
if (empty($settings['flood_limit']) || $settings['flood_limit'] == 0) {
return;
}
if (!$ip) {
$ip = self::getUserIP();
}
if (!$ip) {
$ip = self::getUserIP();
}
$allowedIPs = self::getAllowedIPs();
if (empty($ip) || in_array($ip, $allowedIPs)) {
return;
}
$allowedIPs = self::getAllowedIPs();
if (empty($ip) || in_array($ip, $allowedIPs)) {
return;
}
$floodExclude = array_filter(array_unique(explode(',', $settings['flood_ips_exclude'] ?? '')));
if (in_array($ip, $floodExclude)) {
return;
}
$floodExclude = array_filter(array_unique(explode(',', $settings['flood_ips_exclude'] ?? '')));
if (in_array($ip, $floodExclude)) {
return;
}
$ipFile = FLOOD_TMP_PATH . $ip;
if (file_exists($ipFile)) {
$floodRow = json_decode(file_get_contents($ipFile), true);
$floodSeconds = $settings['flood_seconds'];
$floodLimit = $settings['flood_limit'];
$ipFile = FLOOD_TMP_PATH . $ip;
if (file_exists($ipFile)) {
$floodRow = json_decode(file_get_contents($ipFile), true);
$floodSeconds = $settings['flood_seconds'];
$floodLimit = $settings['flood_limit'];
if (time() - $floodRow['last_request'] <= $floodSeconds) {
$floodRow['requests']++;
if ($floodLimit > $floodRow['requests']) {
$floodRow['last_request'] = time();
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
} else {
$blockedIPs = self::getBlockedIPs();
if (!in_array($ip, $blockedIPs)) {
self::blockIP($ip, 'FLOOD ATTACK', $useCachedMode);
} else {
touch(FLOOD_TMP_PATH . 'block_' . $ip);
}
unlink($ipFile);
return;
}
} else {
$floodRow['requests'] = 0;
$floodRow['last_request'] = time();
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
}
} else {
file_put_contents($ipFile, json_encode(array('requests' => 0, 'last_request' => time())), LOCK_EX);
}
}
if (time() - $floodRow['last_request'] <= $floodSeconds) {
$floodRow['requests']++;
if ($floodLimit > $floodRow['requests']) {
$floodRow['last_request'] = time();
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
} else {
$blockedIPs = self::getBlockedIPs();
if (!in_array($ip, $blockedIPs)) {
self::blockIP($ip, 'FLOOD ATTACK', $useCachedMode);
} else {
touch(FLOOD_TMP_PATH . 'block_' . $ip);
}
unlink($ipFile);
return;
}
} else {
$floodRow['requests'] = 0;
$floodRow['last_request'] = time();
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
}
} else {
file_put_contents($ipFile, json_encode(['requests' => 0, 'last_request' => time()]), LOCK_EX);
}
}
/**
* Check for brute-force attacks (too many unique MACs/usernames).
*
* @param string|null $ip IP address (auto-detected if null)
* @param string|null $mac MAC address
* @param string|null $username Username
* @param bool $useCachedMode Use signal-based blocking for streaming context
*/
public static function checkBruteforce(?string $ip = null, ?string $mac = null, ?string $username = null, bool $useCachedMode = false): void {
if (!$mac && !$username) {
return;
}
/**
* Check for brute-force attacks (too many unique MACs/usernames).
*
* @param string|null $ip IP address (auto-detected if null)
* @param string|null $mac MAC address
* @param string|null $username Username
* @param bool $useCachedMode Use signal-based blocking for streaming context
*/
public static function checkBruteforce(?string $ip = null, ?string $mac = null, ?string $username = null, bool $useCachedMode = false): void {
if (!$mac && !$username) {
return;
}
$settings = self::getSettings();
$settings = self::getSettings();
if ($mac && $settings['bruteforce_mac_attempts'] == 0) {
return;
}
if ($username && $settings['bruteforce_username_attempts'] == 0) {
return;
}
if ($mac && $settings['bruteforce_mac_attempts'] == 0) {
return;
}
if ($username && $settings['bruteforce_username_attempts'] == 0) {
return;
}
if (!$ip) {
$ip = self::getUserIP();
}
if (!$ip) {
$ip = self::getUserIP();
}
$allowedIPs = self::getAllowedIPs();
if (empty($ip) || in_array($ip, $allowedIPs)) {
return;
}
$allowedIPs = self::getAllowedIPs();
if (empty($ip) || in_array($ip, $allowedIPs)) {
return;
}
$floodExclude = array_filter(array_unique(explode(',', $settings['flood_ips_exclude'] ?? '')));
if (in_array($ip, $floodExclude)) {
return;
}
$floodExclude = array_filter(array_unique(explode(',', $settings['flood_ips_exclude'] ?? '')));
if (in_array($ip, $floodExclude)) {
return;
}
$floodType = (!is_null($mac) ? 'mac' : 'user');
$term = (!is_null($mac) ? $mac : $username);
$ipFile = FLOOD_TMP_PATH . $ip . '_' . $floodType;
$floodType = (!is_null($mac) ? 'mac' : 'user');
$term = (!is_null($mac) ? $mac : $username);
$ipFile = FLOOD_TMP_PATH . $ip . '_' . $floodType;
if (file_exists($ipFile)) {
$floodRow = json_decode(file_get_contents($ipFile), true);
$floodSeconds = intval($settings['bruteforce_frequency']);
$floodLimit = intval($settings[array('mac' => 'bruteforce_mac_attempts', 'user' => 'bruteforce_username_attempts')[$floodType]]);
$floodRow['attempts'] = self::truncateAttempts($floodRow['attempts'], $floodSeconds);
if (file_exists($ipFile)) {
$floodRow = json_decode(file_get_contents($ipFile), true);
$floodSeconds = intval($settings['bruteforce_frequency']);
$floodLimit = intval($settings[['mac' => 'bruteforce_mac_attempts', 'user' => 'bruteforce_username_attempts'][$floodType]]);
$floodRow['attempts'] = self::truncateAttempts($floodRow['attempts'], $floodSeconds);
if (!in_array($term, array_keys($floodRow['attempts']))) {
$floodRow['attempts'][$term] = time();
if ($floodLimit > count($floodRow['attempts'])) {
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
} else {
$blockedIPs = self::getBlockedIPs();
if (!in_array($ip, $blockedIPs)) {
self::blockIP($ip, 'BRUTEFORCE ' . strtoupper($floodType) . ' ATTACK', $useCachedMode);
} else {
touch(FLOOD_TMP_PATH . 'block_' . $ip);
}
unlink($ipFile);
return;
}
}
} else {
$floodRow = array('attempts' => array($term => time()));
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
}
}
if (!in_array($term, array_keys($floodRow['attempts']))) {
$floodRow['attempts'][$term] = time();
if ($floodLimit > count($floodRow['attempts'])) {
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
} else {
$blockedIPs = self::getBlockedIPs();
if (!in_array($ip, $blockedIPs)) {
self::blockIP($ip, 'BRUTEFORCE ' . strtoupper($floodType) . ' ATTACK', $useCachedMode);
} else {
touch(FLOOD_TMP_PATH . 'block_' . $ip);
}
unlink($ipFile);
return;
}
}
} else {
$floodRow = ['attempts' => [$term => time()]];
file_put_contents($ipFile, json_encode($floodRow), LOCK_EX);
}
}
/**
* Check for auth flood (too many auth requests from same user+IP).
*
* @param array $user User info array (must have 'id' and 'is_restreamer')
* @param string|null $ip IP address (auto-detected if null)
*/
public static function checkAuthFlood(array $user, ?string $ip = null): void {
$settings = self::getSettings();
if (empty($settings['auth_flood_limit']) || $settings['auth_flood_limit'] == 0) {
return;
}
/**
* Check for auth flood (too many auth requests from same user+IP).
*
* @param array $user User info array (must have 'id' and 'is_restreamer')
* @param string|null $ip IP address (auto-detected if null)
*/
public static function checkAuthFlood(array $user, ?string $ip = null): void {
$settings = self::getSettings();
if (empty($settings['auth_flood_limit']) || $settings['auth_flood_limit'] == 0) {
return;
}
if (!empty($user['is_restreamer'])) {
return;
}
if (!empty($user['is_restreamer'])) {
return;
}
if (!$ip) {
$ip = self::getUserIP();
}
if (!$ip) {
$ip = self::getUserIP();
}
$allowedIPs = self::getAllowedIPs();
if (empty($ip) || in_array($ip, $allowedIPs)) {
return;
}
$allowedIPs = self::getAllowedIPs();
if (empty($ip) || in_array($ip, $allowedIPs)) {
return;
}
$floodExclude = array_filter(array_unique(explode(',', $settings['flood_ips_exclude'] ?? '')));
if (in_array($ip, $floodExclude)) {
return;
}
$floodExclude = array_filter(array_unique(explode(',', $settings['flood_ips_exclude'] ?? '')));
if (in_array($ip, $floodExclude)) {
return;
}
$userFile = FLOOD_TMP_PATH . intval($user['id']) . '_' . $ip;
if (file_exists($userFile)) {
$floodRow = json_decode(file_get_contents($userFile), true);
$userFile = FLOOD_TMP_PATH . intval($user['id']) . '_' . $ip;
if (file_exists($userFile)) {
$floodRow = json_decode(file_get_contents($userFile), true);
if (isset($floodRow['block_until']) && time() < $floodRow['block_until']) {
sleep(intval($settings['auth_flood_sleep']));
}
if (isset($floodRow['block_until']) && time() < $floodRow['block_until']) {
sleep(intval($settings['auth_flood_sleep']));
}
$floodSeconds = intval($settings['auth_flood_seconds']);
$floodLimit = intval($settings['auth_flood_limit']);
$floodRow['attempts'] = self::truncateAttempts($floodRow['attempts'], $floodSeconds, true);
$floodSeconds = intval($settings['auth_flood_seconds']);
$floodLimit = intval($settings['auth_flood_limit']);
$floodRow['attempts'] = self::truncateAttempts($floodRow['attempts'], $floodSeconds, true);
if (!($floodLimit > count($floodRow['attempts']))) {
$floodRow['block_until'] = time() + intval($settings['auth_flood_seconds']);
}
if (!($floodLimit > count($floodRow['attempts']))) {
$floodRow['block_until'] = time() + intval($settings['auth_flood_seconds']);
}
$floodRow['attempts'][] = time();
file_put_contents($userFile, json_encode($floodRow), LOCK_EX);
} else {
file_put_contents($userFile, json_encode(array('attempts' => array(time()))), LOCK_EX);
}
}
$floodRow['attempts'][] = time();
file_put_contents($userFile, json_encode($floodRow), LOCK_EX);
} else {
file_put_contents($userFile, json_encode(['attempts' => [time()]]), LOCK_EX);
}
}
/**
* Filter out expired attempts from the list.
*
* @param array $attempts Array of attempts (keyed or indexed by time)
* @param int $frequency Time window in seconds
* @param bool $list If true, treat as indexed array; otherwise as associative
* @return array Filtered attempts
*/
public static function truncateAttempts(array $attempts, int $frequency, bool $list = false): array {
$allowed = array();
$now = time();
/**
* Filter out expired attempts from the list.
*
* @param array $attempts Array of attempts (keyed or indexed by time)
* @param int $frequency Time window in seconds
* @param bool $list If true, treat as indexed array; otherwise as associative
* @return array Filtered attempts
*/
public static function truncateAttempts(array $attempts, int $frequency, bool $list = false): array {
$allowed = [];
$now = time();
if ($list) {
foreach ($attempts as $attemptTime) {
if ($now - $attemptTime <= $frequency) {
$allowed[] = $attemptTime;
}
}
} else {
foreach ($attempts as $attempt => $attemptTime) {
if ($now - $attemptTime <= $frequency) {
$allowed[$attempt] = $attemptTime;
}
}
}
if ($list) {
foreach ($attempts as $attemptTime) {
if ($now - $attemptTime <= $frequency) {
$allowed[] = $attemptTime;
}
}
} else {
foreach ($attempts as $attempt => $attemptTime) {
if ($now - $attemptTime <= $frequency) {
$allowed[$attempt] = $attemptTime;
}
}
}
return $allowed;
}
return $allowed;
}
}
-1
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Auth;
+218 -218
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Auth;
@@ -49,256 +48,257 @@ namespace XcVm\Core\Auth;
*/
class SessionManager {
public const DEFAULT_TIMEOUT = 60;
public const DEFAULT_TIMEOUT = 60;
protected static ?string $context = null;
protected static ?string $context = null;
protected static int $timeout = self::DEFAULT_TIMEOUT;
protected static bool $started = false;
protected static int $timeout = self::DEFAULT_TIMEOUT;
/** @var array<string, array<string, string>> */
protected static array $keyMap = [
'admin' => [
'auth' => 'hash',
'activity' => 'last_activity',
'ip' => 'ip',
'code' => 'code',
'verify' => 'verify',
],
'reseller' => [
'auth' => 'reseller',
'activity' => 'rlast_activity',
'ip' => 'rip',
'code' => 'rcode',
'verify' => 'rverify',
],
'player' => [
'auth' => 'phash',
'verify' => 'pverify',
],
];
protected static bool $started = false;
/**
* Start a session for the given context
*
* @param string $context 'admin' or 'reseller'
* @param int $timeout Timeout in minutes (default: 60)
*/
public static function start(string $context, int $timeout = self::DEFAULT_TIMEOUT): void {
self::$context = $context;
self::$timeout = $timeout;
/** @var array<string, array<string, string>> */
protected static array $keyMap = [
'admin' => [
'auth' => 'hash',
'activity' => 'last_activity',
'ip' => 'ip',
'code' => 'code',
'verify' => 'verify',
],
'reseller' => [
'auth' => 'reseller',
'activity' => 'rlast_activity',
'ip' => 'rip',
'code' => 'rcode',
'verify' => 'rverify',
],
'player' => [
'auth' => 'phash',
'verify' => 'pverify',
],
];
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
/**
* Start a session for the given context
*
* @param string $context 'admin' or 'reseller'
* @param int $timeout Timeout in minutes (default: 60)
*/
public static function start(string $context, int $timeout = self::DEFAULT_TIMEOUT): void {
self::$context = $context;
self::$timeout = $timeout;
self::$started = true;
self::checkTimeout();
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
/**
* Require authentication redirect to login if not authenticated
*
* If called as an AJAX endpoint (script_filename == session.php),
* returns JSON result instead of redirecting.
*
* @param string|null $loginUrl Override login redirect URL
*/
public static function requireAuth(?string $loginUrl = null): void {
$authKey = self::getKey('auth');
self::$started = true;
self::checkTimeout();
}
// Direct access to session.php endpoint — return JSON status
if (basename($_SERVER['SCRIPT_FILENAME']) === 'session.php') {
$isAuth = isset($_SESSION[$authKey]);
echo json_encode(['result' => $isAuth]);
exit;
}
/**
* Require authentication redirect to login if not authenticated
*
* If called as an AJAX endpoint (script_filename == session.php),
* returns JSON result instead of redirecting.
*
* @param string|null $loginUrl Override login redirect URL
*/
public static function requireAuth(?string $loginUrl = null): void {
$authKey = self::getKey('auth');
// Not authenticated — redirect to login
if (!isset($_SESSION[$authKey])) {
if ($loginUrl === null) {
$prefix = (self::$context === 'reseller') ? '' : './';
$loginUrl = $prefix . 'login?referrer=' . urlencode(basename($_SERVER['REQUEST_URI'], '.php'));
}
// Direct access to session.php endpoint — return JSON status
if (basename($_SERVER['SCRIPT_FILENAME']) === 'session.php') {
$isAuth = isset($_SESSION[$authKey]);
echo json_encode(['result' => $isAuth]);
exit;
}
header('Location: ' . $loginUrl);
exit;
}
// Not authenticated — redirect to login
if (!isset($_SESSION[$authKey])) {
if ($loginUrl === null) {
$prefix = (self::$context === 'reseller') ? '' : './';
$loginUrl = $prefix . 'login?referrer=' . urlencode(basename($_SERVER['REQUEST_URI'], '.php'));
}
// Authenticated — update activity timestamp and close session
self::touch();
}
header('Location: ' . $loginUrl);
exit;
}
/**
* Check if user is authenticated (non-blocking, no redirect)
*
* @return bool
*/
public static function isAuthenticated(): bool {
if (!self::$started) {
return false;
}
// Authenticated — update activity timestamp and close session
self::touch();
}
$authKey = self::getKey('auth');
return isset($_SESSION[$authKey]);
}
/**
* Check if user is authenticated (non-blocking, no redirect)
*
* @return bool
*/
public static function isAuthenticated(): bool {
if (!self::$started) {
return false;
}
/**
* Get the auth token/hash for current session
*
* @return mixed
*/
public static function getUser(): mixed {
$authKey = self::getKey('auth');
return isset($_SESSION[$authKey]) ? $_SESSION[$authKey] : null;
}
$authKey = self::getKey('auth');
return isset($_SESSION[$authKey]);
}
/**
* Get a session value by logical name
*
* @param string $name Logical name: 'auth', 'activity', 'ip', 'code', 'verify'
* @return mixed
*/
public static function getValue(string $name): mixed {
$key = self::getKey($name);
return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
}
/**
* Get the auth token/hash for current session
*
* @return mixed
*/
public static function getUser(): mixed {
$authKey = self::getKey('auth');
return isset($_SESSION[$authKey]) ? $_SESSION[$authKey] : null;
}
/**
* Set a session value by logical name
*
* @param string $name Logical name
* @param mixed $value Value to store
*/
public static function setValue(string $name, mixed $value): void {
$key = self::getKey($name);
$_SESSION[$key] = $value;
}
/**
* Get a session value by logical name
*
* @param string $name Logical name: 'auth', 'activity', 'ip', 'code', 'verify'
* @return mixed
*/
public static function getValue(string $name): mixed {
$key = self::getKey($name);
return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
}
/**
* Create an authenticated session
*
* @param mixed $hash Authentication hash/token
* @param string|null $ip Client IP address
*/
public static function login(mixed $hash, ?string $ip = null): void {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
/**
* Set a session value by logical name
*
* @param string $name Logical name
* @param mixed $value Value to store
*/
public static function setValue(string $name, mixed $value): void {
$key = self::getKey($name);
$_SESSION[$key] = $value;
}
self::setValue('auth', $hash);
self::setValue('activity', time());
/**
* Create an authenticated session
*
* @param mixed $hash Authentication hash/token
* @param string|null $ip Client IP address
*/
public static function login(mixed $hash, ?string $ip = null): void {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if ($ip !== null) {
self::setValue('ip', $ip);
}
}
self::setValue('auth', $hash);
self::setValue('activity', time());
/**
* Destroy the current session (logout)
*/
public static function destroy(): void {
if (!self::$started && session_status() === PHP_SESSION_NONE) {
session_start();
}
if ($ip !== null) {
self::setValue('ip', $ip);
}
}
// Clear context-specific keys
foreach (self::$keyMap[self::$context] as $key) {
if (isset($_SESSION[$key])) {
unset($_SESSION[$key]);
}
}
/**
* Destroy the current session (logout)
*/
public static function destroy(): void {
if (!self::$started && session_status() === PHP_SESSION_NONE) {
session_start();
}
// If no other context is active, destroy the whole session
$otherContext = (self::$context === 'admin') ? 'reseller' : 'admin';
$otherAuthKey = self::$keyMap[$otherContext]['auth'];
// Clear context-specific keys
foreach (self::$keyMap[self::$context] as $key) {
if (isset($_SESSION[$key])) {
unset($_SESSION[$key]);
}
}
if (!isset($_SESSION[$otherAuthKey])) {
session_destroy();
}
// If no other context is active, destroy the whole session
$otherContext = (self::$context === 'admin') ? 'reseller' : 'admin';
$otherAuthKey = self::$keyMap[$otherContext]['auth'];
self::$started = false;
}
if (!isset($_SESSION[$otherAuthKey])) {
session_destroy();
}
/**
* Clear session keys for a specific context without destroying the session.
* Drop-in replacement for legacy destroySession($type).
*
* @param string $context 'admin', 'reseller', or 'player'
*/
public static function clearContext(string $context): void {
if (!isset(self::$keyMap[$context])) {
return;
}
foreach (self::$keyMap[$context] as $key) {
unset($_SESSION[$key]);
}
}
self::$started = false;
}
/**
* Update the last activity timestamp and close session for writing
*/
public static function touch(): void {
$activityKey = self::getKey('activity');
$_SESSION[$activityKey] = time();
session_write_close();
}
/**
* Clear session keys for a specific context without destroying the session.
* Drop-in replacement for legacy destroySession($type).
*
* @param string $context 'admin', 'reseller', or 'player'
*/
public static function clearContext(string $context): void {
if (!isset(self::$keyMap[$context])) {
return;
}
foreach (self::$keyMap[$context] as $key) {
unset($_SESSION[$key]);
}
}
/**
* Get current context
*
* @return string|null
*/
public static function getContext(): ?string {
return self::$context;
}
/**
* Update the last activity timestamp and close session for writing
*/
public static function touch(): void {
$activityKey = self::getKey('activity');
$_SESSION[$activityKey] = time();
session_write_close();
}
// ───────────────────────────────────────────────────────────
// Internal Methods
// ───────────────────────────────────────────────────────────
/**
* Get current context
*
* @return string|null
*/
public static function getContext(): ?string {
return self::$context;
}
/**
* Check for session timeout and expire if needed
*/
protected static function checkTimeout(): void {
$authKey = self::getKey('auth');
$activityKey = self::getKey('activity');
// ───────────────────────────────────────────────────────────
// Internal Methods
// ───────────────────────────────────────────────────────────
if (isset($_SESSION[$authKey]) && isset($_SESSION[$activityKey])) {
$elapsed = time() - $_SESSION[$activityKey];
/**
* Check for session timeout and expire if needed
*/
protected static function checkTimeout(): void {
$authKey = self::getKey('auth');
$activityKey = self::getKey('activity');
if ($elapsed > (self::$timeout * 60)) {
// Session expired — clear all context-specific keys
foreach (self::$keyMap[self::$context] as $key) {
if (isset($_SESSION[$key])) {
unset($_SESSION[$key]);
}
}
if (isset($_SESSION[$authKey]) && isset($_SESSION[$activityKey])) {
$elapsed = time() - $_SESSION[$activityKey];
// Restart session if it was destroyed
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
}
}
if ($elapsed > (self::$timeout * 60)) {
// Session expired — clear all context-specific keys
foreach (self::$keyMap[self::$context] as $key) {
if (isset($_SESSION[$key])) {
unset($_SESSION[$key]);
}
}
/**
* Get the actual $_SESSION key for a logical name in current context
*
* @param string $name Logical name: 'auth', 'activity', 'ip', 'code', 'verify'
* @return string
*/
protected static function getKey(string $name): string {
if (self::$context === null) {
self::$context = 'admin'; // default fallback
}
// Restart session if it was destroyed
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
}
}
if (isset(self::$keyMap[self::$context][$name])) {
return self::$keyMap[self::$context][$name];
}
/**
* Get the actual $_SESSION key for a logical name in current context
*
* @param string $name Logical name: 'auth', 'activity', 'ip', 'code', 'verify'
* @return string
*/
protected static function getKey(string $name): string {
if (self::$context === null) {
self::$context = 'admin'; // default fallback
}
// Unknown key — return as-is (allows extending)
return $name;
}
if (isset(self::$keyMap[self::$context][$name])) {
return self::$keyMap[self::$context][$name];
}
// Unknown key — return as-is (allows extending)
return $name;
}
}
+17 -18
View File
@@ -19,7 +19,6 @@ use XcVm\Core\Storage\DropboxClient;
*/
class BackupService {
private static array $ignoreTables = [
'detect_restream_logs',
'epg_data',
@@ -49,7 +48,7 @@ class BackupService {
*
* @param string $filename Output SQL file path
*/
public static function create($filename) {
public static function create(string $filename) {
\XC_VM::db_dump($filename, self::$ignoreTables);
}
@@ -60,7 +59,7 @@ class BackupService {
* @param string $filename SQL file path to restore
* @return bool Whether the import succeeded
*/
public static function restore($filename) {
public static function restore(string $filename) {
if (!\XC_VM::db_restore($filename)) {
return false;
}
@@ -73,7 +72,7 @@ class BackupService {
*
* @param string $host Remote host IP
*/
public static function grantPrivileges($host) {
public static function grantPrivileges(string $host) {
\XC_VM::db_grant($host);
}
@@ -82,7 +81,7 @@ class BackupService {
*
* @param string $host Remote host IP
*/
public static function revokePrivileges($host) {
public static function revokePrivileges(string $host) {
\XC_VM::db_revoke($host);
}
@@ -92,14 +91,14 @@ class BackupService {
* @return array[] Each entry: filename, timestamp, date, filesize.
*/
public static function getLocal() {
$rBackups = array();
$rBackups = [];
foreach (scandir(MAIN_HOME . 'backups/') as $rBackup) {
$rInfo = pathinfo(MAIN_HOME . 'backups/' . $rBackup);
if ($rInfo['extension'] != 'sql') {
} else {
$rBackups[] = array('filename' => $rBackup, 'timestamp' => filemtime(MAIN_HOME . 'backups/' . $rBackup), 'date' => date('Y-m-d H:i:s', filemtime(MAIN_HOME . 'backups/' . $rBackup)), 'filesize' => filesize(MAIN_HOME . 'backups/' . $rBackup));
$rBackups[] = ['filename' => $rBackup, 'timestamp' => filemtime(MAIN_HOME . 'backups/' . $rBackup), 'date' => date('Y-m-d H:i:s', filemtime(MAIN_HOME . 'backups/' . $rBackup)), 'filesize' => filesize(MAIN_HOME . 'backups/' . $rBackup)];
}
}
usort(
@@ -121,7 +120,7 @@ class BackupService {
try {
$rClient = new DropboxClient();
$rClient->SetBearerToken(array('t' => SettingsManager::get('dropbox_token')));
$rClient->SetBearerToken(['t' => SettingsManager::get('dropbox_token')]);
$rClient->GetFiles();
return true;
@@ -139,12 +138,12 @@ class BackupService {
try {
$rClient = new DropboxClient();
$rClient->SetBearerToken(array('t' => SettingsManager::get('dropbox_token')));
$rClient->SetBearerToken(['t' => SettingsManager::get('dropbox_token')]);
$rFiles = $rClient->GetFiles();
} catch (\exception $e) {
$rFiles = array();
$rFiles = [];
}
$rBackups = array();
$rBackups = [];
foreach ($rFiles as $rFile) {
try {
@@ -169,11 +168,11 @@ class BackupService {
* @param string $rFilename Local destination path.
* @return bool True on success.
*/
public static function downloadRemote($rPath, $rFilename) {
public static function downloadRemote(string $rPath, string $rFilename) {
$rClient = new DropboxClient();
try {
$rClient->SetBearerToken(array('t' => SettingsManager::get('dropbox_token')));
$rClient->SetBearerToken(['t' => SettingsManager::get('dropbox_token')]);
$rClient->downloadFile($rPath, $rFilename);
return true;
@@ -190,15 +189,15 @@ class BackupService {
* @param bool $rOverwrite Overwrite an existing remote file.
* @return mixed Upload result, or an object with an 'error' key on failure.
*/
public static function uploadRemote($rPath, $rFilename, $rOverwrite = true) {
public static function uploadRemote(string $rPath, string $rFilename, bool $rOverwrite = true) {
$rClient = new DropboxClient();
try {
$rClient->SetBearerToken(array('t' => SettingsManager::get('dropbox_token')));
$rClient->SetBearerToken(['t' => SettingsManager::get('dropbox_token')]);
return $rClient->UploadFile($rFilename, $rPath, $rOverwrite);
} catch (\exception $e) {
return (object) array('error' => $e);
return (object) ['error' => $e];
}
}
@@ -208,11 +207,11 @@ class BackupService {
* @param string $rPath Remote path to delete.
* @return bool True on success.
*/
public static function deleteRemote($rPath) {
public static function deleteRemote(string $rPath) {
$rClient = new DropboxClient();
try {
$rClient->SetBearerToken(array('t' => SettingsManager::get('dropbox_token')));
$rClient->SetBearerToken(['t' => SettingsManager::get('dropbox_token')]);
$rClient->Delete($rPath);
return true;
+38 -39
View File
@@ -42,47 +42,46 @@ namespace XcVm\Core\Cache;
*/
interface CacheInterface {
/**
* Retrieve a value from cache
*
* @param string $key Cache key
* @param int|null $maxAge Maximum age in seconds (null = no limit)
* @return mixed|false Cached data or false if not found / expired
*/
public function get(string $key, ?int $maxAge = null);
/**
* Retrieve a value from cache
*
* @param string $key Cache key
* @param int|null $maxAge Maximum age in seconds (null = no limit)
* @return mixed|false Cached data or false if not found / expired
*/
public function get($key, $maxAge = null);
/**
* Store a value in cache
*
* @param string $key Cache key
* @param mixed $data Data to store
* @param int $ttl Time to live in seconds (0 = forever)
* @return bool Success
*/
public function set(string $key, mixed $data, int $ttl = 0);
/**
* Store a value in cache
*
* @param string $key Cache key
* @param mixed $data Data to store
* @param int $ttl Time to live in seconds (0 = forever)
* @return bool Success
*/
public function set($key, $data, $ttl = 0);
/**
* Delete a cache entry
*
* @param string $key Cache key
* @return bool Success
*/
public function delete(string $key);
/**
* Delete a cache entry
*
* @param string $key Cache key
* @return bool Success
*/
public function delete($key);
/**
* Check if a cache key exists (and is not expired)
*
* @param string $key Cache key
* @param int|null $maxAge Maximum age in seconds (null = no limit)
* @return bool
*/
public function has(string $key, ?int $maxAge = null);
/**
* Check if a cache key exists (and is not expired)
*
* @param string $key Cache key
* @param int|null $maxAge Maximum age in seconds (null = no limit)
* @return bool
*/
public function has($key, $maxAge = null);
/**
* Clear all cache entries
*
* @return bool Success
*/
public function flush();
/**
* Clear all cache entries
*
* @return bool Success
*/
public function flush();
}
+232 -235
View File
@@ -34,278 +34,275 @@ namespace XcVm\Core\Cache;
*/
class FileCache implements CacheInterface {
/** @var string Base directory for cache files */
protected $basePath;
/** @var string Base directory for cache files */
protected $basePath;
/** @var bool Whether igbinary extension is available */
protected $useIgbinary;
/** @var bool Whether igbinary extension is available */
protected $useIgbinary;
/**
* @param string $basePath Directory for cache files (must end with /)
*/
public function __construct(string $basePath) {
$this->basePath = rtrim($basePath, '/') . '/';
$this->useIgbinary = function_exists('igbinary_serialize');
/**
* @param string $basePath Directory for cache files (must end with /)
*/
public function __construct($basePath) {
$this->basePath = rtrim($basePath, '/') . '/';
$this->useIgbinary = function_exists('igbinary_serialize');
if (!is_dir($this->basePath)) {
@mkdir($this->basePath, 0755, true);
}
// A root-context boot (installer, `console.php status`) must leave the
// cache dir owned by the panel user, or xc_vm processes cannot create
// cache files in it afterwards. Same pattern as set().
if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
@chown($this->basePath, 'xc_vm');
@chgrp($this->basePath, 'xc_vm');
}
}
if (!is_dir($this->basePath)) {
@mkdir($this->basePath, 0755, true);
}
// A root-context boot (installer, `console.php status`) must leave the
// cache dir owned by the panel user, or xc_vm processes cannot create
// cache files in it afterwards. Same pattern as set().
if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
@chown($this->basePath, 'xc_vm');
@chgrp($this->basePath, 'xc_vm');
}
}
/**
* {@inheritdoc}
*/
public function get($key, $maxAge = null) {
$file = $this->basePath . $key;
/**
* {@inheritdoc}
*/
public function get($key, $maxAge = null) {
$file = $this->basePath . $key;
if (!file_exists($file)) {
return false;
}
if (!file_exists($file)) {
return false;
}
// Check TTL based on file modification time
if ($maxAge !== null) {
$age = time() - filemtime($file);
if ($age >= $maxAge) {
return false;
}
}
// Check TTL based on file modification time
if ($maxAge !== null) {
$age = time() - filemtime($file);
if ($age >= $maxAge) {
return false;
}
}
$data = @file_get_contents($file);
$data = @file_get_contents($file);
if ($data === false || $data === '') {
@unlink($file);
return false;
}
if ($data === false || $data === '') {
@unlink($file);
return false;
}
$result = $this->deserialize($data);
$result = $this->deserialize($data);
if ($result === false) {
@unlink($file);
}
if ($result === false) {
@unlink($file);
}
return $result;
}
return $result;
}
/**
* {@inheritdoc}
*/
public function set($key, $data, $ttl = 0) {
$file = $this->basePath . $key;
$serialized = $this->serialize($data);
/**
* {@inheritdoc}
*/
public function set($key, $data, $ttl = 0) {
$file = $this->basePath . $key;
$serialized = $this->serialize($data);
$tmp = $file . '.' . getmypid() . '.tmp';
if (@file_put_contents($tmp, $serialized, LOCK_EX) === false) {
@unlink($tmp);
$this->warnWriteFailure($file);
return false;
}
if (!@rename($tmp, $file)) {
@unlink($tmp);
$this->warnWriteFailure($file);
return false;
}
// A root-context write (installer, `console.php status`) must stay
// owned by the panel user, or xc_vm daemons cannot refresh the file
// later. Same pattern as Logger.
if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
@chown($file, 'xc_vm');
@chgrp($file, 'xc_vm');
}
return true;
}
$tmp = $file . '.' . getmypid() . '.tmp';
if (@file_put_contents($tmp, $serialized, LOCK_EX) === false) {
@unlink($tmp);
$this->warnWriteFailure($file);
return false;
}
if (!@rename($tmp, $file)) {
@unlink($tmp);
$this->warnWriteFailure($file);
return false;
}
// A root-context write (installer, `console.php status`) must stay
// owned by the panel user, or xc_vm daemons cannot refresh the file
// later. Same pattern as Logger.
if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
@chown($file, 'xc_vm');
@chgrp($file, 'xc_vm');
}
return true;
}
/**
* Report a failed cache write once per process.
*
* A silently stale cache (bad tmp/ ownership, full or missing tmpfs) keeps
* the panel running on outdated settings with no visible symptom, so the
* first failure must reach the panel log via the error handler.
*
* @param string $file Cache file path that could not be written.
*/
protected function warnWriteFailure(string $file) {
static $warned = false;
if ($warned) {
return;
}
$warned = true;
trigger_error('FileCache: cache write failed for ' . $file . ' — serving stale cache', E_USER_WARNING);
}
/**
* Report a failed cache write once per process.
*
* A silently stale cache (bad tmp/ ownership, full or missing tmpfs) keeps
* the panel running on outdated settings with no visible symptom, so the
* first failure must reach the panel log via the error handler.
*
* @param string $file Cache file path that could not be written.
*/
protected function warnWriteFailure($file) {
static $warned = false;
if ($warned) {
return;
}
$warned = true;
trigger_error('FileCache: cache write failed for ' . $file . ' — serving stale cache', E_USER_WARNING);
}
/**
* {@inheritdoc}
*/
public function delete($key) {
$file = $this->basePath . $key;
/**
* {@inheritdoc}
*/
public function delete($key) {
$file = $this->basePath . $key;
if (file_exists($file)) {
return unlink($file);
}
if (file_exists($file)) {
return unlink($file);
}
return true;
}
return true;
}
/**
* {@inheritdoc}
*/
public function has($key, $maxAge = null) {
$file = $this->basePath . $key;
/**
* {@inheritdoc}
*/
public function has($key, $maxAge = null) {
$file = $this->basePath . $key;
if (!file_exists($file)) {
return false;
}
if (!file_exists($file)) {
return false;
}
if ($maxAge !== null) {
$age = time() - filemtime($file);
if ($age >= $maxAge) {
return false;
}
}
if ($maxAge !== null) {
$age = time() - filemtime($file);
if ($age >= $maxAge) {
return false;
}
}
return true;
}
return true;
}
/**
* {@inheritdoc}
*/
public function flush() {
$files = glob($this->basePath . '*');
/**
* {@inheritdoc}
*/
public function flush() {
$files = glob($this->basePath . '*');
if ($files === false) {
return false;
}
if ($files === false) {
return false;
}
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
return true;
}
return true;
}
/**
* Get the file path for a cache key
*
* Useful for direct file operations (e.g., file_exists checks
* in legacy code during migration).
*
* @param string $key Cache key
* @return string Full file path
*/
public function getPath(string $key) {
return $this->basePath . $key;
}
/**
* Get the file path for a cache key
*
* Useful for direct file operations (e.g., file_exists checks
* in legacy code during migration).
*
* @param string $key Cache key
* @return string Full file path
*/
public function getPath($key) {
return $this->basePath . $key;
}
/**
* Get the base directory path
*
* @return string
*/
public function getBasePath() {
return $this->basePath;
}
/**
* Get the base directory path
*
* @return string
*/
public function getBasePath() {
return $this->basePath;
}
/**
* Get modification time of a cache entry
*
* @param string $key Cache key
* @return int|false Unix timestamp or false if not found
*/
public function getAge(string $key) {
$file = $this->basePath . $key;
/**
* Get modification time of a cache entry
*
* @param string $key Cache key
* @return int|false Unix timestamp or false if not found
*/
public function getAge($key) {
$file = $this->basePath . $key;
if (!file_exists($file)) {
return false;
}
if (!file_exists($file)) {
return false;
}
return time() - filemtime($file);
}
return time() - filemtime($file);
}
/**
* Serialize data using igbinary (if available) or PHP serialize
*
* @return string
*/
protected function serialize(mixed $data) {
if ($this->useIgbinary) {
return igbinary_serialize($data);
}
/**
* Serialize data using igbinary (if available) or PHP serialize
*
* @param mixed $data
* @return string
*/
protected function serialize($data) {
if ($this->useIgbinary) {
return igbinary_serialize($data);
}
return serialize($data);
}
return serialize($data);
}
/**
* Deserialize data using igbinary (if available) or PHP unserialize
*
* Returns false on corrupted data (cache miss).
*
* @return mixed|false
*/
protected function deserialize(string $data) {
if ($this->useIgbinary) {
$result = @igbinary_unserialize($data);
if ($result === false && $data !== igbinary_serialize(false)) {
return false;
}
return $result;
}
/**
* Deserialize data using igbinary (if available) or PHP unserialize
*
* Returns false on corrupted data (cache miss).
*
* @param string $data
* @return mixed|false
*/
protected function deserialize($data) {
if ($this->useIgbinary) {
$result = @igbinary_unserialize($data);
if ($result === false && $data !== igbinary_serialize(false)) {
return false;
}
return $result;
}
$result = @unserialize($data);
if ($result === false && $data !== serialize(false)) {
return false;
}
return $result;
}
$result = @unserialize($data);
if ($result === false && $data !== serialize(false)) {
return false;
}
return $result;
}
// ------------------------------------------------------------------
// Static convenience API (drop-in replacement for CoreUtilities)
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// Static convenience API (drop-in replacement for CoreUtilities)
// ------------------------------------------------------------------
/** @var self|null Singleton instance for static calls */
private static $defaultInstance;
/** @var self|null Singleton instance for static calls */
private static $defaultInstance;
/**
* Get the default singleton instance (uses CACHE_TMP_PATH)
*
* @return self
*/
private static function getDefault() {
if (!self::$defaultInstance) {
self::$defaultInstance = new self(CACHE_TMP_PATH);
}
return self::$defaultInstance;
}
/**
* Get the default singleton instance (uses CACHE_TMP_PATH)
*
* @return self
*/
private static function getDefault() {
if (!self::$defaultInstance) {
self::$defaultInstance = new self(CACHE_TMP_PATH);
}
return self::$defaultInstance;
}
/**
* Static write drop-in for CoreUtilities::setCache()
*
* @param string $key Cache key
* @param mixed $data Data to cache
* @return bool
*/
public static function setCache(string $key, mixed $data) {
return self::getDefault()->set($key, $data);
}
/**
* Static write drop-in for CoreUtilities::setCache()
*
* @param string $key Cache key
* @param mixed $data Data to cache
* @return bool
*/
public static function setCache($key, $data) {
return self::getDefault()->set($key, $data);
}
/**
* Static read drop-in for CoreUtilities::getCache()
*
* @param string $key Cache key
* @param int|null $maxAge Maximum age in seconds (null = no limit)
* @return mixed|false
*/
public static function getCache($key, $maxAge = null) {
return self::getDefault()->get($key, $maxAge);
}
/**
* Static read drop-in for CoreUtilities::getCache()
*
* @param string $key Cache key
* @param int|null $maxAge Maximum age in seconds (null = no limit)
* @return mixed|false
*/
public static function getCache(string $key, ?int $maxAge = null) {
return self::getDefault()->get($key, $maxAge);
}
}
+175 -176
View File
@@ -41,217 +41,216 @@ use XcVm\Infrastructure\Redis\RedisManager;
*/
class RedisCache implements CacheInterface {
/** @var \Redis|null phpredis connection */
protected $redis = null;
/** @var \Redis|null phpredis connection */
protected $redis = null;
/** @var string \Redis host */
protected $host;
/** @var string \Redis host */
protected $host;
/** @var int \Redis port */
protected $port;
/** @var int \Redis port */
protected $port;
/** @var string|null \Redis password */
protected $password;
/** @var string|null \Redis password */
protected $password;
/** @var bool Whether connection is established */
protected $connected = false;
/** @var bool Whether connection is established */
protected $connected = false;
/** @var string Key prefix to avoid collisions */
protected $prefix = '';
/** @var string Key prefix to avoid collisions */
protected $prefix = '';
/**
* @param string $host \Redis host
* @param int $port \Redis port
* @param string|null $password \Redis AUTH password
* @param string $prefix Optional key prefix
*/
public function __construct(string $host = '127.0.0.1', int $port = 6379, ?string $password = null, string $prefix = '') {
$this->host = $host;
$this->port = $port;
$this->password = $password;
$this->prefix = $prefix;
}
/**
* @param string $host \Redis host
* @param int $port \Redis port
* @param string|null $password \Redis AUTH password
* @param string $prefix Optional key prefix
*/
public function __construct($host = '127.0.0.1', $port = 6379, $password = null, $prefix = '') {
$this->host = $host;
$this->port = $port;
$this->password = $password;
$this->prefix = $prefix;
}
/**
* Establish \Redis connection (lazy called on first operation)
*
* @return bool
*/
public function connect() {
if ($this->connected && $this->redis !== null) {
return true;
}
/**
* Establish \Redis connection (lazy called on first operation)
*
* @return bool
*/
public function connect() {
if ($this->connected && $this->redis !== null) {
return true;
}
try {
$this->redis = new \Redis();
$this->redis->connect($this->host, $this->port);
try {
$this->redis = new \Redis();
$this->redis->connect($this->host, $this->port);
if ($this->password) {
$this->redis->auth($this->password);
}
if ($this->password) {
$this->redis->auth($this->password);
}
// Use igbinary serializer if available for consistency with FileCache
if (defined('Redis::SERIALIZER_IGBINARY')) {
$this->redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_IGBINARY);
}
// Use igbinary serializer if available for consistency with FileCache
if (defined('Redis::SERIALIZER_IGBINARY')) {
$this->redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_IGBINARY);
}
$this->connected = true;
return true;
} catch (\Exception $e) {
$this->redis = null;
$this->connected = false;
return false;
}
}
$this->connected = true;
return true;
} catch (\Exception $e) {
$this->redis = null;
$this->connected = false;
return false;
}
}
/**
* {@inheritdoc}
*/
public function get($key, $maxAge = null) {
if (!$this->ensureConnected()) {
return false;
}
/**
* {@inheritdoc}
*/
public function get($key, $maxAge = null) {
if (!$this->ensureConnected()) {
return false;
}
$prefixedKey = $this->prefix . $key;
$data = $this->redis->get($prefixedKey);
$prefixedKey = $this->prefix . $key;
$data = $this->redis->get($prefixedKey);
if ($data === false) {
return false;
}
if ($data === false) {
return false;
}
// maxAge is handled by \Redis TTL, not by us
// But if caller wants to check age, we can't — \Redis doesn't store creation time
// For file-based TTL compat, we ignore maxAge here (\Redis uses its own TTL)
// maxAge is handled by \Redis TTL, not by us
// But if caller wants to check age, we can't — \Redis doesn't store creation time
// For file-based TTL compat, we ignore maxAge here (\Redis uses its own TTL)
return $data;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function set($key, $data, $ttl = 0) {
if (!$this->ensureConnected()) {
return false;
}
/**
* {@inheritdoc}
*/
public function set($key, $data, $ttl = 0) {
if (!$this->ensureConnected()) {
return false;
}
$prefixedKey = $this->prefix . $key;
$prefixedKey = $this->prefix . $key;
if ($ttl > 0) {
return $this->redis->setex($prefixedKey, $ttl, $data);
}
if ($ttl > 0) {
return $this->redis->setex($prefixedKey, $ttl, $data);
}
return $this->redis->set($prefixedKey, $data);
}
return $this->redis->set($prefixedKey, $data);
}
/**
* {@inheritdoc}
*/
public function delete($key) {
if (!$this->ensureConnected()) {
return false;
}
/**
* {@inheritdoc}
*/
public function delete($key) {
if (!$this->ensureConnected()) {
return false;
}
$prefixedKey = $this->prefix . $key;
$this->redis->del($prefixedKey);
$prefixedKey = $this->prefix . $key;
$this->redis->del($prefixedKey);
return true;
}
return true;
}
/**
* {@inheritdoc}
*/
public function has($key, $maxAge = null) {
if (!$this->ensureConnected()) {
return false;
}
/**
* {@inheritdoc}
*/
public function has($key, $maxAge = null) {
if (!$this->ensureConnected()) {
return false;
}
$prefixedKey = $this->prefix . $key;
$prefixedKey = $this->prefix . $key;
return (bool) $this->redis->exists($prefixedKey);
}
return (bool) $this->redis->exists($prefixedKey);
}
/**
* {@inheritdoc}
*
* WARNING: Flushes the ENTIRE \Redis database. Use with caution.
*/
public function flush() {
if (!$this->ensureConnected()) {
return false;
}
/**
* {@inheritdoc}
*
* WARNING: Flushes the ENTIRE \Redis database. Use with caution.
*/
public function flush() {
if (!$this->ensureConnected()) {
return false;
}
return $this->redis->flushDB();
}
return $this->redis->flushDB();
}
// ───────────────────────────────────────────────────────────
// Raw \Redis Access (for migration period)
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Raw \Redis Access (for migration period)
// ───────────────────────────────────────────────────────────
/**
* Get the raw phpredis connection
*
* Allows legacy code to use \Redis-specific operations (sorted sets,
* pipelines, pub/sub, etc.) that don't fit the CacheInterface.
*
* @return \Redis|null
*/
public function getConnection() {
$this->ensureConnected();
return $this->redis;
}
/**
* Get the raw phpredis connection
*
* Allows legacy code to use \Redis-specific operations (sorted sets,
* pipelines, pub/sub, etc.) that don't fit the CacheInterface.
*
* @return \Redis|null
*/
public function getConnection() {
$this->ensureConnected();
return $this->redis;
}
/**
* Check if connection is alive
*
* @return bool
*/
public function isConnected() {
if (!$this->connected || !$this->redis) {
return false;
}
/**
* Check if connection is alive
*
* @return bool
*/
public function isConnected() {
if (!$this->connected || !$this->redis) {
return false;
}
try {
return $this->redis->ping() !== false;
} catch (\Exception $e) {
$this->connected = false;
return false;
}
}
try {
return $this->redis->ping() !== false;
} catch (\Exception $e) {
$this->connected = false;
return false;
}
}
/**
* Close connection
*/
public function disconnect() {
if ($this->redis) {
try {
$this->redis->close();
} catch (\Exception $e) {
// ignore
}
}
/**
* Close connection
*/
public function disconnect() {
if ($this->redis) {
try {
$this->redis->close();
} catch (\Exception $e) {
// ignore
}
}
$this->redis = null;
$this->connected = false;
}
$this->redis = null;
$this->connected = false;
}
/**
* Ensure connection is active, reconnect if needed
*
* @return bool
*/
protected function ensureConnected() {
if ($this->connected && $this->redis !== null) {
return true;
}
/**
* Ensure connection is active, reconnect if needed
*
* @return bool
*/
protected function ensureConnected() {
if ($this->connected && $this->redis !== null) {
return true;
}
return $this->connect();
}
return $this->connect();
}
/**
* Disconnect from \Redis when the instance is destroyed.
*/
public function __destruct() {
$this->disconnect();
}
/**
* Disconnect from \Redis when the instance is destroyed.
*/
public function __destruct() {
$this->disconnect();
}
}
+5 -5
View File
@@ -32,14 +32,14 @@ define('DEV_MODE', false);
define('XC_VM_VERSION', '2.5.1');
define('GIT_OWNER', 'Vateron-Media');
define('GIT_REPO_MAIN', 'XC_VM');
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_BIN', 'XC_VM_Binaries');
define('GIT_REPO_FANOUT', 'XC_VM_Fanout'); // xc_fanout daemon: source repo, binaries as release assets
define('GIT_REPO_PROXY', 'XC_VM_Proxy');
define('GIT_REPO_PROXY', 'XC_VM_Proxy');
// ── Miscellaneous Settings ─────────────────────────────────────
define('MONITOR_CALLS', 3); // Number of retry attempts for monitoring tasks
define('OPENSSL_EXTRA', 'fNiu3XD448xTDa27xoY4'); // Additional OpenSSL entropy/seed (review necessity)
define('OPENSSL_EXTRA', 'fNiu3XD448xTDa27xoY4'); // Additional OpenSSL entropy/seed (review necessity)
+4 -5
View File
@@ -1,6 +1,5 @@
<?php
/**
* Пути к бинарным файлам
*
@@ -15,14 +14,14 @@
*/
// ── PHP & утилиты ─────────────────────────────────────────────
define('PHP_BIN', BIN_PATH . 'php/bin/php');
define('PHP_BIN', BIN_PATH . 'php/bin/php');
define('YOUTUBE_BIN', BIN_PATH . 'yt-dlp');
define('FFMPEG_FONT', BIN_PATH . 'free-sans.ttf');
// ── GeoIP базы данных ─────────────────────────────────────────
define('GEOLITE2_BIN', BIN_PATH . 'maxmind/GeoLite2-Country.mmdb');
define('GEOLITE2_BIN', BIN_PATH . 'maxmind/GeoLite2-Country.mmdb');
define('GEOLITE2C_BIN', BIN_PATH . 'maxmind/GeoLite2-City.mmdb');
define('GEOISP_BIN', BIN_PATH . 'maxmind/GeoIP2-ISP.mmdb');
define('GEOISP_BIN', BIN_PATH . 'maxmind/GeoIP2-ISP.mmdb');
// ── FFmpeg/FFprobe — legacy 4.0 "DTS" build anchor ────────────
// Only the 4.0 build is pinned as a constant: it is the legacy/DTS-safe binary
@@ -30,5 +29,5 @@ define('GEOISP_BIN', BIN_PATH . 'maxmind/GeoIP2-ISP.mmdb');
// other version is resolved dynamically from bin/ffmpeg_bin/<version>/ by
// FfmpegPaths (paths) and FfmpegBinaries (discovery + capabilities) — no more
// per-version constants to keep in sync with what actually ships in the folder.
define('FFMPEG_BIN_40', BIN_PATH . 'ffmpeg_bin/4.0/ffmpeg');
define('FFMPEG_BIN_40', BIN_PATH . 'ffmpeg_bin/4.0/ffmpeg');
define('FFPROBE_BIN_40', BIN_PATH . 'ffmpeg_bin/4.0/ffprobe');
+1 -1
View File
@@ -39,7 +39,7 @@ class ConfigReader {
* @param mixed $default Значение по умолчанию
* @return mixed
*/
public static function get(string $key, $default = null) {
public static function get(string $key, mixed $default = null) {
return self::getAll()[$key] ?? $default;
}
}
+5 -5
View File
@@ -27,7 +27,7 @@ class DomainResolver {
* @param bool $rForceSSL Force the https protocol.
* @return string Public base URL (trailing slash), or '' if no proxy is available.
*/
public static function resolve($rServerID, $rForceSSL = false) {
public static function resolve(int $rServerID, bool $rForceSSL = false) {
global $rServers, $rSettings;
$rOriginatorID = null;
if ($rForceSSL) {
@@ -62,14 +62,14 @@ class DomainResolver {
}
if ($rProxied || $rSettings['use_mdomain_in_lists'] == 1) {
$rResellerDomains = CacheReader::get('reseller_domains') ?: array();
$rResellerDomains = CacheReader::get('reseller_domains') ?: [];
if (!(strlen($rDomain) > 0 && in_array(strtolower($rDomain), $rResellerDomains))) {
if (empty($rServers[$rServerID]['domain_name'])) {
$rDomain = escapeshellcmd($rServers[$rServerID]['server_ip']);
} else if (filter_var($rDomain, FILTER_VALIDATE_IP)) {
} elseif (filter_var($rDomain, FILTER_VALIDATE_IP)) {
$rDomain = escapeshellcmd($rServers[$rServerID]['server_ip']);
} else {
$rDomain = str_replace(array('http://', '/', 'https://'), '', escapeshellcmd(explode(',', $rServers[$rServerID]['domain_name'])[0]));
$rDomain = str_replace(['http://', '/', 'https://'], '', escapeshellcmd(explode(',', $rServers[$rServerID]['domain_name'])[0]));
}
}
} else {
@@ -77,7 +77,7 @@ class DomainResolver {
if (empty($rServers[$rServerID]['domain_name'])) {
$rDomain = escapeshellcmd($rServers[$rServerID]['server_ip']);
} else {
$rDomain = str_replace(array('http://', '/', 'https://'), '', escapeshellcmd(explode(',', $rServers[$rServerID]['domain_name'])[0]));
$rDomain = str_replace(['http://', '/', 'https://'], '', escapeshellcmd(explode(',', $rServers[$rServerID]['domain_name'])[0]));
}
}
}
+29 -29
View File
@@ -25,66 +25,66 @@
// ─────────────────────────────────────────────────────────────────
if (!defined('CONTENT_PATH')) {
define('CONTENT_PATH', MAIN_HOME . 'content/');
define('CONTENT_PATH', MAIN_HOME . 'content/');
}
if (!defined('TMP_PATH')) {
define('TMP_PATH', MAIN_HOME . 'tmp/');
define('TMP_PATH', MAIN_HOME . 'tmp/');
}
// ─────────────────────────────────────────────────────────────────
// 2. Системные директории
// ─────────────────────────────────────────────────────────────────
define('CONFIG_PATH', MAIN_HOME . 'config/');
define('BIN_PATH', MAIN_HOME . 'bin/');
define('STORAGE_PATH', MAIN_HOME . 'storage/');
define('SIGNALS_PATH', MAIN_HOME . 'signals/');
define('CONFIG_PATH', MAIN_HOME . 'config/');
define('BIN_PATH', MAIN_HOME . 'bin/');
define('STORAGE_PATH', MAIN_HOME . 'storage/');
define('SIGNALS_PATH', MAIN_HOME . 'signals/');
// xc_fanout daemon sockets (ADR 0002, P2/P3). Kept in the app bin tree next to
// the daemon binary, mirroring the php-fpm sockets layout (bin/php/sockets/):
// FANOUT_HTTP_SOCK is the nginx-facing client surface, FANOUT_CTL_SOCK the
// PHP-only control surface. A unix socket stores no stream bytes (IPC only), so
// this is orthogonal to the tmpfs-free byte-path goal.
define('FANOUT_RUN_PATH', BIN_PATH . 'xc_fanout/sockets/');
define('FANOUT_CTL_SOCK', FANOUT_RUN_PATH . 'control.sock');
define('FANOUT_RUN_PATH', BIN_PATH . 'xc_fanout/sockets/');
define('FANOUT_CTL_SOCK', FANOUT_RUN_PATH . 'control.sock');
define('FANOUT_HTTP_SOCK', FANOUT_RUN_PATH . 'http.sock');
// ─────────────────────────────────────────────────────────────────
// 3. Контент-директории
// ─────────────────────────────────────────────────────────────────
define('STREAMS_PATH', CONTENT_PATH . 'streams/');
define('EPG_PATH', CONTENT_PATH . 'epg/');
define('VOD_PATH', CONTENT_PATH . 'vod/');
define('ARCHIVE_PATH', CONTENT_PATH . 'archive/');
define('CREATED_PATH', CONTENT_PATH . 'created/');
define('DELAY_PATH', CONTENT_PATH . 'delayed/');
define('VIDEO_PATH', CONTENT_PATH . 'video/');
define('STREAMS_PATH', CONTENT_PATH . 'streams/');
define('EPG_PATH', CONTENT_PATH . 'epg/');
define('VOD_PATH', CONTENT_PATH . 'vod/');
define('ARCHIVE_PATH', CONTENT_PATH . 'archive/');
define('CREATED_PATH', CONTENT_PATH . 'created/');
define('DELAY_PATH', CONTENT_PATH . 'delayed/');
define('VIDEO_PATH', CONTENT_PATH . 'video/');
define('PLAYLIST_PATH', CONTENT_PATH . 'playlists/');
// ─────────────────────────────────────────────────────────────────
// 4. Временные директории
// ─────────────────────────────────────────────────────────────────
define('CONS_TMP_PATH', TMP_PATH . 'opened_cons/');
define('CRONS_TMP_PATH', TMP_PATH . 'crons/');
define('CIDR_TMP_PATH', TMP_PATH . 'cidr/');
define('CACHE_TMP_PATH', TMP_PATH . 'cache/');
define('STREAMS_TMP_PATH', TMP_PATH . 'cache/streams/');
define('SERIES_TMP_PATH', TMP_PATH . 'cache/series/');
define('LINES_TMP_PATH', TMP_PATH . 'cache/lines/');
define('CONS_TMP_PATH', TMP_PATH . 'opened_cons/');
define('CRONS_TMP_PATH', TMP_PATH . 'crons/');
define('CIDR_TMP_PATH', TMP_PATH . 'cidr/');
define('CACHE_TMP_PATH', TMP_PATH . 'cache/');
define('STREAMS_TMP_PATH', TMP_PATH . 'cache/streams/');
define('SERIES_TMP_PATH', TMP_PATH . 'cache/series/');
define('LINES_TMP_PATH', TMP_PATH . 'cache/lines/');
define('DIVERGENCE_TMP_PATH', TMP_PATH . 'divergence/');
define('FLOOD_TMP_PATH', TMP_PATH . 'flood/');
define('PLAYER_TMP_PATH', TMP_PATH . 'player/');
define('MINISTRA_TMP_PATH', TMP_PATH . 'ministra/');
define('SIGNALS_TMP_PATH', TMP_PATH . 'signals/');
define('LOGS_TMP_PATH', TMP_PATH . 'logs/');
define('WATCH_TMP_PATH', TMP_PATH . 'watch/');
define('FLOOD_TMP_PATH', TMP_PATH . 'flood/');
define('PLAYER_TMP_PATH', TMP_PATH . 'player/');
define('MINISTRA_TMP_PATH', TMP_PATH . 'ministra/');
define('SIGNALS_TMP_PATH', TMP_PATH . 'signals/');
define('LOGS_TMP_PATH', TMP_PATH . 'logs/');
define('WATCH_TMP_PATH', TMP_PATH . 'watch/');
// ─────────────────────────────────────────────────────────────────
// 5. Хранилище файлов
// ─────────────────────────────────────────────────────────────────
define('IMAGES_PATH', STORAGE_PATH . 'images/');
define('IMAGES_PATH', STORAGE_PATH . 'images/');
define('E2_IMAGES_PATH', IMAGES_PATH . 'enigma2/');
+3 -13
View File
@@ -16,7 +16,7 @@ namespace XcVm\Core\Config;
class SettingsManager {
/** @var array */
private static $settings = array();
private static $settings = [];
/**
* Сохраняет весь массив настроек.
@@ -35,11 +35,9 @@ class SettingsManager {
/**
* Возвращает значение по ключу.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public static function get(string $key, $default = null) {
public static function get(string $key, mixed $default = null) {
return self::$settings[$key] ?? $default;
}
@@ -53,7 +51,6 @@ class SettingsManager {
/**
* Проверяет наличие ключа в настройках.
*
* @param string $key
* @return bool
*/
public static function has(string $key): bool {
@@ -66,7 +63,6 @@ class SettingsManager {
* Повторяет PHP-truthiness существующих проверок `if (getAll()['key'])`:
* '0' и '' false, '1' и любое непустое значение true.
*
* @param string $key
* @param bool $default Значение, если ключ отсутствует.
* @return bool
*/
@@ -77,8 +73,6 @@ class SettingsManager {
/**
* Возвращает значение как int.
*
* @param string $key
* @param int $default
* @return int
*/
public static function getInt(string $key, int $default = 0): int {
@@ -88,8 +82,6 @@ class SettingsManager {
/**
* Возвращает значение как строку.
*
* @param string $key
* @param string $default
* @return string
*/
public static function getString(string $key, string $default = ''): string {
@@ -102,11 +94,9 @@ class SettingsManager {
* JSON-поля декодируются в массивы ещё в SettingsRepository, поэтому здесь
* достаточно проверить тип; для скаляров/null возвращается $default.
*
* @param string $key
* @param array $default
* @return array
*/
public static function getArray(string $key, array $default = array()): array {
public static function getArray(string $key, array $default = []): array {
$rValue = self::$settings[$key] ?? null;
return is_array($rValue) ? $rValue : $default;
}
+4 -4
View File
@@ -21,7 +21,7 @@ class SettingsRepository {
* @param bool $rForce Bypass the file cache and re-read from the database.
* @return array Settings map (with normalized array fields).
*/
public static function getAll($rForce = false) {
public static function getAll(bool $rForce = false) {
global $db;
if (!$rForce) {
$rCache = FileCache::getCache('settings', 20);
@@ -30,17 +30,17 @@ class SettingsRepository {
}
}
$rOutput = array();
$rOutput = [];
$db->query('SELECT * FROM `settings`');
$rRows = $db->get_row();
foreach ($rRows ?: array() as $rKey => $rValue) {
foreach ($rRows ?: [] as $rKey => $rValue) {
$rOutput[$rKey] = $rValue;
}
$rOutput['allow_countries'] = json_decode($rOutput['allow_countries'] ?? '', true);
$decodedAllowedSTB = json_decode($rOutput['allowed_stb_types'] ?? '', true);
$rOutput['allowed_stb_types'] = array();
$rOutput['allowed_stb_types'] = [];
if (is_array($decodedAllowedSTB)) {
// Drop blank entries so an "empty" selection (an unset multiselect is
// commonly stored as [""]) collapses to a truly empty array. An empty
@@ -10,4 +10,5 @@ namespace XcVm\Core\Container\Psr;
* @package XC_VM_Core_Container
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
interface ContainerExceptionInterface extends \Throwable {}
interface ContainerExceptionInterface extends \Throwable {
}
+12 -13
View File
@@ -11,18 +11,17 @@ namespace XcVm\Core\Container\Psr;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
interface ContainerInterface {
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id Identifier of the entry to look for.
* @throws NotFoundExceptionInterface No entry was found for this identifier.
* @throws ContainerExceptionInterface Error while retrieving the entry.
*/
public function get(string $id): mixed;
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id Identifier of the entry to look for.
* @throws NotFoundExceptionInterface No entry was found for this identifier.
* @throws ContainerExceptionInterface Error while retrieving the entry.
*/
public function get(string $id): mixed;
/**
* Returns true if the container can return an entry for the given identifier.
*/
public function has(string $id): bool;
/**
* Returns true if the container can return an entry for the given identifier.
*/
public function has(string $id): bool;
}
+2 -1
View File
@@ -10,4 +10,5 @@ use XcVm\Core\Exception\Container\ContainerException;
* @package XC_VM_Core_Container
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class NotFoundException extends ContainerException implements NotFoundExceptionInterface {}
class NotFoundException extends ContainerException implements NotFoundExceptionInterface {
}
@@ -10,4 +10,5 @@ namespace XcVm\Core\Container\Psr;
* @package XC_VM_Core_Container
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
interface NotFoundExceptionInterface extends ContainerExceptionInterface {}
interface NotFoundExceptionInterface extends ContainerExceptionInterface {
}
+402 -407
View File
@@ -87,470 +87,465 @@ use XcVm\Core\Exception\XcVmException;
*/
class ServiceContainer implements ContainerInterface {
private static ?self $instance = null;
private static ?self $instance = null;
/** @var array<string, callable> */
private array $factories = [];
/** @var array<string, callable> */
private array $factories = [];
/** @var array<string, mixed> */
private array $resolved = [];
/** @var array<string, mixed> */
private array $resolved = [];
private array $isFactory = [];
private array $isFactory = [];
private array $creating = [];
private array $creating = [];
/** @var array<string, string[]> */
private array $tags = [];
/** @var array<string, string[]> */
private array $tags = [];
/**
* Decoration chains: id => priority => decorator[]
* Each decorator is a class-string or callable(inner, container): mixed
* @var array<string, array<int, array<class-string|callable>>>
*/
private array $decorators = [];
/**
* Decoration chains: id => priority => decorator[]
* Each decorator is a class-string or callable(inner, container): mixed
* @var array<string, array<int, array<class-string|callable>>>
*/
private array $decorators = [];
/**
* Services that modules are not allowed to decorate.
* @var string[]
*/
private array $protectedServices = ['db', 'settings', 'config', 'auth'];
/**
* Services that modules are not allowed to decorate.
* @var string[]
*/
private array $protectedServices = ['db', 'settings', 'config', 'auth'];
// ─────────────────────────────────────────────────────────
// Singleton
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Singleton
// ─────────────────────────────────────────────────────────
/**
* Получить единственный экземпляр.
*
* @internal Bootstrap only. Modules receive ServiceContainer via boot(ServiceContainer $c).
*/
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Получить единственный экземпляр.
*
* @internal Bootstrap only. Modules receive ServiceContainer via boot(ServiceContainer $c).
*/
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Сбросить контейнер (для тестов).
*/
public static function resetInstance(): void {
if (self::$instance !== null) {
self::$instance->factories = [];
self::$instance->resolved = [];
self::$instance->isFactory = [];
self::$instance->creating = [];
self::$instance->tags = [];
}
self::$instance = null;
}
/**
* Сбросить контейнер (для тестов).
*/
public static function resetInstance(): void {
if (self::$instance !== null) {
self::$instance->factories = [];
self::$instance->resolved = [];
self::$instance->isFactory = [];
self::$instance->creating = [];
self::$instance->tags = [];
}
self::$instance = null;
}
/**
* Приватный конструктор (singleton).
*/
private function __construct() {
}
/**
* Приватный конструктор (singleton).
*/
private function __construct() {
}
// ─────────────────────────────────────────────────────────
// Регистрация
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Регистрация
// ─────────────────────────────────────────────────────────
/**
* Зарегистрировать сервис.
*
* Если $value callable (замыкание или [class, method]),
* он будет вызван ОДИН раз при первом get(). Результат кэшируется.
*
* Если $value не callable, сохраняется как готовое значение.
*
* @param string $id Уникальный идентификатор (например, 'db', 'settings')
* @param mixed $value Фабрика (callable) или готовое значение
* @return $this
*/
public function set(string $id, mixed $value): static {
// Удаляем ранее разрешённый сервис при перерегистрации
unset($this->resolved[$id]);
unset($this->isFactory[$id]);
/**
* Зарегистрировать сервис.
*
* Если $value callable (замыкание или [class, method]),
* он будет вызван ОДИН раз при первом get(). Результат кэшируется.
*
* Если $value не callable, сохраняется как готовое значение.
*
* @param string $id Уникальный идентификатор (например, 'db', 'settings')
* @param mixed $value Фабрика (callable) или готовое значение
* @return $this
*/
public function set(string $id, mixed $value): static {
// Удаляем ранее разрешённый сервис при перерегистрации
unset($this->resolved[$id]);
unset($this->isFactory[$id]);
if (is_callable($value) && !is_string($value) && !is_array($value)) {
// Замыкание → ленивая фабрика (singleton)
$this->factories[$id] = $value;
} else {
// Готовое значение → сразу в resolved
$this->resolved[$id] = $value;
}
if (is_callable($value) && !is_string($value) && !is_array($value)) {
// Замыкание → ленивая фабрика (singleton)
$this->factories[$id] = $value;
} else {
// Готовое значение → сразу в resolved
$this->resolved[$id] = $value;
}
return $this;
}
return $this;
}
/**
* Зарегистрировать фабричный сервис (новый экземпляр при каждом get).
*
* @param string $id Идентификатор
* @param callable $factory Фабрика: function(ServiceContainer $c): mixed
* @return $this
*/
public function factory(string $id, callable $factory): static {
unset($this->resolved[$id]);
$this->factories[$id] = $factory;
$this->isFactory[$id] = true;
/**
* Зарегистрировать фабричный сервис (новый экземпляр при каждом get).
*
* @param string $id Идентификатор
* @param callable $factory Фабрика: function(ServiceContainer $c): mixed
* @return $this
*/
public function factory(string $id, callable $factory): static {
unset($this->resolved[$id]);
$this->factories[$id] = $factory;
$this->isFactory[$id] = true;
return $this;
}
return $this;
}
/**
* Добавить тег к сервису.
*
* Теги позволяют группировать связанные сервисы и получать их пакетно.
* Используются модульной системой для сбора event subscribers,
* cron-задач, маршрутов и т.д.
*
* @param string $id Идентификатор сервиса
* @param string $tag Имя тега (например, 'event.subscriber', 'cron')
* @return $this
*/
public function tag(string $id, string $tag): static {
if (!isset($this->tags[$tag])) {
$this->tags[$tag] = [];
}
if (!in_array($id, $this->tags[$tag], true)) {
$this->tags[$tag][] = $id;
}
/**
* Добавить тег к сервису.
*
* Теги позволяют группировать связанные сервисы и получать их пакетно.
* Используются модульной системой для сбора event subscribers,
* cron-задач, маршрутов и т.д.
*
* @param string $id Идентификатор сервиса
* @param string $tag Имя тега (например, 'event.subscriber', 'cron')
* @return $this
*/
public function tag(string $id, string $tag): static {
if (!isset($this->tags[$tag])) {
$this->tags[$tag] = [];
}
if (!in_array($id, $this->tags[$tag], true)) {
$this->tags[$tag][] = $id;
}
return $this;
}
return $this;
}
/**
* Wrap a service with a decorator.
*
* The decorator receives the inner service as first constructor argument (class-string)
* or as the first callable argument (callable form).
*
* Multiple decorators on the same service are applied in priority order:
* highest priority wraps outermost (called first by callers).
*
* Example:
* $c->decorate('stream.service', \FingerprintDecorator::class, priority: 20);
* $c->decorate('stream.service', \LoggingDecorator::class, priority: 10);
* // Call order: \FingerprintDecorator → \LoggingDecorator → original
*
* @param string $id Service identifier
* @param class-string|callable $decorator Class-string or callable($inner, $container): mixed
* @param int $priority Higher = outer layer (default 0)
* @throws \RuntimeException If the service is protected or not registered as a factory
*/
public function decorate(string $id, string|callable $decorator, int $priority = 0): static {
if (in_array($id, $this->protectedServices, true)) {
throw new ContainerException(
"ServiceContainer: сервис '{$id}' защищён от декорирования модулями."
);
}
/**
* Wrap a service with a decorator.
*
* The decorator receives the inner service as first constructor argument (class-string)
* or as the first callable argument (callable form).
*
* Multiple decorators on the same service are applied in priority order:
* highest priority wraps outermost (called first by callers).
*
* Example:
* $c->decorate('stream.service', \FingerprintDecorator::class, priority: 20);
* $c->decorate('stream.service', \LoggingDecorator::class, priority: 10);
* // Call order: \FingerprintDecorator → \LoggingDecorator → original
*
* @param string $id Service identifier
* @param class-string|callable $decorator Class-string or callable($inner, $container): mixed
* @param int $priority Higher = outer layer (default 0)
* @throws \RuntimeException If the service is protected or not registered as a factory
*/
public function decorate(string $id, string|callable $decorator, int $priority = 0): static {
if (in_array($id, $this->protectedServices, true)) {
throw new ContainerException(
"ServiceContainer: сервис '{$id}' защищён от декорирования модулями."
);
}
if (!isset($this->factories[$id]) && !array_key_exists($id, $this->resolved)) {
throw new ContainerException(
"ServiceContainer: невозможно декорировать незарегистрированный сервис '{$id}'."
);
}
if (!isset($this->factories[$id]) && !array_key_exists($id, $this->resolved)) {
throw new ContainerException(
"ServiceContainer: невозможно декорировать незарегистрированный сервис '{$id}'."
);
}
$this->decorators[$id][$priority][] = $decorator;
unset($this->resolved[$id]); // rebuild chain on next get()
$this->decorators[$id][$priority][] = $decorator;
unset($this->resolved[$id]); // rebuild chain on next get()
return $this;
}
return $this;
}
/**
* Return decorator layers for a service (for debugging).
*
* @return array<int, array<class-string|callable>> priority => decorators[]
*/
public function getDecoratorChain(string $id): array {
return $this->decorators[$id] ?? [];
}
/**
* Return decorator layers for a service (for debugging).
*
* @param string $id
* @return array<int, array<class-string|callable>> priority => decorators[]
*/
public function getDecoratorChain(string $id): array {
return $this->decorators[$id] ?? [];
}
// ─────────────────────────────────────────────────────────
// Получение
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Получение
// ─────────────────────────────────────────────────────────
/**
* Получить сервис по идентификатору.
*
* @param string $id Идентификатор
* @return mixed
* @throws NotFoundException Если сервис не зарегистрирован
* @throws \RuntimeException Если обнаружена циклическая зависимость или фабрика бросила исключение
*/
public function get(string $id): mixed {
// 1. Уже разрешён (singleton) — мгновенный возврат
if (array_key_exists($id, $this->resolved) && empty($this->isFactory[$id])) {
return $this->resolved[$id];
}
/**
* Получить сервис по идентификатору.
*
* @param string $id Идентификатор
* @return mixed
* @throws NotFoundException Если сервис не зарегистрирован
* @throws \RuntimeException Если обнаружена циклическая зависимость или фабрика бросила исключение
*/
public function get(string $id): mixed {
// 1. Уже разрешён (singleton) — мгновенный возврат
if (array_key_exists($id, $this->resolved) && empty($this->isFactory[$id])) {
return $this->resolved[$id];
}
// 2. Есть фабрика — вызываем
if (isset($this->factories[$id])) {
// Детекция циклических зависимостей
if (isset($this->creating[$id])) {
$this->throwCircularDependency($id);
}
// 2. Есть фабрика — вызываем
if (isset($this->factories[$id])) {
// Детекция циклических зависимостей
if (isset($this->creating[$id])) {
$this->throwCircularDependency($id);
}
$this->creating[$id] = true;
$this->creating[$id] = true;
try {
$service = call_user_func($this->factories[$id], $this);
$service = $this->applyDecorators($id, $service);
} catch (XcVmException $e) {
unset($this->creating[$id]);
throw $e;
} catch (\Exception $e) {
unset($this->creating[$id]);
throw new ServiceCreationException(
"ServiceContainer: ошибка при создании сервиса '{$id}': " . $e->getMessage(),
0,
$e
);
}
try {
$service = call_user_func($this->factories[$id], $this);
$service = $this->applyDecorators($id, $service);
} catch (XcVmException $e) {
unset($this->creating[$id]);
throw $e;
} catch (\Exception $e) {
unset($this->creating[$id]);
throw new ServiceCreationException(
"ServiceContainer: ошибка при создании сервиса '{$id}': " . $e->getMessage(),
0,
$e
);
}
unset($this->creating[$id]);
unset($this->creating[$id]);
// Фабричные сервисы не кэшируются
if (empty($this->isFactory[$id])) {
$this->resolved[$id] = $service;
}
// Фабричные сервисы не кэшируются
if (empty($this->isFactory[$id])) {
$this->resolved[$id] = $service;
}
return $service;
}
return $service;
}
throw new NotFoundException(
"ServiceContainer: сервис '{$id}' не зарегистрирован. "
. "Доступные сервисы: " . implode(', ', $this->keys())
);
}
throw new NotFoundException(
"ServiceContainer: сервис '{$id}' не зарегистрирован. "
. "Доступные сервисы: " . implode(', ', $this->keys())
);
}
/**
* Получить сервис или вернуть значение по умолчанию.
*
* @param string $id Идентификатор
* @param mixed $default Значение по умолчанию (если сервис не найден)
* @return mixed
*/
public function getOrDefault(string $id, mixed $default = null): mixed {
if ($this->has($id)) {
return $this->get($id);
}
return $default;
}
/**
* Получить сервис или вернуть значение по умолчанию.
*
* @param string $id Идентификатор
* @param mixed $default Значение по умолчанию (если сервис не найден)
* @return mixed
*/
public function getOrDefault(string $id, mixed $default = null): mixed {
if ($this->has($id)) {
return $this->get($id);
}
return $default;
}
/**
* Получить все сервисы с указанным тегом.
*
* @param string $tag Имя тега
* @return array Массив сервисов [id => service]
*/
public function getTagged(string $tag): array {
$services = [];
if (isset($this->tags[$tag])) {
foreach ($this->tags[$tag] as $id) {
if ($this->has($id)) {
$services[$id] = $this->get($id);
}
}
}
return $services;
}
/**
* Получить все сервисы с указанным тегом.
*
* @param string $tag Имя тега
* @return array Массив сервисов [id => service]
*/
public function getTagged(string $tag): array {
$services = [];
if (isset($this->tags[$tag])) {
foreach ($this->tags[$tag] as $id) {
if ($this->has($id)) {
$services[$id] = $this->get($id);
}
}
}
return $services;
}
/**
* Проверить, зарегистрирован ли сервис.
*
* @param string $id Идентификатор
* @return bool
*/
public function has(string $id): bool {
return array_key_exists($id, $this->resolved) || isset($this->factories[$id]);
}
/**
* Проверить, зарегистрирован ли сервис.
*
* @param string $id Идентификатор
* @return bool
*/
public function has(string $id): bool {
return array_key_exists($id, $this->resolved) || isset($this->factories[$id]);
}
/**
* Список всех зарегистрированных идентификаторов.
*
* @return string[]
*/
public function keys(): array {
return array_unique(
array_merge(
array_keys($this->resolved),
array_keys($this->factories)
)
);
}
/**
* Список всех зарегистрированных идентификаторов.
*
* @return string[]
*/
public function keys(): array {
return array_unique(
array_merge(
array_keys($this->resolved),
array_keys($this->factories)
)
);
}
/**
* Удалить сервис из контейнера.
*
* @param string $id Идентификатор
* @return $this
*/
public function remove(string $id): static {
unset(
$this->factories[$id],
$this->resolved[$id],
$this->isFactory[$id]
);
/**
* Удалить сервис из контейнера.
*
* @param string $id Идентификатор
* @return $this
*/
public function remove(string $id): static {
unset(
$this->factories[$id],
$this->resolved[$id],
$this->isFactory[$id]
);
// Удалить из тегов
foreach ($this->tags as &$ids) {
$ids = array_values(array_filter($ids, function ($v) use ($id) {
return $v !== $id;
}));
}
// Удалить из тегов
foreach ($this->tags as &$ids) {
$ids = array_values(array_filter($ids, function ($v) use ($id) {
return $v !== $id;
}));
}
return $this;
}
return $this;
}
// ─────────────────────────────────────────────────────────
// Массовая регистрация
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Массовая регистрация
// ─────────────────────────────────────────────────────────
/**
* Зарегистрировать несколько сервисов из массива.
*
* @param array $services Массив [id => value/callable, ...]
* @return $this
*/
public function register(array $services): static {
foreach ($services as $id => $value) {
$this->set($id, $value);
}
return $this;
}
/**
* Зарегистрировать несколько сервисов из массива.
*
* @param array $services Массив [id => value/callable, ...]
* @return $this
*/
public function register(array $services): static {
foreach ($services as $id => $value) {
$this->set($id, $value);
}
return $this;
}
// ─────────────────────────────────────────────────────────
// ArrayAccess-подобный синтаксис (без implements — не нужны интерфейсы)
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// ArrayAccess-подобный синтаксис (без implements — не нужны интерфейсы)
// ─────────────────────────────────────────────────────────
/**
* Магический доступ: $container->db вместо $container->get('db')
*
* @return mixed
*/
public function __get(string $id): mixed {
return $this->get($id);
}
/**
* Магический доступ: $container->db вместо $container->get('db')
*
* @param string $id
* @return mixed
*/
public function __get(string $id): mixed {
return $this->get($id);
}
/**
* Магическая проверка: isset($container->db)
*
* @return bool
*/
public function __isset(string $id): bool {
return $this->has($id);
}
/**
* Магическая проверка: isset($container->db)
*
* @param string $id
* @return bool
*/
public function __isset(string $id): bool {
return $this->has($id);
}
// ─────────────────────────────────────────────────────────
// Decoration
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Decoration
// ─────────────────────────────────────────────────────────
/**
* Apply registered decorators to a freshly created service.
*
* Decorators are sorted by priority descending so the highest-priority
* decorator becomes the outermost wrapper (first to intercept callers).
*
* @param string $id Service identifier
* @param mixed $service The base service instance
* @return mixed Decorated service (or original if no decorators registered)
*/
private function applyDecorators(string $id, mixed $service): mixed {
if (empty($this->decorators[$id])) {
return $service;
}
/**
* Apply registered decorators to a freshly created service.
*
* Decorators are sorted by priority descending so the highest-priority
* decorator becomes the outermost wrapper (first to intercept callers).
*
* @param string $id Service identifier
* @param mixed $service The base service instance
* @return mixed Decorated service (or original if no decorators registered)
*/
private function applyDecorators(string $id, mixed $service): mixed {
if (empty($this->decorators[$id])) {
return $service;
}
$buckets = $this->decorators[$id];
krsort($buckets); // highest priority first = outermost wrapper last in application order
$buckets = $this->decorators[$id];
krsort($buckets); // highest priority first = outermost wrapper last in application order
// Build inside-out: iterate from lowest to highest priority
// so the highest-priority decorator ends up as the outermost layer.
$layers = array_merge(...array_reverse(array_values($buckets)));
// Build inside-out: iterate from lowest to highest priority
// so the highest-priority decorator ends up as the outermost layer.
$layers = array_merge(...array_reverse(array_values($buckets)));
foreach ($layers as $decorator) {
if (is_string($decorator)) {
$service = new $decorator($service);
} else {
$service = $decorator($service, $this);
}
}
foreach ($layers as $decorator) {
if (is_string($decorator)) {
$service = new $decorator($service);
} else {
$service = $decorator($service, $this);
}
}
return $service;
}
return $service;
}
// ─────────────────────────────────────────────────────────
// Internal throw helpers (never return type — R2-4)
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Internal throw helpers (never return type — R2-4)
// ─────────────────────────────────────────────────────────
/**
* Throw a CircularDependencyException describing the resolution chain.
*
* @param string $id Service id whose creation closed the cycle.
* @return never
* @throws CircularDependencyException Always.
*/
private function throwCircularDependency(string $id): never {
throw new CircularDependencyException(
"ServiceContainer: циклическая зависимость при создании сервиса '{$id}'. "
. "Цепочка: " . implode(' → ', array_keys($this->creating)) . "{$id}"
);
}
/**
* Throw a CircularDependencyException describing the resolution chain.
*
* @param string $id Service id whose creation closed the cycle.
* @return never
* @throws CircularDependencyException Always.
*/
private function throwCircularDependency(string $id): never {
throw new CircularDependencyException(
"ServiceContainer: циклическая зависимость при создании сервиса '{$id}'. "
. "Цепочка: " . implode(' → ', array_keys($this->creating)) . "{$id}"
);
}
// ─────────────────────────────────────────────────────────
// Отладка
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Отладка
// ─────────────────────────────────────────────────────────
/**
* Дамп содержимого контейнера (для отладки).
*
* @return array
*/
public function dump(): array {
$result = [];
foreach ($this->keys() as $id) {
$status = 'pending';
$type = 'unknown';
/**
* Дамп содержимого контейнера (для отладки).
*
* @return array
*/
public function dump(): array {
$result = [];
foreach ($this->keys() as $id) {
$status = 'pending';
$type = 'unknown';
if (array_key_exists($id, $this->resolved)) {
$status = 'resolved';
$type = is_object($this->resolved[$id])
? get_class($this->resolved[$id])
: gettype($this->resolved[$id]);
} elseif (isset($this->factories[$id])) {
$status = !empty($this->isFactory[$id]) ? 'factory' : 'lazy';
$type = 'callable';
}
if (array_key_exists($id, $this->resolved)) {
$status = 'resolved';
$type = is_object($this->resolved[$id])
? get_class($this->resolved[$id])
: gettype($this->resolved[$id]);
} elseif (isset($this->factories[$id])) {
$status = !empty($this->isFactory[$id]) ? 'factory' : 'lazy';
$type = 'callable';
}
$result[$id] = [
'status' => $status,
'type' => $type,
'tags' => $this->getTagsFor($id),
];
}
$result[$id] = [
'status' => $status,
'type' => $type,
'tags' => $this->getTagsFor($id),
];
}
ksort($result);
return $result;
}
ksort($result);
return $result;
}
/**
* Получить все теги для сервиса.
*
* @param string $id
* @return string[]
*/
private function getTagsFor(string $id): array {
$result = [];
foreach ($this->tags as $tag => $ids) {
if (in_array($id, $ids, true)) {
$result[] = $tag;
}
}
return $result;
}
/**
* Получить все теги для сервиса.
*
* @return string[]
*/
private function getTagsFor(string $id): array {
$result = [];
foreach ($this->tags as $tag => $ids) {
if (in_array($id, $ids, true)) {
$result[] = $tag;
}
}
return $result;
}
}
+25 -18
View File
@@ -16,17 +16,24 @@ use XcVm\Core\Logging\FileLogger;
class Database {
public $result = null;
public $last_query = null;
public $dbh = null;
public $connected = false;
/** Last PDO error message (empty when the last query succeeded). */
protected $lastError = '';
protected $dbuser = null;
protected $dbpassword = null;
protected $dbname = null;
protected $dbhost = null;
protected $dbport = null;
/**
@@ -38,7 +45,7 @@ class Database {
* @param string $host Database host
* @param int $db_port Database port number
*/
public function __construct($db_user = null, $db_pass = null, $db_name = null, $host = null, $db_port = 3306, $migrate = false) {
public function __construct(string $db_user = null, string $db_pass = null, string $db_name = null, string $host = null, int $db_port = 3306, $migrate = false) {
$this->dbh = false;
$this->dbuser = $db_user;
$this->dbpassword = $db_pass;
@@ -54,7 +61,7 @@ class Database {
* @param string $rHost Host name.
* @return string '127.0.0.1' for 'localhost', otherwise the host unchanged.
*/
private function normalizeHost($rHost) {
private function normalizeHost(string $rHost) {
if ($rHost === 'localhost') {
return '127.0.0.1';
}
@@ -122,7 +129,7 @@ class Database {
* null it defaults to $migrate (legacy behaviour).
* @return bool True on success.
*/
public function db_connect($migrate = false, $graceful = null) {
public function db_connect(bool $migrate = false, ?bool $graceful = null) {
if ($graceful === null) {
$graceful = $migrate;
}
@@ -131,14 +138,14 @@ class Database {
$this->dbh = \XC_VM::db_connect($migrate);
if (!$this->dbh) {
if (!$graceful) {
exit(json_encode(array('error' => 'MySQL: Cannot connect to database! Please check credentials.')));
exit(json_encode(['error' => 'MySQL: Cannot connect to database! Please check credentials.']));
}
return false;
}
} catch (\PDOException $e) {
if (!$graceful) {
exit(json_encode(array('error' => 'MySQL: ' . $e->getMessage())));
exit(json_encode(['error' => 'MySQL: ' . $e->getMessage()]));
}
return false;
}
@@ -184,7 +191,7 @@ class Database {
* @param string $rPassword Password.
* @return bool True on success, false on failure.
*/
public function db_explicit_connect($rHost, $rPort, $rDatabase, $rUsername, $rPassword) {
public function db_explicit_connect(string $rHost, int $rPort, string $rDatabase, string $rUsername, string $rPassword) {
try {
$this->dbh = new \PDO('mysql:host=' . $this->normalizeHost($rHost) . ';port=' . $rPort . ';dbname=' . $rDatabase, $rUsername, $rPassword);
} catch (\PDOException $e) {
@@ -204,7 +211,7 @@ class Database {
* @param \PDOStatement $stmt Prepared statement.
* @return string The dumped parameter/SQL debug text.
*/
public function debugString($stmt) {
public function debugString(\PDOStatement $stmt) {
ob_start();
$stmt->debugDumpParams();
$r = ob_get_contents();
@@ -230,7 +237,7 @@ class Database {
* for extraction into a dedicated unbuffered_query().
* @return bool True on success, false on failure.
*/
public function query($query, $buffered = false) {
public function query(string $query, mixed $buffered = false) {
if (!$this->dbh) {
return false;
}
@@ -238,7 +245,7 @@ class Database {
$numargs = func_num_args();
$arg_list = func_get_args();
$next_arg_list = array();
$next_arg_list = [];
$i = 1;
while ($i < $numargs) {
@@ -301,7 +308,7 @@ class Database {
* @param string $query Raw SQL.
* @return bool True on success, false on failure.
*/
public function simple_query($query) {
public function simple_query(string $query) {
try {
$this->result = $this->dbh->query($query);
} catch (\Exception $e) {
@@ -321,19 +328,19 @@ class Database {
* @param string $sub_row_id Optional column used as the sub-key when grouping.
* @return array|false Rows (cleaned), or false if no active result.
*/
public function get_rows($use_id = false, $column_as_id = '', $unique_row = true, $sub_row_id = '') {
public function get_rows(bool $use_id = false, string $column_as_id = '', bool $unique_row = true, string $sub_row_id = '') {
if (!($this->dbh && $this->result)) {
return false;
}
$rows = array();
$rows = [];
if (0 >= $this->result->rowCount()) {
} else {
foreach ($this->result->fetchAll(\PDO::FETCH_ASSOC) as $row) {
if ($use_id && array_key_exists($column_as_id, $row)) {
if (!isset($rows[$row[$column_as_id]])) {
$rows[$row[$column_as_id]] = array();
$rows[$row[$column_as_id]] = [];
}
if (!$unique_row) {
@@ -366,7 +373,7 @@ class Database {
return false;
}
$row = array();
$row = [];
if (0 >= $this->result->rowCount()) {
} else {
@@ -426,7 +433,7 @@ class Database {
* @param string $string Value to quote.
* @return string|null Quoted string, or null if not connected.
*/
public function escape($string) {
public function escape(string $string) {
if ($this->dbh) {
return $this->dbh->quote($string);
}
@@ -483,9 +490,9 @@ class Database {
* @param string $rValue Raw value.
* @return string Cleaned value ('' for empty input).
*/
public static function parseCleanValue($rValue) {
public static function parseCleanValue(string $rValue) {
if ($rValue != '') {
$rValue = str_replace(array("\r\n", "\n\r", "\r"), "\n", $rValue);
$rValue = str_replace(["\r\n", "\n\r", "\r"], "\n", $rValue);
$rValue = str_replace('<', '&lt;', str_replace('>', '&gt;', $rValue));
$rValue = str_replace('<!--', '&#60;&#33;--', $rValue);
$rValue = str_replace('-->', '--&#62;', $rValue);
@@ -504,7 +511,7 @@ class Database {
* @param array<string, mixed> $row Associative row.
* @return array<string, mixed> Row with sanitized values.
*/
public function clean_row($row) {
public function clean_row(array $row) {
foreach ($row as $key => $value) {
if ($value) {
$row[$key] = self::parseCleanValue($value);
+394 -395
View File
@@ -70,462 +70,461 @@ namespace XcVm\Core\Database;
*/
class DatabaseHandler extends Database {
/** @var int Total queries executed in this request */
protected $queryCount = 0;
/** @var int Total queries executed in this request */
protected $queryCount = 0;
/** @var float Total query execution time (seconds) */
protected $queryTime = 0.0;
/** @var float Total query execution time (seconds) */
protected $queryTime = 0.0;
/** @var bool Whether a transaction is currently active */
protected $inTransaction = false;
/** @var bool Whether a transaction is currently active */
protected $inTransaction = false;
/** @var callable|null Optional logger callback: function(string $level, string $message, array $context) */
protected $logger = null;
/** @var callable|null Optional logger callback: function(string $level, string $message, array $context) */
protected $logger = null;
/** @var int Maximum reconnection attempts */
protected $maxReconnectAttempts = 3;
/** @var int Maximum reconnection attempts */
protected $maxReconnectAttempts = 3;
/**
* Factory method creates DatabaseHandler from config array
*
* @param array $config Associative array with keys:
* 'username', 'password', 'database', 'hostname', 'port' (optional, default 3306)
* @param bool $migrate Whether this is a migration connection (no exit on failure)
* @return DatabaseHandler|false
*/
public static function create(array $config, bool $migrate = false) {
$port = isset($config['port']) ? (int) $config['port'] : 3306;
/**
* Factory method creates DatabaseHandler from config array
*
* @param array $config Associative array with keys:
* 'username', 'password', 'database', 'hostname', 'port' (optional, default 3306)
* @param bool $migrate Whether this is a migration connection (no exit on failure)
* @return DatabaseHandler|false
*/
public static function create(array $config, $migrate = false) {
$port = isset($config['port']) ? (int)$config['port'] : 3306;
return new self(
$config['username'],
$config['password'],
$config['database'],
$config['hostname'],
$port,
$migrate
);
}
return new self(
$config['username'],
$config['password'],
$config['database'],
$config['hostname'],
$port,
$migrate
);
}
/**
* Set a logger callback for query and error logging
*
* @param callable $logger function(string $level, string $message, array $context = [])
* @return $this
*/
public function setLogger(callable $logger) {
$this->logger = $logger;
return $this;
}
/**
* Set a logger callback for query and error logging
*
* @param callable $logger function(string $level, string $message, array $context = [])
* @return $this
*/
public function setLogger($logger) {
$this->logger = $logger;
return $this;
}
// ───────────────────────────────────────────────────────────
// Transaction Support
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Transaction Support
// ───────────────────────────────────────────────────────────
/**
* Begin a database transaction
*
* @return bool
*/
public function beginTransaction() {
if (!$this->dbh) {
return false;
}
/**
* Begin a database transaction
*
* @return bool
*/
public function beginTransaction() {
if (!$this->dbh) {
return false;
}
if ($this->inTransaction) {
$this->log('warning', 'beginTransaction called while already in transaction');
return false;
}
if ($this->inTransaction) {
$this->log('warning', 'beginTransaction called while already in transaction');
return false;
}
$this->inTransaction = $this->dbh->beginTransaction();
return $this->inTransaction;
}
$this->inTransaction = $this->dbh->beginTransaction();
return $this->inTransaction;
}
/**
* Commit the current transaction
*
* @return bool
*/
public function commit() {
if (!$this->dbh || !$this->inTransaction) {
return false;
}
/**
* Commit the current transaction
*
* @return bool
*/
public function commit() {
if (!$this->dbh || !$this->inTransaction) {
return false;
}
$result = $this->dbh->commit();
$this->inTransaction = false;
return $result;
}
$result = $this->dbh->commit();
$this->inTransaction = false;
return $result;
}
/**
* Rollback the current transaction
*
* @return bool
*/
public function rollback() {
if (!$this->dbh || !$this->inTransaction) {
return false;
}
/**
* Rollback the current transaction
*
* @return bool
*/
public function rollback() {
if (!$this->dbh || !$this->inTransaction) {
return false;
}
$result = $this->dbh->rollBack();
$this->inTransaction = false;
return $result;
}
$result = $this->dbh->rollBack();
$this->inTransaction = false;
return $result;
}
/**
* Execute a callback within a transaction
*
* Automatically commits on success, rolls back on anything thrown an Error
* (a TypeError, an undefined method) as well as an Exception, or the
* transaction would stay open on the connection.
*
* @param callable $callback function(DatabaseHandler $db)
* @return mixed The return value of the callback
* @throws \Throwable Re-throws after rollback
*/
public function transactional(callable $callback) {
$this->beginTransaction();
/**
* Execute a callback within a transaction
*
* Automatically commits on success, rolls back on anything thrown an Error
* (a TypeError, an undefined method) as well as an Exception, or the
* transaction would stay open on the connection.
*
* @param callable $callback function(DatabaseHandler $db)
* @return mixed The return value of the callback
* @throws \Throwable Re-throws after rollback
*/
public function transactional($callback) {
$this->beginTransaction();
try {
$result = $callback($this);
$this->commit();
return $result;
} catch (\Throwable $e) {
$this->rollback();
throw $e;
}
}
try {
$result = $callback($this);
$this->commit();
return $result;
} catch (\Throwable $e) {
$this->rollback();
throw $e;
}
}
/**
* Check if currently inside a transaction
*
* @return bool
*/
public function isInTransaction() {
return $this->inTransaction;
}
/**
* Check if currently inside a transaction
*
* @return bool
*/
public function isInTransaction() {
return $this->inTransaction;
}
// ───────────────────────────────────────────────────────────
// Improved Query Methods (shorthand)
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Improved Query Methods (shorthand)
// ───────────────────────────────────────────────────────────
/**
* Execute query and return all rows
*
* Shorthand for: $db->query(...); return $db->get_rows();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchAll(string $sql, mixed ...$params) {
$args = func_get_args();
if (!call_user_func_array([$this, 'query'], $args)) {
return false;
}
return $this->get_rows();
}
/**
* Execute query and return all rows
*
* Shorthand for: $db->query(...); return $db->get_rows();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchAll($sql, ...$params) {
$args = func_get_args();
if (!call_user_func_array(array($this, 'query'), $args)) {
return false;
}
return $this->get_rows();
}
/**
* Execute query and return all rows keyed by a column
*
* Shorthand for: $db->query(...); return $db->get_rows(true, $column);
*
* @param string $keyColumn Column name to use as array key
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchAllKeyed(string $keyColumn, string $sql, mixed ...$params) {
$args = func_get_args();
array_shift($args); // remove $keyColumn
if (!call_user_func_array([$this, 'query'], $args)) {
return false;
}
return $this->get_rows(true, $keyColumn);
}
/**
* Execute query and return all rows keyed by a column
*
* Shorthand for: $db->query(...); return $db->get_rows(true, $column);
*
* @param string $keyColumn Column name to use as array key
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchAllKeyed($keyColumn, $sql, ...$params) {
$args = func_get_args();
array_shift($args); // remove $keyColumn
if (!call_user_func_array(array($this, 'query'), $args)) {
return false;
}
return $this->get_rows(true, $keyColumn);
}
/**
* Execute query and return single row
*
* Shorthand for: $db->query(...); return $db->get_row();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchOne(string $sql, mixed ...$params) {
$args = func_get_args();
if (!call_user_func_array([$this, 'query'], $args)) {
return false;
}
return $this->get_row();
}
/**
* Execute query and return single row
*
* Shorthand for: $db->query(...); return $db->get_row();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchOne($sql, ...$params) {
$args = func_get_args();
if (!call_user_func_array(array($this, 'query'), $args)) {
return false;
}
return $this->get_row();
}
/**
* Execute query and return a single scalar value
*
* Shorthand for: $db->query(...); return $db->get_col();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return mixed|false
*/
public function fetchValue(string $sql, mixed ...$params) {
$args = func_get_args();
if (!call_user_func_array([$this, 'query'], $args)) {
return false;
}
return $this->get_col();
}
/**
* Execute query and return a single scalar value
*
* Shorthand for: $db->query(...); return $db->get_col();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return mixed|false
*/
public function fetchValue($sql, ...$params) {
$args = func_get_args();
if (!call_user_func_array(array($this, 'query'), $args)) {
return false;
}
return $this->get_col();
}
/**
* Execute query and return a single column as flat array
*
* Shorthand for: $db->query(...); return $db->get_column();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchColumn(string $sql, mixed ...$params) {
$args = func_get_args();
if (!call_user_func_array([$this, 'query'], $args)) {
return false;
}
return $this->get_column();
}
/**
* Execute query and return a single column as flat array
*
* Shorthand for: $db->query(...); return $db->get_column();
*
* @param string $sql SQL query with ? placeholders
* @param mixed ...$params Bound parameter values
* @return array|false
*/
public function fetchColumn($sql, ...$params) {
$args = func_get_args();
if (!call_user_func_array(array($this, 'query'), $args)) {
return false;
}
return $this->get_column();
}
// ───────────────────────────────────────────────────────────
// Query Override with Timing and Reconnect
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Query Override with Timing and Reconnect
// ───────────────────────────────────────────────────────────
/**
* Execute a prepared query with automatic reconnection and timing
*
* Overrides Database::query() to add:
* - Query counting
* - Execution time tracking
* - Automatic reconnection on "MySQL server has gone away"
*
* @param string $query SQL with ? placeholders
* @param mixed $buffered First bind value, or boolean true to disable
* buffered query mode (legacy positional overload;
* all args from index 1 are collected as binds).
* @return bool
*/
public function query(string $query, mixed $buffered = false) {
$start = microtime(true);
$this->queryCount++;
/**
* Execute a prepared query with automatic reconnection and timing
*
* Overrides Database::query() to add:
* - Query counting
* - Execution time tracking
* - Automatic reconnection on "MySQL server has gone away"
*
* @param string $query SQL with ? placeholders
* @param mixed $buffered First bind value, or boolean true to disable
* buffered query mode (legacy positional overload;
* all args from index 1 are collected as binds).
* @return bool
*/
public function query($query, $buffered = false) {
$start = microtime(true);
$this->queryCount++;
$result = call_user_func_array('parent::query', func_get_args());
$result = call_user_func_array('parent::query', func_get_args());
// If query failed — try reconnection (only if not in transaction)
if ($result === false && !$this->inTransaction && $this->shouldReconnect()) {
$this->log('warning', 'Query failed, attempting reconnect', ['query' => $query]);
// If query failed — try reconnection (only if not in transaction)
if ($result === false && !$this->inTransaction && $this->shouldReconnect()) {
$this->log('warning', 'Query failed, attempting reconnect', ['query' => $query]);
if ($this->reconnect()) {
$result = call_user_func_array('parent::query', func_get_args());
}
}
if ($this->reconnect()) {
$result = call_user_func_array('parent::query', func_get_args());
}
}
$elapsed = microtime(true) - $start;
$this->queryTime += $elapsed;
$elapsed = microtime(true) - $start;
$this->queryTime += $elapsed;
return $result;
}
return $result;
}
// ───────────────────────────────────────────────────────────
// Connection Management
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Connection Management
// ───────────────────────────────────────────────────────────
/**
* Attempt to reconnect to the database
*
* @return bool
*/
public function reconnect() {
$attempts = 0;
/**
* Attempt to reconnect to the database
*
* @return bool
*/
public function reconnect() {
$attempts = 0;
while ($attempts < $this->maxReconnectAttempts) {
$attempts++;
$this->log('info', "Reconnect attempt {$attempts}/{$this->maxReconnectAttempts}");
while ($attempts < $this->maxReconnectAttempts) {
$attempts++;
$this->log('info', "Reconnect attempt {$attempts}/{$this->maxReconnectAttempts}");
$this->close_mysql();
$this->close_mysql();
// Reconnect to the MAIN configured schema, gracefully (no exit()) so
// the retry/backoff loop can continue. migrate=true here would
// silently reconnect to `xc_vm_migrate` instead of the panel DB.
if ($this->db_connect(false, true)) {
$this->log('info', 'Reconnected successfully');
return true;
}
// Reconnect to the MAIN configured schema, gracefully (no exit()) so
// the retry/backoff loop can continue. migrate=true here would
// silently reconnect to `xc_vm_migrate` instead of the panel DB.
if ($this->db_connect(false, true)) {
$this->log('info', 'Reconnected successfully');
return true;
}
// Exponential backoff: 100ms, 200ms, 400ms
usleep(100000 * pow(2, $attempts - 1));
}
// Exponential backoff: 100ms, 200ms, 400ms
usleep(100000 * pow(2, $attempts - 1));
}
$this->log('error', 'Failed to reconnect after ' . $this->maxReconnectAttempts . ' attempts');
return false;
}
$this->log('error', 'Failed to reconnect after ' . $this->maxReconnectAttempts . ' attempts');
return false;
}
/**
* Check if a reconnection should be attempted
*
* @return bool
*/
protected function shouldReconnect() {
if (!$this->dbh) {
return true;
}
/**
* Check if a reconnection should be attempted
*
* @return bool
*/
protected function shouldReconnect() {
if (!$this->dbh) {
return true;
}
return !$this->ping();
}
return !$this->ping();
}
// ───────────────────────────────────────────────────────────
// Diagnostics
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Diagnostics
// ───────────────────────────────────────────────────────────
/**
* Get total number of queries executed
*
* @return int
*/
public function getQueryCount() {
return $this->queryCount;
}
/**
* Get total number of queries executed
*
* @return int
*/
public function getQueryCount() {
return $this->queryCount;
}
/**
* Get total query execution time in seconds
*
* @return float
*/
public function getQueryTime() {
return $this->queryTime;
}
/**
* Get total query execution time in seconds
*
* @return float
*/
public function getQueryTime() {
return $this->queryTime;
}
/**
* Get diagnostic summary
*
* @return array
*/
public function getDiagnostics() {
return [
'connected' => $this->connected,
'queryCount' => $this->queryCount,
'queryTime' => round($this->queryTime, 4),
'inTransaction' => $this->inTransaction,
'database' => $this->dbname,
'host' => $this->dbhost,
'port' => $this->dbport,
];
}
/**
* Get diagnostic summary
*
* @return array
*/
public function getDiagnostics() {
return [
'connected' => $this->connected,
'queryCount' => $this->queryCount,
'queryTime' => round($this->queryTime, 4),
'inTransaction' => $this->inTransaction,
'database' => $this->dbname,
'host' => $this->dbhost,
'port' => $this->dbport,
];
}
// ───────────────────────────────────────────────────────────
// Bulk Operations
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Bulk Operations
// ───────────────────────────────────────────────────────────
/**
* Insert a row from an associative array
*
* @param string $table Table name
* @param array $data Associative array of column => value
* @return int|false Last insert ID on success, false on failure
*/
public function insert(string $table, array $data) {
if (empty($data)) {
return false;
}
/**
* Insert a row from an associative array
*
* @param string $table Table name
* @param array $data Associative array of column => value
* @return int|false Last insert ID on success, false on failure
*/
public function insert($table, array $data) {
if (empty($data)) {
return false;
}
$columns = array_keys($data);
$placeholders = array_fill(0, count($columns), '?');
$columns = array_keys($data);
$placeholders = array_fill(0, count($columns), '?');
$sql = sprintf(
'INSERT INTO `%s` (`%s`) VALUES (%s)',
$table,
implode('`, `', $columns),
implode(', ', $placeholders)
);
$sql = sprintf(
'INSERT INTO `%s` (`%s`) VALUES (%s)',
$table,
implode('`, `', $columns),
implode(', ', $placeholders)
);
$args = array_merge([$sql], array_values($data));
$args = array_merge([$sql], array_values($data));
if (call_user_func_array([$this, 'query'], $args)) {
return $this->last_insert_id();
}
if (call_user_func_array([$this, 'query'], $args)) {
return $this->last_insert_id();
}
return false;
}
return false;
}
/**
* Update rows from an associative array
*
* @param string $table Table name
* @param array $data Associative array of column => value
* @param string $where WHERE clause (with ? placeholders)
* @param mixed ...$whereParams Values for WHERE placeholders
* @return bool
*/
public function update(string $table, array $data, string $where, mixed ...$whereParams) {
if (empty($data)) {
return false;
}
/**
* Update rows from an associative array
*
* @param string $table Table name
* @param array $data Associative array of column => value
* @param string $where WHERE clause (with ? placeholders)
* @param mixed ...$whereParams Values for WHERE placeholders
* @return bool
*/
public function update($table, array $data, $where, ...$whereParams) {
if (empty($data)) {
return false;
}
$setParts = [];
$values = [];
$setParts = [];
$values = [];
foreach ($data as $column => $value) {
$setParts[] = "`{$column}` = ?";
$values[] = $value;
}
foreach ($data as $column => $value) {
$setParts[] = "`{$column}` = ?";
$values[] = $value;
}
$sql = sprintf(
'UPDATE `%s` SET %s WHERE %s',
$table,
implode(', ', $setParts),
$where
);
$sql = sprintf(
'UPDATE `%s` SET %s WHERE %s',
$table,
implode(', ', $setParts),
$where
);
// Add WHERE params from extra func_get_args
$allArgs = func_get_args();
$whereParams = array_slice($allArgs, 3);
$values = array_merge($values, $whereParams);
// Add WHERE params from extra func_get_args
$allArgs = func_get_args();
$whereParams = array_slice($allArgs, 3);
$values = array_merge($values, $whereParams);
$args = array_merge([$sql], $values);
$args = array_merge([$sql], $values);
return call_user_func_array([$this, 'query'], $args);
}
return call_user_func_array([$this, 'query'], $args);
}
/**
* Delete rows with a WHERE clause
*
* @param string $table Table name
* @param string $where WHERE clause with ? placeholders
* @param mixed ...$params Values for WHERE placeholders
* @return bool
*/
public function delete(string $table, string $where, mixed ...$params) {
$sql = sprintf('DELETE FROM `%s` WHERE %s', $table, $where);
/**
* Delete rows with a WHERE clause
*
* @param string $table Table name
* @param string $where WHERE clause with ? placeholders
* @param mixed ...$params Values for WHERE placeholders
* @return bool
*/
public function delete($table, $where, ...$params) {
$sql = sprintf('DELETE FROM `%s` WHERE %s', $table, $where);
$allArgs = func_get_args();
$whereParams = array_slice($allArgs, 2);
$args = array_merge([$sql], $whereParams);
$allArgs = func_get_args();
$whereParams = array_slice($allArgs, 2);
$args = array_merge([$sql], $whereParams);
return call_user_func_array([$this, 'query'], $args);
}
return call_user_func_array([$this, 'query'], $args);
}
// ───────────────────────────────────────────────────────────
// Internal Logging
// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Internal Logging
// ───────────────────────────────────────────────────────────
/**
* Log a message through the configured logger
*
* @param string $level 'info', 'warning', 'error'
* @param string $message Log message
* @param array $context Additional context
*/
protected function log($level, $message, array $context = []) {
if ($this->logger) {
call_user_func($this->logger, $level, '[DatabaseHandler] ' . $message, $context);
}
}
/**
* Log a message through the configured logger
*
* @param string $level 'info', 'warning', 'error'
* @param string $message Log message
* @param array $context Additional context
*/
protected function log(string $level, string $message, array $context = []) {
if ($this->logger) {
call_user_func($this->logger, $level, '[DatabaseHandler] ' . $message, $context);
}
}
}
+2 -3
View File
@@ -13,7 +13,6 @@ namespace XcVm\Core\Database;
*/
class MigrationRunner {
/**
* Apply pending SQL migrations from the migrations/ directory.
*
@@ -23,7 +22,7 @@ class MigrationRunner {
* @param Database $db Database handle.
* @return void
*/
public static function run($db): void {
public static function run(Database $db): void {
echo "Migrations\n------------------------------\n";
$db->query("CREATE TABLE IF NOT EXISTS `migrations` (
@@ -33,7 +32,7 @@ class MigrationRunner {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
$db->query("SELECT `migration` FROM `migrations`;");
$rApplied = array();
$rApplied = [];
if ($db->num_rows() > 0) {
foreach ($db->get_rows() as $rRow) {
$rApplied[] = $rRow['migration'];
+10 -10
View File
@@ -15,7 +15,7 @@ class QueryHelper {
* @param string $rValue Raw column/table name.
* @return string Sanitized identifier.
*/
public static function prepareColumn($rValue) {
public static function prepareColumn(string $rValue) {
return strtolower(preg_replace('/[^a-z0-9_]+/i', '', $rValue));
}
@@ -27,8 +27,8 @@ class QueryHelper {
* @param array $rArray Column => value map.
* @return array ['columns' => string, 'placeholder' => string, 'data' => array, 'update' => string].
*/
public static function prepareArray($rArray) {
$UpdateData = $rColumns = $rPlaceholder = $rData = array();
public static function prepareArray(array $rArray) {
$UpdateData = $rColumns = $rPlaceholder = $rData = [];
foreach (array_keys($rArray) as $rKey) {
$rColumns[] = '`' . self::prepareColumn($rKey) . '`';
@@ -48,7 +48,7 @@ class QueryHelper {
$rData[] = $rValue;
}
return array('placeholder' => implode(',', $rPlaceholder), 'columns' => implode(',', $rColumns), 'data' => $rData, 'update' => implode(',', $UpdateData));
return ['placeholder' => implode(',', $rPlaceholder), 'columns' => implode(',', $rColumns), 'data' => $rData, 'update' => implode(',', $UpdateData)];
}
/**
@@ -62,9 +62,9 @@ class QueryHelper {
* @param bool $rOnlyExisting When true, skip columns absent from $rData.
* @return array Sanitized column => value map ready for prepareArray().
*/
public static function verifyPostTable($rTable, $rData = array(), $rOnlyExisting = false) {
public static function verifyPostTable(string $rTable, array $rData = [], bool $rOnlyExisting = false) {
global $db;
$rReturn = array();
$rReturn = [];
$db->query('SELECT `column_name`, `column_default`, `is_nullable`, `data_type` FROM `information_schema`.`columns` WHERE `table_schema` = (SELECT DATABASE()) AND `table_name` = ? ORDER BY `ordinal_position`;', $rTable);
foreach ($db->get_rows() as $rRow) {
@@ -81,7 +81,7 @@ class QueryHelper {
if ($rRow['is_nullable'] != 'NO' || $rRow['column_default']) {
} else {
if (in_array($rRow['data_type'], array('int', 'float', 'tinyint', 'double', 'decimal', 'smallint', 'mediumint', 'bigint', 'bit'))) {
if (in_array($rRow['data_type'], ['int', 'float', 'tinyint', 'double', 'decimal', 'smallint', 'mediumint', 'bigint', 'bit'])) {
$rRow['column_default'] = 0;
} else {
$rRow['column_default'] = '';
@@ -92,11 +92,11 @@ class QueryHelper {
if (array_key_exists($rRow['column_name'], $rData)) {
// coerce empty string for numeric columns to a safe default
$rNumericTypes = array('int', 'float', 'tinyint', 'double', 'decimal', 'smallint', 'mediumint', 'bigint', 'bit');
$rNumericTypes = ['int', 'float', 'tinyint', 'double', 'decimal', 'smallint', 'mediumint', 'bigint', 'bit'];
$rValue = $rData[$rRow['column_name']];
if ($rValue === '' && in_array($rRow['data_type'], $rNumericTypes)) {
$rReturn[$rRow['column_name']] = is_null($rRow['column_default']) ? ($rForceDefault ? 0 : null) : $rRow['column_default'];
} else if (empty($rValue) && !is_numeric($rValue) && is_null($rRow['column_default'])) {
} elseif (empty($rValue) && !is_numeric($rValue) && is_null($rRow['column_default'])) {
$rReturn[$rRow['column_name']] = ($rForceDefault ? $rRow['column_default'] : null);
} else {
$rReturn[$rRow['column_name']] = $rValue;
@@ -122,7 +122,7 @@ class QueryHelper {
* @param mixed $rExclude Value to exclude for $rExcludeColumn.
* @return bool True if at least one matching row exists.
*/
public static function checkExists($rTable, $rColumn, $rValue, $rExcludeColumn = null, $rExclude = null) {
public static function checkExists(string $rTable, string $rColumn, mixed $rValue, ?string $rExcludeColumn = null, mixed $rExclude = null) {
global $db;
if ($rExcludeColumn && $rExclude) {
+16 -17
View File
@@ -21,7 +21,6 @@ use XcVm\Infrastructure\Database\DatabaseAware;
*/
class DiagnosticsService {
use DatabaseAware;
/**
@@ -30,7 +29,7 @@ class DiagnosticsService {
* @param string|null $certificate Path to certificate file (auto-detects from nginx if null)
* @return array|null ['serial', 'expiration', 'subject', 'path'], or null if the certificate is missing/unreadable
*/
public static function getCertificateInfo($certificate = null) {
public static function getCertificateInfo(?string $certificate = null) {
$result = ['serial' => null, 'expiration' => null, 'subject' => null, 'path' => null];
if (!$certificate) {
@@ -74,7 +73,7 @@ class DiagnosticsService {
* @param bool $allowHEVC Whether HEVC/H265 + AC3 are allowed
* @return bool
*/
public static function checkCompatibility($data, $allowHEVC = false) {
public static function checkCompatibility(array|string $data, bool $allowHEVC = false) {
if (!is_array($data)) {
$data = json_decode($data, true);
}
@@ -140,9 +139,9 @@ class DiagnosticsService {
'type' => isset($error['type']) ? htmlspecialchars($error['type'], ENT_QUOTES, 'UTF-8') : 'unknown',
'message' => isset($error['log_message']) ? htmlspecialchars($error['log_message'], ENT_QUOTES, 'UTF-8') : '',
'file' => isset($error['log_extra']) ? htmlspecialchars($error['log_extra'], ENT_QUOTES, 'UTF-8') : '',
'line' => isset($error['line']) ? (int)$error['line'] : 0,
'date' => isset($error['date']) ? (int)$error['date'] : 0,
'version' => isset($error['version']) ? htmlspecialchars((string)$error['version'], ENT_QUOTES, 'UTF-8') : '',
'line' => isset($error['line']) ? (int) $error['line'] : 0,
'date' => isset($error['date']) ? (int) $error['date'] : 0,
'version' => isset($error['version']) ? htmlspecialchars((string) $error['version'], ENT_QUOTES, 'UTF-8') : '',
];
try {
@@ -202,19 +201,19 @@ class DiagnosticsService {
$ids = [];
foreach ($rows as $row) {
$ts = isset($row['date']) ? (int)$row['date'] : 0;
$ts = isset($row['date']) ? (int) $row['date'] : 0;
$errorsForApi[] = [
'type' => $row['type'] ?? '',
'log_message' => $row['log_message'] ?? '',
'log_extra' => $row['log_extra'] ?? '',
'line' => isset($row['line']) ? (string)$row['line'] : '',
'line' => isset($row['line']) ? (string) $row['line'] : '',
'date' => $ts > 0 ? gmdate('Y-m-d H:i:s', $ts) : '',
// Per-error panel version frozen when the error occurred. The log
// server attributes the entry to THIS, not the batch/current version.
'version' => (string) ($row['version'] ?? ''),
];
if (isset($row['id'])) {
$ids[] = (int)$row['id'];
$ids[] = (int) $row['id'];
}
}
@@ -257,9 +256,9 @@ class DiagnosticsService {
* @param int $rServerID Server id to query.
* @return array Process info keyed/listed as returned by the server.
*/
public static function getPIDs($rServerID) {
$rReturn = array();
$rProcesses = json_decode(ApiClient::systemRequest($rServerID, array('action' => 'get_pids')), true);
public static function getPIDs(int $rServerID) {
$rReturn = [];
$rProcesses = json_decode(ApiClient::systemRequest($rServerID, ['action' => 'get_pids']), true);
if (!is_array($rProcesses)) {
return $rReturn;
}
@@ -269,7 +268,7 @@ class DiagnosticsService {
$rSplit = explode(' ', preg_replace('!\\s+!', ' ', trim($rProcess)));
if ($rSplit[0] == 'xc_vm') {
$rUsage = array(0, 0, 0);
$rUsage = [0, 0, 0];
$rTimer = explode('-', $rSplit[9]);
if (1 < count($rTimer)) {
@@ -322,7 +321,7 @@ class DiagnosticsService {
$rUsage[2] = 0;
}
$rReturn[] = array('user' => $rSplit[0], 'pid' => $rSplit[1], 'cpu' => $rSplit[2], 'mem' => $rSplit[3], 'vsz' => $rSplit[4], 'rss' => $rSplit[5], 'tty' => $rSplit[6], 'stat' => $rSplit[7], 'time' => $rUsage[1], 'etime' => $rUsage[0], 'load_average' => $rUsage[2], 'command' => implode(' ', array_splice($rSplit, 10, count($rSplit) - 10)));
$rReturn[] = ['user' => $rSplit[0], 'pid' => $rSplit[1], 'cpu' => $rSplit[2], 'mem' => $rSplit[3], 'vsz' => $rSplit[4], 'rss' => $rSplit[5], 'tty' => $rSplit[6], 'stat' => $rSplit[7], 'time' => $rUsage[1], 'etime' => $rUsage[0], 'load_average' => $rUsage[2], 'command' => implode(' ', array_splice($rSplit, 10, count($rSplit) - 10))];
}
}
@@ -335,9 +334,9 @@ class DiagnosticsService {
* @param int $rServerID Server id to inspect.
* @return array NVENC process details.
*/
public static function getNVENCProcesses($rServerID) {
public static function getNVENCProcesses(int $rServerID) {
$db = self::db();
$rProcesses = array();
$rProcesses = [];
$rServer = ServerRepository::getById($rServerID);
$rGPUInfo = json_decode($rServer['gpu_info'], true);
@@ -345,7 +344,7 @@ class DiagnosticsService {
} else {
foreach ($rGPUInfo['gpus'] as $rGPU) {
foreach ($rGPU['processes'] as $rProcess) {
$rArray = array('pid' => $rProcess['pid'], 'memory' => $rProcess['memory'], 'stream_id' => null);
$rArray = ['pid' => $rProcess['pid'], 'memory' => $rProcess['memory'], 'stream_id' => null];
$db->query('SELECT `stream_id` FROM `streams_servers` WHERE `pid` = ? AND `server_id` = ?;', $rProcess['pid'], $rServerID);
if (0 >= $db->num_rows()) {
+8 -8
View File
@@ -13,15 +13,15 @@ namespace XcVm\Core\Enum;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
enum BootContext: string {
/** Constants + config only. No DB. */
case Minimal = 'minimal';
/** Constants + config only. No DB. */
case Minimal = 'minimal';
/** + Database + LegacyInitializer. For cron jobs and CLI scripts. */
case Cli = 'cli';
/** + Database + LegacyInitializer. For cron jobs and CLI scripts. */
case Cli = 'cli';
/** + Database (cached). Lightweight path for streaming endpoints. */
case Stream = 'stream';
/** + Database (cached). Lightweight path for streaming endpoints. */
case Stream = 'stream';
/** Full initialization: DB + API + Translator + session. */
case Admin = 'admin';
/** Full initialization: DB + API + Translator + session. */
case Admin = 'admin';
}
+79 -80
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Enum;
@@ -18,86 +17,86 @@ namespace XcVm\Core\Enum;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
enum ClientFilter: string {
case LbTokenInvalid = 'LB_TOKEN_INVALID';
case NotInBouquet = 'NOT_IN_BOUQUET';
case BlockedAsn = 'BLOCKED_ASN';
case IspLockFailed = 'ISP_LOCK_FAILED';
case UserDisallowExt = 'USER_DISALLOW_EXT';
case AuthFailed = 'AUTH_FAILED';
case UserExpired = 'USER_EXPIRED';
case UserDisabled = 'USER_DISABLED';
case UserBan = 'USER_BAN';
case MagTokenInvalid = 'MAG_TOKEN_INVALID';
case StalkerChannelMismatch = 'STALKER_CHANNEL_MISMATCH';
case StalkerIpMismatch = 'STALKER_IP_MISMATCH';
case StalkerKeyExpired = 'STALKER_KEY_EXPIRED';
case StalkerDecryptFailed = 'STALKER_DECRYPT_FAILED';
case EmptyUa = 'EMPTY_UA';
case IpBan = 'IP_BAN';
case CountryDisallow = 'COUNTRY_DISALLOW';
case UserAgentBan = 'USER_AGENT_BAN';
case UserAlreadyConnected = 'USER_ALREADY_CONNECTED';
case RestreamDetect = 'RESTREAM_DETECT';
case ProxyDetect = 'PROXY_DETECT';
case HostingDetect = 'HOSTING_DETECT';
case LineCreateFail = 'LINE_CREATE_FAIL';
case ConnectionLoop = 'CONNECTION_LOOP';
case TokenExpired = 'TOKEN_EXPIRED';
case IpMismatch = 'IP_MISMATCH';
case LbTokenInvalid = 'LB_TOKEN_INVALID';
case NotInBouquet = 'NOT_IN_BOUQUET';
case BlockedAsn = 'BLOCKED_ASN';
case IspLockFailed = 'ISP_LOCK_FAILED';
case UserDisallowExt = 'USER_DISALLOW_EXT';
case AuthFailed = 'AUTH_FAILED';
case UserExpired = 'USER_EXPIRED';
case UserDisabled = 'USER_DISABLED';
case UserBan = 'USER_BAN';
case MagTokenInvalid = 'MAG_TOKEN_INVALID';
case StalkerChannelMismatch = 'STALKER_CHANNEL_MISMATCH';
case StalkerIpMismatch = 'STALKER_IP_MISMATCH';
case StalkerKeyExpired = 'STALKER_KEY_EXPIRED';
case StalkerDecryptFailed = 'STALKER_DECRYPT_FAILED';
case EmptyUa = 'EMPTY_UA';
case IpBan = 'IP_BAN';
case CountryDisallow = 'COUNTRY_DISALLOW';
case UserAgentBan = 'USER_AGENT_BAN';
case UserAlreadyConnected = 'USER_ALREADY_CONNECTED';
case RestreamDetect = 'RESTREAM_DETECT';
case ProxyDetect = 'PROXY_DETECT';
case HostingDetect = 'HOSTING_DETECT';
case LineCreateFail = 'LINE_CREATE_FAIL';
case ConnectionLoop = 'CONNECTION_LOOP';
case TokenExpired = 'TOKEN_EXPIRED';
case IpMismatch = 'IP_MISMATCH';
/**
* Human-readable filter label (as shown in the client request log).
*/
public function label(): string {
return match ($this) {
self::LbTokenInvalid => 'Token Failure',
self::NotInBouquet => 'Not in Bouquet',
self::BlockedAsn => 'Blocked ASN',
self::IspLockFailed => 'ISP Lock Failed',
self::UserDisallowExt => 'Extension Disallowed',
self::AuthFailed => 'Authentication Failed',
self::UserExpired => 'User Expired',
self::UserDisabled => 'User Disabled',
self::UserBan => 'User Banned',
self::MagTokenInvalid => 'MAG Token Invalid',
self::StalkerChannelMismatch => 'Stalker Channel Mismatch',
self::StalkerIpMismatch => 'Stalker IP Mismatch',
self::StalkerKeyExpired => 'Stalker Key Expired',
self::StalkerDecryptFailed => 'Stalker Decrypt Failed',
self::EmptyUa => 'Empty User-Agent',
self::IpBan => 'IP Banned',
self::CountryDisallow => 'Country Disallowed',
self::UserAgentBan => 'User-Agent Disallowed',
self::UserAlreadyConnected => 'IP Limit Reached',
self::RestreamDetect => 'Restream Detected',
self::ProxyDetect => 'Proxy / VPN Detected',
self::HostingDetect => 'Hosting Server Detected',
self::LineCreateFail => 'Connection Failed',
self::ConnectionLoop => 'Connection Loop',
self::TokenExpired => 'Token Expired',
self::IpMismatch => 'IP Mismatch',
};
}
/**
* Human-readable filter label (as shown in the client request log).
*/
public function label(): string {
return match ($this) {
self::LbTokenInvalid => 'Token Failure',
self::NotInBouquet => 'Not in Bouquet',
self::BlockedAsn => 'Blocked ASN',
self::IspLockFailed => 'ISP Lock Failed',
self::UserDisallowExt => 'Extension Disallowed',
self::AuthFailed => 'Authentication Failed',
self::UserExpired => 'User Expired',
self::UserDisabled => 'User Disabled',
self::UserBan => 'User Banned',
self::MagTokenInvalid => 'MAG Token Invalid',
self::StalkerChannelMismatch => 'Stalker Channel Mismatch',
self::StalkerIpMismatch => 'Stalker IP Mismatch',
self::StalkerKeyExpired => 'Stalker Key Expired',
self::StalkerDecryptFailed => 'Stalker Decrypt Failed',
self::EmptyUa => 'Empty User-Agent',
self::IpBan => 'IP Banned',
self::CountryDisallow => 'Country Disallowed',
self::UserAgentBan => 'User-Agent Disallowed',
self::UserAlreadyConnected => 'IP Limit Reached',
self::RestreamDetect => 'Restream Detected',
self::ProxyDetect => 'Proxy / VPN Detected',
self::HostingDetect => 'Hosting Server Detected',
self::LineCreateFail => 'Connection Failed',
self::ConnectionLoop => 'Connection Loop',
self::TokenExpired => 'Token Expired',
self::IpMismatch => 'IP Mismatch',
};
}
/**
* Resolve a raw status key to its label, falling back to the raw key
* itself when it is unknown (tolerant lookup for log rendering).
*/
public static function labelFor(string $key): string {
return self::tryFrom($key)?->label() ?? $key;
}
/**
* Resolve a raw status key to its label, falling back to the raw key
* itself when it is unknown (tolerant lookup for log rendering).
*/
public static function labelFor(string $key): string {
return self::tryFrom($key)?->label() ?? $key;
}
/**
* Ordered key => label map for building select/filter dropdowns,
* preserving the legacy `$rClientFilters` ordering.
*
* @return array<string, string>
*/
public static function options(): array {
$options = [];
foreach (self::cases() as $case) {
$options[$case->value] = $case->label();
}
return $options;
}
/**
* Ordered key => label map for building select/filter dropdowns,
* preserving the legacy `$rClientFilters` ordering.
*
* @return array<string, string>
*/
public static function options(): array {
$options = [];
foreach (self::cases() as $case) {
$options[$case->value] = $case->label();
}
return $options;
}
}
+31 -32
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Enum;
@@ -20,40 +19,40 @@ namespace XcVm\Core\Enum;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
enum ModuleState: string {
/** Module is active and loaded by ModuleLoader. */
case Enabled = 'enabled';
/** Module is active and loaded by ModuleLoader. */
case Enabled = 'enabled';
/** Module is present but suppressed from loading. */
case Disabled = 'disabled';
/** Module is present but suppressed from loading. */
case Disabled = 'disabled';
/** Module install is in progress (transient; set before install(), cleared on completion). */
case Installing = 'installing';
/** Module install is in progress (transient; set before install(), cleared on completion). */
case Installing = 'installing';
/** Module install or update failed; manual intervention required. */
case Failed = 'failed';
/** Module install or update failed; manual intervention required. */
case Failed = 'failed';
/**
* Return true when this state means the module should be loaded by ModuleLoader.
*/
public function isLoadable(): bool {
return $this === self::Enabled;
}
/**
* Return true when this state means the module should be loaded by ModuleLoader.
*/
public function isLoadable(): bool {
return $this === self::Enabled;
}
/**
* Convert a raw overrides value (bool|string|null) to a ModuleState.
*
* Handles legacy `true` / `false` boolean values written by the old
* setEnabled(name, bool) API, as well as new string values.
*
* @param mixed $raw The raw value from config/modules.php overrides.
*/
public static function fromRaw(mixed $raw): self {
if ($raw === true || $raw === null) {
return self::Enabled;
}
if ($raw === false) {
return self::Disabled;
}
return self::tryFrom((string) $raw) ?? self::Enabled;
}
/**
* Convert a raw overrides value (bool|string|null) to a ModuleState.
*
* Handles legacy `true` / `false` boolean values written by the old
* setEnabled(name, bool) API, as well as new string values.
*
* @param mixed $raw The raw value from config/modules.php overrides.
*/
public static function fromRaw(mixed $raw): self {
if ($raw === true || $raw === null) {
return self::Enabled;
}
if ($raw === false) {
return self::Disabled;
}
return self::tryFrom((string) $raw) ?? self::Enabled;
}
}
+38 -39
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Enum;
@@ -18,44 +17,44 @@ namespace XcVm\Core\Enum;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
enum ResellerAction: string {
case New = 'new';
case Extend = 'extend';
case Convert = 'convert';
case Edit = 'edit';
case Enable = 'enable';
case Disable = 'disable';
case Delete = 'delete';
case SendEvent = 'send_event';
case AdjustCredits = 'adjust_credits';
case New = 'new';
case Extend = 'extend';
case Convert = 'convert';
case Edit = 'edit';
case Enable = 'enable';
case Disable = 'disable';
case Delete = 'delete';
case SendEvent = 'send_event';
case AdjustCredits = 'adjust_credits';
/**
* Human-readable action label (as shown in log filters).
*/
public function label(): string {
return match ($this) {
self::New => 'Create',
self::Extend => 'Extend',
self::Convert => 'Convert',
self::Edit => 'Edit',
self::Enable => 'Enable',
self::Disable => 'Disable',
self::Delete => 'Delete',
self::SendEvent => 'MAG Event',
self::AdjustCredits => 'Adjust Credits',
};
}
/**
* Human-readable action label (as shown in log filters).
*/
public function label(): string {
return match ($this) {
self::New => 'Create',
self::Extend => 'Extend',
self::Convert => 'Convert',
self::Edit => 'Edit',
self::Enable => 'Enable',
self::Disable => 'Disable',
self::Delete => 'Delete',
self::SendEvent => 'MAG Event',
self::AdjustCredits => 'Adjust Credits',
};
}
/**
* Ordered key => label map for building select/filter dropdowns,
* preserving the legacy `$rResellerActions` ordering.
*
* @return array<string, string>
*/
public static function options(): array {
$options = [];
foreach (self::cases() as $case) {
$options[$case->value] = $case->label();
}
return $options;
}
/**
* Ordered key => label map for building select/filter dropdowns,
* preserving the legacy `$rResellerActions` ordering.
*
* @return array<string, string>
*/
public static function options(): array {
$options = [];
foreach (self::cases() as $case) {
$options[$case->value] = $case->label();
}
return $options;
}
}
+4 -4
View File
@@ -14,9 +14,9 @@ namespace XcVm\Core\Enum;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
enum ServerEnvironment: string {
/** Standard panel installation. */
case Main = 'main';
/** Standard panel installation. */
case Main = 'main';
/** Load-balancer node (SERVER_TYPE=lb). */
case LoadBalancer = 'lb';
/** Load-balancer node (SERVER_TYPE=lb). */
case LoadBalancer = 'lb';
}
+41 -42
View File
@@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Enum;
@@ -19,50 +18,50 @@ namespace XcVm\Core\Enum;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
enum Theme: int {
/** Default light theme. */
case Light = 0;
/** Default light theme. */
case Light = 0;
/** Dark theme. */
case Dark = 1;
/** Dark theme. */
case Dark = 1;
/**
* Return true when this theme renders the dark layout variant.
*/
public function isDark(): bool {
return $this === self::Dark;
}
/**
* Return true when this theme renders the dark layout variant.
*/
public function isDark(): bool {
return $this === self::Dark;
}
/**
* Human-readable theme name (as shown in the profile selector).
*/
public function label(): string {
return match ($this) {
self::Light => 'Light',
self::Dark => 'Dark',
};
}
/**
* Human-readable theme name (as shown in the profile selector).
*/
public function label(): string {
return match ($this) {
self::Light => 'Light',
self::Dark => 'Dark',
};
}
/**
* Resolve a stored profile theme value to a Theme, defaulting to Light
* for missing or out-of-range values.
*
* @param mixed $id The raw `theme` value from the user profile.
*/
public static function fromId(mixed $id): self {
return self::tryFrom((int) $id) ?? self::Light;
}
/**
* Resolve a stored profile theme value to a Theme, defaulting to Light
* for missing or out-of-range values.
*
* @param mixed $id The raw `theme` value from the user profile.
*/
public static function fromId(mixed $id): self {
return self::tryFrom((int) $id) ?? self::Light;
}
/**
* Ordered id => label map for building the theme selector, preserving
* the legacy `$rThemes` ordering (0 => Light, 1 => Dark).
*
* @return array<int, string>
*/
public static function options(): array {
$options = [];
foreach (self::cases() as $case) {
$options[$case->value] = $case->label();
}
return $options;
}
/**
* Ordered id => label map for building the theme selector, preserving
* the legacy `$rThemes` ordering (0 => Light, 1 => Dark).
*
* @return array<int, string>
*/
public static function options(): array {
$options = [];
foreach (self::cases() as $case) {
$options[$case->value] = $case->label();
}
return $options;
}
}
+68 -68
View File
@@ -18,72 +18,72 @@
*/
global $rErrorCodes;
$rErrorCodes = array(
'API_IP_NOT_ALLOWED' => 'IP is not allowed to access the API.',
'ARCHIVE_DOESNT_EXIST' => 'Archive files are missing for this stream ID.',
'ASN_BLOCKED' => 'ASN has been blocked.',
'BANNED' => 'Line has been banned.',
'BLOCKED_USER_AGENT' => 'User-agent has been blocked.',
'DEVICE_NOT_ALLOWED' => 'MAG & Enigma devices are not allowed to access this.',
'DISABLED' => 'Line has been disabled.',
'DOWNLOAD_LIMIT_REACHED' => 'Reached the simultaneous download limit.',
'E2_DEVICE_LOCK_FAILED' => 'Device lock checks failed.',
'E2_DISABLED' => 'Device has been disabled.',
'E2_NO_TOKEN' => 'No token has been specified.',
'E2_TOKEN_DOESNT_MATCH' => "Token doesn't match records.",
'E2_WATCHDOG_TIMEOUT' => 'Time limit reached.',
'EMPTY_USER_AGENT' => 'Empty user-agents are disallowed.',
'EPG_DISABLED' => 'EPG has been disabled.',
'EPG_FILE_MISSING' => 'Cached EPG files are missing.',
'EXPIRED' => 'Line has expired.',
'FORCED_COUNTRY_INVALID' => 'Country does not match forced country.',
'GENERATE_PLAYLIST_FAILED' => 'Playlist failed to generate.',
'HLS_DISABLED' => 'HLS has been disabled.',
'HOSTING_DETECT' => 'Hosting server has been detected.',
'INVALID_API_PASSWORD' => 'API password is invalid.',
'INVALID_CREDENTIALS' => 'Username or password is invalid.',
'INVALID_HOST' => 'Domain name not recognised.',
'INVALID_STREAM_ID' => "Stream ID doesn't exist.",
'INVALID_TYPE_TOKEN' => "Tokens can't be used for this stream type.",
'IP_BLOCKED' => 'IP has been blocked.',
'IP_MISMATCH' => "Current IP doesn't match initial connection IP.",
'ISP_BLOCKED' => 'ISP has been blocked.',
'LB_TOKEN_INVALID' => 'AES Token cannot be decrypted.',
'LEGACY_EPG_DISABLED' => 'Legacy epg.php access has been disabled.',
'LEGACY_GET_DISABLED' => 'Legacy get.php access has been disabled.',
'LEGACY_PANEL_API_DISABLED' => 'Legacy panel_api.php access has been disabled.',
'LINE_CREATE_FAIL' => 'Line failed to insert into database.',
'NO_CREDENTIALS' => 'No credentials have been specified.',
'NO_TIMESTAMP' => 'No archive timestamp has been specified.',
'NO_TOKEN_SPECIFIED' => 'No AES encrypted token has been specified.',
'NOT_ENIGMA_DEVICE' => "Line isn't an enigma device.",
'NOT_IN_ALLOWED_COUNTRY' => 'Not in allowed country list.',
'NOT_IN_ALLOWED_IPS' => 'Not in allowed IP list.',
'NOT_IN_ALLOWED_UAS' => 'Not in allowed user-agent list.',
'NOT_IN_BOUQUET' => "Line doesn't have access to this stream ID.",
'PLAYER_API_DISABLED' => 'Player API has been disabled.',
'PROXY_DETECT' => 'Proxy has been detected.',
'PROXY_NO_API_ACCESS' => "Can't access API's via proxy.",
'RESTREAM_DETECT' => 'Restreaming has been detected.',
'STALKER_CHANNEL_MISMATCH' => "Stream ID doesn't match stalker token.",
'STALKER_DECRYPT_FAILED' => 'Failed to decrypt stalker token.',
'STALKER_INVALID_KEY' => 'Invalid stalker key.',
'STALKER_IP_MISMATCH' => "IP doesn't match stalker token.",
'STALKER_KEY_EXPIRED' => 'Stalker token has expired.',
'STREAM_OFFLINE' => 'Stream is currently offline.',
'THUMBNAIL_DOESNT_EXIST' => "Thumbnail file doesn't exist.",
'THUMBNAILS_NOT_ENABLED' => 'Thumbnail not enabled for this stream.',
'TOKEN_ERROR' => 'AES token has incomplete data.',
'TOKEN_EXPIRED' => 'AES token has expired.',
'TS_DISABLED' => 'MPEG-TS has been disabled.',
'USER_ALREADY_CONNECTED' => 'Line already connected on a different IP.',
'USER_DISALLOW_EXT' => 'Extension is not in allowed list.',
'VOD_DOESNT_EXIST' => "VOD file doesn't exist.",
'WAIT_TIME_EXPIRED' => 'Stream start has timed out, failed to start.',
$rErrorCodes = [
'API_IP_NOT_ALLOWED' => 'IP is not allowed to access the API.',
'ARCHIVE_DOESNT_EXIST' => 'Archive files are missing for this stream ID.',
'ASN_BLOCKED' => 'ASN has been blocked.',
'BANNED' => 'Line has been banned.',
'BLOCKED_USER_AGENT' => 'User-agent has been blocked.',
'DEVICE_NOT_ALLOWED' => 'MAG & Enigma devices are not allowed to access this.',
'DISABLED' => 'Line has been disabled.',
'DOWNLOAD_LIMIT_REACHED' => 'Reached the simultaneous download limit.',
'E2_DEVICE_LOCK_FAILED' => 'Device lock checks failed.',
'E2_DISABLED' => 'Device has been disabled.',
'E2_NO_TOKEN' => 'No token has been specified.',
'E2_TOKEN_DOESNT_MATCH' => "Token doesn't match records.",
'E2_WATCHDOG_TIMEOUT' => 'Time limit reached.',
'EMPTY_USER_AGENT' => 'Empty user-agents are disallowed.',
'EPG_DISABLED' => 'EPG has been disabled.',
'EPG_FILE_MISSING' => 'Cached EPG files are missing.',
'EXPIRED' => 'Line has expired.',
'FORCED_COUNTRY_INVALID' => 'Country does not match forced country.',
'GENERATE_PLAYLIST_FAILED' => 'Playlist failed to generate.',
'HLS_DISABLED' => 'HLS has been disabled.',
'HOSTING_DETECT' => 'Hosting server has been detected.',
'INVALID_API_PASSWORD' => 'API password is invalid.',
'INVALID_CREDENTIALS' => 'Username or password is invalid.',
'INVALID_HOST' => 'Domain name not recognised.',
'INVALID_STREAM_ID' => "Stream ID doesn't exist.",
'INVALID_TYPE_TOKEN' => "Tokens can't be used for this stream type.",
'IP_BLOCKED' => 'IP has been blocked.',
'IP_MISMATCH' => "Current IP doesn't match initial connection IP.",
'ISP_BLOCKED' => 'ISP has been blocked.',
'LB_TOKEN_INVALID' => 'AES Token cannot be decrypted.',
'LEGACY_EPG_DISABLED' => 'Legacy epg.php access has been disabled.',
'LEGACY_GET_DISABLED' => 'Legacy get.php access has been disabled.',
'LEGACY_PANEL_API_DISABLED' => 'Legacy panel_api.php access has been disabled.',
'LINE_CREATE_FAIL' => 'Line failed to insert into database.',
'NO_CREDENTIALS' => 'No credentials have been specified.',
'NO_TIMESTAMP' => 'No archive timestamp has been specified.',
'NO_TOKEN_SPECIFIED' => 'No AES encrypted token has been specified.',
'NOT_ENIGMA_DEVICE' => "Line isn't an enigma device.",
'NOT_IN_ALLOWED_COUNTRY' => 'Not in allowed country list.',
'NOT_IN_ALLOWED_IPS' => 'Not in allowed IP list.',
'NOT_IN_ALLOWED_UAS' => 'Not in allowed user-agent list.',
'NOT_IN_BOUQUET' => "Line doesn't have access to this stream ID.",
'PLAYER_API_DISABLED' => 'Player API has been disabled.',
'PROXY_DETECT' => 'Proxy has been detected.',
'PROXY_NO_API_ACCESS' => "Can't access API's via proxy.",
'RESTREAM_DETECT' => 'Restreaming has been detected.',
'STALKER_CHANNEL_MISMATCH' => "Stream ID doesn't match stalker token.",
'STALKER_DECRYPT_FAILED' => 'Failed to decrypt stalker token.',
'STALKER_INVALID_KEY' => 'Invalid stalker key.',
'STALKER_IP_MISMATCH' => "IP doesn't match stalker token.",
'STALKER_KEY_EXPIRED' => 'Stalker token has expired.',
'STREAM_OFFLINE' => 'Stream is currently offline.',
'THUMBNAIL_DOESNT_EXIST' => "Thumbnail file doesn't exist.",
'THUMBNAILS_NOT_ENABLED' => 'Thumbnail not enabled for this stream.',
'TOKEN_ERROR' => 'AES token has incomplete data.',
'TOKEN_EXPIRED' => 'AES token has expired.',
'TS_DISABLED' => 'MPEG-TS has been disabled.',
'USER_ALREADY_CONNECTED' => 'Line already connected on a different IP.',
'USER_DISALLOW_EXT' => 'Extension is not in allowed list.',
'VOD_DOESNT_EXIST' => "VOD file doesn't exist.",
'WAIT_TIME_EXPIRED' => 'Stream start has timed out, failed to start.',
// ── Дополнительные коды (из stream/init.php) ──────────────
'CACHE_INCOMPLETE' => 'Cache is being generated...',
'SUBTITLE_DOESNT_EXIST' => "Subtitle file doesn't exist.",
'NO_SERVERS_AVAILABLE' => 'No servers are currently available for this stream.',
'PROXY_ACCESS_DENIED' => 'You cannot access this stream directly while proxy is enabled.',
);
// ── Дополнительные коды (из stream/init.php) ──────────────
'CACHE_INCOMPLETE' => 'Cache is being generated...',
'SUBTITLE_DOESNT_EXIST' => "Subtitle file doesn't exist.",
'NO_SERVERS_AVAILABLE' => 'No servers are currently available for this stream.',
'PROXY_ACCESS_DENIED' => 'You cannot access this stream directly while proxy is enabled.',
];
+26 -26
View File
@@ -32,28 +32,28 @@
* @param bool $rKill Завершить выполнение после вывода (default: true)
* @param int|null $rCode HTTP-код ответа (null = 404)
*/
function generateError($rError, $rKill = true, $rCode = null) {
global $rErrorCodes;
global $rSettings;
function generateError(string $rError, bool $rKill = true, ?int $rCode = null) {
global $rErrorCodes;
global $rSettings;
if (isset($rSettings['debug_show_errors']) && $rSettings['debug_show_errors']) {
$rErrorDescription = (isset($rErrorCodes[$rError]) ? $rErrorCodes[$rError] : '');
$rStyle = '*{-webkit-box-sizing:border-box;box-sizing:border-box}body{padding:0;margin:0}#notfound{position:relative;height:100vh}#notfound .notfound{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.notfound{max-width:520px;width:100%;line-height:1.4;text-align:center}.notfound .notfound-404{position:relative;height:200px;margin:0 auto 20px;z-index:-1}.notfound .notfound-404 h1{font-family:Montserrat,sans-serif;font-size:236px;font-weight:200;margin:0;color:#211b19;text-transform:uppercase;position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.notfound .notfound-404 h2{font-family:Montserrat,sans-serif;font-size:28px;font-weight:400;text-transform:uppercase;color:#211b19;background:#fff;padding:10px 5px;margin:auto;display:inline-block;position:absolute;bottom:0;left:0;right:0}.notfound p{font-family:Montserrat,sans-serif;font-size:14px;font-weight:300;text-transform:uppercase}@media only screen and (max-width:767px){.notfound .notfound-404 h1{font-size:148px}}@media only screen and (max-width:480px){.notfound .notfound-404{height:148px;margin:0 auto 10px}.notfound .notfound-404 h1{font-size:86px}.notfound .notfound-404 h2{font-size:16px}}';
echo '<html><head><title>XC_VM - Debug Mode</title><link href="https://fonts.googleapis.com/css?family=Montserrat:200,400,700" rel="stylesheet"><style>' . $rStyle . '</style></head><body><div id="notfound"><div class="notfound"><div class="notfound-404"><h1>XC_VM</h1><h2>' . $rError . '</h2><br/></div><p>' . $rErrorDescription . '</p></div></div></body></html>';
if (isset($rSettings['debug_show_errors']) && $rSettings['debug_show_errors']) {
$rErrorDescription = (isset($rErrorCodes[$rError]) ? $rErrorCodes[$rError] : '');
$rStyle = '*{-webkit-box-sizing:border-box;box-sizing:border-box}body{padding:0;margin:0}#notfound{position:relative;height:100vh}#notfound .notfound{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.notfound{max-width:520px;width:100%;line-height:1.4;text-align:center}.notfound .notfound-404{position:relative;height:200px;margin:0 auto 20px;z-index:-1}.notfound .notfound-404 h1{font-family:Montserrat,sans-serif;font-size:236px;font-weight:200;margin:0;color:#211b19;text-transform:uppercase;position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.notfound .notfound-404 h2{font-family:Montserrat,sans-serif;font-size:28px;font-weight:400;text-transform:uppercase;color:#211b19;background:#fff;padding:10px 5px;margin:auto;display:inline-block;position:absolute;bottom:0;left:0;right:0}.notfound p{font-family:Montserrat,sans-serif;font-size:14px;font-weight:300;text-transform:uppercase}@media only screen and (max-width:767px){.notfound .notfound-404 h1{font-size:148px}}@media only screen and (max-width:480px){.notfound .notfound-404{height:148px;margin:0 auto 10px}.notfound .notfound-404 h1{font-size:86px}.notfound .notfound-404 h2{font-size:16px}}';
echo '<html><head><title>XC_VM - Debug Mode</title><link href="https://fonts.googleapis.com/css?family=Montserrat:200,400,700" rel="stylesheet"><style>' . $rStyle . '</style></head><body><div id="notfound"><div class="notfound"><div class="notfound-404"><h1>XC_VM</h1><h2>' . $rError . '</h2><br/></div><p>' . $rErrorDescription . '</p></div></div></body></html>';
if ($rKill) {
exit();
}
} else {
if ($rKill) {
if (!$rCode) {
generate404();
} else {
http_response_code($rCode);
exit();
}
}
}
if ($rKill) {
exit();
}
} else {
if ($rKill) {
if (!$rCode) {
generate404();
} else {
http_response_code($rCode);
exit();
}
}
}
}
/**
@@ -61,11 +61,11 @@ function generateError($rError, $rKill = true, $rCode = null) {
*
* @param bool $rKill Завершить выполнение после вывода (default: true)
*/
function generate404($rKill = true) {
echo '<html>' . "\r\n" . '<head><title>404 Not Found</title></head>' . "\r\n" . '<body>' . "\r\n" . '<center><h1>404 Not Found</h1></center>' . "\r\n" . '<hr><center>nginx</center>' . "\r\n" . '</body>' . "\r\n" . '</html>' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->';
http_response_code(404);
function generate404(bool $rKill = true) {
echo '<html>' . "\r\n" . '<head><title>404 Not Found</title></head>' . "\r\n" . '<body>' . "\r\n" . '<center><h1>404 Not Found</h1></center>' . "\r\n" . '<hr><center>nginx</center>' . "\r\n" . '</body>' . "\r\n" . '</html>' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->' . "\r\n" . '<!-- a padding to disable MSIE and Chrome friendly error page -->';
http_response_code(404);
if ($rKill) {
exit();
}
if ($rKill) {
exit();
}
}
+17 -18
View File
@@ -16,24 +16,23 @@ use XcVm\Core\Events\Contract\StoppableEventInterface;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
abstract class AbstractEvent implements StoppableEventInterface {
private bool $propagationStopped = false;
private bool $propagationStopped = false;
/**
* Whether a listener has stopped propagation of this event.
*
* @return bool True if propagation was stopped.
*/
public function isPropagationStopped(): bool {
return $this->propagationStopped;
}
/**
* Whether a listener has stopped propagation of this event.
*
* @return bool True if propagation was stopped.
*/
public function isPropagationStopped(): bool {
return $this->propagationStopped;
}
/**
* Stop propagation so no further listeners receive this event.
*
* @return void
*/
public function stopPropagation(): void {
$this->propagationStopped = true;
}
/**
* Stop propagation so no further listeners receive this event.
*
* @return void
*/
public function stopPropagation(): void {
$this->propagationStopped = true;
}
}
+13 -12
View File
@@ -11,16 +11,17 @@ namespace XcVm\Core\Events\Auth;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
final class UserAuthenticatedEvent {
/**
* @param int $userId Authenticated user id.
* @param string $username Authenticated username.
* @param string $role User role.
* @param float $authenticatedAt Unix timestamp (with microseconds) of authentication.
*/
public function __construct(
public readonly int $userId,
public readonly string $username,
public readonly string $role,
public readonly float $authenticatedAt,
) {}
/**
* @param int $userId Authenticated user id.
* @param string $username Authenticated username.
* @param string $role User role.
* @param float $authenticatedAt Unix timestamp (with microseconds) of authentication.
*/
public function __construct(
public readonly int $userId,
public readonly string $username,
public readonly string $role,
public readonly float $authenticatedAt,
) {
}
}
+11 -10
View File
@@ -11,14 +11,15 @@ namespace XcVm\Core\Events\Auth;
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
final class UserLoggedOutEvent {
/**
* @param int $userId User that logged out.
* @param string $username Username that logged out.
* @param float $loggedOutAt Unix timestamp (with microseconds) of logout.
*/
public function __construct(
public readonly int $userId,
public readonly string $username,
public readonly float $loggedOutAt,
) {}
/**
* @param int $userId User that logged out.
* @param string $username Username that logged out.
* @param float $loggedOutAt Unix timestamp (with microseconds) of logout.
*/
public function __construct(
public readonly int $userId,
public readonly string $username,
public readonly float $loggedOutAt,
) {
}
}

Some files were not shown because too many files have changed in this diff Show More