Passed
Push — master ( cbb991...62e47c )
by Albert
07:16 queued 05:08
created

Manager::isMacOS()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
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 Illuminate\Support\Str;
9
use SwooleTW\Http\Helpers\OS;
10
use SwooleTW\Http\Server\Sandbox;
11
use SwooleTW\Http\Task\SwooleTaskJob;
12
use Illuminate\Support\Facades\Facade;
13
use SwooleTW\Http\Websocket\Websocket;
14
use SwooleTW\Http\Transformers\Request;
15
use SwooleTW\Http\Server\Facades\Server;
16
use SwooleTW\Http\Transformers\Response;
17
use SwooleTW\Http\Concerns\WithApplication;
18
use Illuminate\Contracts\Container\Container;
19
use Illuminate\Contracts\Debug\ExceptionHandler;
20
use SwooleTW\Http\Concerns\InteractsWithWebsocket;
21
use SwooleTW\Http\Concerns\InteractsWithSwooleTable;
22
use Symfony\Component\Debug\Exception\FatalThrowableError;
23
24
/**
25
 * Class Manager
26
 */
27
class Manager
28
{
29
    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, $connections
Loading history...
30
        InteractsWithSwooleTable,
31
        WithApplication;
32
33
    /**
34
     * Container.
35
     *
36
     * @var \Illuminate\Contracts\Container\Container
37
     */
38
    protected $container;
39
40
    /**
41
     * @var string
42
     */
43
    protected $framework;
44
45
    /**
46
     * @var string
47
     */
48
    protected $basePath;
49
50
    /**
51
     * Server events.
52
     *
53
     * @var array
54
     */
55
    protected $events = [
56
        'start',
57
        'shutDown',
58
        'workerStart',
59
        'workerStop',
60
        'packet',
61
        'bufferFull',
62
        'bufferEmpty',
63
        'task',
64
        'finish',
65
        'pipeMessage',
66
        'workerError',
67
        'managerStart',
68
        'managerStop',
69
        'request',
70
    ];
71
72
    /**
73
     * HTTP server manager constructor.
74
     *
75
     * @param \Illuminate\Contracts\Container\Container $container
76
     * @param string $framework
77
     * @param string $basePath
78
     *
79
     * @throws \Exception
80
     */
81
    public function __construct(Container $container, $framework, $basePath = null)
82
    {
83
        $this->container = $container;
84
        $this->setFramework($framework);
85
        $this->setBasepath($basePath);
86
        $this->initialize();
87
    }
88
89
    /**
90
     * Run swoole server.
91
     */
92
    public function run()
93
    {
94
        $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

94
        $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...
95
    }
96
97
    /**
98
     * Stop swoole server.
99
     */
100
    public function stop()
101
    {
102
        $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

102
        $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...
103
    }
104
105
    /**
106
     * Initialize.
107
     */
108
    protected function initialize()
109
    {
110
        $this->createTables();
111
        $this->prepareWebsocket();
112
        $this->setSwooleServerListeners();
113
    }
114
115
    /**
116
     * Set swoole server listeners.
117
     */
118
    protected function setSwooleServerListeners()
119
    {
120
        foreach ($this->events as $event) {
121
            $listener = Str::camel("on_$event");
122
            $callback = method_exists($this, $listener) ? [$this, $listener] : function () use ($event) {
123
                $this->container->make('events')->fire("swoole.$event", func_get_args());
124
            };
125
126
            $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

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

401
        $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...
402
    }
403
404
    /**
405
     * Indicates if it's in phpunit environment.
406
     *
407
     * @return bool
408
     */
409
    protected function isInTesting()
410
    {
411
        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...
412
    }
413
414
    /**
415
     * Log server error.
416
     *
417
     * @param \Throwable|\Exception $e
418
     */
419
    public function logServerError(Throwable $e)
420
    {
421
        $this->container
422
            ->make(ExceptionHandler::class)
423
            ->report(
424
                $this->normalizeException($e)
425
            );
426
    }
427
428
    /**
429
     * Normalize a throwable/exception to exception.
430
     *
431
     * @param \Throwable|\Exception $e
432
     */
433
    protected function normalizeException(Throwable $e)
434
    {
435
        if (! $e instanceof Exception) {
436
            $e = new FatalThrowableError($e);
437
        }
438
439
        return $e;
440
    }
441
442
    /**
443
     * Indicates if the payload is async task.
444
     *
445
     * @param mixed $payload
446
     *
447
     * @return boolean
448
     */
449
    protected function isAsyncTaskPayload($payload): bool
450
    {
451
        $data = json_decode($payload, true);
452
453
        if (JSON_ERROR_NONE !== json_last_error()) {
454
            return false;
455
        }
456
457
        return isset($data['job']);
458
    }
459
}
460