mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
425 lines
11 KiB
PHP
425 lines
11 KiB
PHP
<?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');
|
||
}
|
||
}
|
||
}
|