You've already forked Atomcms-edit
1359 lines
56 KiB
PHP
Executable File
1359 lines
56 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Process;
|
|
|
|
class NitroUpdateService
|
|
{
|
|
private readonly ?SettingsService $settings;
|
|
|
|
private readonly ?UpdateHistoryService $historyService;
|
|
|
|
private readonly string $clientRepo;
|
|
|
|
private readonly string $rendererRepo;
|
|
|
|
private readonly string $gitlabHost;
|
|
|
|
private readonly string $clientPath;
|
|
|
|
private readonly string $rendererPath;
|
|
|
|
private readonly string $clientWebRoot;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->settings = app(SettingsService::class);
|
|
$this->settings->clearInstanceCache();
|
|
$this->clientRepo = 'duckietm/Nitro-V3';
|
|
$this->rendererRepo = 'duckietm/Nitro_Render_V3';
|
|
$this->gitlabHost = 'gitlab.epicnabbo.nl/duckietm';
|
|
|
|
$basePath = $this->detectBasePath();
|
|
|
|
$this->clientPath = $this->resolvePath(setting('nitro_client_path', $basePath . '/nitro-client'));
|
|
$this->rendererPath = $this->resolvePath(setting('nitro_renderer_path', $basePath . '/nitro-renderer'));
|
|
$this->clientWebRoot = $this->resolvePath(setting('nitro_webroot', '/var/www/Client'));
|
|
|
|
$this->historyService = app(UpdateHistoryService::class);
|
|
}
|
|
|
|
private function isDirAccessible(string $path): bool
|
|
{
|
|
if ($path === '' || $path === '0') {
|
|
return false;
|
|
}
|
|
$result = Process::timeout(5)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
|
|
|
|
return trim($result->output()) === 'yes';
|
|
}
|
|
|
|
private function detectBasePath(): string
|
|
{
|
|
$possiblePaths = [
|
|
base_path(),
|
|
'/var/www/atomcms',
|
|
'/var/www/html',
|
|
'/var/www',
|
|
dirname(base_path()),
|
|
];
|
|
|
|
foreach ($possiblePaths as $path) {
|
|
if ($this->isDirAccessible($path)) {
|
|
$clientCheck = Process::timeout(5)->run('test -d ' . escapeshellarg($path . '/nitro-client') . ' && echo "yes" || echo "no"');
|
|
$rendererCheck = Process::timeout(5)->run('test -d ' . escapeshellarg($path . '/nitro-renderer') . ' && echo "yes" || echo "no"');
|
|
if (trim($clientCheck->output()) === 'yes' || trim($rendererCheck->output()) === 'yes') {
|
|
return $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
return base_path();
|
|
}
|
|
|
|
private function resolvePath(string $path): string
|
|
{
|
|
if (str_starts_with($path, '/')) {
|
|
if ($this->isDirAccessible($path)) {
|
|
return $path;
|
|
}
|
|
$parts = explode('/', trim($path, '/'));
|
|
$root = '/' . $parts[0];
|
|
if (! $this->isDirAccessible($root)) {
|
|
return base_path() . '/' . ltrim($path, '/');
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
return base_path() . '/' . ltrim($path, '/');
|
|
}
|
|
|
|
public function isConfigured(): bool
|
|
{
|
|
return $this->isDirAccessible($this->clientPath) || $this->isDirAccessible($this->rendererPath) || $this->isDirAccessible($this->clientWebRoot);
|
|
}
|
|
|
|
public function getClientPath(): string
|
|
{
|
|
return $this->clientPath;
|
|
}
|
|
|
|
public function getRendererPath(): string
|
|
{
|
|
return $this->rendererPath;
|
|
}
|
|
|
|
public function getClientWebRoot(): string
|
|
{
|
|
return $this->clientWebRoot;
|
|
}
|
|
|
|
public function getStatus(): array
|
|
{
|
|
$buildPath = setting('nitro_build_path', '/var/www/atomcms/nitro-client/dist');
|
|
$webroot = $this->clientWebRoot;
|
|
|
|
// Use shell commands to bypass open_basedir
|
|
$checkDir = function (string $path): bool {
|
|
$result = Process::timeout(5)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
|
|
|
|
return trim($result->output()) === 'yes';
|
|
};
|
|
|
|
$checkFile = function (string $path): bool {
|
|
$result = Process::timeout(5)->run('test -f ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
|
|
|
|
return trim($result->output()) === 'yes';
|
|
};
|
|
|
|
// Check nginx config via shell to bypass open_basedir
|
|
$nginxCheck = Process::timeout(5)->run('test -f /etc/nginx/sites-enabled/cms.conf -o -f /etc/nginx/sites-enabled/atom.conf -o -f /etc/nginx/conf.d/atom.conf && echo "yes" || echo "no"');
|
|
$nginxValid = trim($nginxCheck->output()) === 'yes';
|
|
|
|
// Check open_basedir - fixed if empty OR includes all required paths
|
|
$openBaseDir = ini_get('open_basedir');
|
|
$clientPath = $this->clientPath;
|
|
$rendererPath = $this->rendererPath;
|
|
$openBasedirFixed = in_array($openBaseDir, ['', '0', false], true) || (
|
|
str_contains($openBaseDir, $clientPath) &&
|
|
str_contains($openBaseDir, $rendererPath)
|
|
);
|
|
|
|
Log::info('[NitroUpdate] open_basedir check:', [
|
|
'open_basedir' => $openBaseDir,
|
|
'clientPath' => $clientPath,
|
|
'rendererPath' => $rendererPath,
|
|
'fixed' => $openBasedirFixed,
|
|
]);
|
|
|
|
// Build files (check for hashed filenames like index-CLCACmcI.js)
|
|
$assetsPath = $buildPath . '/assets';
|
|
|
|
$indexJsCheck = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/index.js') . ' && echo "yes" || echo "no"');
|
|
$indexJsHashCheck = Process::timeout(5)->run('ls ' . escapeshellarg($assetsPath) . '/index-*.js 2>/dev/null | head -1');
|
|
$hasIndexJs = trim($indexJsCheck->output()) === 'yes' || ! in_array(trim($indexJsHashCheck->output()), ['', '0'], true);
|
|
|
|
$rendererJsCheck = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/renderer.js') . ' && echo "yes" || echo "no"');
|
|
$rendererJsHashCheck = Process::timeout(5)->run('ls ' . escapeshellarg($assetsPath) . '/nitro-renderer-*.js 2>/dev/null | head -1');
|
|
$hasRendererJs = trim($rendererJsCheck->output()) === 'yes' || ! in_array(trim($rendererJsHashCheck->output()), ['', '0'], true);
|
|
|
|
$vendorJsCheck = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/vendor.js') . ' && echo "yes" || echo "no"');
|
|
$vendorJsHashCheck = Process::timeout(5)->run('ls ' . escapeshellarg($assetsPath) . '/vendor-*.js 2>/dev/null | head -1');
|
|
$hasVendorJs = trim($vendorJsCheck->output()) === 'yes' || ! in_array(trim($vendorJsHashCheck->output()), ['', '0'], true);
|
|
|
|
// Check for CSS files
|
|
$hasCssCheck = Process::timeout(5)->run('find ' . escapeshellarg((string) $buildPath) . " -name '*.css' -type f 2>/dev/null | head -1");
|
|
$hasCss = ! in_array(trim($hasCssCheck->output()), ['', '0'], true);
|
|
|
|
// Renderer (monorepo - packages are built into client, so just check node_modules)
|
|
$hasRendererDist = $checkDir($this->rendererPath . '/node_modules');
|
|
$hasRendererIndex = $checkDir($this->rendererPath . '/packages');
|
|
|
|
// Config files
|
|
$rendererConfigValid = $checkFile($webroot . '/renderer-config.json');
|
|
$uiConfigValid = $checkFile($webroot . '/ui-config.json');
|
|
$nitroConfigValid = $checkFile($webroot . '/UITexts.json');
|
|
$hasRendererExample = $checkFile($buildPath . '/renderer-config.example') || $checkFile($webroot . '/renderer-config.example');
|
|
$hasUiExample = $checkFile($buildPath . '/ui-config.example') || $checkFile($webroot . '/ui-config.example');
|
|
|
|
// Assets
|
|
$hasImageAssets = $checkDir($webroot . '/assets') || $checkDir($buildPath . '/assets');
|
|
$hasFavicon = $checkFile($webroot . '/favicon.ico') || $checkFile($buildPath . '/favicon.ico');
|
|
|
|
// System
|
|
$webrootExists = $checkDir($webroot);
|
|
$webrootWritableCheck = Process::timeout(5)->run('test -w ' . escapeshellarg($webroot) . ' && echo "yes" || echo "no"');
|
|
$webrootWritable = trim($webrootWritableCheck->output()) === 'yes';
|
|
$webrootFileCount = $this->countFiles($webroot);
|
|
$buildSize = $this->getDirSize($buildPath);
|
|
$webrootSize = $this->getDirSize($webroot);
|
|
|
|
// Socket URL from renderer config
|
|
$socketUrl = 'N/A';
|
|
if ($rendererConfigValid) {
|
|
$configResult = Process::timeout(5)->run('cat ' . escapeshellarg($webroot . '/renderer-config.json') . ' 2>/dev/null');
|
|
if ($configResult->successful()) {
|
|
$config = json_decode($configResult->output(), true);
|
|
$socketUrl = $config['socket.url'] ?? 'N/A';
|
|
}
|
|
}
|
|
|
|
// Check for gamedata - could be a symlink or directory
|
|
$symlinkValid = false;
|
|
|
|
// Check if gamedata exists as symlink or directory
|
|
$gamedataCheck = Process::timeout(5)->run('test -e ' . escapeshellarg($webroot . '/gamedata') . ' && echo "yes" || echo "no"');
|
|
if (trim($gamedataCheck->output()) === 'yes') {
|
|
$symlinkValid = true;
|
|
} else {
|
|
// Try alternative locations
|
|
$altPaths = [
|
|
'/var/www/Gamedata',
|
|
'/var/www/gamedata',
|
|
'/var/www/atomcms/public/gamedata',
|
|
];
|
|
foreach ($altPaths as $altPath) {
|
|
$altCheck = Process::timeout(5)->run('test -d ' . escapeshellarg($altPath) . ' && echo "yes" || echo "no"');
|
|
if (trim($altCheck->output()) === 'yes') {
|
|
$symlinkValid = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'client_path' => $this->clientPath,
|
|
'renderer_path' => $this->rendererPath,
|
|
'build_path' => $buildPath,
|
|
'webroot' => $webroot,
|
|
'client_commit' => $this->getCommit($this->clientPath),
|
|
'renderer_commit' => $this->getCommit($this->rendererPath),
|
|
'client_installed' => $checkDir($this->clientPath . '/.git'),
|
|
'renderer_installed' => $checkDir($this->rendererPath . '/.git'),
|
|
'build_exists' => $checkDir($buildPath),
|
|
'deployed' => $rendererConfigValid,
|
|
'symlink_valid' => $symlinkValid,
|
|
'client_node_modules' => $checkDir($this->clientPath . '/node_modules'),
|
|
'renderer_node_modules' => $checkDir($this->rendererPath . '/node_modules'),
|
|
'client_latest_commit' => null,
|
|
'renderer_latest_commit' => null,
|
|
'last_checked' => setting('nitro_last_checked'),
|
|
'nginx_config_valid' => $nginxValid,
|
|
'open_basedir_fixed' => $openBasedirFixed,
|
|
'has_index_js' => $hasIndexJs,
|
|
'has_renderer_js' => $hasRendererJs,
|
|
'has_vendor_js' => $hasVendorJs,
|
|
'has_css_file' => $hasCss,
|
|
'vite_config_valid' => $checkFile($this->clientPath . '/vite.config.js') || $checkFile($this->clientPath . '/vite.config.mjs'),
|
|
'renderer_config_valid' => $rendererConfigValid,
|
|
'ui_config_valid' => $uiConfigValid,
|
|
'nitro_config_valid' => $nitroConfigValid,
|
|
'has_uitexts_json' => $nitroConfigValid,
|
|
'has_renderer_example' => $hasRendererExample,
|
|
'has_uiconfig_example' => $hasUiExample,
|
|
'has_image_assets' => $hasImageAssets,
|
|
'has_favicon' => $hasFavicon,
|
|
'assets_writable' => $webrootWritable,
|
|
'assets_file_count' => $webrootFileCount,
|
|
'has_renderer_dist' => $hasRendererDist,
|
|
'has_renderer_index' => $hasRendererIndex,
|
|
'webroot_exists' => $webrootExists,
|
|
'webroot_writable' => $webrootWritable,
|
|
'webroot_file_count' => $webrootFileCount,
|
|
'build_size' => $buildSize,
|
|
'webroot_size' => $webrootSize,
|
|
'socket_url' => $socketUrl,
|
|
'websocket_accessible' => $this->checkWebSocketAccessible($socketUrl),
|
|
'emulator_connected' => $this->checkEmulatorConnected(),
|
|
];
|
|
}
|
|
|
|
private function checkWebSocketAccessible(string $socketUrl): bool
|
|
{
|
|
if (in_array($socketUrl, ['', '0', 'N/A'], true)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$wsUrl = $socketUrl;
|
|
if (str_starts_with($wsUrl, 'wss://')) {
|
|
$wsUrl = 'https://' . substr($wsUrl, 6);
|
|
} elseif (str_starts_with($wsUrl, 'ws://')) {
|
|
$wsUrl = 'http://' . substr($wsUrl, 4);
|
|
}
|
|
|
|
$host = parse_url($wsUrl, PHP_URL_HOST);
|
|
$port = parse_url($wsUrl, PHP_URL_PORT);
|
|
|
|
if (! $port) {
|
|
$port = str_starts_with($socketUrl, 'wss://') ? 443 : 80;
|
|
}
|
|
|
|
$socket = @fsockopen($host, $port, $errno, $errstr, 5);
|
|
if ($socket) {
|
|
fclose($socket);
|
|
|
|
return true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Fallback to curl check
|
|
}
|
|
|
|
// Fallback: try curl
|
|
try {
|
|
$ch = curl_init($socketUrl);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
|
|
curl_setopt($ch, CURLOPT_NOBODY, true);
|
|
curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
return in_array($httpCode, [200, 301, 302, 400, 404]);
|
|
} catch (\Exception) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function checkEmulatorConnected(): bool
|
|
{
|
|
try {
|
|
$rconService = new RconService;
|
|
|
|
return $rconService->isConnected();
|
|
} catch (\Exception) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function countFiles(string $path): int
|
|
{
|
|
$check = Process::timeout(10)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
|
|
if (trim($check->output()) !== 'yes') {
|
|
return 0;
|
|
}
|
|
$result = Process::timeout(10)->run('find ' . escapeshellarg($path) . ' -type f 2>/dev/null | wc -l');
|
|
|
|
return (int) trim($result->output() ?? '0');
|
|
}
|
|
|
|
private function getDirSize(string $path): int
|
|
{
|
|
$check = Process::timeout(10)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
|
|
if (trim($check->output()) !== 'yes') {
|
|
return 0;
|
|
}
|
|
$result = Process::timeout(10)->run('du -sb ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1');
|
|
|
|
return (int) trim($result->output() ?? '0');
|
|
}
|
|
|
|
public function updateNitro(bool $forceBuild = false): array
|
|
{
|
|
$lockFile = '/tmp/nitro-update.lock';
|
|
|
|
if (file_exists($lockFile)) {
|
|
$lockAge = time() - filemtime($lockFile);
|
|
if ($lockAge < 300) {
|
|
$pid = @file_get_contents($lockFile);
|
|
|
|
return ['success' => false, 'error' => "Update al bezig (PID: {$pid})"];
|
|
}
|
|
@unlink($lockFile);
|
|
}
|
|
|
|
file_put_contents($lockFile, getmypid());
|
|
|
|
try {
|
|
Log::info('[Nitro] Starting update');
|
|
$this->historyService->log('nitro', 'update', null, 'pending', 'Update gestart');
|
|
|
|
$this->ensureVpsRequirements();
|
|
|
|
$this->setupDirectories();
|
|
|
|
$this->syncRepos();
|
|
|
|
$this->installDependencies();
|
|
|
|
$this->fixKnownBugs();
|
|
|
|
$buildResult = $this->buildClient();
|
|
|
|
if ($buildResult['success']) {
|
|
$this->deployClient();
|
|
$this->generateConfigs();
|
|
|
|
$this->settings->set('nitro_client_commit', $this->getCommit($this->clientPath));
|
|
$this->settings->set('nitro_renderer_commit', $this->getCommit($this->rendererPath));
|
|
|
|
Log::info('[Nitro] Update completed successfully');
|
|
$this->historyService->log('nitro', 'update', 'client', 'success', 'Update succesvol');
|
|
|
|
return ['success' => true, 'message' => 'Nitro client succesvol geüpdatet!'];
|
|
}
|
|
|
|
$this->historyService->log('nitro', 'update', 'client', 'failed', $buildResult['error'] ?? 'Build mislukt');
|
|
|
|
return $buildResult;
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('[Nitro] Update failed', ['error' => $e->getMessage()]);
|
|
$this->historyService->log('nitro', 'update', null, 'failed', $e->getMessage());
|
|
|
|
return ['success' => false, 'error' => $e->getMessage()];
|
|
} finally {
|
|
@unlink($lockFile);
|
|
}
|
|
}
|
|
|
|
private function ensureVpsRequirements(): array
|
|
{
|
|
$actions = [];
|
|
$errors = [];
|
|
|
|
$nodeCheck = Process::timeout(5)->run('which node && node --version');
|
|
if (! $nodeCheck->successful()) {
|
|
$errors[] = 'Node.js niet geïnstalleerd';
|
|
Process::timeout(60)->run('curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs 2>&1');
|
|
$nodeCheck2 = Process::timeout(5)->run('which node && node --version');
|
|
if ($nodeCheck2->successful()) {
|
|
$actions[] = 'Node.js geïnstalleerd: ' . trim($nodeCheck2->output());
|
|
}
|
|
} else {
|
|
$actions[] = 'Node.js: ' . trim($nodeCheck->output());
|
|
}
|
|
|
|
$yarnCheck = Process::timeout(5)->run('which yarn && yarn --version');
|
|
if (! $yarnCheck->successful()) {
|
|
$actions[] = 'Yarn wordt geïnstalleerd...';
|
|
Process::timeout(60)->run('npm install -g yarn 2>&1');
|
|
$yarnCheck2 = Process::timeout(5)->run('which yarn && yarn --version');
|
|
if ($yarnCheck2->successful()) {
|
|
$actions[] = 'Yarn geïnstalleerd: ' . trim($yarnCheck2->output());
|
|
}
|
|
} else {
|
|
$actions[] = 'Yarn: ' . trim($yarnCheck->output());
|
|
}
|
|
|
|
$npmCacheDir = Process::timeout(5)->run('echo $HOME');
|
|
$npmHome = trim($npmCacheDir->output()) ?: '/root';
|
|
Process::timeout(5)->run('mkdir -p ' . escapeshellarg($npmHome . '/.npm'));
|
|
Process::timeout(5)->run('mkdir -p /var/www/.npm');
|
|
Process::timeout(5)->run('chown -R www-data:www-data /var/www/.npm');
|
|
|
|
Process::timeout(10)->run('git config --global --add safe.directory "*" 2>/dev/null || true');
|
|
|
|
return [
|
|
'actions' => $actions,
|
|
'errors' => $errors,
|
|
];
|
|
}
|
|
|
|
private function setupDirectories(): void
|
|
{
|
|
$dirs = [
|
|
$this->clientPath => 'Nitro Client',
|
|
$this->rendererPath => 'Nitro Renderer',
|
|
$this->clientWebRoot => 'Client Webroot',
|
|
];
|
|
|
|
foreach ($dirs as $path => $label) {
|
|
$existsCheck = Process::timeout(5)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
|
|
if (trim($existsCheck->output()) !== 'yes') {
|
|
Log::info("[Nitro] Creating {$label} directory: {$path}");
|
|
Process::timeout(10)->run('mkdir -p ' . escapeshellarg($path));
|
|
}
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($path));
|
|
}
|
|
|
|
// Create gamedata symlink if it doesn't exist
|
|
$gamedataSymlink = $this->clientWebRoot . '/gamedata';
|
|
$gamedataCheck = Process::timeout(5)->run('test -e ' . escapeshellarg($gamedataSymlink) . ' && echo "yes" || echo "no"');
|
|
|
|
if (trim($gamedataCheck->output()) !== 'yes') {
|
|
// Try to find gamedata folder
|
|
$possibleGamedata = [
|
|
'/var/www/Gamedata',
|
|
'/var/www/gamedata',
|
|
'/var/www/atomcms/public/gamedata',
|
|
base_path() . '/../Gamedata',
|
|
];
|
|
|
|
foreach ($possibleGamedata as $gdPath) {
|
|
$gdCheck = Process::timeout(5)->run('test -d ' . escapeshellarg($gdPath) . ' && echo "yes" || echo "no"');
|
|
if (trim($gdCheck->output()) === 'yes') {
|
|
Process::timeout(10)->run('ln -s ' . escapeshellarg($gdPath) . ' ' . escapeshellarg($gamedataSymlink));
|
|
Log::info('[Nitro] Created gamedata symlink: ' . $gdPath . ' -> ' . $gamedataSymlink);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function syncRepos(): void
|
|
{
|
|
$repos = [
|
|
['path' => $this->rendererPath, 'repo' => $this->rendererRepo],
|
|
['path' => $this->clientPath, 'repo' => $this->clientRepo],
|
|
];
|
|
|
|
Process::timeout(5)->run('git config --global --add safe.directory "*" 2>/dev/null || true');
|
|
|
|
foreach ($repos as $r) {
|
|
$gitCheck = Process::timeout(5)->run('test -d ' . escapeshellarg($r['path']) . '/.git && echo "yes" || echo "no"');
|
|
$isGitRepo = trim($gitCheck->output()) === 'yes';
|
|
|
|
if ($isGitRepo) {
|
|
Log::info("[Nitro] Updating {$r['repo']}");
|
|
|
|
$fetchResult = Process::timeout(120)->run('cd ' . escapeshellarg($r['path']) . ' && git fetch nitro-v3 2>&1');
|
|
|
|
$resetResult = Process::timeout(60)->run('cd ' . escapeshellarg($r['path']) . ' && git checkout main 2>&1 || git checkout master 2>&1 || true');
|
|
|
|
$pullResult = Process::timeout(120)->run('cd ' . escapeshellarg($r['path']) . ' && git reset --hard nitro-v3/main 2>&1 || git reset --hard nitro-v3/master 2>&1 || true');
|
|
|
|
if (! $pullResult->successful()) {
|
|
Log::warning("[Nitro] Update failed, re-cloning {$r['repo']}");
|
|
Process::timeout(30)->run('rm -rf ' . escapeshellarg($r['path']));
|
|
|
|
$cloneResult = Process::timeout(300)->run(
|
|
"git clone --branch main --depth 1 https://{$this->gitlabHost}/{$r['repo']}.git " . escapeshellarg($r['path']) . ' 2>&1',
|
|
);
|
|
}
|
|
} else {
|
|
Log::info("[Nitro] Cloning {$r['repo']}");
|
|
|
|
$dirCheck = Process::timeout(5)->run('test -d ' . escapeshellarg($r['path']) . ' && echo "yes" || echo "no"');
|
|
if (trim($dirCheck->output()) === 'yes') {
|
|
Process::timeout(30)->run('rm -rf ' . escapeshellarg($r['path']));
|
|
}
|
|
|
|
Process::timeout(300)->run(
|
|
"git clone --branch main --depth 1 https://{$this->gitlabHost}/{$r['repo']}.git " . escapeshellarg($r['path']) . ' 2>&1',
|
|
);
|
|
}
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($r['path']));
|
|
}
|
|
}
|
|
|
|
private function fixKnownBugs(): void
|
|
{
|
|
$this->fixChatsCss();
|
|
|
|
// Fix 1: Chats.css case sensitivity
|
|
$indexFile = $this->clientPath . '/src/index.tsx';
|
|
if (file_exists($indexFile)) {
|
|
$content = file_get_contents($indexFile);
|
|
if (str_contains($content, './css/chat/Chats.css')) {
|
|
$content = str_replace('./css/chat/Chats.css', './css/chat/chats.css', $content);
|
|
file_put_contents($indexFile, $content);
|
|
Log::info('[Nitro] Fixed Chats.css case');
|
|
}
|
|
}
|
|
|
|
// Fix 2: Duplicate CatalogView export
|
|
$catalogFile = $this->clientPath . '/src/components/catalog/CatalogView.tsx';
|
|
if (file_exists($catalogFile)) {
|
|
$content = file_get_contents($catalogFile);
|
|
$lines = explode("\n", $content);
|
|
$cleanLines = [];
|
|
$seenExport = false;
|
|
foreach ($lines as $line) {
|
|
if (str_contains($line, 'export const CatalogView')) {
|
|
if ($seenExport) {
|
|
continue;
|
|
}
|
|
$seenExport = true;
|
|
}
|
|
$cleanLines[] = $line;
|
|
}
|
|
file_put_contents($catalogFile, implode("\n", $cleanLines));
|
|
Log::info('[Nitro] Fixed duplicate CatalogView');
|
|
}
|
|
|
|
// Fix 3: Create/update vite config
|
|
$this->updateViteConfig();
|
|
|
|
// Fix 4: Create symlink for renderer
|
|
$this->createYarnLink();
|
|
|
|
// Fix 5: Add missing renderer stubs
|
|
$this->addMissingRendererStubs();
|
|
|
|
// Fix 6: Duplicate FurniEditorResultEvent (case sensitivity conflict)
|
|
$duplicateFile = $this->rendererPath . '/packages/communication/src/messages/incoming/furniture/FurniEditorResultEvent.ts';
|
|
if (file_exists($duplicateFile)) {
|
|
unlink($duplicateFile);
|
|
$furnitureIndexFile = $this->rendererPath . '/packages/communication/src/messages/incoming/furniture/index.ts';
|
|
if (file_exists($furnitureIndexFile)) {
|
|
$content = file_get_contents($furnitureIndexFile);
|
|
$content = str_replace("export * from './FurniEditorResultEvent';\n", '', $content);
|
|
file_put_contents($furnitureIndexFile, $content);
|
|
}
|
|
Log::info('[Nitro] Fixed duplicate FurniEditorResultEvent');
|
|
}
|
|
|
|
// Fix 7: Rename Chats.css to lowercase
|
|
$upperCaseCss = $this->clientPath . '/src/css/chat/Chats.css';
|
|
$lowerCaseCss = $this->clientPath . '/src/css/chat/chats.css';
|
|
if (file_exists($upperCaseCss) && ! file_exists($lowerCaseCss)) {
|
|
rename($upperCaseCss, $lowerCaseCss);
|
|
Log::info('[Nitro] Fixed Chats.css case sensitivity');
|
|
}
|
|
|
|
// Fix 8: Remove all duplicate FurniEditor files from furniture/ (keep only furnieditor/)
|
|
$outgoingDir = $this->rendererPath . '/packages/communication/src/messages/outgoing/furniture';
|
|
$incomingDir = $this->rendererPath . '/packages/communication/src/messages/incoming/furniture';
|
|
|
|
$outgoingDupes = ['BySprite', 'Create', 'Delete', 'Detail', 'Interactions', 'Search', 'Update'];
|
|
foreach ($outgoingDupes as $name) {
|
|
$file = $outgoingDir . '/FurniEditor' . $name . 'Composer.ts';
|
|
if (file_exists($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
if (file_exists($outgoingDir . '/index.ts')) {
|
|
$content = file_get_contents($outgoingDir . '/index.ts');
|
|
$lines = explode("\n", $content);
|
|
$cleanLines = array_filter($lines, fn ($line) => ! str_contains((string) $line, 'FurniEditor'));
|
|
file_put_contents($outgoingDir . '/index.ts', implode("\n", $cleanLines));
|
|
}
|
|
|
|
$incomingDupes = ['Detail', 'Interactions', 'Result', 'Search'];
|
|
foreach ($incomingDupes as $name) {
|
|
$file = $incomingDir . '/FurniEditor' . $name . 'Event.ts';
|
|
if (file_exists($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
if (file_exists($incomingDir . '/index.ts')) {
|
|
$content = file_get_contents($incomingDir . '/index.ts');
|
|
$lines = explode("\n", $content);
|
|
$cleanLines = array_filter($lines, fn ($line) => ! str_contains((string) $line, 'FurniEditor'));
|
|
file_put_contents($incomingDir . '/index.ts', implode("\n", $cleanLines));
|
|
}
|
|
|
|
Log::info('[Nitro] Removed duplicate FurniEditor files from furniture/');
|
|
}
|
|
|
|
private function fixChatsCss(): void
|
|
{
|
|
$chatCssFile = $this->clientPath . '/src/css/chat/chats.css';
|
|
if (! file_exists($chatCssFile)) {
|
|
return;
|
|
}
|
|
|
|
$content = file_get_contents($chatCssFile);
|
|
$modified = false;
|
|
|
|
$content = preg_replace('/\/\/\s*normal\s*\n\s*\.message/', '.message', $content, -1, $count);
|
|
if ($count > 0) {
|
|
$modified = true;
|
|
}
|
|
$content = preg_replace('/\/\/\s*whisper\s*\n\s*\.message/', '.message', $content, -1, $count);
|
|
if ($count > 0) {
|
|
$modified = true;
|
|
}
|
|
$content = preg_replace('/\/\/\s*shout\s*\n\s*\.message/', '.message', $content, -1, $count);
|
|
if ($count > 0) {
|
|
$modified = true;
|
|
}
|
|
|
|
if ($modified) {
|
|
file_put_contents($chatCssFile, $content);
|
|
Log::info('[Nitro] Fixed chat CSS comments');
|
|
}
|
|
}
|
|
|
|
private function addMissingRendererStubs(): void
|
|
{
|
|
$outgoingDir = $this->rendererPath . '/packages/communication/src/messages/outgoing/furniture';
|
|
$incomingDir = $this->rendererPath . '/packages/communication/src/messages/incoming/furniture';
|
|
$outgoingFurnieditorDir = $this->rendererPath . '/packages/communication/src/messages/outgoing/furnieditor';
|
|
$incomingFurnieditorDir = $this->rendererPath . '/packages/communication/src/messages/incoming/furnieditor';
|
|
|
|
if (! is_dir($outgoingDir)) {
|
|
mkdir($outgoingDir, 0755, true);
|
|
}
|
|
if (! is_dir($incomingDir)) {
|
|
mkdir($incomingDir, 0755, true);
|
|
}
|
|
|
|
$composers = ['BySprite', 'Create', 'Delete', 'Detail', 'Interactions', 'Search', 'Update'];
|
|
$composerLines = [];
|
|
foreach ($composers as $name) {
|
|
$furnieditorFile = $outgoingFurnieditorDir . '/FurniEditor' . $name . 'Composer.ts';
|
|
$stubFile = $outgoingDir . '/FurniEditor' . $name . 'Composer.ts';
|
|
if (! file_exists($furnieditorFile) && ! file_exists($stubFile)) {
|
|
file_put_contents($stubFile, "import { IMessageComposer } from '@nitrots/api';\nexport class FurniEditor{$name}Composer implements IMessageComposer {\n constructor(k: any) {}\n dispose() {}\n getMessageArray() { return []; }\n}\n");
|
|
$composerLines[] = "export * from './FurniEditor{$name}Composer';";
|
|
}
|
|
}
|
|
if ($composerLines !== []) {
|
|
$composerContent = implode("\n", $composerLines) . "\n";
|
|
file_put_contents($outgoingDir . '/index.ts', $composerContent);
|
|
}
|
|
|
|
$events = ['Detail', 'Interactions', 'Result', 'Search'];
|
|
$eventLines = [];
|
|
foreach ($events as $name) {
|
|
$furnieditorFile = $incomingFurnieditorDir . '/FurniEditor' . $name . 'Event.ts';
|
|
$stubFile = $incomingDir . '/FurniEditor' . $name . 'Event.ts';
|
|
if (! file_exists($furnieditorFile) && ! file_exists($stubFile)) {
|
|
file_put_contents($stubFile, "import { IMessageEvent } from '@nitrots/api';\nimport { MessageEvent } from '@nitrots/events';\nexport class FurniEditor{$name}Event extends MessageEvent implements IMessageEvent {\n constructor(callBack: Function) { super(callBack, (k: any) => k); }\n getParser() { return this.parser; }\n}\n");
|
|
$eventLines[] = "export * from './FurniEditor{$name}Event';";
|
|
}
|
|
}
|
|
if ($eventLines !== []) {
|
|
$eventContent = implode("\n", $eventLines) . "\n";
|
|
file_put_contents($incomingDir . '/index.ts', $eventContent);
|
|
}
|
|
|
|
Log::info('[Nitro] Added missing renderer stubs');
|
|
}
|
|
|
|
private function updateViteConfig(): void
|
|
{
|
|
$viteConfigPath = $this->clientPath . '/vite.config.mjs';
|
|
|
|
$content = file_exists($viteConfigPath) ? file_get_contents($viteConfigPath) : '';
|
|
|
|
if (str_contains($content, '@nitrots/nitro-renderer') && str_contains($content, '/var/www/nitro-renderer')) {
|
|
return;
|
|
}
|
|
|
|
$viteConfig = <<<'CONFIG'
|
|
import react from "@vitejs/plugin-react";
|
|
import { resolve } from "path";
|
|
import { defineConfig } from "vite";
|
|
|
|
const rendererPath = "/var/www/nitro-renderer";
|
|
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
server: {
|
|
fs: { allow: [resolve(__dirname), rendererPath] },
|
|
proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true } },
|
|
},
|
|
resolve: {
|
|
alias: [
|
|
{ find: "@nitrots/nitro-renderer", replacement: resolve(rendererPath, "src") },
|
|
{ find: "@nitrots/api", replacement: resolve(rendererPath, "packages", "api", "src") },
|
|
{ find: "@nitrots/assets", replacement: resolve(rendererPath, "packages", "assets", "src") },
|
|
{ find: "@nitrots/avatar", replacement: resolve(rendererPath, "packages", "avatar", "src") },
|
|
{ find: "@nitrots/camera", replacement: resolve(rendererPath, "packages", "camera", "src") },
|
|
{ find: "@nitrots/communication", replacement: resolve(rendererPath, "packages", "communication", "src") },
|
|
{ find: "@nitrots/configuration", replacement: resolve(rendererPath, "packages", "configuration", "src") },
|
|
{ find: "@nitrots/events", replacement: resolve(rendererPath, "packages", "events", "src") },
|
|
{ find: "@nitrots/localization", replacement: resolve(rendererPath, "packages", "localization", "src") },
|
|
{ find: "@nitrots/room", replacement: resolve(rendererPath, "packages", "room", "src") },
|
|
{ find: "@nitrots/session", replacement: resolve(rendererPath, "packages", "session", "src") },
|
|
{ find: "@nitrots/sound", replacement: resolve(rendererPath, "packages", "sound", "src") },
|
|
{ find: "@nitrots/utils/src", replacement: resolve(rendererPath, "packages", "utils", "src") },
|
|
{ find: "@nitrots/utils", replacement: resolve(rendererPath, "packages", "utils", "src") },
|
|
{ find: "@", replacement: resolve(__dirname, "src") },
|
|
{ find: "~", replacement: resolve(__dirname, "node_modules") },
|
|
{ find: "pixi.js", replacement: resolve(rendererPath, "node_modules", "pixi.js") },
|
|
{ find: "pixi-filters", replacement: resolve(rendererPath, "node_modules", "pixi-filters") },
|
|
{ find: "howler", replacement: resolve(rendererPath, "node_modules", "howler") },
|
|
],
|
|
},
|
|
build: {
|
|
assetsInlineLimit: 102400,
|
|
chunkSizeWarningLimit: 200000,
|
|
rollupOptions: {
|
|
output: {
|
|
assetFileNames: "src/assets/[name]-[hash].[ext]",
|
|
manualChunks: (id) => {
|
|
if (id.includes("node_modules")) {
|
|
if (id.includes("@nitrots") || id.includes("nitro-renderer")) {
|
|
return "nitro-renderer";
|
|
}
|
|
return "vendor";
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
CONFIG;
|
|
|
|
file_put_contents($this->clientPath . '/vite.config.mjs', $viteConfig);
|
|
Process::timeout(5)->run('chmod 644 ' . escapeshellarg($this->clientPath . '/vite.config.mjs'));
|
|
Process::timeout(5)->run('chown www-data:www-data ' . escapeshellarg($this->clientPath . '/vite.config.mjs'));
|
|
Log::info('[Nitro] Updated vite.config.mjs');
|
|
}
|
|
|
|
private function createYarnLink(): void
|
|
{
|
|
$nitrotsDir = $this->clientPath . '/node_modules/@nitrots';
|
|
if (! is_dir($nitrotsDir)) {
|
|
mkdir($nitrotsDir, 0755, true);
|
|
}
|
|
|
|
Process::timeout(10)->run('rm -rf ' . escapeshellarg($nitrotsDir) . '/*');
|
|
Process::timeout(30)->run(
|
|
'cp -r ' . escapeshellarg($this->rendererPath . '/node_modules/@nitrots/*') . ' ' . escapeshellarg($nitrotsDir) . '/',
|
|
);
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($nitrotsDir));
|
|
Log::info('[Nitro] Copied @nitrots packages');
|
|
}
|
|
|
|
private function installDependencies(): void
|
|
{
|
|
Log::info('[Nitro] Cleaning renderer node_modules');
|
|
|
|
Process::timeout(30)->run('rm -rf ' . escapeshellarg($this->rendererPath . '/node_modules'));
|
|
|
|
Log::info('[Nitro] Installing renderer deps');
|
|
|
|
$rendererYarn = Process::timeout(300)->run(
|
|
'cd ' . escapeshellarg($this->rendererPath) . ' && yarn install --ignore-engines 2>&1',
|
|
);
|
|
|
|
if (! $rendererYarn->successful()) {
|
|
Log::warning('[Nitro] Yarn failed for renderer, trying npm');
|
|
Process::timeout(300)->run(
|
|
'cd ' . escapeshellarg($this->rendererPath) . ' && npm install --legacy-peer-deps 2>&1',
|
|
);
|
|
}
|
|
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($this->rendererPath));
|
|
|
|
Log::info('[Nitro] Cleaning client node_modules');
|
|
|
|
Process::timeout(30)->run('rm -rf ' . escapeshellarg($this->clientPath . '/node_modules'));
|
|
|
|
Log::info('[Nitro] Installing client deps');
|
|
|
|
$clientYarn = Process::timeout(300)->run(
|
|
'cd ' . escapeshellarg($this->clientPath) . ' && yarn install --ignore-engines 2>&1',
|
|
);
|
|
|
|
if (! $clientYarn->successful()) {
|
|
Log::warning('[Nitro] Yarn failed for client, trying npm');
|
|
Process::timeout(300)->run(
|
|
'cd ' . escapeshellarg($this->clientPath) . ' && npm install --legacy-peer-deps 2>&1',
|
|
);
|
|
}
|
|
|
|
$this->createYarnLink();
|
|
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($this->clientPath));
|
|
}
|
|
|
|
public function buildClient(): array
|
|
{
|
|
Log::info('[Nitro] Building client');
|
|
|
|
$yarnBuild = Process::timeout(600)->run(
|
|
'cd ' . escapeshellarg($this->clientPath) . ' && yarn build 2>&1',
|
|
);
|
|
|
|
if ($yarnBuild->successful()) {
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($this->clientPath));
|
|
Log::info('[Nitro] Build successful (yarn)');
|
|
|
|
return ['success' => true, 'method' => 'yarn'];
|
|
}
|
|
|
|
Log::warning('[Nitro] Yarn build failed, trying npm build');
|
|
|
|
$npmBuild = Process::timeout(600)->run(
|
|
'cd ' . escapeshellarg($this->clientPath) . ' && npm run build 2>&1',
|
|
);
|
|
|
|
if ($npmBuild->successful()) {
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($this->clientPath));
|
|
Log::info('[Nitro] Build successful (npm)');
|
|
|
|
return ['success' => true, 'method' => 'npm'];
|
|
}
|
|
|
|
$output = $yarnBuild->output() . $yarnBuild->errorOutput() . "\n" . $npmBuild->output() . $npmBuild->errorOutput();
|
|
Log::error('[Nitro] Build failed', ['output' => substr($output, 0, 500)]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Build mislukt: ' . substr($output, 0, 300),
|
|
];
|
|
}
|
|
|
|
public function deployClient(): void
|
|
{
|
|
Log::info('[Nitro] Deploying client');
|
|
|
|
// Clear and copy
|
|
Process::timeout(30)->run('rm -rf ' . escapeshellarg($this->clientWebRoot) . '/*');
|
|
Process::timeout(60)->run(
|
|
'cp -r ' . escapeshellarg($this->clientPath . '/dist') . '/* ' .
|
|
escapeshellarg($this->clientWebRoot) . '/ 2>&1',
|
|
);
|
|
|
|
// Always add/update toolbar color CSS variables to root
|
|
$indexFile = $this->clientWebRoot . '/index.html';
|
|
if (file_exists($indexFile)) {
|
|
$content = file_get_contents($indexFile);
|
|
$cssVars = '<style>:root{--toolbar__primary__color:' .
|
|
($this->settings->getOrDefault('button_primary_color', '#eeb425')) . ';--toolbar__hover__color:' .
|
|
($this->settings->getOrDefault('button_hover_color', '#cf9d15')) . ';--toolbar__border__color:' .
|
|
($this->settings->getOrDefault('button_border_color', '#cf9d15')) . ';--toolbar__text__color:' .
|
|
($this->settings->getOrDefault('button_text_color', '#1a1a2e')) . ';}</style>';
|
|
|
|
// Replace existing CSS vars or add new
|
|
if (preg_match('/<style>:root\{--toolbar[^}]*}<\/style>/', $content, $matches)) {
|
|
$content = str_replace($matches[0], $cssVars, $content);
|
|
} else {
|
|
$content = str_replace('<head>', '<head>' . $cssVars, $content);
|
|
}
|
|
file_put_contents($indexFile, $content);
|
|
}
|
|
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($this->clientWebRoot));
|
|
Process::timeout(10)->run('chmod -R 755 ' . escapeshellarg($this->clientWebRoot));
|
|
Log::info('[Nitro] Deployed');
|
|
}
|
|
|
|
public function generateConfigs(): void
|
|
{
|
|
try {
|
|
$siteUrl = $this->settings->getOrDefault('nitro_site_url', config('app.url'));
|
|
Artisan::call('app:generate-nitro-configs', ['--site-url' => $siteUrl]);
|
|
Log::info('[Nitro] Generated configs');
|
|
} catch (\Exception $e) {
|
|
Log::warning('[Nitro] Config generation failed', ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
public function repairConfigs(): array
|
|
{
|
|
$actions = [];
|
|
$errors = [];
|
|
|
|
$webroot = $this->clientWebRoot;
|
|
|
|
$requiredConfigs = [
|
|
'renderer-config.json',
|
|
'ui-config.json',
|
|
'UITexts.json',
|
|
];
|
|
|
|
foreach ($requiredConfigs as $config) {
|
|
$path = $webroot . '/' . $config;
|
|
if (! file_exists($path) || filesize($path) < 10) {
|
|
$actions[] = "{$config} ontbreekt of leeg - genereren...";
|
|
|
|
try {
|
|
$siteUrl = $this->settings->getOrDefault('nitro_site_url', config('app.url'));
|
|
|
|
if ($config === 'renderer-config.json') {
|
|
$this->generateSimpleRendererConfig($webroot, $siteUrl);
|
|
} elseif ($config === 'ui-config.json') {
|
|
$this->generateSimpleUiConfig($webroot, $siteUrl);
|
|
} elseif ($config === 'UITexts.json') {
|
|
$this->generateSimpleUITexts($webroot);
|
|
}
|
|
|
|
$actions[] = "{$config} gegenereerd";
|
|
} catch (\Exception $e) {
|
|
$errors[] = "{$config}: " . $e->getMessage();
|
|
}
|
|
} else {
|
|
$json = @file_get_contents($path);
|
|
$data = @json_decode($json, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$actions[] = "{$config} is ongeldig - regenereren...";
|
|
try {
|
|
$siteUrl = $this->settings->getOrDefault('nitro_site_url', config('app.url'));
|
|
|
|
if ($config === 'renderer-config.json') {
|
|
$this->generateSimpleRendererConfig($webroot, $siteUrl);
|
|
} elseif ($config === 'ui-config.json') {
|
|
$this->generateSimpleUiConfig($webroot, $siteUrl);
|
|
} elseif ($config === 'UITexts.json') {
|
|
$this->generateSimpleUITexts($webroot);
|
|
}
|
|
|
|
$actions[] = "{$config} geregenereerd";
|
|
} catch (\Exception $e) {
|
|
$errors[] = "{$config}: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process::timeout(10)->run("chown www-data:www-data {$webroot}/*.json 2>/dev/null || true");
|
|
|
|
return [
|
|
'success' => $errors === [],
|
|
'actions' => $actions,
|
|
'errors' => $errors,
|
|
];
|
|
}
|
|
|
|
private function generateSimpleRendererConfig(string $webroot, string $siteUrl): void
|
|
{
|
|
$protocol = str_starts_with($siteUrl, 'https') ? 'https' : 'http';
|
|
$host = preg_replace('/^https?:\/\//', '', $siteUrl);
|
|
$wsProtocol = $protocol === 'https' ? 'wss' : 'ws';
|
|
|
|
$config = [
|
|
'socket.url' => $wsProtocol . '://ws.' . $host,
|
|
'socket.wsEngine' => 'websocket',
|
|
'site.url' => $protocol . '://' . $host,
|
|
'habboImagingUrl' => $protocol . '://' . $host . '/habbo-imaging',
|
|
'habboAvatarRendering' => [
|
|
'useLegacyAvatarRender' => false,
|
|
],
|
|
'navigator' => [
|
|
'forceOwnerView' => false,
|
|
'forceRoomView' => false,
|
|
],
|
|
'client.allowRedirect' => true,
|
|
'client.url' => $protocol . '://' . $host . '/',
|
|
'externalVariables' => $protocol . '://' . $host . '/gamedata/external_variables.txt',
|
|
'externalOverrideVariables' => $protocol . '://' . $host . '/gamedata/external_override_variables.txt',
|
|
'externalTexts' => $protocol . '://' . $host . '/gamedata/external_flash_texts.txt',
|
|
'externalFlashRanks' => $protocol . '://' . $host . '/gamedata/external_flash_userrank.txt',
|
|
'productData' => $protocol . '://' . $host . '/gamedata/productdata.txt',
|
|
'furnidata' => $protocol . '://' . $host . '/gamedata/furnidata.json',
|
|
'furnitureData' => $protocol . '://' . $host . '/gamedata/furniture.json',
|
|
'roomQueue' => [
|
|
'enabled' => true,
|
|
'maxQueueSize' => 500,
|
|
'minQueueSize' => 100,
|
|
],
|
|
'client.starting' => 'Nitro',
|
|
'client.defaultConnection' => 'default',
|
|
'connections.default' => [
|
|
'socket' => [
|
|
'host' => $host,
|
|
'port' => 30000,
|
|
'scheme' => $protocol,
|
|
'wsScheme' => $wsProtocol,
|
|
],
|
|
],
|
|
];
|
|
|
|
$gamedataPath = $webroot . '/gamedata';
|
|
if (is_dir($gamedataPath)) {
|
|
$dirs = array_filter(scandir($gamedataPath), fn ($d) => ! in_array($d, ['.', '..']) && is_dir($gamedataPath . '/' . $d));
|
|
|
|
$assetMap = [
|
|
'furniture' => ['furni.asset.url', 'furnidata'],
|
|
'pets' => ['pet.asset.url', 'petdata'],
|
|
'clothes' => ['avatar.asset.url', 'figureparts'],
|
|
'effects' => ['avatar.asset.effect.url', 'effects'],
|
|
'badges' => ['badge.asset.url', 'base_styles'],
|
|
];
|
|
|
|
foreach ($assetMap as $dir => $keys) {
|
|
foreach ($dirs as $d) {
|
|
if (stripos($d, $dir) !== false) {
|
|
$config[$keys[0]] = $protocol . '://' . $host . '/gamedata/' . $d . '/';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
file_put_contents($webroot . '/renderer-config.json', json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
private function generateSimpleUiConfig(string $webroot, string $siteUrl): void
|
|
{
|
|
preg_replace('/^https?:\/\//', '', $siteUrl);
|
|
$config = [
|
|
'application' => [
|
|
'name' => 'Nitro Hotel',
|
|
'description' => 'Nitro Hotel',
|
|
'premium' => [
|
|
'title' => 'VIP',
|
|
'description' => 'Word VIP en krijg extra features!',
|
|
],
|
|
],
|
|
'catalog' => [
|
|
'pages' => [
|
|
'hide unavailable' => false,
|
|
'hide info' => false,
|
|
],
|
|
],
|
|
'friendlist' => [
|
|
'maxFriends' => 1000,
|
|
],
|
|
'inventory' => [
|
|
'maxItems' => 2500,
|
|
],
|
|
'achievements' => [
|
|
'enabled' => true,
|
|
],
|
|
'room' => [
|
|
'chat' => [
|
|
'bubbleWidth' => 200,
|
|
'bubbleHeight' => 60,
|
|
],
|
|
],
|
|
'notifications' => [
|
|
'showNew' => true,
|
|
],
|
|
'groups' => [
|
|
'enabled' => true,
|
|
'maxMembers' => 500,
|
|
],
|
|
'recycler' => [
|
|
'enabled' => true,
|
|
],
|
|
'trading' => [
|
|
'enabled' => true,
|
|
],
|
|
'camera' => [
|
|
'enabled' => true,
|
|
'saveToInventory' => true,
|
|
],
|
|
];
|
|
|
|
file_put_contents($webroot . '/ui-config.json', json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
private function generateSimpleUITexts(string $webroot): void
|
|
{
|
|
$texts = [
|
|
'welcome' => 'Welkom bij Nitro Hotel!',
|
|
'welcome.sub' => 'Veel plezier!',
|
|
'pick.color' => 'Kies een kleur',
|
|
'pick.type' => 'Kies een type',
|
|
'buy.now' => 'Koop nu',
|
|
'cancel' => 'Annuleren',
|
|
'ok' => 'OK',
|
|
'close' => 'Sluiten',
|
|
'save' => 'Opslaan',
|
|
'delete' => 'Verwijderen',
|
|
'edit' => 'Bewerken',
|
|
'error' => 'Fout',
|
|
'success' => 'Succes',
|
|
'loading' => 'Laden...',
|
|
];
|
|
|
|
file_put_contents($webroot . '/UITexts.json', json_encode($texts, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
private function getCommit(string $path): ?string
|
|
{
|
|
try {
|
|
$result = Process::timeout(10)->run(
|
|
'cd ' . escapeshellarg($path) . ' && git rev-parse HEAD 2>&1',
|
|
);
|
|
if ($result->successful()) {
|
|
return trim($result->output());
|
|
}
|
|
} catch (\Exception) {
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function checkForUpdates(): array
|
|
{
|
|
$localClient = $this->getCommit($this->clientPath);
|
|
$localRenderer = $this->getCommit($this->rendererPath);
|
|
|
|
$status = $this->getStatus();
|
|
|
|
$hasUpdates = ! ($status['deployed'] ?? false) || ! ($status['client_installed'] ?? false) || ! ($status['renderer_installed'] ?? false);
|
|
|
|
return [
|
|
'client_commit' => $localClient,
|
|
'renderer_commit' => $localRenderer,
|
|
'client_path' => $this->clientPath,
|
|
'renderer_path' => $this->rendererPath,
|
|
'has_updates' => $hasUpdates,
|
|
'client_update' => ! ($status['client_installed'] ?? false),
|
|
'renderer_update' => ! ($status['renderer_installed'] ?? false),
|
|
'current_client_commit' => $localClient,
|
|
'current_renderer_commit' => $localRenderer,
|
|
'rate_limited' => false,
|
|
];
|
|
}
|
|
|
|
public function repair(): array
|
|
{
|
|
$actions = [];
|
|
$errors = [];
|
|
|
|
Log::info('[NitroUpdate] Starting repair process');
|
|
|
|
try {
|
|
$dirs = [
|
|
$this->clientPath => 'client',
|
|
$this->rendererPath => 'renderer',
|
|
$this->clientWebRoot => 'webroot',
|
|
];
|
|
|
|
foreach ($dirs as $path => $label) {
|
|
$exists = is_dir($path);
|
|
if (! $exists) {
|
|
$actions[] = "{$label} map bestaat niet - aanmaken...";
|
|
@mkdir($path, 0755, true);
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($path));
|
|
$actions[] = "{$label} map aangemaakt: {$path}";
|
|
}
|
|
}
|
|
|
|
if (! is_dir($this->rendererPath . '/node_modules')) {
|
|
$actions[] = 'Renderer node_modules ontbreekt - installeren...';
|
|
Process::timeout(300)->run(
|
|
'cd ' . escapeshellarg($this->rendererPath) . ' && yarn install --ignore-engines 2>&1',
|
|
);
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($this->rendererPath));
|
|
$actions[] = 'Renderer dependencies geïnstalleerd';
|
|
}
|
|
|
|
if (! is_dir($this->clientPath . '/node_modules')) {
|
|
$actions[] = 'Client node_modules ontbreekt - installeren...';
|
|
Process::timeout(300)->run(
|
|
'cd ' . escapeshellarg($this->clientPath) . ' && yarn install --ignore-engines 2>&1',
|
|
);
|
|
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg($this->clientPath));
|
|
$actions[] = 'Client dependencies geïnstalleerd';
|
|
}
|
|
|
|
$nitrotsDir = $this->clientPath . '/node_modules/@nitrots';
|
|
if (! is_dir($nitrotsDir)) {
|
|
@mkdir($nitrotsDir, 0755, true);
|
|
}
|
|
$linkPath = $nitrotsDir . '/nitro-renderer';
|
|
if (! is_link($linkPath)) {
|
|
if (is_dir($linkPath) || is_file($linkPath)) {
|
|
Process::timeout(5)->run('rm -rf ' . escapeshellarg($linkPath));
|
|
}
|
|
symlink($this->rendererPath, $linkPath);
|
|
$actions[] = 'Renderer symlink hersteld';
|
|
}
|
|
|
|
$viteConfig = $this->clientPath . '/vite.config.mjs';
|
|
if (! file_exists($viteConfig)) {
|
|
$actions[] = 'vite.config.mjs ontbreekt - aanmaken...';
|
|
$this->updateViteConfig();
|
|
$actions[] = 'vite.config.mjs aangemaakt';
|
|
}
|
|
|
|
$configResult = $this->repairConfigs();
|
|
if (! empty($configResult['actions'])) {
|
|
$actions = array_merge($actions, $configResult['actions']);
|
|
}
|
|
if (! empty($configResult['errors'])) {
|
|
$errors = array_merge($errors, $configResult['errors']);
|
|
}
|
|
|
|
$status = $this->getStatus();
|
|
if (! ($status['deployed'] ?? false)) {
|
|
$actions[] = 'Client niet gedeployed - deploying...';
|
|
$buildResult = $this->buildClient();
|
|
if ($buildResult['success']) {
|
|
$this->deployClient();
|
|
$this->generateConfigs();
|
|
$actions[] = 'Client gedeployed';
|
|
} else {
|
|
$errors[] = 'Build/deploy mislukt: ' . ($buildResult['error'] ?? 'Onbekend');
|
|
}
|
|
}
|
|
|
|
if ($errors !== []) {
|
|
Log::warning('[NitroUpdate] Repair completed with errors', ['errors' => $errors]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'actions' => $actions,
|
|
'errors' => $errors,
|
|
];
|
|
}
|
|
|
|
Log::info('[NitroUpdate] Repair completed successfully', ['actions' => $actions]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'actions' => $actions,
|
|
'message' => count($actions) . ' acties uitgevoerd',
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('[NitroUpdate] Repair exception', ['error' => $e->getMessage()]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'actions' => $actions,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
public function diagnose(): array
|
|
{
|
|
$diagnosis = [
|
|
'timestamp' => now()->toIso8601String(),
|
|
'checks' => [],
|
|
'issues' => [],
|
|
'recommendations' => [],
|
|
];
|
|
|
|
try {
|
|
$status = $this->getStatus();
|
|
|
|
$diagnosis['checks']['client_path_exists'] = is_dir($this->clientPath);
|
|
$diagnosis['checks']['renderer_path_exists'] = is_dir($this->rendererPath);
|
|
$diagnosis['checks']['webroot_exists'] = is_dir($this->clientWebRoot);
|
|
$diagnosis['checks']['client_installed'] = $status['client_installed'] ?? false;
|
|
$diagnosis['checks']['renderer_installed'] = $status['renderer_installed'] ?? false;
|
|
$diagnosis['checks']['build_exists'] = $status['build_exists'] ?? false;
|
|
$diagnosis['checks']['deployed'] = $status['deployed'] ?? false;
|
|
$diagnosis['checks']['client_node_modules'] = $status['client_node_modules'] ?? false;
|
|
$diagnosis['checks']['renderer_node_modules'] = $status['renderer_node_modules'] ?? false;
|
|
$diagnosis['checks']['nginx_config_valid'] = $status['nginx_config_valid'] ?? false;
|
|
$diagnosis['checks']['open_basedir_fixed'] = $status['open_basedir_fixed'] ?? false;
|
|
$diagnosis['checks']['client_path'] = $this->clientPath;
|
|
$diagnosis['checks']['renderer_path'] = $this->rendererPath;
|
|
$diagnosis['checks']['webroot'] = $this->clientWebRoot;
|
|
|
|
if (! is_dir($this->clientPath)) {
|
|
$diagnosis['issues'][] = 'Client map bestaat niet';
|
|
$diagnosis['recommendations'][] = 'Voer nitro:auto --full uit';
|
|
}
|
|
|
|
if (! is_dir($this->rendererPath)) {
|
|
$diagnosis['issues'][] = 'Renderer map bestaat niet';
|
|
$diagnosis['recommendations'][] = 'Voer nitro:auto --full uit';
|
|
}
|
|
|
|
if (! ($status['client_node_modules'] ?? false)) {
|
|
$diagnosis['issues'][] = 'Client node_modules ontbreken';
|
|
$diagnosis['recommendations'][] = 'Voer repair uit of nitro:auto --full';
|
|
}
|
|
|
|
if (! ($status['renderer_node_modules'] ?? false)) {
|
|
$diagnosis['issues'][] = 'Renderer node_modules ontbreken';
|
|
$diagnosis['recommendations'][] = 'Voer repair uit of nitro:auto --full';
|
|
}
|
|
|
|
if (! ($status['deployed'] ?? false)) {
|
|
$diagnosis['issues'][] = 'Client niet gedeployed';
|
|
$diagnosis['recommendations'][] = 'Voer nitro:auto --build-only uit';
|
|
}
|
|
|
|
if (! ($status['nginx_config_valid'] ?? false)) {
|
|
$diagnosis['issues'][] = 'Nginx configuratie niet gevonden';
|
|
$diagnosis['recommendations'][] = 'Controleer nginx configuratie';
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$diagnosis['error'] = $e->getMessage();
|
|
}
|
|
|
|
return $diagnosis;
|
|
}
|
|
}
|