Files
newznab-tmux/app/Console/Commands/UpdateNNTmux.php
T
2026-06-24 20:33:02 +02:00

425 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
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 bool Whether composer dependencies were refreshed during this update
*/
private bool $composerWasRun = 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->composerWasRun = true;
$this->progressBar->advance();
}
/**
* Perform database updates
*/
private function performDatabaseUpdate(): void
{
$this->info('🗄️ Updating database...');
$dbResult = $this->composerWasRun
? $this->runFreshArtisanCommand(['nntmux:db'])
: $this->call('nntmux:db');
if ($dbResult !== 0) {
throw new \Exception('Database update failed');
}
$this->progressBar->advance();
}
/**
* Run an Artisan command in a new PHP process so updated dependencies are loaded consistently.
*
* @param list<string> $arguments
*/
private function runFreshArtisanCommand(array $arguments): int
{
$this->line(' Restarting Artisan for updated dependencies...');
$result = Process::timeout(3600)
->path(base_path())
->run(array_merge([PHP_BINARY, 'artisan'], $arguments), function (string $type, string $output): void {
$this->output->write($output);
});
if (! $result->successful() && $result->errorOutput() !== '') {
$this->error($result->errorOutput());
}
return $result->exitCode();
}
/**
* 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');
$this->info('Clearing route cache...');
$this->call('route:clear');
$this->info('Cache cleared successfully');
$this->info('Caching routes...');
$this->call('route:cache');
$this->info('Routes cached successfully');
// 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');
}
}
}