Files
newznab-tmux/app/Console/Commands/UpdateNNTmux.php
T
2026-02-11 14:02:26 +01:00

386 lines
10 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Services\Tmux\Tmux;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Symfony\Component\Console\Helper\ProgressBar;
class UpdateNNTmux extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'nntmux:all
{--skip-git : Skip git operations}
{--skip-composer : Skip composer update}
{--skip-npm : Skip npm operations}
{--skip-db : Skip database migrations}
{--force : Force update even if up-to-date}';
/**
* The console command description.
*/
protected $description = 'Update NNTmux installation with improved performance and error handling';
/**
* @var bool Whether the app was in maintenance mode before we started
*/
private bool $wasInMaintenance = false;
/**
* @var bool Whether tmux was running before we started
*/
private bool $tmuxWasRunning = false;
/**
* @var ProgressBar Progress bar for tracking operations
*/
private ProgressBar $progressBar;
/**
* Execute the console command.
*/
public function handle(): int
{
$this->info('🚀 Starting NNTmux update process...');
// Initialize progress tracking
$totalSteps = $this->calculateTotalSteps();
$this->progressBar = $this->output->createProgressBar($totalSteps);
$this->progressBar->start();
try {
// Prepare environment
$this->prepareEnvironment();
// Execute update steps
$this->executeUpdateSteps();
// Finalize
$this->finalizeUpdate();
$this->progressBar->finish();
$this->newLine(2);
$this->info('✅ NNTmux update completed successfully!');
return Command::SUCCESS;
} catch (\Exception $e) {
$this->progressBar->finish();
$this->newLine(2);
$this->error('❌ Update failed: '.$e->getMessage());
$this->restoreEnvironment();
return Command::FAILURE;
}
}
/**
* Calculate total steps for progress tracking
*/
private function calculateTotalSteps(): int
{
$steps = 3; // prepare, finalize, cleanup
if (! $this->option('skip-git')) {
$steps++;
}
if (! $this->option('skip-composer')) {
$steps++;
}
if (! $this->option('skip-npm')) {
$steps += 2;
} // install + build
if (! $this->option('skip-db')) {
$steps++;
}
$steps++; // env merge
return $steps;
}
/**
* Prepare the environment for updates
*/
private function prepareEnvironment(): void
{
$this->info('🔧 Preparing environment...');
// Check if app is in maintenance mode
$this->wasInMaintenance = App::isDownForMaintenance();
if (! $this->wasInMaintenance) {
$this->call('down', [
'--render' => 'errors::maintenance',
'--retry' => 120,
'--secret' => config('app.key'),
]);
}
// Check if tmux is running
$tmux = new Tmux;
$this->tmuxWasRunning = $tmux->isRunning();
if ($this->tmuxWasRunning) {
$this->call('tmux:stop', ['--force' => true]);
}
$this->progressBar->advance();
}
/**
* Execute the main update steps
*/
private function executeUpdateSteps(): void
{
// Git operations
if (! $this->option('skip-git')) {
$this->performGitUpdate();
}
// Composer operations
if (! $this->option('skip-composer')) {
$this->performComposerUpdate();
}
// Database migrations
if (! $this->option('skip-db')) {
$this->performDatabaseUpdate();
}
// NPM operations
if (! $this->option('skip-npm')) {
$this->performNpmOperations();
}
// Clear caches and perform maintenance
$this->performMaintenanceTasks();
}
/**
* Perform git update with better error handling
*/
private function performGitUpdate(): void
{
$this->info('📥 Updating from git repository...');
$gitResult = $this->call('nntmux:git');
if ($gitResult !== 0) {
throw new \Exception('Git update failed');
}
$this->progressBar->advance();
}
/**
* Perform composer update with optimization
*/
private function performComposerUpdate(): void
{
$this->info('📦 Updating composer dependencies...');
$composerResult = $this->call('nntmux:composer');
if ($composerResult !== 0) {
throw new \Exception('Composer update failed');
}
$this->progressBar->advance();
}
/**
* Perform database updates
*/
private function performDatabaseUpdate(): void
{
$this->info('🗄️ Updating database...');
$dbResult = $this->call('nntmux:db');
if ($dbResult !== 0) {
throw new \Exception('Database update failed');
}
$this->progressBar->advance();
}
/**
* Perform NPM operations with parallel processing where possible
*/
private function performNpmOperations(): void
{
$this->info('📦 Installing npm packages...');
$process = Process::timeout(600)
->path(base_path())
->run('npm ci --silent');
if (! $process->successful()) {
// Fallback to npm install if ci fails
$process = Process::timeout(600)
->path(base_path())
->run('npm install --silent');
if (! $process->successful()) {
throw new \Exception('NPM install failed: '.$process->errorOutput());
}
}
$this->progressBar->advance();
// Build assets
$this->info('🔨 Building assets...');
$buildProcess = Process::timeout(600)
->path(base_path())
->run('npm run build');
if (! $buildProcess->successful()) {
throw new \Exception('Asset build failed: '.$buildProcess->errorOutput());
}
$this->progressBar->advance();
}
/**
* Perform maintenance tasks
*/
private function performMaintenanceTasks(): void
{
// Clear view cache
$this->info('🧹 Clearing view cache...');
$this->call('view:clear');
// Merge environment variables
$this->info('⚙️ Merging environment configuration...');
$this->mergeEnvironmentConfig();
$this->progressBar->advance();
}
/**
* Merge environment configuration with improved error handling
*/
private function mergeEnvironmentConfig(): void
{
try {
$envExamplePath = base_path('.env.example');
$envPath = base_path('.env');
if (! File::exists($envExamplePath)) {
$this->warn(' ⚠ .env.example not found, skipping environment merge');
return;
}
if (! File::exists($envPath)) {
$this->warn(' ⚠ .env not found, skipping environment merge');
return;
}
$envExampleVars = $this->parseEnvFile($envExamplePath);
$envVars = $this->parseEnvFile($envPath);
$missingKeys = array_diff_key($envExampleVars, $envVars);
if (empty($missingKeys)) {
$this->line(' ✓ No new environment variables to merge');
return;
}
$this->addMissingEnvVars($envPath, $missingKeys);
$this->line(' ✓ Merged '.count($missingKeys).' new environment variables');
} catch (\Exception $e) {
$this->warn(' ⚠ Environment merge failed: '.$e->getMessage());
}
}
/**
* Parse environment file into key-value pairs
*
* @return array<string, mixed>
*/
private function parseEnvFile(string $path): array
{
$content = File::get($path);
$vars = [];
foreach (preg_split("/\r\n|\n|\r/", $content) as $line) {
$line = trim($line);
if (empty($line) || str_starts_with($line, '#')) {
continue;
}
if (preg_match('/^([^=]+)=(.*)$/', $line, $matches)) {
$key = trim($matches[1]);
$value = $matches[2];
$vars[$key] = $value;
}
}
return $vars;
}
/**
* Add missing environment variables to .env file
*
* @param array<string, mixed> $missingKeys
*/
private function addMissingEnvVars(string $envPath, array $missingKeys): void
{
$content = File::get($envPath);
if (! str_ends_with($content, "\n")) {
$content .= "\n";
}
$content .= "\n# New settings added from .env.example on ".now()->toDateTimeString()."\n";
foreach ($missingKeys as $key => $value) {
$content .= "$key=$value\n";
}
File::put($envPath, $content);
}
/**
* Finalize the update process
*/
private function finalizeUpdate(): void
{
$this->info('🏁 Finalizing update...');
// Clear application caches
Cache::flush();
// Restore application state
$this->restoreEnvironment();
$this->progressBar->advance();
}
/**
* Restore the original environment state
*/
private function restoreEnvironment(): void
{
// Restore maintenance mode state
if (! $this->wasInMaintenance && App::isDownForMaintenance()) {
$this->call('up');
}
// Restore tmux state
if ($this->tmuxWasRunning) {
$this->call('tmux:start');
}
}
}