Files
XC_VM/src/Core/Diagnostics/DiagnosticsService.php
T
Divarion-D 1a296d0985 chore(psr4): phase 1 — rename 7 top-level dirs to PascalCase
Atomic case-rename of the seven class-holding source directories to match the
PSR-4 namespace casing, plus every consequent path reference. No code logic
changes — pure structural rename. (config/content/resources/signals/migrations/
ministra/storage/tmp/vendor/www/bin stay lowercase.)

- git mv: core→Core, domain→Domain, infrastructure→Infrastructure,
  streaming→Streaming, modules→Modules, cli→Cli, public→Public (module subdirs
  like Modules/plex stay lowercase; loaded by ModuleLoader, not Composer).
- PHP filesystem paths updated across src/ + tests/: require/include, glob/scandir
  bases, view includes, asset/nginx-alias paths, autoload.php registerDirectories(),
  ModuleLoader/ModuleManager module roots, console.php discovery dirs.
- src/composer.json PSR-4 vendored-lib paths → Core/Parsing/...; vendor/ regenerated.
- nginx.conf: docroot + SCRIPT_FILENAME → Public/. SCRIPT_NAME and the public-facing
  /streaming/*.php compat routes are URLs, left unchanged.
- Build/CI: Makefile LB_DIRS / LB_DIRS_TO_REMOVE / LB_FILES_TO_REMOVE PascalCased
  (closes the LB-archive privileged-leak blocker); phpstan.dist.neon paths/
  scanDirectories/excludePaths; phpunit.xml.dist coverage; phpstan-baseline.neon
  regenerated (294→294 errors, no new resolution failures).
- migrations/deleted_files.txt: old lowercase trees listed for client cleanup
  (assumes case-sensitive FS — the supported Linux target).

Verified: grep-gate 0 live lowercase-dir require/include in src+tests; php -l clean;
PHPUnit 292/292; PHPStan no errors; Composer first / XC_Autoloader last in SPL stack;
global classes resolve from the renamed dirs.
2026-06-24 20:10:34 +03:00

352 lines
10 KiB
PHP

<?php
/**
* Diagnostics Service
*
* downloadPanelLogs, submitPanelLogs.
*
* Panel-log methods accept a $db parameter; other methods are stateless.
*
* @package XC_VM_Core_Diagnostics
* @author Divarion_D <https://github.com/Divarion-D>
* @copyright 2025-2026 Vateron Media
* @link https://github.com/Vateron-Media/XC_VM
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class DiagnosticsService {
/**
* Parse SSL certificate info from nginx config or a specific file
*
* @param string|null $certificate Path to certificate file (auto-detects from nginx if null)
* @return array ['serial', 'expiration', 'subject', 'path']
*/
public static function getCertificateInfo($certificate = null) {
$result = ['serial' => null, 'expiration' => null, 'subject' => null, 'path' => null];
if (!$certificate) {
$config = explode("\n", file_get_contents(BIN_PATH . 'nginx/conf/ssl.conf'));
foreach ($config as $line) {
$rTrimmed = trim($line);
if (strncasecmp($rTrimmed, 'ssl_certificate ', 16) === 0) {
$certificate = trim(explode(';', substr($rTrimmed, 16), 2)[0]);
break;
}
}
}
if (!$certificate || !file_exists($certificate)) {
return null;
}
$result['path'] = pathinfo($certificate)['dirname'];
$output = [];
exec('openssl x509 -serial -enddate -subject -noout -in ' . escapeshellarg($certificate), $output, $returnVar);
if ($returnVar !== 0) {
return null;
}
foreach ($output as $line) {
if (stripos($line, 'serial=') !== false) {
$result['serial'] = trim(explode('serial=', $line)[1]);
} elseif (stripos($line, 'subject=') !== false) {
$result['subject'] = trim(explode('subject=', $line)[1]);
} elseif (stripos($line, 'notAfter=') !== false) {
$result['expiration'] = strtotime(trim(explode('notAfter=', $line)[1]));
}
}
return $result;
}
/**
* Check if stream codecs are compatible with the player
*
* @param array|string $data FFProbe output (array or JSON string)
* @param bool $allowHEVC Whether HEVC/H265 + AC3 are allowed
* @return bool
*/
public static function checkCompatibility($data, $allowHEVC = false) {
if (!is_array($data)) {
$data = json_decode($data, true);
}
if (!is_array($data) || !isset($data['codecs']) || !is_array($data['codecs'])) {
return false;
}
$audioCodec = $data['codecs']['audio']['codec_name'] ?? null;
$videoCodec = $data['codecs']['video']['codec_name'] ?? null;
$audioCodecs = ['aac', 'libfdk_aac', 'opus', 'vorbis', 'pcm_s16le', 'mp2', 'mp3', 'flac'];
$videoCodecs = ['h264', 'vp8', 'vp9', 'ogg', 'av1'];
if ($allowHEVC) {
$videoCodecs[] = 'hevc';
$videoCodecs[] = 'h265';
$audioCodecs[] = 'ac3';
}
if (!$videoCodec) {
return false;
}
if (!in_array(strtolower($videoCodec), $videoCodecs, true)) {
return false;
}
if ($audioCodec && !in_array(strtolower($audioCodec), $audioCodecs, true)) {
return false;
}
return true;
}
/**
* Download panel logs from database, format them and clear the logs table
*
* @param \DatabaseHandler $db Database handler (must have ->query(), ->get_rows())
* @return array ['errors' => [...], 'version' => string]
* @throws Exception
*/
public static function downloadPanelLogs($db): array {
ini_set('default_socket_timeout', 60);
$errors = [];
try {
$query = "SELECT `type`, `log_message`, `log_extra`, `line`, `date`
FROM `panel_logs`
WHERE `type` <> 'epg'
ORDER BY `date` DESC
LIMIT 1000";
$result = $db->query($query);
if (!$result) {
throw new Exception('Failed to execute database query');
}
$allErrors = $db->get_rows() ?: [];
foreach ($allErrors as $error) {
$errorData = [
'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,
];
try {
if ($errorData['date'] > 0) {
$dt = new DateTime('@' . $errorData['date']);
$dt->setTimezone(new DateTimeZone('UTC'));
$errorData['human_date'] = $dt->format('Y-m-d H:i:s');
} else {
$errorData['human_date'] = 'invalid_timestamp';
}
} catch (Exception $e) {
$errorData['human_date'] = 'conversion_error';
}
$errors[] = $errorData;
}
if (!empty($errors)) {
$truncateResult = $db->query('TRUNCATE `panel_logs`;');
if (!$truncateResult) {
throw new Exception('Failed to truncate panel logs table');
}
}
} catch (Exception $e) {
throw new Exception('Failed to process panel logs');
}
return [
'errors' => $errors,
'version' => defined('XC_VM_VERSION') ? XC_VM_VERSION : 'unknown',
];
}
/**
* Submit panel logs to the central API server
*
* @param \DatabaseHandler $db Database handler
* @return string|false API response or false on failure
*/
public static function submitPanelLogs($db) {
ini_set('default_socket_timeout', 60);
// Select only logs not yet marked as sent
$db->query("SELECT `id`, `type`, `log_message`, `log_extra`, `line`, `date` FROM `panel_logs` WHERE `type` <> 'epg' AND IFNULL(`sent`,0)=0 GROUP BY CONCAT(`type`, `log_message`, `log_extra`) ORDER BY `date` DESC LIMIT 1000;");
$rows = $db->get_rows() ?: [];
$rAPI = base64_decode(implode('', [
'aHR0cDov',
'L3hjdm0u',
'dGVjaC9h',
'cGkvdjEv',
'cmVwb3J0'
]));
$errorsForApi = [];
$ids = [];
foreach ($rows as $row) {
$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'] : '',
'date' => $ts > 0 ? gmdate('Y-m-d H:i:s', $ts) : '',
];
if (isset($row['id'])) {
$ids[] = (int)$row['id'];
}
}
$payload = json_encode([
'errors' => $errorsForApi,
'version' => defined('XC_VM_VERSION') ? XC_VM_VERSION : 'unknown',
], JSON_UNESCAPED_UNICODE);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $rAPI);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($payload),
]);
$response = curl_exec($ch);
curl_close($ch);
if ($response !== false) {
$responseData = json_decode($response, true);
if (isset($responseData['status']) && $responseData['status'] === 'success' && !empty($ids)) {
// mark sent logs instead of truncating the whole table
$idList = implode(',', array_map('intval', $ids));
$db->query("UPDATE `panel_logs` SET `sent` = 1 WHERE `id` IN ($idList);");
}
}
return $response;
}
/**
* Fetch the list of running process PIDs from a server via the system API.
*
* @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);
if (!is_array($rProcesses)) {
return $rReturn;
}
array_shift($rProcesses);
foreach ($rProcesses as $rProcess) {
$rSplit = explode(' ', preg_replace('!\\s+!', ' ', trim($rProcess)));
if ($rSplit[0] == 'xc_vm') {
$rUsage = array(0, 0, 0);
$rTimer = explode('-', $rSplit[9]);
if (1 < count($rTimer)) {
$rDays = intval($rTimer[0]);
$rTime = $rTimer[1];
} else {
$rDays = 0;
$rTime = $rTimer[0];
}
$rTime = explode(':', $rTime);
if (count($rTime) == 3) {
$rSeconds = intval($rTime[0]) * 3600 + intval($rTime[1]) * 60 + intval($rTime[2]);
} else {
if (count($rTime) == 2) {
$rSeconds = intval($rTime[0]) * 60 + intval($rTime[1]);
} else {
$rSeconds = intval($rTime[2]);
}
}
$rUsage[0] = $rSeconds + $rDays * 86400;
$rTimer = explode('-', $rSplit[8]);
if (1 < count($rTimer)) {
$rDays = intval($rTimer[0]);
$rTime = $rTimer[1];
} else {
$rDays = 0;
$rTime = $rTimer[0];
}
$rTime = explode(':', $rTime);
if (count($rTime) == 3) {
$rSeconds = intval($rTime[0]) * 3600 + intval($rTime[1]) * 60 + intval($rTime[2]);
} else {
if (count($rTime) == 2) {
$rSeconds = intval($rTime[0]) * 60 + intval($rTime[1]);
} else {
$rSeconds = intval($rTime[2]);
}
}
$rUsage[1] = $rSeconds + $rDays * 86400;
if ($rUsage[0] != 0) {
$rUsage[2] = $rUsage[1] / $rUsage[0] * 100;
} else {
$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)));
}
}
return $rReturn;
}
/**
* List NVENC (GPU) encoding processes for a server.
*
* @param int $rServerID Server id to inspect.
* @return array NVENC process details.
*/
public static function getNVENCProcesses($rServerID) {
global $db;
$rProcesses = array();
$rServer = ServerRepository::getById($rServerID);
$rGPUInfo = json_decode($rServer['gpu_info'], true);
if (!is_array($rGPUInfo)) {
} else {
foreach ($rGPUInfo['gpus'] as $rGPU) {
foreach ($rGPU['processes'] as $rProcess) {
$rArray = array('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()) {
} else {
$rArray['stream_id'] = $db->get_row()['stream_id'];
}
$rProcesses[] = $rArray;
}
}
}
return $rProcesses;
}
}