Passed
Push — master ( b8d851...d127a4 )
by Darko
10:26
created

TmuxTaskRunner   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 562
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 76
eloc 249
c 4
b 0
f 0
dl 0
loc 562
rs 2.32

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A runTask() 0 22 5
A buildCommand() 0 20 3
A buildSleepCommand() 0 10 2
A getRandomColor() 0 24 4
A disablePane() 0 6 1
A getLogFile() 0 17 3
A runIRCScraper() 0 15 2
A runMainNonSequential() 0 9 1
A runPaneTask() 0 13 1
A runBinariesUpdate() 0 29 4
A runReleasesUpdate() 0 15 2
A runMainBasic() 0 3 1
A runMainTask() 0 7 1
B runBackfill() 0 39 7
A runFixNamesTask() 0 28 4
A runMainSequential() 0 11 1
A runPostProcessAdditional() 0 15 2
A runNonAmazonTask() 0 32 5
C runRemoveCrapTask() 0 74 14
B runAmazonTask() 0 25 7
A loadCrapState() 0 10 3
A saveCrapState() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like TmuxTaskRunner often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TmuxTaskRunner, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Services\Tmux;
4
5
use App\Models\Settings;
6
use Blacklight\ColorCLI;
7
8
/**
9
 * Service for running tasks in tmux panes
10
 */
11
class TmuxTaskRunner
12
{
13
    protected TmuxPaneManager $paneManager;
14
15
    protected ColorCLI $colorCli;
16
17
    protected string $sessionName;
18
19
    public function __construct(string $sessionName)
20
    {
21
        $this->sessionName = $sessionName;
22
        $this->paneManager = new TmuxPaneManager($sessionName);
23
        $this->colorCli = new ColorCLI;
24
    }
25
26
    /**
27
     * Run a task in a specific pane
28
     */
29
    public function runTask(string $taskName, array $config): bool
30
    {
31
        $pane = $config['pane'] ?? null;
32
        $command = $config['command'] ?? null;
33
        $enabled = $config['enabled'] ?? true;
34
        $workAvailable = $config['work_available'] ?? true;
35
36
        if (! $pane || ! $command) {
37
            return false;
38
        }
39
40
        // Check if task is enabled and has work
41
        if (! $enabled) {
42
            return $this->disablePane($pane, $taskName, 'disabled in settings');
43
        }
44
45
        if (! $workAvailable) {
46
            return $this->disablePane($pane, $taskName, 'no work available');
47
        }
48
49
        // Respawn the pane with the command
50
        return $this->paneManager->respawnPane($pane, $command);
51
    }
52
53
    /**
54
     * Disable a pane with a message
55
     */
56
    protected function disablePane(string $pane, string $taskName, string $reason): bool
57
    {
58
        $color = $this->getRandomColor();
59
        $message = "echo \"\\033[38;5;{$color}m\\n{$taskName} has been disabled: {$reason}\"";
60
61
        return $this->paneManager->respawnPane($pane, $message, kill: true);
62
    }
63
64
    /**
65
     * Build a command with logging
66
     */
67
    public function buildCommand(string $baseCommand, array $options = []): string
68
    {
69
        $parts = [$baseCommand];
70
71
        // Add sleep timer at the end if specified
72
        if (isset($options['sleep'])) {
73
            $sleepCommand = $this->buildSleepCommand($options['sleep']);
74
            $parts[] = 'date +"%Y-%m-%d %T"';
75
            $parts[] = $sleepCommand;
76
        }
77
78
        // Add logging if enabled
79
        if (isset($options['log_pane'])) {
80
            $logFile = $this->getLogFile($options['log_pane']);
81
            $command = implode('; ', $parts);
82
83
            return "{$command} 2>&1 | tee -a {$logFile}";
84
        }
85
86
        return implode('; ', $parts);
87
    }
88
89
    /**
90
     * Build sleep command
91
     */
92
    protected function buildSleepCommand(int $seconds): string
93
    {
94
        $niceness = Settings::settingValue('niceness') ?? 2;
95
        $sleepScript = base_path('app/Services/Tmux/Scripts/showsleep.php');
96
97
        if (file_exists($sleepScript)) {
98
            return "nice -n{$niceness} php {$sleepScript} {$seconds}";
99
        }
100
101
        return "sleep {$seconds}";
102
    }
103
104
    /**
105
     * Get log file path for a pane
106
     */
107
    protected function getLogFile(string $paneName): string
108
    {
109
        $logsEnabled = (int) Settings::settingValue('write_logs') === 1;
110
111
        if (! $logsEnabled) {
112
            return '/dev/null';
113
        }
114
115
        $logDir = config('tmux.paths.logs', storage_path('logs/tmux'));
116
117
        if (! is_dir($logDir)) {
118
            mkdir($logDir, 0755, true);
119
        }
120
121
        $date = now()->format('Y_m_d');
122
123
        return "{$logDir}/{$paneName}-{$date}.log";
124
    }
125
126
    /**
127
     * Get a random color for terminal output
128
     */
129
    protected function getRandomColor(): int
130
    {
131
        $start = (int) Settings::settingValue('colors_start') ?? 0;
132
        $end = (int) Settings::settingValue('colors_end') ?? 255;
133
        $exclude = Settings::settingValue('colors_exc') ?? '';
134
135
        if (empty($exclude)) {
136
            return random_int($start, $end);
137
        }
138
139
        $exceptions = array_map('intval', explode(',', $exclude));
140
        sort($exceptions);
141
142
        $number = random_int($start, $end - count($exceptions));
143
144
        foreach ($exceptions as $exception) {
145
            if ($number >= $exception) {
146
                $number++;
147
            } else {
148
                break;
149
            }
150
        }
151
152
        return $number;
153
    }
154
155
    /**
156
     * Run the IRC scraper
157
     */
158
    public function runIRCScraper(array $config): bool
159
    {
160
        $runScraper = (int) ($config['constants']['run_ircscraper'] ?? 0);
161
        $pane = '3.0';
162
163
        if ($runScraper !== 1) {
164
            return $this->disablePane($pane, 'IRC Scraper', 'disabled in settings');
165
        }
166
167
        $niceness = Settings::settingValue('niceness') ?? 2;
168
        $artisan = base_path('artisan');
169
        $command = "nice -n{$niceness} php {$artisan} irc:scrape";
170
        $command = $this->buildCommand($command, ['log_pane' => 'scraper']);
171
172
        return $this->paneManager->respawnPane($pane, $command);
173
    }
174
175
    /**
176
     * Run binaries update
177
     */
178
    public function runBinariesUpdate(array $config): bool
179
    {
180
        $enabled = (int) ($config['settings']['binaries_run'] ?? 0);
181
        $killswitch = $config['killswitch']['pp'] ?? false;
182
        $pane = '0.1';
183
184
        if (! $enabled) {
185
            return $this->disablePane($pane, 'Update Binaries', 'disabled in settings');
186
        }
187
188
        if ($killswitch) {
189
            return $this->disablePane($pane, 'Update Binaries', 'postprocess kill limit exceeded');
190
        }
191
192
        $artisanCommand = match ((int) $enabled) {
193
            1 => 'multiprocessing:safe binaries',
194
            default => null,
195
        };
196
197
        if (! $artisanCommand) {
198
            return false;
199
        }
200
201
        $niceness = Settings::settingValue('niceness') ?? 2;
202
        $command = "nice -n{$niceness} ".PHP_BINARY." artisan {$artisanCommand}";
203
        $sleep = (int) ($config['settings']['bins_timer'] ?? 60);
204
        $command = $this->buildCommand($command, ['log_pane' => 'binaries', 'sleep' => $sleep]);
205
206
        return $this->paneManager->respawnPane($pane, $command);
207
    }
208
209
    /**
210
     * Run backfill
211
     */
212
    public function runBackfill(array $config): bool
213
    {
214
        $enabled = (int) ($config['settings']['backfill'] ?? 0);
215
        $collKillswitch = $config['killswitch']['coll'] ?? false;
216
        $ppKillswitch = $config['killswitch']['pp'] ?? false;
217
        $pane = '0.2';
218
219
        if (! $enabled) {
220
            return $this->disablePane($pane, 'Backfill', 'disabled in settings');
221
        }
222
223
        if ($collKillswitch || $ppKillswitch) {
224
            return $this->disablePane($pane, 'Backfill', 'kill limit exceeded');
225
        }
226
227
        $artisanCommand = match ((int) $enabled) {
228
            1 => 'multiprocessing:backfill',
229
            4 => 'multiprocessing:safe backfill',
230
            default => null,
231
        };
232
233
        if (! $artisanCommand) {
234
            return false;
235
        }
236
237
        // Calculate sleep time (progressive if enabled)
238
        $baseSleep = (int) ($config['settings']['back_timer'] ?? 600);
239
        $collections = (int) ($config['counts']['now']['collections_table'] ?? 0);
240
        $progressive = (int) ($config['settings']['progressive'] ?? 0);
241
242
        $sleep = ($progressive === 1 && floor($collections / 500) > $baseSleep)
243
            ? floor($collections / 500)
244
            : $baseSleep;
245
246
        $niceness = Settings::settingValue('niceness') ?? 2;
247
        $command = "nice -n{$niceness} ".PHP_BINARY." artisan {$artisanCommand}";
248
        $command = $this->buildCommand($command, ['log_pane' => 'backfill', 'sleep' => $sleep]);
249
250
        return $this->paneManager->respawnPane($pane, $command);
251
    }
252
253
    /**
254
     * Run releases update
255
     */
256
    public function runReleasesUpdate(array $config): bool
257
    {
258
        $enabled = (int) ($config['settings']['releases_run'] ?? 0);
259
        $pane = $config['pane'] ?? '0.3';
260
261
        if (! $enabled) {
262
            return $this->disablePane($pane, 'Update Releases', 'disabled in settings');
263
        }
264
265
        $niceness = Settings::settingValue('niceness') ?? 2;
266
        $command = "nice -n{$niceness} ".PHP_BINARY.' artisan multiprocessing:releases';
267
        $sleep = (int) ($config['settings']['rel_timer'] ?? 60);
268
        $command = $this->buildCommand($command, ['log_pane' => 'releases', 'sleep' => $sleep]);
269
270
        return $this->paneManager->respawnPane($pane, $command);
271
    }
272
273
    /**
274
     * Run a specific pane task based on task name
275
     *
276
     * @param  string  $taskName  The name of the task to run
277
     * @param  array  $config  Configuration for the task (target pane, etc.)
278
     * @param  array  $runVar  Runtime variables and settings
279
     * @return bool Success status
280
     */
281
    public function runPaneTask(string $taskName, array $config, array $runVar): bool
0 ignored issues
show
Unused Code introduced by
The parameter $config is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

281
    public function runPaneTask(string $taskName, /** @scrutinizer ignore-unused */ array $config, array $runVar): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
282
    {
283
        $sequential = (int) ($runVar['constants']['sequential'] ?? 0);
284
285
        return match ($taskName) {
286
            'main' => $this->runMainTask($sequential, $runVar),
287
            'fixnames' => $this->runFixNamesTask($runVar),
288
            'removecrap' => $this->runRemoveCrapTask($runVar),
289
            'ppadditional' => $this->runPostProcessAdditional($runVar),
290
            'nonamazon' => $this->runNonAmazonTask($runVar),
291
            'amazon' => $this->runAmazonTask($runVar),
292
            'scraper' => $this->runIRCScraper($runVar),
293
            default => false,
294
        };
295
    }
296
297
    /**
298
     * Run main task (varies by sequential mode)
299
     */
300
    protected function runMainTask(int $sequential, array $runVar): bool
301
    {
302
        return match ($sequential) {
303
            0 => $this->runMainNonSequential($runVar),
304
            1 => $this->runMainBasic($runVar),
305
            2 => $this->runMainSequential($runVar),
306
            default => false,
307
        };
308
    }
309
310
    /**
311
     * Run main non-sequential task (binaries, backfill, releases)
312
     */
313
    protected function runMainNonSequential(array $runVar): bool
314
    {
315
        // This runs in pane 0.1, 0.2, 0.3
316
        // For now, delegate to existing methods
317
        $this->runBinariesUpdate($runVar);
318
        $this->runBackfill($runVar);
319
        $this->runReleasesUpdate(array_merge($runVar, ['pane' => '0.3']));
320
321
        return true;
322
    }
323
324
    /**
325
     * Run main basic sequential task (just releases)
326
     */
327
    protected function runMainBasic(array $runVar): bool
328
    {
329
        return $this->runReleasesUpdate(array_merge($runVar, ['pane' => '0.1']));
330
    }
331
332
    /**
333
     * Run main full sequential task
334
     */
335
    protected function runMainSequential(array $runVar): bool
0 ignored issues
show
Unused Code introduced by
The parameter $runVar is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

335
    protected function runMainSequential(/** @scrutinizer ignore-unused */ array $runVar): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
336
    {
337
        // Full sequential mode - runs group:update-all for each group
338
        $pane = '0.1';
339
340
        $niceness = Settings::settingValue('niceness') ?? 2;
341
        $artisan = base_path('artisan');
342
        $command = "nice -n{$niceness} php {$artisan} group:update-all";
343
        $command = $this->buildCommand($command, ['log_pane' => 'sequential']);
344
345
        return $this->paneManager->respawnPane($pane, $command);
346
    }
347
348
    /**
349
     * Run fix release names task
350
     */
351
    protected function runFixNamesTask(array $runVar): bool
352
    {
353
        $enabled = (int) ($runVar['settings']['fix_names'] ?? 0);
354
        $work = (int) ($runVar['counts']['now']['processrenames'] ?? 0);
355
        $pane = '1.0';
356
357
        if ($enabled !== 1) {
358
            return $this->disablePane($pane, 'Fix Release Names', 'disabled in settings');
359
        }
360
361
        if ($work === 0) {
362
            return $this->disablePane($pane, 'Fix Release Names', 'no releases to process');
363
        }
364
365
        $artisan = base_path('artisan');
366
        $log = $this->getLogFile('fixnames');
367
368
        // Run multiple fix-names passes
369
        $commands = [];
370
        foreach ([3, 5, 7, 9, 11, 13, 15, 17, 19] as $level) {
371
            $commands[] = "php {$artisan} releases:fix-names {$level} --update --category=other --set-status --show 2>&1 | tee -a {$log}";
372
        }
373
374
        $sleep = (int) ($runVar['settings']['fix_timer'] ?? 300);
375
        $allCommands = implode('; ', $commands);
376
        $fullCommand = "{$allCommands}; date +'%Y-%m-%d %T'; sleep {$sleep}";
377
378
        return $this->paneManager->respawnPane($pane, $fullCommand);
379
    }
380
381
    /**
382
     * Run remove crap releases task
383
     */
384
    protected function runRemoveCrapTask(array $runVar): bool
385
    {
386
        $option = $runVar['settings']['fix_crap_opt'] ?? 'Disabled';
387
        $pane = '1.1';
388
389
        // Handle disabled state
390
        if ($option === 'Disabled' || $option === 0 || $option === '0') {
391
            return $this->disablePane($pane, 'Remove Crap', 'disabled in settings');
392
        }
393
394
        $niceness = Settings::settingValue('niceness') ?? 2;
395
        $artisan = base_path('artisan');
396
        $sleep = (int) ($runVar['settings']['crap_timer'] ?? 300);
397
398
        // Handle 'All' mode - run all types with 2 hour time limit
399
        if ($option === 'All') {
400
            $command = "nice -n{$niceness} php {$artisan} releases:remove-crap --time=2 --delete";
401
            $command = $this->buildCommand($command, ['log_pane' => 'removecrap', 'sleep' => $sleep]);
402
403
            return $this->paneManager->respawnPane($pane, $command);
404
        }
405
406
        // Handle 'Custom' mode - run all selected types sequentially
407
        if ($option === 'Custom') {
408
            $selectedTypes = $runVar['settings']['fix_crap'] ?? '';
409
410
            // Convert numeric 0 or empty values to empty string
411
            if (empty($selectedTypes) || $selectedTypes === 0 || $selectedTypes === '0') {
412
                return $this->disablePane($pane, 'Remove Crap', 'no crap types selected');
413
            }
414
415
            $types = is_array($selectedTypes) ? $selectedTypes : explode(',', $selectedTypes);
416
417
            // Trim whitespace and filter out empty values and '0'
418
            $types = array_map('trim', $types);
419
            $types = array_filter($types, fn ($type) => ! empty($type) && $type !== '0');
420
421
            // Re-index array to ensure sequential keys
422
            $types = array_values($types);
423
424
            if (empty($types)) {
425
                return $this->disablePane($pane, 'Remove Crap', 'no crap types selected');
426
            }
427
428
            // Get state to determine if this is first run
429
            $stateFile = storage_path('tmux/removecrap_state.json');
430
            $state = $this->loadCrapState($stateFile);
431
            $isFirstRun = $state['first_run'] ?? true;
432
433
            // Determine time limit: full on first run, 4 hours otherwise
434
            $time = $isFirstRun ? 'full' : '4';
0 ignored issues
show
introduced by
The condition $isFirstRun is always true.
Loading history...
435
436
            // Build commands for all enabled types to run sequentially
437
            $log = $this->getLogFile('removecrap');
438
            $commands = [];
439
            foreach ($types as $type) {
440
                $commands[] = "echo \"\\nRunning removeCrapReleases for {$type}\"; nice -n{$niceness} php {$artisan} releases:remove-crap --type={$type} --time={$time} --delete 2>&1 | tee -a {$log}";
441
            }
442
443
            // Join all commands with semicolons and add final timestamp and sleep
444
            $allCommands = implode('; ', $commands);
445
            $fullCommand = "{$allCommands}; date +'%Y-%m-%d %T'; sleep {$sleep}";
446
447
            // Mark that we're not on the first run anymore for next cycle
448
            $this->saveCrapState($stateFile, [
449
                'first_run' => false,
450
                'types' => $types,
451
            ]);
452
453
            return $this->paneManager->respawnPane($pane, $fullCommand);
454
        }
455
456
        // Default fallback - disabled
457
        return $this->disablePane($pane, 'Remove Crap', 'invalid configuration');
458
    }
459
460
    /**
461
     * Load crap removal state
462
     */
463
    protected function loadCrapState(string $file): array
464
    {
465
        if (! file_exists($file)) {
466
            return ['first_run' => true];
467
        }
468
469
        $content = file_get_contents($file);
470
        $state = json_decode($content, true);
471
472
        return $state ?: ['first_run' => true];
473
    }
474
475
    /**
476
     * Save crap removal state
477
     */
478
    protected function saveCrapState(string $file, array $state): void
479
    {
480
        $dir = dirname($file);
481
        if (! is_dir($dir)) {
482
            mkdir($dir, 0755, true);
483
        }
484
485
        file_put_contents($file, json_encode($state, JSON_PRETTY_PRINT));
486
    }
487
488
    /**
489
     * Run post-process additional task
490
     */
491
    protected function runPostProcessAdditional(array $runVar): bool
492
    {
493
        $enabled = (int) ($runVar['settings']['post'] ?? 0);
494
        $pane = '2.0';
495
496
        if ($enabled !== 1) {
497
            return $this->disablePane($pane, 'Post-process Additional', 'disabled in settings');
498
        }
499
500
        $niceness = Settings::settingValue('niceness') ?? 2;
501
        $command = "nice -n{$niceness} ".PHP_BINARY.' artisan update:postprocess additional true';
502
        $sleep = (int) ($runVar['settings']['post_timer'] ?? 300);
503
        $command = $this->buildCommand($command, ['log_pane' => 'post_additional', 'sleep' => $sleep]);
504
505
        return $this->paneManager->respawnPane($pane, $command);
506
    }
507
508
    /**
509
     * Run non-Amazon post-processing (TV, Movies, Anime)
510
     */
511
    protected function runNonAmazonTask(array $runVar): bool
512
    {
513
        $enabled = (int) ($runVar['settings']['post_non'] ?? 0);
514
        $pane = '2.1';
515
516
        if ($enabled !== 1) {
517
            return $this->disablePane($pane, 'Post-process Non-Amazon', 'disabled in settings');
518
        }
519
520
        $hasWork = (int) ($runVar['counts']['now']['processmovies'] ?? 0) > 0
521
            || (int) ($runVar['counts']['now']['processtv'] ?? 0) > 0
522
            || (int) ($runVar['counts']['now']['processanime'] ?? 0) > 0;
523
524
        if (! $hasWork) {
525
            return $this->disablePane($pane, 'Post-process Non-Amazon', 'no movies/tv/anime to process');
526
        }
527
528
        $niceness = Settings::settingValue('niceness') ?? 2;
529
        $log = $this->getLogFile('post_non');
530
531
        $artisan = PHP_BINARY.' artisan';
532
        $commands = [
533
            "{$artisan} update:postprocess tv true 2>&1 | tee -a {$log}",
534
            "{$artisan} update:postprocess movies true 2>&1 | tee -a {$log}",
535
            "{$artisan} update:postprocess anime true 2>&1 | tee -a {$log}",
536
        ];
537
538
        $sleep = (int) ($runVar['settings']['post_timer_non'] ?? 300);
539
        $allCommands = "nice -n{$niceness} ".implode('; nice -n{$niceness} ', $commands);
540
        $fullCommand = "{$allCommands}; date +'%Y-%m-%d %T'; sleep {$sleep}";
541
542
        return $this->paneManager->respawnPane($pane, $fullCommand);
543
    }
544
545
    /**
546
     * Run Amazon post-processing (Books, Music, Games, Console, XXX)
547
     */
548
    protected function runAmazonTask(array $runVar): bool
549
    {
550
        $enabled = (int) ($runVar['settings']['post_amazon'] ?? 0);
551
        $pane = '2.2';
552
553
        if ($enabled !== 1) {
554
            return $this->disablePane($pane, 'Post-process Amazon', 'disabled in settings');
555
        }
556
557
        $hasWork = (int) ($runVar['counts']['now']['processmusic'] ?? 0) > 0
558
            || (int) ($runVar['counts']['now']['processbooks'] ?? 0) > 0
559
            || (int) ($runVar['counts']['now']['processconsole'] ?? 0) > 0
560
            || (int) ($runVar['counts']['now']['processgames'] ?? 0) > 0
561
            || (int) ($runVar['counts']['now']['processxxx'] ?? 0) > 0;
562
563
        if (! $hasWork) {
564
            return $this->disablePane($pane, 'Post-process Amazon', 'no music/books/games to process');
565
        }
566
567
        $niceness = Settings::settingValue('niceness') ?? 2;
568
        $command = "nice -n{$niceness} ".PHP_BINARY.' artisan update:postprocess amazon true';
569
        $sleep = (int) ($runVar['settings']['post_timer_amazon'] ?? 300);
570
        $command = $this->buildCommand($command, ['log_pane' => 'post_amazon', 'sleep' => $sleep]);
571
572
        return $this->paneManager->respawnPane($pane, $command);
573
    }
574
}
575