Passed
Push — develop ( 5a71b5...d30961 )
by Nikolay
22:52
created

WorkerApiCommands::breakpointHere()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
dl 0
loc 6
rs 9.2222
c 1
b 0
f 0
cc 6
nc 4
nop 1
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\PBXCoreREST\Workers;
22
23
use MikoPBX\Common\Handlers\CriticalErrorsHandler;
24
use MikoPBX\Common\Providers\BeanstalkConnectionWorkerApiProvider;
25
use MikoPBX\Core\System\{BeanstalkClient, Configs\BeanstalkConf, Directories, Processes, SystemMessages};
26
use MikoPBX\Core\Workers\WorkerBase;
27
use MikoPBX\PBXCoreREST\Lib\ModulesManagementProcessor;
28
use MikoPBX\PBXCoreREST\Lib\PBXApiResult;
29
use MikoPBX\PBXCoreREST\Lib\PbxExtensionsProcessor;
30
use MikoPBX\PBXCoreREST\Lib\SystemManagementProcessor;
31
use Throwable;
32
33
use function xdebug_break;
34
35
require_once 'Globals.php';
36
37
38
/**
39
 * The WorkerApiCommands class is responsible for handling API command requests from the frontend.
40
 *
41
 * It handles API command requests, delegates the processing to the appropriate processor classes,
42
 * and checks for restart requirements based on the received requests.
43
 *
44
 *
45
 * @package MikoPBX\PBXCoreREST\Workers
46
 */
47
class WorkerApiCommands extends WorkerBase
48
{
49
    /**
50
     * The maximum parallel worker processes
51
     *
52
     * @var int
53
     */
54
    public int $maxProc = 4;
55
56
57
    /**
58
     * Starts the worker.
59
     *
60
     * @param array $argv The command-line arguments passed to the worker.
61
     *
62
     * @return void
63
     */
64
    public function start(array $argv): void
65
    {
66
        /** @var BeanstalkConnectionWorkerApiProvider $beanstalk */
67
        $beanstalk = $this->di->getShared(BeanstalkConnectionWorkerApiProvider::SERVICE_NAME);
68
        if ($beanstalk->isConnected() === false) {
69
            SystemMessages::sysLogMsg(self::class, 'Fail connect to beanstalkd...');
70
            sleep(2);
71
            return;
72
        }
73
        $beanstalk->subscribe($this->makePingTubeName(self::class), [$this, 'pingCallBack']);
74
        $beanstalk->subscribe(__CLASS__, [$this, 'prepareAnswer']);
75
76
        while ($this->needRestart === false) {
77
            $beanstalk->wait();
78
        }
79
    }
80
81
    /**
82
     * Process API request from frontend
83
     *
84
     * @param BeanstalkClient $message
85
     *
86
     */
87
    public function prepareAnswer(BeanstalkClient $message): void
88
    {
89
        // Use fork to run the callback in a separate process
90
        $pid = pcntl_fork();
91
        if ($pid === -1) {
92
            // Fork failed
93
            throw new \RuntimeException("Failed to fork a new process.");
94
        }
95
        if ($pid === 0) {
96
            $res = new PBXApiResult();
97
            $res->processor = __METHOD__;
98
            $async = false;
99
            try {
100
                $request = json_decode($message->getBody(), true, 512, JSON_THROW_ON_ERROR);
101
                $async   = ($request['async'] ?? false) === true;
102
                $processor = $request['processor'];
103
                $res->processor = $processor;
104
                // Old style, we can remove it in 2025
105
                if ($processor === 'modules') {
106
                    $processor = PbxExtensionsProcessor::class;
107
                }
108
109
                $this->breakpointHere($request);
110
111
                // This is the child process
112
                if (method_exists($processor, 'callback')) {
113
                    cli_set_process_title(__CLASS__ . '-' . $request['action']);
114
                    // Execute async job
115
                    if ($async) {
116
                        $res->success = true;
117
                        $res->messages['info'][] = "The async job {$request['action']} starts in background, you will receive answer on {$request['asyncChannelId']} nchan channel";
118
                        $encodedResult = json_encode($res->getResult());
119
                        $message->reply($encodedResult);
120
                        $processor::callback($request);
121
                    } else {
122
                        $res = $processor::callback($request);
123
                    }
124
                } else {
125
                    $res->success = false;
126
                    $res->messages['error'][] = "Unknown processor - $processor in prepareAnswer";
127
                }
128
            } catch (Throwable $exception) {
129
                $request = [];
130
                $this->needRestart = true;
131
                // Prepare answer with pretty error description
132
                $res->messages['error'][] = CriticalErrorsHandler::handleExceptionWithSyslog($exception);
133
            } finally {
134
                if ($async === false) {
135
                    $encodedResult = json_encode($res->getResult());
136
                    if ($encodedResult === false) {
137
                        $res->data = [];
138
                        $res->messages['error'][] = 'It is impossible to encode to json current processor answer';
139
                        $encodedResult = json_encode($res->getResult());
140
                    }
141
142
                    // Check the response size and write in on file if it bigger than Beanstalk can digest
143
                    if (strlen($encodedResult) > BeanstalkConf::JOB_DATA_SIZE_LIMIT) {
144
                        $downloadCacheDir = Directories::getDir(Directories::WWW_DOWNLOAD_CACHE_DIR);
145
                        $filenameTmp = $downloadCacheDir . '/temp-' . __FUNCTION__ . '_' . microtime() . '.data';
146
                        if (file_put_contents($filenameTmp, serialize($res->getResult()))) {
147
                            $encodedResult = json_encode([BeanstalkClient::RESPONSE_IN_FILE => $filenameTmp]);
148
                        } else {
149
                            $res->data = [];
150
                            $res->messages['error'][] = 'It is impossible to write answer into file ' . $filenameTmp;
151
                            $encodedResult = json_encode($res->getResult());
152
                        }
153
                    }
154
                    $message->reply($encodedResult);
155
                }
156
                if ($res->success) {
157
                    $this->checkNeedReload($request);
158
                }
159
            }
160
            exit(0); // Exit the child process
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
161
        }
162
        // This is the parent process
163
        pcntl_wait($status); // Wait for the child process to complete
164
    }
165
166
    /**
167
     * Checks if the module or worker needs to be reloaded.
168
     *
169
     * @param array $request
170
     */
171
    private function checkNeedReload(array $request): void
172
    {
173
        // Check if new code added from modules
174
        $restartActions = $this->getNeedRestartActions();
175
        foreach ($restartActions as $processor => $actions) {
176
            foreach ($actions as $action) {
177
                if (
178
                    $processor === $request['processor']
179
                    && $action === $request['action']
180
                ) {
181
                    $this->needRestart = true;
182
                    Processes::restartAllWorkers();
183
                    return;
184
                }
185
            }
186
        }
187
    }
188
189
    /**
190
     * Prepares array of processor => action depends restart this kind worker
191
     *
192
     * @return array
193
     */
194
    private function getNeedRestartActions(): array
195
    {
196
        return [
197
            SystemManagementProcessor::class => [
198
                'restoreDefault',
199
            ],
200
            ModulesManagementProcessor::class => [
201
                'enableModule',
202
                'disableModule',
203
                'uninstallModule',
204
            ],
205
        ];
206
    }
207
208
    /**
209
     * Start xdebug session if request called with special header: "X-Debug-The-Request"
210
     *
211
     * Add xdebug.start_with_request = trigger to xdebug.ini
212
     *
213
     * @examples
214
     * curl -X POST \
215
     * -H 'Content-Type: application/json' \
216
     * -H 'Cookie: XDEBUG_SESSION=PHPSTORM' \
217
     * -H 'X-Debug-The-Request: 1' \
218
     * -d '{"filename": "/storage/usbdisk1/mikopbx/tmp/mikopbx-2023.1.223-x86_64.img"}' \
219
     * http://127.0.0.1/pbxcore/api/system/upgrade
220
     *
221
     * Or add a header at any semantic API request
222
     * $.api({
223
     *      url: ...,
224
     *      on: 'now',
225
     *      method: 'POST',
226
     *      beforeXHR(xhr) {
227
     *          xhr.setRequestHeader ('X-Debug-The-Request', 1);
228
     *          return xhr;
229
     *      },
230
     *      ...
231
     * });
232
     */
233
    private function breakpointHere(array $request): void
234
    {
235
        if (isset($request['debug']) && $request['debug'] === true && extension_loaded('xdebug')) {
236
            if (function_exists('xdebug_connect_to_client')) {
237
                if (xdebug_connect_to_client()) {
238
                    xdebug_break();
239
                }
240
            }
241
        }
242
    }
243
}
244
245
// Start a worker process
246
WorkerApiCommands::startWorker($argv ?? []);
247