Passed
Push — develop ( 68c7cf...919cf3 )
by Портнов
21:22
created

Processes::safeStartDaemon()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 35
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 35
rs 8.9297
cc 6
nc 12
nop 4
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright (C) 2017-2020 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
23
use MikoPBX\Core\Workers\Cron\WorkerSafeScriptsCore;
24
use Phalcon\Di;
25
26
class Processes
27
{
28
29
    /**
30
     * Kills process/daemon by name
31
     *
32
     * @param $procName
33
     *
34
     * @return int|null
35
     */
36
    public static function killByName($procName): ?int
37
    {
38
        $killallPath = Util::which('killall');
39
40
        return self::mwExec($killallPath . ' ' . escapeshellarg($procName));
41
    }
42
43
    /**
44
     * Executes command exec().
45
     *
46
     * @param $command
47
     * @param $outArr
48
     * @param $retVal
49
     *
50
     * @return int
51
     */
52
    public static function mwExec($command, &$outArr = null, &$retVal = null): int
53
    {
54
        $retVal = 0;
55
        $outArr = [];
56
        $di     = Di::getDefault();
57
58
        if ($di !== null && $di->getShared('config')->path('core.debugMode')) {
59
            echo "mwExec(): $command\n";
60
        } else {
61
            exec("$command 2>&1", $outArr, $retVal);
62
        }
63
        return $retVal;
64
    }
65
66
    /**
67
     * Executes command exec() as background process with an execution timeout.
68
     *
69
     * @param        $command
70
     * @param int    $timeout
71
     * @param string $logname
72
     */
73
    public static function mwExecBgWithTimeout($command, $timeout = 4, $logname = '/dev/null'): void
74
    {
75
        $di = Di::getDefault();
76
77
        if ($di !== null && $di->getShared('config')->path('core.debugMode')) {
78
            echo "mwExecBg(): $command\n";
79
80
            return;
81
        }
82
        $nohupPath   = Util::which('nohup');
83
        $timeoutPath = Util::which('timeout');
84
        exec("{$nohupPath} {$timeoutPath} {$timeout} {$command} > {$logname} 2>&1 &");
85
    }
86
87
    /**
88
     * Executes multiple commands.
89
     *
90
     * @param        $arr_cmds
91
     * @param array  $out
92
     * @param string $logname
93
     */
94
    public static function mwExecCommands($arr_cmds, &$out = [], $logname = ''): void
95
    {
96
        $out = [];
97
        foreach ($arr_cmds as $cmd) {
98
            $out[]   = "$cmd;";
99
            $out_cmd = [];
100
            self::mwExec($cmd, $out_cmd);
101
            $out = array_merge($out, $out_cmd);
102
        }
103
104
        if ($logname !== '') {
105
            $result = implode("\n", $out);
106
            file_put_contents("/tmp/{$logname}_commands.log", $result);
107
        }
108
    }
109
110
    /**
111
     * Restart all workers in separate process,
112
     * we use this method after module install or delete
113
     */
114
    public static function restartAllWorkers(): void
115
    {
116
        $workerSafeScriptsPath = Util::getFilePathByClassName(WorkerSafeScriptsCore::class);
117
        $phpPath               = Util::which('php');
118
        $WorkerSafeScripts     = "{$phpPath} -f {$workerSafeScriptsPath} restart > /dev/null 2> /dev/null";
119
        self::mwExec($WorkerSafeScripts);
120
        Util::sysLogMsg(static::class, "Service asked for WorkerSafeScriptsCore restart", LOG_DEBUG);
121
    }
122
123
    /**
124
     * Process PHP workers
125
     *
126
     * @param string $className
127
     * @param string $paramForPHPWorker
128
     * @param string $action
129
     */
130
    public static function processPHPWorker(
131
        string $className,
132
        string $paramForPHPWorker = 'start',
133
        string $action = 'restart'
134
    ): void {
135
        Util::sysLogMsg(__METHOD__, "processPHPWorker ". $className." action-".$action, LOG_DEBUG);
136
        $workerPath = Util::getFilePathByClassName($className);
137
        if (empty($workerPath)) {
138
            return;
139
        }
140
        $command         = "php -f {$workerPath}";
141
        $path_kill       = Util::which('kill');
142
        $activeProcesses = self::getPidOfProcess($className);
143
        $processes       = explode(' ', $activeProcesses);
144
        if (empty($processes[0])) {
145
            array_shift($processes);
146
        }
147
        $currentProcCount = count($processes);
148
149
        if ( ! class_exists($className)) {
150
            return;
151
        }
152
        $workerObject    = new $className();
153
        $neededProcCount = $workerObject->maxProc;
154
155
        switch ($action) {
156
            case 'restart':
157
                // Stop all old workers
158
                if ($activeProcesses !== '') {
159
                    self::mwExec("{$path_kill} -SIGUSR1 {$activeProcesses}  > /dev/null 2>&1 &");
160
                    self::mwExecBg("{$path_kill} -SIGTERM {$activeProcesses}", '/dev/null', 10);
161
                    $currentProcCount = 0;
162
                }
163
164
                // Start new processes
165
                while ($currentProcCount < $neededProcCount) {
166
                    self::mwExecBg("{$command} {$paramForPHPWorker}");
167
                    $currentProcCount++;
168
                }
169
170
                break;
171
            case 'stop':
172
                if ($activeProcesses !== '') {
173
                    self::mwExec("{$path_kill} -SIGUSR2 {$activeProcesses}  > /dev/null 2>&1 &");
174
                    self::mwExecBg("{$path_kill} -SIGTERM {$activeProcesses}", '/dev/null', 10);
175
                }
176
                break;
177
            case 'start':
178
                if ($currentProcCount === $neededProcCount) {
179
                    return;
180
                }
181
182
                if ($neededProcCount > $currentProcCount) {
183
                    // Start additional processes
184
                    while ($currentProcCount < $neededProcCount) {
185
                        self::mwExecBg("{$command} {$paramForPHPWorker}");
186
                        $currentProcCount++;
187
                    }
188
                } elseif ($currentProcCount > $neededProcCount) {
189
                    // Find redundant processes
190
                    $countProc4Kill = $neededProcCount - $currentProcCount;
191
                    // Send SIGUSR1 command to them
192
                    while ($countProc4Kill >= 0) {
193
                        if ( ! isset($processes[$countProc4Kill])) {
194
                            break;
195
                        }
196
                        // Kill old processes with timeout, maybe it is soft restart and worker die without any help
197
                        self::mwExec("{$path_kill} -SIGUSR1 {$processes[$countProc4Kill]}  > /dev/null 2>&1 &");
198
                        self::mwExecBg("{$path_kill} -SIGTERM {$activeProcesses}", '/dev/null', 10);
199
                        $countProc4Kill--;
200
                    }
201
                }
202
                break;
203
            default:
204
        }
205
    }
206
207
    /**
208
     * Возвращает PID процесса по его имени.
209
     *
210
     * @param        $name
211
     * @param string $exclude
212
     *
213
     * @return string
214
     */
215
    public static function getPidOfProcess($name, $exclude = ''): string
216
    {
217
        $path_ps   = Util::which('ps');
218
        $path_grep = Util::which('grep');
219
        $path_awk  = Util::which('awk');
220
221
        $name       = addslashes($name);
222
        $filter_cmd = '';
223
        if ( ! empty($exclude)) {
224
            $filter_cmd = "| $path_grep -v " . escapeshellarg($exclude);
225
        }
226
        $out = [];
227
        self::mwExec(
228
            "{$path_ps} -A -o 'pid,args' {$filter_cmd} | {$path_grep} '{$name}' | {$path_grep} -v grep | {$path_awk} ' {print $1} '",
229
            $out
230
        );
231
232
        return trim(implode(' ', $out));
233
    }
234
235
    /**
236
     * Executes command exec() as background process.
237
     *
238
     * @param $command
239
     * @param $out_file
240
     * @param $sleep_time
241
     */
242
    public static function mwExecBg($command, $out_file = '/dev/null', $sleep_time = 0): void
243
    {
244
        $nohupPath = Util::which('nohup');
245
        $shPath    = Util::which('sh');
246
        $rmPath    = Util::which('rm');
247
        $sleepPath = Util::which('sleep');
248
        if ($sleep_time > 0) {
249
            $filename = '/tmp/' . time() . '_noop.sh';
250
            file_put_contents($filename, "{$sleepPath} {$sleep_time}; {$command}; {$rmPath} -rf {$filename}");
251
            $noop_command = "{$nohupPath} {$shPath} {$filename} > {$out_file} 2>&1 &";
252
        } else {
253
            $noop_command = "{$nohupPath} {$command} > {$out_file} 2>&1 &";
254
        }
255
        exec($noop_command);
256
    }
257
258
    /**
259
     * Manages a daemon/worker process
260
     * Returns process statuses by name of it
261
     *
262
     * @param $cmd
263
     * @param $param
264
     * @param $proc_name
265
     * @param $action
266
     * @param $out_file
267
     *
268
     * @return array | bool
269
     */
270
    public static function processWorker($cmd, $param, $proc_name, $action, $out_file = '/dev/null')
271
    {
272
        $path_kill  = Util::which('kill');
273
        $path_nohup = Util::which('nohup');
274
275
        $WorkerPID = self::getPidOfProcess($proc_name);
276
277
        switch ($action) {
278
            case 'status':
279
                $status = ($WorkerPID !== '') ? 'Started' : 'Stoped';
280
281
                return ['status' => $status, 'app' => $proc_name, 'PID' => $WorkerPID];
282
            case 'restart':
283
                if ($WorkerPID !== '') {
284
                    self::mwExec("{$path_kill} -9 {$WorkerPID}  > /dev/null 2>&1 &");
285
                }
286
                self::mwExec("{$path_nohup} {$cmd} {$param}  > {$out_file} 2>&1 &");
287
                break;
288
            case 'stop':
289
                if ($WorkerPID !== '') {
290
                    self::mwExec("{$path_kill} -9 {$WorkerPID}  > /dev/null 2>&1 &");
291
                }
292
                break;
293
            case 'start':
294
                if ($WorkerPID === '') {
295
                    self::mwExec("{$path_nohup} {$cmd} {$param}  > {$out_file} 2>&1 &");
296
                }
297
                break;
298
            default:
299
        }
300
301
        return true;
302
    }
303
304
    /**
305
     * Запуск демона с контролем аварийного завершения
306
     * worker_reload - в случае падения пишет ошибку в syslog и рестартует сдемона.
307
     * @param  string    $procName
308
     * @param  string    $args
309
     * @param  int       $attemptsCount
310
     * @param  int       $timout
311
     * @return bool
312
     */
313
    public static function safeStartDaemon(string $procName, string $args, int $attemptsCount = 20, int $timout = 100000):bool
314
    {
315
        $result   = true;
316
        $baseName = "safe-{$procName}";
317
        $safeLink = "/sbin/{$baseName}";
318
        Util::createUpdateSymlink('/etc/rc/worker_reload', $safeLink);
319
320
        self::killByName($baseName);
321
        self::killByName($procName);
322
        // Ожидаем завершения процесса
323
        $ch = 1;
324
        while (! empty(self::getPidOfProcess($procName, $baseName)) && $ch < $attemptsCount) {
325
            usleep($timout);
326
            $ch ++ ;
327
        }
328
        // Запускаем процесс в фоне.
329
        self::mwExecBg("{$safeLink} {$args}");
330
331
        // Ожидаем запуска процесса.
332
        $ch = 1;
333
        while ($ch < $attemptsCount) {
334
            $pid = self::getPidOfProcess($procName, $baseName);
335
            if (empty($pid)) {
336
                usleep($timout);
337
            } else {
338
                break;
339
            }
340
            $ch ++ ;
341
        }
342
        if(empty($pid)){
343
            Util::echoWithSyslog(" - Wait for start '{$procName}' fail" . PHP_EOL);
344
            $result = false;
345
        }
346
347
        return $result;
348
    }
349
350
}