Passed
Push — master ( 0ddaf0...97ef82 )
by Darko
07:24
created

UpdateNNTmux::handle()   A

Complexity

Conditions 2
Paths 7

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 18
dl 0
loc 32
rs 9.6666
c 2
b 1
f 0
cc 2
nc 7
nop 0
1
<?php
2
3
namespace App\Console\Commands;
4
5
use Blacklight\Tmux;
6
use Illuminate\Console\Command;
7
use Illuminate\Support\Facades\App;
8
use Illuminate\Support\Facades\Cache;
9
use Illuminate\Support\Facades\File;
10
use Illuminate\Support\Facades\Process;
11
use Symfony\Component\Console\Helper\ProgressBar;
12
use Ytake\LaravelSmarty\Smarty;
13
14
class UpdateNNTmux extends Command
15
{
16
    /**
17
     * The name and signature of the console command.
18
     */
19
    protected $signature = 'nntmux:all
20
                            {--skip-git : Skip git operations}
21
                            {--skip-composer : Skip composer update}
22
                            {--skip-npm : Skip npm operations}
23
                            {--skip-db : Skip database migrations}
24
                            {--force : Force update even if up-to-date}';
25
26
    /**
27
     * The console command description.
28
     */
29
    protected $description = 'Update NNTmux installation with improved performance and error handling';
30
31
    /**
32
     * @var bool Whether the app was in maintenance mode before we started
33
     */
34
    private bool $wasInMaintenance = false;
35
36
    /**
37
     * @var bool Whether tmux was running before we started
38
     */
39
    private bool $tmuxWasRunning = false;
40
41
    /**
42
     * @var ProgressBar Progress bar for tracking operations
43
     */
44
    private ProgressBar $progressBar;
45
46
    /**
47
     * Execute the console command.
48
     */
49
    public function handle(): int
50
    {
51
        $this->info('🚀 Starting NNTmux update process...');
52
53
        // Initialize progress tracking
54
        $totalSteps = $this->calculateTotalSteps();
55
        $this->progressBar = $this->output->createProgressBar($totalSteps);
56
        $this->progressBar->start();
57
58
        try {
59
            // Prepare environment
60
            $this->prepareEnvironment();
61
62
            // Execute update steps
63
            $this->executeUpdateSteps();
64
65
            // Finalize
66
            $this->finalizeUpdate();
67
68
            $this->progressBar->finish();
69
            $this->newLine(2);
70
            $this->info('✅ NNTmux update completed successfully!');
71
72
            return Command::SUCCESS;
73
74
        } catch (\Exception $e) {
75
            $this->progressBar->finish();
76
            $this->newLine(2);
77
            $this->error('❌ Update failed: ' . $e->getMessage());
78
            $this->restoreEnvironment();
79
80
            return Command::FAILURE;
81
        }
82
    }
83
84
    /**
85
     * Calculate total steps for progress tracking
86
     */
87
    private function calculateTotalSteps(): int
88
    {
89
        $steps = 3; // prepare, finalize, cleanup
90
91
        if (!$this->option('skip-git')) $steps++;
92
        if (!$this->option('skip-composer')) $steps++;
93
        if (!$this->option('skip-npm')) $steps += 2; // install + build
94
        if (!$this->option('skip-db')) $steps++;
95
96
        $steps++; // smarty cache clear
97
        $steps++; // env merge
98
99
        return $steps;
100
    }
101
102
    /**
103
     * Prepare the environment for updates
104
     */
105
    private function prepareEnvironment(): void
106
    {
107
        $this->info('🔧 Preparing environment...');
108
109
        // Check if app is in maintenance mode
110
        $this->wasInMaintenance = App::isDownForMaintenance();
111
        if (!$this->wasInMaintenance) {
112
            $this->call('down', [
113
                '--render' => 'errors::maintenance',
114
                '--retry' => 120,
115
                '--secret' => config('app.key')
116
            ]);
117
        }
118
119
        // Check if tmux is running
120
        $tmux = new Tmux();
121
        $this->tmuxWasRunning = $tmux->isRunning();
122
        if ($this->tmuxWasRunning) {
123
            $this->call('tmux-ui:stop', ['--kill' => true]);
124
        }
125
126
        $this->progressBar->advance();
127
    }
128
129
    /**
130
     * Execute the main update steps
131
     */
132
    private function executeUpdateSteps(): void
133
    {
134
        // Git operations
135
        if (!$this->option('skip-git')) {
136
            $this->performGitUpdate();
137
        }
138
139
        // Composer operations
140
        if (!$this->option('skip-composer')) {
141
            $this->performComposerUpdate();
142
        }
143
144
        // Database migrations
145
        if (!$this->option('skip-db')) {
146
            $this->performDatabaseUpdate();
147
        }
148
149
        // NPM operations
150
        if (!$this->option('skip-npm')) {
151
            $this->performNpmOperations();
152
        }
153
154
        // Clear caches and perform maintenance
155
        $this->performMaintenanceTasks();
156
    }
157
158
    /**
159
     * Perform git update with better error handling
160
     */
161
    private function performGitUpdate(): void
162
    {
163
        $this->info('📥 Updating from git repository...');
164
165
        $gitResult = $this->call('nntmux:git');
166
167
        if ($gitResult !== 0) {
168
            throw new \Exception('Git update failed');
169
        }
170
171
        $this->progressBar->advance();
172
    }
173
174
    /**
175
     * Perform composer update with optimization
176
     */
177
    private function performComposerUpdate(): void
178
    {
179
        $this->info('📦 Updating composer dependencies...');
180
181
        $composerResult = $this->call('nntmux:composer');
182
183
        if ($composerResult !== 0) {
184
            throw new \Exception('Composer update failed');
185
        }
186
187
        $this->progressBar->advance();
188
    }
189
190
    /**
191
     * Perform database updates
192
     */
193
    private function performDatabaseUpdate(): void
194
    {
195
        $this->info('🗄️ Updating database...');
196
197
        $dbResult = $this->call('nntmux:db');
198
199
        if ($dbResult !== 0) {
200
            throw new \Exception('Database update failed');
201
        }
202
203
        $this->progressBar->advance();
204
    }
205
206
    /**
207
     * Perform NPM operations with parallel processing where possible
208
     */
209
    private function performNpmOperations(): void
210
    {
211
        // Check if package.json has changed
212
        $packageLockExists = File::exists(base_path('package-lock.json'));
213
        $shouldInstall = !$packageLockExists || $this->option('force');
214
215
        if ($shouldInstall) {
216
            $this->info('📦 Installing npm packages...');
217
218
            $process = Process::timeout(600)
219
                ->path(base_path())
220
                ->run('npm ci --silent');
221
222
            if (!$process->successful()) {
223
                // Fallback to npm install if ci fails
224
                $process = Process::timeout(600)
225
                    ->path(base_path())
226
                    ->run('npm install --silent');
227
228
                if (!$process->successful()) {
229
                    throw new \Exception('NPM install failed: ' . $process->errorOutput());
230
                }
231
            }
232
        }
233
234
        $this->progressBar->advance();
235
236
        // Build assets
237
        $this->info('🔨 Building assets...');
238
239
        $buildProcess = Process::timeout(600)
240
            ->path(base_path())
241
            ->run('npm run build');
242
243
        if (!$buildProcess->successful()) {
244
            throw new \Exception('Asset build failed: ' . $buildProcess->errorOutput());
245
        }
246
247
        $this->progressBar->advance();
248
    }
249
250
    /**
251
     * Perform maintenance tasks
252
     */
253
    private function performMaintenanceTasks(): void
254
    {
255
        // Clear Smarty cache
256
        $this->info('🧹 Clearing Smarty cache...');
257
        $this->clearSmartyCache();
258
        $this->progressBar->advance();
259
260
        // Merge environment variables
261
        $this->info('⚙️ Merging environment configuration...');
262
        $this->mergeEnvironmentConfig();
263
        $this->progressBar->advance();
264
    }
265
266
    /**
267
     * Clear Smarty compiled templates
268
     */
269
    private function clearSmartyCache(): void
270
    {
271
        try {
272
            $smarty = new Smarty();
273
            $smarty->setCompileDir(config('ytake-laravel-smarty.compile_path'));
274
275
            if ($smarty->clearCompiledTemplate()) {
276
                $this->line('  ✓ Smarty compiled template cache cleared');
277
            } else {
278
                $this->warn('  ⚠ Could not clear Smarty cache automatically');
279
                $this->line('    Please clear manually: ' . config('ytake-laravel-smarty.compile_path'));
280
            }
281
        } catch (\Exception $e) {
282
            $this->warn('  ⚠ Smarty cache clear failed: ' . $e->getMessage());
283
        }
284
    }
285
286
    /**
287
     * Merge environment configuration with improved error handling
288
     */
289
    private function mergeEnvironmentConfig(): void
290
    {
291
        try {
292
            $envExamplePath = base_path('.env.example');
293
            $envPath = base_path('.env');
294
295
            if (!File::exists($envExamplePath)) {
296
                $this->warn('  ⚠ .env.example not found, skipping environment merge');
297
                return;
298
            }
299
300
            if (!File::exists($envPath)) {
301
                $this->warn('  ⚠ .env not found, skipping environment merge');
302
                return;
303
            }
304
305
            $envExampleVars = $this->parseEnvFile($envExamplePath);
306
            $envVars = $this->parseEnvFile($envPath);
307
308
            $missingKeys = array_diff_key($envExampleVars, $envVars);
309
310
            if (empty($missingKeys)) {
311
                $this->line('  ✓ No new environment variables to merge');
312
                return;
313
            }
314
315
            $this->addMissingEnvVars($envPath, $missingKeys);
316
            $this->line("  ✓ Merged " . count($missingKeys) . " new environment variables");
317
318
        } catch (\Exception $e) {
319
            $this->warn('  ⚠ Environment merge failed: ' . $e->getMessage());
320
        }
321
    }
322
323
    /**
324
     * Parse environment file into key-value pairs
325
     */
326
    private function parseEnvFile(string $path): array
327
    {
328
        $content = File::get($path);
329
        $vars = [];
330
331
        foreach (preg_split("/\r\n|\n|\r/", $content) as $line) {
332
            $line = trim($line);
333
334
            if (empty($line) || str_starts_with($line, '#')) {
335
                continue;
336
            }
337
338
            if (preg_match('/^([^=]+)=(.*)$/', $line, $matches)) {
339
                $key = trim($matches[1]);
340
                $value = $matches[2];
341
                $vars[$key] = $value;
342
            }
343
        }
344
345
        return $vars;
346
    }
347
348
    /**
349
     * Add missing environment variables to .env file
350
     */
351
    private function addMissingEnvVars(string $envPath, array $missingKeys): void
352
    {
353
        $content = File::get($envPath);
354
355
        if (!str_ends_with($content, "\n")) {
356
            $content .= "\n";
357
        }
358
359
        $content .= "\n# New settings added from .env.example on " . now()->toDateTimeString() . "\n";
360
361
        foreach ($missingKeys as $key => $value) {
362
            $content .= "$key=$value\n";
363
        }
364
365
        File::put($envPath, $content);
366
    }
367
368
    /**
369
     * Finalize the update process
370
     */
371
    private function finalizeUpdate(): void
372
    {
373
        $this->info('🏁 Finalizing update...');
374
375
        // Clear application caches
376
        Cache::flush();
377
378
        // Restore application state
379
        $this->restoreEnvironment();
380
381
        $this->progressBar->advance();
382
    }
383
384
    /**
385
     * Restore the original environment state
386
     */
387
    private function restoreEnvironment(): void
388
    {
389
        // Restore maintenance mode state
390
        if (!$this->wasInMaintenance && App::isDownForMaintenance()) {
391
            $this->call('up');
392
        }
393
394
        // Restore tmux state
395
        if ($this->tmuxWasRunning) {
396
            $this->call('tmux-ui:start');
397
        }
398
    }
399
}
400