Passed
Pull Request — master (#271)
by Bill
03:10
created

Manager::removePidFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SwooleTW\Http\Server;
4
5
use Exception;
6
use Throwable;
7
use Swoole\Process;
8
use Swoole\Server\Task;
9
use Illuminate\Support\Str;
10
use SwooleTW\Http\Helpers\OS;
11
use SwooleTW\Http\Server\Sandbox;
12
use SwooleTW\Http\Task\SwooleTaskJob;
13
use Illuminate\Support\Facades\Facade;
14
use SwooleTW\Http\Websocket\Websocket;
15
use SwooleTW\Http\Transformers\Request;
16
use SwooleTW\Http\Server\Facades\Server;
17
use SwooleTW\Http\Transformers\Response;
18
use SwooleTW\Http\Concerns\WithApplication;
19
use Illuminate\Contracts\Container\Container;
20
use Illuminate\Contracts\Debug\ExceptionHandler;
21
use SwooleTW\Http\Concerns\InteractsWithWebsocket;
22
use Symfony\Component\Console\Output\ConsoleOutput;
23
use SwooleTW\Http\Concerns\InteractsWithSwooleQueue;
24
use SwooleTW\Http\Concerns\InteractsWithSwooleTable;
25
use Symfony\Component\Debug\Exception\FatalThrowableError;
26
27
/**
28
 * Class Manager
29
 */
30
class Manager
31
{
32
    use InteractsWithWebsocket,
0 ignored issues
show
introduced by
The trait SwooleTW\Http\Concerns\InteractsWithWebsocket requires some properties which are not provided by SwooleTW\Http\Server\Manager: $fd, $isWebsocket
Loading history...
33
        InteractsWithSwooleTable,
34
        InteractsWithSwooleQueue,
35
        WithApplication;
36
37
    /**
38
     * Container.
39
     *
40
     * @var \Illuminate\Contracts\Container\Container
41
     */
42
    protected $container;
43
44
    /**
45
     * A manager to handle pid about the application.
46
     *
47
     * @var PidManager
48
     */
49
    protected $pidManager;
50
51
    /**
52
     * @var string
53
     */
54
    protected $framework;
55
56
    /**
57
     * @var string
58
     */
59
    protected $basePath;
60
61
    /**
62
     * Server events.
63
     *
64
     * @var array
65
     */
66
    protected $events = [
67
        'start',
68
        'shutDown',
69
        'workerStart',
70
        'workerStop',
71
        'packet',
72
        'bufferFull',
73
        'bufferEmpty',
74
        'task',
75
        'finish',
76
        'pipeMessage',
77
        'workerError',
78
        'managerStart',
79
        'managerStop',
80
        'request',
81
    ];
82
83
    /**
84
     * HTTP server manager constructor.
85
     *
86
     * @param \Illuminate\Contracts\Container\Container $container
87
     * @param string $framework
88
     * @param string $basePath
89
     *
90
     * @throws \Exception
91
     */
92
    public function __construct(Container $container, $framework, $basePath = null, PidManager $pidManager)
93
    {
94
        $this->container = $container;
95
        $this->setFramework($framework);
96
        $this->setBasepath($basePath);
97
        $this->pidManager = $pidManager;
98
        $this->initialize();
99
    }
100
101
    /**
102
     * Run swoole server.
103
     */
104
    public function run()
105
    {
106
        $this->container->make(Server::class)->start();
0 ignored issues
show
Bug introduced by
The method start() does not exist on SwooleTW\Http\Server\Facades\Server. ( Ignorable by Annotation )

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

106
        $this->container->make(Server::class)->/** @scrutinizer ignore-call */ start();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
107
    }
108
109
    /**
110
     * Stop swoole server.
111
     */
112
    public function stop()
113
    {
114
        $this->container->make(Server::class)->shutdown();
0 ignored issues
show
Bug introduced by
The method shutdown() does not exist on SwooleTW\Http\Server\Facades\Server. ( Ignorable by Annotation )

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

114
        $this->container->make(Server::class)->/** @scrutinizer ignore-call */ shutdown();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
115
    }
116
117
    /**
118
     * Initialize.
119
     */
120
    protected function initialize()
121
    {
122
        $this->createTables();
123
        $this->prepareWebsocket();
124
        $this->setSwooleServerListeners();
125
    }
126
127
    /**
128
     * Set swoole server listeners.
129
     */
130
    protected function setSwooleServerListeners()
131
    {
132
        foreach ($this->events as $event) {
133
            $listener = Str::camel("on_$event");
134
            $callback = method_exists($this, $listener) ? [$this, $listener] : function () use ($event) {
135
                $this->container->make('events')->dispatch("swoole.$event", func_get_args());
136
            };
137
138
            $this->container->make(Server::class)->on($event, $callback);
0 ignored issues
show
Bug introduced by
The method on() does not exist on SwooleTW\Http\Server\Facades\Server. ( Ignorable by Annotation )

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

138
            $this->container->make(Server::class)->/** @scrutinizer ignore-call */ on($event, $callback);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
139
        }
140
    }
141
142
    /**
143
     * "onStart" listener.
144
     */
145
    public function onStart()
146
    {
147
        $this->setProcessName('master process');
148
149
        $server = $this->container->make(Server::class);
150
151
        $this->pidManager->write($server->master_pid, $server->manager_pid ?? 0);
0 ignored issues
show
Bug introduced by
The property manager_pid does not seem to exist on SwooleTW\Http\Server\Facades\Server.
Loading history...
Bug introduced by
The property master_pid does not seem to exist on SwooleTW\Http\Server\Facades\Server.
Loading history...
152
153
        $this->container->make('events')->dispatch('swoole.start', func_get_args());
154
    }
155
156
    /**
157
     * The listener of "managerStart" event.
158
     *
159
     * @return void
160
     */
161
    public function onManagerStart()
162
    {
163
        $this->setProcessName('manager process');
164
165
        $this->container->make('events')->dispatch('swoole.managerStart', func_get_args());
166
    }
167
168
    /**
169
     * "onWorkerStart" listener.
170
     *
171
     * @param \Swoole\Http\Server|mixed $server
172
     *
173
     * @throws \Exception
174
     */
175
    public function onWorkerStart($server)
176
    {
177
        $this->clearCache();
178
179
        $this->container->make('events')->dispatch('swoole.workerStart', func_get_args());
180
181
        // don't init laravel app in task workers
182
        if ($server->taskworker) {
183
            $this->setProcessName('task process');
184
185
            return;
186
        }
187
        $this->setProcessName('worker process');
188
189
        // clear events instance in case of repeated listeners in worker process
190
        Facade::clearResolvedInstance('events');
191
192
        // prepare laravel app
193
        $this->getApplication();
194
195
        // bind after setting laravel app
196
        $this->bindToLaravelApp();
197
198
        // prepare websocket handler and routes
199
        if ($this->isServerWebsocket) {
200
            $this->prepareWebsocketHandler();
201
            $this->loadWebsocketRoutes();
202
        }
203
    }
204
205
    /**
206
     * "onRequest" listener.
207
     *
208
     * @param \Swoole\Http\Request $swooleRequest
209
     * @param \Swoole\Http\Response $swooleResponse
210
     */
211
    public function onRequest($swooleRequest, $swooleResponse)
212
    {
213
        $this->app->make('events')->dispatch('swoole.request');
214
215
        $this->resetOnRequest();
216
        $sandbox = $this->app->make(Sandbox::class);
217
        $handleStatic = $this->container->make('config')->get('swoole_http.handle_static_files', true);
218
        $publicPath = $this->container->make('config')->get('swoole_http.server.public_path', base_path('public'));
219
220
        try {
221
            // handle static file request first
222
            if ($handleStatic && Request::handleStatic($swooleRequest, $swooleResponse, $publicPath)) {
223
                return;
224
            }
225
            // transform swoole request to illuminate request
226
            $illuminateRequest = Request::make($swooleRequest)->toIlluminate();
227
228
            // set current request to sandbox
229
            $sandbox->setRequest($illuminateRequest);
230
231
            // enable sandbox
232
            $sandbox->enable();
233
234
            // handle request via laravel/lumen's dispatcher
235
            $illuminateResponse = $sandbox->run($illuminateRequest);
236
237
            // send response
238
            Response::make($illuminateResponse, $swooleResponse)->send();
239
        } catch (Throwable $e) {
240
            try {
241
                $exceptionResponse = $this->app
242
                    ->make(ExceptionHandler::class)
243
                    ->render(
244
                        $illuminateRequest,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $illuminateRequest does not seem to be defined for all execution paths leading up to this point.
Loading history...
245
                        $this->normalizeException($e)
246
                    );
247
                Response::make($exceptionResponse, $swooleResponse)->send();
248
            } catch (Throwable $e) {
249
                $this->logServerError($e);
250
            }
251
        } finally {
252
            // disable and recycle sandbox resource
253
            $sandbox->disable();
254
        }
255
    }
256
257
    /**
258
     * Reset on every request.
259
     */
260
    protected function resetOnRequest()
261
    {
262
        // Reset websocket data
263
        if ($this->isServerWebsocket) {
264
            $this->app->make(Websocket::class)->reset(true);
265
        }
266
    }
267
268
    /**
269
     * Set onTask listener.
270
     *
271
     * @param mixed $server
272
     * @param string|\Swoole\Server\Task $taskId or $task
273
     * @param string $srcWorkerId
274
     * @param mixed $data
275
     */
276
    public function onTask($server, $taskId, $srcWorkerId, $data)
277
    {
278
        $this->container->make('events')->dispatch('swoole.task', func_get_args());
279
280
        try {
281
            // push websocket message
282
            if ($this->isWebsocketPushPayload($data)) {
283
                $this->pushMessage($server, $data['data']);
284
            // push async task to queue
285
            } elseif ($this->isAsyncTaskPayload($data)) {
286
                (new SwooleTaskJob($this->container, $server, $data, $taskId, $srcWorkerId))->fire();
287
            }
288
        } catch (Throwable $e) {
289
            $this->logServerError($e);
290
        }
291
    }
292
293
    /**
294
     * Set onFinish listener.
295
     *
296
     * @param mixed $server
297
     * @param string $taskId
298
     * @param mixed $data
299
     */
300
    public function onFinish($server, $taskId, $data)
301
    {
302
        // task worker callback
303
        $this->container->make('events')->dispatch('swoole.finish', func_get_args());
304
305
        return;
306
    }
307
308
    /**
309
     * Set onShutdown listener.
310
     */
311
    public function onShutdown()
312
    {
313
        $this->pidManager->delete();
314
    }
315
316
    /**
317
     * Set bindings to Laravel app.
318
     */
319
    protected function bindToLaravelApp()
320
    {
321
        $this->bindSandbox();
322
        $this->bindSwooleTable();
323
324
        if ($this->isServerWebsocket) {
325
            $this->bindRoom();
326
            $this->bindWebsocket();
327
        }
328
    }
329
330
    /**
331
     * Bind sandbox to Laravel app container.
332
     */
333
    protected function bindSandbox()
334
    {
335
        $this->app->singleton(Sandbox::class, function ($app) {
336
            return new Sandbox($app, $this->framework);
337
        });
338
339
        $this->app->alias(Sandbox::class, 'swoole.sandbox');
340
    }
341
342
    /**
343
     * Clear APC or OPCache.
344
     */
345
    protected function clearCache()
346
    {
347
        if (extension_loaded('apc')) {
348
            apc_clear_cache();
349
        }
350
351
        if (extension_loaded('Zend OPcache')) {
352
            opcache_reset();
353
        }
354
    }
355
356
    /**
357
     * Set process name.
358
     *
359
     * @codeCoverageIgnore
360
     *
361
     * @param $process
362
     */
363
    protected function setProcessName($process)
364
    {
365
        // MacOS doesn't support modifying process name.
366
        if (OS::is(OS::MAC_OS) || $this->isInTesting()) {
367
            return;
368
        }
369
        $serverName = 'swoole_http_server';
370
        $appName = $this->container->make('config')->get('app.name', 'Laravel');
371
372
        $name = sprintf('%s: %s for %s', $serverName, $process, $appName);
373
374
        swoole_set_process_name($name);
375
    }
376
377
    /**
378
     * Add process to http server
379
     *
380
     * @param \Swoole\Process $process
381
     */
382
    public function addProcess(Process $process): void
383
    {
384
        $this->container->make(Server::class)->addProcess($process);
0 ignored issues
show
Bug introduced by
The method addProcess() does not exist on SwooleTW\Http\Server\Facades\Server. ( Ignorable by Annotation )

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

384
        $this->container->make(Server::class)->/** @scrutinizer ignore-call */ addProcess($process);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
385
    }
386
387
    /**
388
     * Indicates if it's in phpunit environment.
389
     *
390
     * @return bool
391
     */
392
    protected function isInTesting()
393
    {
394
        return defined('IN_PHPUNIT') && IN_PHPUNIT;
0 ignored issues
show
Bug introduced by
The constant SwooleTW\Http\Server\IN_PHPUNIT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
395
    }
396
397
    /**
398
     * Log server error.
399
     *
400
     * @param \Throwable|\Exception $e
401
     */
402
    public function logServerError(Throwable $e)
403
    {
404
        if ($this->isInTesting()) {
405
            return;
406
        }
407
408
        $exception = $this->normalizeException($e);
409
        $this->container->make(ConsoleOutput::class)
410
            ->writeln(sprintf("<error>%s</error>", $exception));
411
412
        $this->container->make(ExceptionHandler::class)
413
            ->report($exception);
414
    }
415
416
    /**
417
     * Normalize a throwable/exception to exception.
418
     *
419
     * @param \Throwable|\Exception $e
420
     */
421
    protected function normalizeException(Throwable $e)
422
    {
423
        if (! $e instanceof Exception) {
424
            $e = new FatalThrowableError($e);
425
        }
426
427
        return $e;
428
    }
429
430
    /**
431
     * Indicates if the payload is async task.
432
     *
433
     * @param mixed $payload
434
     *
435
     * @return boolean
436
     */
437
    protected function isAsyncTaskPayload($payload): bool
438
    {
439
        $data = json_decode($payload, true);
440
441
        if (JSON_ERROR_NONE !== json_last_error()) {
442
            return false;
443
        }
444
445
        return isset($data['job']);
446
    }
447
}
448