mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-16 04:01:37 +02:00
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:
@@ -16,7 +16,6 @@ namespace XcVm\Cli;
|
||||
*/
|
||||
|
||||
interface CommandInterface {
|
||||
|
||||
/**
|
||||
* Уникальное имя команды.
|
||||
*
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace XcVm\Cli;
|
||||
*/
|
||||
|
||||
class CommandRegistry {
|
||||
|
||||
/** @var CommandInterface[] name → command */
|
||||
private $rCommands = [];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -16,7 +16,6 @@ use XcVm\Core\Database\MigrationRunner;
|
||||
*/
|
||||
|
||||
class DbMigrateCommand implements CommandInterface {
|
||||
|
||||
public function getName(): string {
|
||||
return 'db:migrate';
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -15,7 +15,6 @@ use XcVm\Cli\CommandInterface;
|
||||
*/
|
||||
|
||||
class MigrateCommand implements CommandInterface {
|
||||
|
||||
public function getName(): string {
|
||||
return 'migrate';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ use XcVm\Cli\CommandInterface;
|
||||
*/
|
||||
|
||||
class ServiceCommand implements CommandInterface {
|
||||
|
||||
public function getName(): string {
|
||||
return 'service';
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ use XcVm\Streaming\Codec\FfmpegPaths;
|
||||
*/
|
||||
|
||||
class ThumbnailCommand implements CommandInterface {
|
||||
|
||||
public function getName(): string {
|
||||
return 'thumbnail';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 . ';');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+617
-616
File diff suppressed because it is too large
Load Diff
+161
-161
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ use XcVm\Infrastructure\Redis\RedisManager;
|
||||
*/
|
||||
|
||||
trait DaemonTrait {
|
||||
|
||||
/** @var string MD5 файла команды при запуске */
|
||||
protected $rDaemonMD5;
|
||||
|
||||
|
||||
+1537
-1537
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XcVm\Core\Auth;
|
||||
|
||||
|
||||
+218
-218
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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/');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('<', '<', str_replace('>', '>', $rValue));
|
||||
$rValue = str_replace('<!--', '<!--', $rValue);
|
||||
$rValue = str_replace('-->', '-->', $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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
Reference in New Issue
Block a user