Processes::handleWorkerAction()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 35
rs 9.568
c 0
b 0
f 0
cc 4
nc 4
nop 9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/*
4
 * MikoPBX - free phone system for small business
5
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along with this program.
18
 * If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
namespace MikoPBX\Core\System;
22
23
use InvalidArgumentException;
24
use MikoPBX\Core\Workers\Cron\WorkerSafeScriptsCore;
25
use Phalcon\Di\Di;
26
use RuntimeException;
27
use Throwable;
28
29
/**
30
 * Class Processes
31
 *
32
 * Manages system and PHP processes with enhanced safety and reliability.
33
 *
34
 * @package MikoPBX\Core\System
35
 */
36
class Processes
37
{
38
    /**
39
     * Default timeout for background process execution in seconds
40
    */
41
    private const DEFAULT_BG_TIMEOUT = 4;
42
43
    /**
44
     * Default number of attempts for safe daemon start
45
     */
46
    private const DEFAULT_START_ATTEMPTS = 20;
47
48
    /**
49
     * Default timeout between attempts in microseconds
50
     */
51
    private const DEFAULT_ATTEMPT_TIMEOUT = 1000000;
52
53
    /**
54
     * Default output file for process logs
55
     */
56
    private const DEFAULT_OUTPUT_FILE = '/dev/null';
57
58
    /**
59
     * Directory for temporary shell scripts
60
     */
61
    private const TEMP_SCRIPTS_DIR = '/tmp';
62
63
    /**
64
     * Valid process actions
65
     */
66
    private const VALID_ACTIONS = ['start', 'stop', 'restart', 'status'];
67
68
69
    /**
70
     * Directory for process lock files
71
     */
72
    private const LOCK_FILE_DIR = '/var/run/php-workers';
73
    private const PID_FILE_SUFFIX = '.pid';
74
75
    /**
76
     * Cleans up stale PID files in the workers directory
77
     * 
78
     * @return void
79
     */
80
    private static function cleanupStalePidFiles(): void
81
    {
82
        if (!is_dir(self::LOCK_FILE_DIR)) {
83
            return;
84
        }
85
86
        $files = glob(self::LOCK_FILE_DIR . '/*' . self::PID_FILE_SUFFIX . '*');
87
        foreach ($files as $file) {
88
            // Read PID from file
89
            $pid = @file_get_contents($file);
90
            if ($pid === false) {
91
                @unlink($file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

91
                /** @scrutinizer ignore-unhandled */ @unlink($file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
92
                continue;
93
            }
94
95
            // Check if process is still running
96
            if (!self::isProcessRunning($pid)) {
97
                @unlink($file);
98
            }
99
        }
100
    }
101
102
    /**
103
     * Gets the path to the PID file for a given class name
104
     *
105
     * @param string $className The class name
106
     * @param int $instanceNum The instance number (for workers with multiple instances)
107
     * @return string Path to the PID file
108
     */
109
    public static function getPidFilePath(string $className, int $instanceNum = 1): string
110
    {
111
        // Ensure PID directory exists
112
        Util::mwMkdir(self::LOCK_FILE_DIR, true);
113
        $name = str_replace('\\', '-', $className);
114
        if ($instanceNum > 1) {
115
            return self::LOCK_FILE_DIR . "/$name-$instanceNum" . self::PID_FILE_SUFFIX;
116
        }
117
        return self::LOCK_FILE_DIR . "/$name" . self::PID_FILE_SUFFIX;
118
    }
119
120
    /**
121
     * Gets the path to the PID file for a forked process
122
     *
123
     * @param string $className The class name
124
     * @param int $pid Process ID
125
     * @param int $instanceNum The instance number
126
     * @return string Path to the forked process PID file
127
     */
128
    public static function getForkedPidFilePath(string $className, int $pid, int $instanceNum = 1): string
129
    {
130
        $basePidFile = self::getPidFilePath($className, $instanceNum);
131
        return sprintf('%s.%d', $basePidFile, $pid);
132
    }
133
134
    /**
135
     * Saves a PID to a file with proper locking
136
     *
137
     * @param string $pidFile Path to the PID file
138
     * @param int $pid Process ID to save
139
     * @throws RuntimeException If unable to write PID file
140
     */
141
    public static function savePidFile(string $pidFile, int $pid): void
142
    {
143
        try {
144
            $pidDir = dirname($pidFile);
145
146
            // Ensure PID directory exists
147
            if (!is_dir($pidDir) && !mkdir($pidDir, 0755, true)) {
148
                throw new RuntimeException("Could not create PID directory: $pidDir");
149
            }
150
151
            // Use exclusive file creation to avoid race conditions
152
            $handle = fopen($pidFile, 'c+');
153
            if ($handle === false) {
154
                throw new RuntimeException("Could not open PID file: $pidFile");
155
            }
156
157
            if (!flock($handle, LOCK_EX | LOCK_NB)) {
158
                fclose($handle);
159
                throw new RuntimeException("Could not acquire lock on PID file: $pidFile");
160
            }
161
162
            // Write PID to file
163
            if (ftruncate($handle, 0) === false || fwrite($handle, (string)$pid) === false) {
164
                flock($handle, LOCK_UN);
165
                fclose($handle);
166
                throw new RuntimeException("Could not write to PID file: $pidFile");
167
            }
168
169
            flock($handle, LOCK_UN);
170
            fclose($handle);
171
172
        } catch (Throwable $e) {
173
            SystemMessages::sysLogMsg(
174
                __CLASS__,
175
                "Failed to save PID file: " . $e->getMessage(),
176
                LOG_WARNING
177
            );
178
            throw new RuntimeException('PID file operation failed', 0, $e);
179
        }
180
    }
181
182
    /**
183
     * Validates process action.
184
     *
185
     * @param string $action Action to validate
186
     * @return bool
187
     */
188
    private static function isValidAction(string $action): bool
189
    {
190
        return in_array($action, self::VALID_ACTIONS, true);
191
    }
192
193
    /**
194
     * Checks if a process is running.
195
     *
196
     * @param string $processIdOrName Process ID or name
197
     * @return bool
198
     */
199
    public static function isProcessRunning(string $processIdOrName): bool
200
    {
201
        if (is_numeric($processIdOrName)) {
202
            // Check by PID
203
            return file_exists("/proc/$processIdOrName");
204
        }
205
        // Check by process name
206
        return self::getPidOfProcess($processIdOrName) !== '';
207
    }
208
209
    /**
210
     * Safely terminates a process with timeout.
211
     *
212
     * @param string $pid Process ID to terminate
213
     * @param int $timeout Timeout in seconds
214
     * @return bool Success status
215
     */
216
    private static function safeTerminateProcess(string $pid, int $timeout = 10): bool
217
    {
218
        if (empty($pid)) {
219
            return false;
220
        }
221
222
        $kill = Util::which('kill');
223
224
        // First try SIGTERM
225
        self::mwExec("$kill -SIGTERM $pid");
226
227
        // Wait for process to terminate
228
        $startTime = time();
229
        while (time() - $startTime < $timeout) {
230
            if (!self::isProcessRunning($pid)) {
231
                return true;
232
            }
233
            usleep(100000); // 100ms
234
        }
235
236
        // Force kill if still running
237
        self::mwExec("$kill -9 $pid");
238
        return !self::isProcessRunning($pid);
239
    }
240
241
242
    /**
243
     * Waits for a process to start.
244
     *
245
     * @param string $procName Process name
246
     * @param string $excludeName Process name to exclude
247
     * @param int $attempts Maximum number of attempts
248
     * @param int $timeout Timeout between attempts
249
     * @return string Process ID or empty string
250
     */
251
    private static function waitForProcessStart(
252
        string $procName,
253
        string $excludeName,
254
        int $attempts,
255
        int $timeout
256
    ): string {
257
        $attempt = 1;
258
        while ($attempt < $attempts) {
259
            $pid = self::getPidOfProcess($procName, $excludeName);
260
            if (!empty($pid)) {
261
                return $pid;
262
            }
263
            usleep($timeout);
264
            $attempt++;
265
        }
266
        return '';
267
    }
268
269
    /**
270
     * Kills a process/daemon by name.
271
     *
272
     * @param string $procName The name of the process/daemon to kill.
273
     * @return int|null The return code of the execution.
274
     */
275
    public static function killByName(string $procName): ?int
276
    {
277
        if (empty(trim($procName))) {
278
            throw new InvalidArgumentException('Process name cannot be empty');
279
        }
280
281
        $killallPath = Util::which('killall');
282
        return self::mwExec($killallPath . ' ' . escapeshellarg($procName));
283
    }
284
285
    /**
286
     * Executes a command using exec().
287
     *
288
     * @param string $command The command to execute.
289
     * @param array|null $outArr Reference to array for command output.
290
     * @param int|null $retVal Reference for return value.
291
     * @return int The return value of the execution.
292
     * @throws RuntimeException If command is empty.
293
     */
294
    public static function mwExec(string $command, ?array &$outArr = null, ?int &$retVal = null): int
295
    {
296
        if (empty(trim($command))) {
297
            throw new RuntimeException('Empty command provided to mwExec');
298
        }
299
300
        $retVal = 0;
301
        $outArr = [];
302
        $di = Di::getDefault();
303
304
        if ($di !== null && $di->getShared('config')->path('core.debugMode')) {
305
            echo "mwExec(): $command\n";
306
        } else {
307
            exec("$command 2>&1", $outArr, $retVal);
308
        }
309
        return $retVal;
310
    }
311
312
    /**
313
     * Executes a command as a background process with timeout.
314
     *
315
     * @param string $command The command to execute.
316
     * @param int $timeout The timeout value in seconds.
317
     * @param string $logName The name of the log file.
318
     */
319
    public static function mwExecBgWithTimeout(
320
        string $command,
321
        int $timeout = self::DEFAULT_BG_TIMEOUT,
0 ignored issues
show
Unused Code introduced by
The parameter $timeout 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

321
        /** @scrutinizer ignore-unused */ int $timeout = self::DEFAULT_BG_TIMEOUT,

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...
322
        string $logName = self::DEFAULT_OUTPUT_FILE
323
    ): void {
324
        $di = Di::getDefault();
325
326
        if ($di !== null && $di->getShared('config')->path('core.debugMode')) {
327
            echo "mwExecBg(): $command\n";
328
            return;
329
        }
330
331
        $nohup = Util::which('nohup');
332
        $timeout = Util::which('timeout');
333
        exec("$nohup $timeout $timeout $command > $logName 2>&1 &");
334
    }
335
336
    /**
337
     * Executes multiple commands sequentially.
338
     *
339
     * @param array $arrCmds The array of commands to execute.
340
     * @param array|null $out Reference to array for output.
341
     * @param string $logname The log file name.
342
     */
343
    public static function mwExecCommands(array $arrCmds, ?array &$out = [], string $logname = ''): void
344
    {
345
        $out = [];
346
        foreach ($arrCmds as $cmd) {
347
            $out[] = "$cmd;";
348
            $outCmd = [];
349
            self::mwExec($cmd, $outCmd);
350
            $out = array_merge($out, $outCmd);
0 ignored issues
show
Bug introduced by
It seems like $outCmd can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

350
            $out = array_merge($out, /** @scrutinizer ignore-type */ $outCmd);
Loading history...
351
        }
352
353
        if ($logname !== '') {
354
            $result = implode("\n", $out);
355
            file_put_contents(
356
                self::TEMP_SCRIPTS_DIR . "/{$logname}_commands.log",
357
                $result
358
            );
359
        }
360
    }
361
362
    /**
363
     * Restarts all workers in a separate process with improved pipeline.
364
     * 
365
     * The restart process:
366
     * 1. Starts WorkerSafeScriptsCore with restart parameter
367
     * 2. WorkerSafeScriptsCore collects all workers and their forks
368
     * 3. Starts new instances of each worker
369
     * 4. Gracefully shuts down old workers
370
     * 
371
     */
372
    public static function restartAllWorkers(): void
373
    {
374
        $workerSafeScriptsPath = Util::getFilePathByClassName(WorkerSafeScriptsCore::class);
375
        $php = Util::which('php');
376
                
377
        // First restart WorkerSafeScriptsCore itself
378
        // This will trigger the full restart pipeline
379
        $workerSafeScripts = "$php -f $workerSafeScriptsPath restart > /dev/null 2> /dev/null";
380
        self::mwExec($workerSafeScripts);
381
    }
382
383
    /**
384
     * Manages a PHP worker process.
385
     *
386
     * @param string $className The class name of the PHP worker.
387
     * @param string $paramForPHPWorker The parameter for the PHP worker.
388
     * @param string $action The action to perform.
389
     */
390
    public static function processPHPWorker(
391
        string $className,
392
        string $paramForPHPWorker = 'start',
393
        string $action = 'restart'
394
    ): void {
395
        // Clean up stale PID files before managing workers
396
        self::cleanupStalePidFiles();
397
398
        if (!self::isValidAction($action)) {
399
            throw new InvalidArgumentException("Invalid action: $action");
400
        }
401
402
        SystemMessages::sysLogMsg(
403
            __METHOD__,
404
            "processPHPWorker $className action-$action",
405
            LOG_DEBUG
406
        );
407
408
        $workerPath = Util::getFilePathByClassName($className);
409
        if (empty($workerPath) || !class_exists($className)) {
410
            return;
411
        }
412
413
        $php = Util::which('php');
414
        $command = "$php -f $workerPath";
415
        $kill = Util::which('kill');
416
417
        $activeProcesses = self::getPidOfProcess($className);
418
        $processes = array_filter(explode(' ', $activeProcesses));
419
        $currentProcCount = count($processes);
420
        
421
        // Определяем количество нужных экземпляров из maxProc класса
422
        $neededProcCount = 1; // По умолчанию 1 процесс
423
        
424
        if (class_exists($className)) {
425
            // Получаем значение maxProc из класса
426
            $reflectionClass = new \ReflectionClass($className);
427
            if ($reflectionClass->hasProperty('maxProc')) {
428
                $defaultProperties = $reflectionClass->getDefaultProperties();
429
                if (isset($defaultProperties['maxProc'])) {
430
                    $neededProcCount = (int)$defaultProperties['maxProc'];
431
                }
432
            }
433
        }
434
435
        self::handleWorkerAction(
436
            $action,
437
            $activeProcesses,
438
            $command,
439
            $paramForPHPWorker,
440
            $kill,
441
            $currentProcCount,
442
            $neededProcCount,
443
            $processes,
444
            $className
445
        );
446
    }
447
448
    /**
449
     * Handles worker process actions.
450
     *
451
     * @param string $action Action to perform
452
     * @param string $activeProcesses Active process IDs
453
     * @param string $command Command to execute
454
     * @param string $paramForPHPWorker Worker parameters
455
     * @param string $kill Kill command path
456
     * @param int $currentProcCount Current process count
457
     * @param int $neededProcCount Needed process count
458
     * @param array $processes Process IDs array
459
     * @param string $className Worker class name
460
     */
461
    private static function handleWorkerAction(
462
        string $action,
463
        string $activeProcesses,
464
        string $command,
465
        string $paramForPHPWorker,
466
        string $kill,
467
        int $currentProcCount,
468
        int $neededProcCount,
469
        array $processes,
470
        string $className = ''
471
    ): void {
472
        switch ($action) {
473
            case 'restart':
474
                self::handleRestartAction(
475
                    $activeProcesses,
476
                    $kill,
477
                    $command,
478
                    $paramForPHPWorker,
479
                    $className,
480
                    $neededProcCount
481
                );
482
                break;
483
            case 'stop':
484
                self::handleStopAction($activeProcesses, $kill);
485
                break;
486
            case 'start':
487
                self::handleStartAction(
488
                    $currentProcCount,
489
                    $neededProcCount,
490
                    $command,
491
                    $paramForPHPWorker,
492
                    $processes,
493
                    $kill
494
                );
495
                break;
496
        }
497
    }
498
499
    /**
500
     * Handles the restart action for a worker.
501
     *
502
     * @param string $activeProcesses Active process IDs
503
     * @param string $kill Kill command path
504
     * @param string $command Command to execute
505
     * @param string $paramForPHPWorker Worker parameters
506
     * @param string $workerClass Worker class name
507
     * @param int $neededProcCount Number of instances needed
508
     */
509
    private static function handleRestartAction(
510
        string $activeProcesses,
511
        string $kill,
512
        string $command,
513
        string $paramForPHPWorker,
514
        string $workerClass = '',
515
        int $neededProcCount = 1
516
    ): void {
517
        // Отладочное логирование
518
        SystemMessages::sysLogMsg(
519
            __METHOD__,
520
            sprintf(
521
                "Restarting worker: %s, neededProcCount=%d, activeProcesses=%s",
522
                $workerClass,
523
                $neededProcCount,
524
                $activeProcesses
525
            ),
526
            LOG_NOTICE
527
        );
528
529
        if ($activeProcesses !== '') {
530
            SystemMessages::sysLogMsg(
531
                __METHOD__,
532
                sprintf("SEND signal: %s", "SIGUSR1 $activeProcesses"),
533
                LOG_WARNING
534
            );
535
536
            // Send SIGUSR1 first to initiate graceful shutdown
537
            self::mwExec("$kill -SIGUSR1 $activeProcesses > /dev/null 2>&1");
538
            
539
            // Wait for 2 seconds to allow worker to handle SIGUSR1
540
            sleep(2);
541
542
            SystemMessages::sysLogMsg(
543
                __METHOD__,
544
                sprintf("SEND signal: %s", "SIGTERM $activeProcesses"),
545
                LOG_WARNING
546
            );
547
548
            // Then send SIGTERM
549
            self::mwExecBg("$kill -SIGTERM $activeProcesses");
550
        }
551
552
        // Запуск воркеров с поддержкой пула
553
        if ($neededProcCount > 1) {
554
            SystemMessages::sysLogMsg(
555
                __METHOD__,
556
                sprintf("Starting worker pool with %d instances for %s", $neededProcCount, $workerClass),
557
                LOG_NOTICE
558
            );
559
            
560
            // Запускаем необходимое количество экземпляров
561
            for ($i = 1; $i <= $neededProcCount; $i++) {
562
                $instanceParam = " --instance-id={$i}";
563
                
564
                SystemMessages::sysLogMsg(
565
                    __METHOD__,
566
                    sprintf("Starting worker instance %d/%d: %s", $i, $neededProcCount, $command . " " . $paramForPHPWorker . $instanceParam),
567
                    LOG_DEBUG
568
                );
569
                
570
                self::mwExecBg("{$command} {$paramForPHPWorker}{$instanceParam}");
571
                
572
                // Небольшая задержка между запусками экземпляров для предотвращения конфликтов
573
                usleep(250000); // 250ms
574
            }
575
        } else {
576
            // Start new instance (for non-pool workers)
577
            self::mwExecBg("$command $paramForPHPWorker");
578
        }
579
    }
580
581
    /**
582
     * Handles the stop action for a worker.
583
     *
584
     * @param string $activeProcesses Active process IDs
585
     * @param string $kill Kill command path
586
     */
587
    private static function handleStopAction(string $activeProcesses, string $kill): void
588
    {
589
        if ($activeProcesses !== '') {
590
            self::mwExec("$kill -SIGUSR2 $activeProcesses > /dev/null 2>&1 &");
591
            self::mwExecBg("$kill -SIGTERM $activeProcesses");
592
        }
593
    }
594
595
    /**
596
     * Handles the start action for a worker.
597
     *
598
     * @param int $currentProcCount Current process count
599
     * @param int $neededProcCount Needed process count
600
     * @param string $command Command to execute
601
     * @param string $paramForPHPWorker Worker parameters
602
     * @param array $processes Process IDs array
603
     * @param string $kill Kill command path
604
     */
605
    private static function handleStartAction(
606
        int $currentProcCount,
607
        int $neededProcCount,
608
        string $command,
609
        string $paramForPHPWorker,
610
        array $processes,
611
        string $kill
612
    ): void {
613
        if ($currentProcCount === $neededProcCount) {
614
            return;
615
        }
616
617
        // Добавляем отладочное логирование
618
        SystemMessages::sysLogMsg(
619
            __METHOD__,
620
            sprintf(
621
                "Starting worker pool: currentCount=%d, neededCount=%d, command=%s",
622
                $currentProcCount,
623
                $neededProcCount,
624
                $command
625
            ),
626
            LOG_DEBUG
627
        );
628
629
        if ($neededProcCount > $currentProcCount) {
630
            // Запускаем новые экземпляры с указанием instance ID
631
            for ($i = 0; $i < $neededProcCount; $i++) {
632
                $instanceParam = '';
633
                // Если мы запускаем больше одного экземпляра, добавляем параметр instanceId
634
                if ($neededProcCount > 1) {
635
                    $instanceId = $i + 1; // Нумеруем с 1
636
                    $instanceParam = " --instance-id={$instanceId}";
637
                }
638
                
639
                // Отладочное логирование
640
                SystemMessages::sysLogMsg(
641
                    __METHOD__,
642
                    sprintf(
643
                        "Launching instance #%d with command: %s %s%s",
644
                        ($i+1),
645
                        $command, 
646
                        $paramForPHPWorker,
647
                        $instanceParam
648
                    ),
649
                    LOG_DEBUG
650
                );
651
                
652
                self::mwExecBg("{$command} {$paramForPHPWorker}{$instanceParam}");
653
            }
654
        } elseif ($currentProcCount > $neededProcCount) {
655
            $countProc4Kill = $currentProcCount - $neededProcCount;
656
            for ($i = 0; $i < $countProc4Kill; $i++) {
657
                if (!isset($processes[$i])) {
658
                    break;
659
                }
660
                self::mwExec("$kill -SIGUSR1 {$processes[$i]} > /dev/null 2>&1 &");
661
                self::mwExecBg("$kill -SIGTERM {$processes[$i]}");
662
            }
663
        }
664
    }
665
666
    /**
667
     * Retrieves the PID of a process by its name.
668
     *
669
     * @param string $name Process name
670
     * @param string $exclude Process name to exclude
671
     * @return string Process IDs
672
     */
673
    public static function getPidOfProcess(string $name, string $exclude = ''): string
674
    {
675
        $ps = Util::which('ps');
676
        $grep = Util::which('grep');
677
        $awk = Util::which('awk');
678
679
        $name = addslashes($name);
680
        $filterCmd = '';
681
        if (!empty($exclude)) {
682
            $filterCmd = "| $grep -v " . escapeshellarg($exclude);
683
        }
684
685
        $out = [];
686
        self::mwExec(
687
            "$ps -A -o 'pid,args' $filterCmd | $grep '$name' | $grep -v grep | $awk '{print $1}'",
688
            $out
689
        );
690
691
        return trim(implode(' ', $out));
0 ignored issues
show
Bug introduced by
It seems like $out can also be of type null; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

691
        return trim(implode(' ', /** @scrutinizer ignore-type */ $out));
Loading history...
692
    }
693
694
    /**
695
     * Executes a command as a background process.
696
     *
697
     * @param string $command Command to execute
698
     * @param string $outFile Output file path
699
     * @param int $sleepTime Sleep time in seconds
700
     */
701
    public static function mwExecBg(
702
        string $command,
703
        string $outFile = self::DEFAULT_OUTPUT_FILE,
704
        int $sleepTime = 0
705
    ): void {
706
        $nohup = Util::which('nohup');
707
        $sh = Util::which('sh');
708
        $rm = Util::which('rm');
709
        $sleep = Util::which('sleep');
710
711
        if ($sleepTime > 0) {
712
            $filename = self::TEMP_SCRIPTS_DIR . '/' . time() . '_noop.sh';
713
            file_put_contents(
714
                $filename,
715
                "$sleep $sleepTime; $command; $rm -rf $filename"
716
            );
717
            $noopCommand = "$nohup $sh $filename > $outFile 2>&1 &";
718
        } else {
719
            $noopCommand = "$nohup $command > $outFile 2>&1 &";
720
        }
721
        exec($noopCommand);
722
    }
723
724
    /**
725
     * Manages a daemon/worker process.
726
     *
727
     * @param string $cmd Command to execute
728
     * @param string $param Command parameters
729
     * @param string $procName Process name
730
     * @param string $action Action to perform
731
     * @param string $outFile Output file path
732
     *
733
     * @return array|bool Process status or operation result
734
     * @throws InvalidArgumentException If action is invalid
735
     */
736
    public static function processWorker(
737
        string $cmd,
738
        string $param,
739
        string $procName,
740
        string $action,
741
        string $outFile = self::DEFAULT_OUTPUT_FILE
742
    ): bool|array {
743
        if (!self::isValidAction($action)) {
744
            throw new InvalidArgumentException("Invalid action: $action");
745
        }
746
747
        $kill = Util::which('kill');
748
        $nohup = Util::which('nohup');
749
        $workerPID = self::getPidOfProcess($procName);
750
751
        return match ($action) {
752
            'status' => [
753
                'status' => ($workerPID !== '') ? 'Started' : 'Stopped',
754
                'app' => $procName,
755
                'PID' => $workerPID
756
            ],
757
            'restart' => self::handleWorkerRestart($kill, $workerPID, $nohup, $cmd, $param, $outFile),
758
            'stop' => self::handleWorkerStop($kill, $workerPID),
759
            'start' => self::handleWorkerStart($workerPID, $nohup, $cmd, $param, $outFile),
760
            default => throw new InvalidArgumentException("Unsupported action: $action"),
761
        };
762
    }
763
764
    /**
765
     * Handles worker restart operation.
766
     *
767
     * @param string $kill Kill command path
768
     * @param string $workerPID Worker process ID
769
     * @param string $nohup Nohup command path
770
     * @param string $cmd Command to execute
771
     * @param string $param Command parameters
772
     * @param string $outFile Output file path
773
     * @return bool
774
     */
775
    private static function handleWorkerRestart(
776
        string $kill,
0 ignored issues
show
Unused Code introduced by
The parameter $kill 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

776
        /** @scrutinizer ignore-unused */ string $kill,

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...
777
        string $workerPID,
778
        string $nohup,
779
        string $cmd,
780
        string $param,
781
        string $outFile
782
    ): bool {
783
        if ($workerPID !== '') {
784
            self::safeTerminateProcess($workerPID);
785
        }
786
        self::mwExec("$nohup $cmd $param > $outFile 2>&1 &");
787
        return true;
788
    }
789
790
    /**
791
     * Handles worker stop operation.
792
     *
793
     * @param string $kill Kill command path
794
     * @param string $workerPID Worker process ID
795
     * @return bool
796
     */
797
    private static function handleWorkerStop(string $kill, string $workerPID): bool
0 ignored issues
show
Unused Code introduced by
The parameter $kill 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

797
    private static function handleWorkerStop(/** @scrutinizer ignore-unused */ string $kill, string $workerPID): 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...
798
    {
799
        if ($workerPID !== '') {
800
            self::safeTerminateProcess($workerPID);
801
        }
802
        return true;
803
    }
804
805
    /**
806
     * Handles worker start operation.
807
     *
808
     * @param string $workerPID Worker process ID
809
     * @param string $nohup Nohup command path
810
     * @param string $cmd Command to execute
811
     * @param string $param Command parameters
812
     * @param string $outFile Output file path
813
     * @return bool
814
     */
815
    private static function handleWorkerStart(
816
        string $workerPID,
817
        string $nohup,
818
        string $cmd,
819
        string $param,
820
        string $outFile
821
    ): bool {
822
        if ($workerPID === '') {
823
            self::mwExec("$nohup $cmd $param > $outFile 2>&1 &");
824
        }
825
        return true;
826
    }
827
828
    /**
829
     * Starts a daemon process with failure control.
830
     *
831
     * @param string $procName Process name
832
     * @param string $args Process arguments
833
     * @param int $attemptsCount Number of attempts to start
834
     * @param int $timeout Timeout between attempts in microseconds
835
     * @param string $outFile Output file path
836
     * @return bool Success status
837
     */
838
    public static function safeStartDaemon(
839
        string $procName,
840
        string $args,
841
        int $attemptsCount = self::DEFAULT_START_ATTEMPTS,
842
        int $timeout = self::DEFAULT_ATTEMPT_TIMEOUT,
843
        string $outFile = self::DEFAULT_OUTPUT_FILE
844
    ): bool {
845
        $result = true;
846
        $baseName = "safe-$procName";
847
        $safeLink = "/sbin/$baseName";
848
849
        try {
850
            Util::createUpdateSymlink('/etc/rc/worker_reload', $safeLink);
851
            self::killByName($baseName);
852
            self::killByName($procName);
853
854
            // Start the process in the background
855
            self::mwExecBg("$safeLink $args", $outFile);
856
857
            // Wait for the process to start with timeout
858
            $pid = self::waitForProcessStart($procName, $baseName, $attemptsCount, $timeout);
859
860
            if (empty($pid)) {
861
                SystemMessages::echoWithSyslog(" - Wait for start '$procName' fail" . PHP_EOL);
862
                $result = false;
863
            }
864
        } catch (Throwable $e) {
865
            SystemMessages::echoWithSyslog("Error starting daemon '$procName': " . $e->getMessage());
866
            $result = false;
867
        }
868
869
        return $result;
870
    }
871
872
}
873