Issues (87)

src/Server/Manager.php (3 issues)

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\Server\PidManager;
13
use SwooleTW\Http\Task\SwooleTaskJob;
14
use Illuminate\Support\Facades\Facade;
15
use SwooleTW\Http\Websocket\Websocket;
16
use SwooleTW\Http\Transformers\Request;
17
use SwooleTW\Http\Server\Facades\Server;
18
use SwooleTW\Http\Transformers\Response;
19
use SwooleTW\Http\Concerns\WithApplication;
20
use Illuminate\Contracts\Container\Container;
21
use Illuminate\Contracts\Debug\ExceptionHandler;
22
use SwooleTW\Http\Concerns\InteractsWithWebsocket;
23
use Symfony\Component\Console\Output\ConsoleOutput;
24
use SwooleTW\Http\Concerns\InteractsWithSwooleQueue;
25
use SwooleTW\Http\Concerns\InteractsWithSwooleTable;
26
use Symfony\Component\ErrorHandler\Error\FatalError;
27
use Laravel\Lumen\Http\Request as LumenRequest;
28
29
/**
30
 * Class Manager
31
 */
32
class Manager
33
{
34
    use InteractsWithWebsocket,
35
        InteractsWithSwooleTable,
36
        InteractsWithSwooleQueue,
37
        WithApplication;
38
39
    /**
40
     * Container.
41
     *
42
     * @var \Illuminate\Contracts\Container\Container
43
     */
44
    protected $container;
45
46
    /**
47
     * @var string
48
     */
49
    protected $framework;
50
51
    /**
52
     * @var string
53
     */
54
    protected $basePath;
55
56
    /**
57
     * Server events.
58
     *
59
     * @var array
60
     */
61
    protected $events = [
62
        'start',
63
        'shutDown',
64
        'workerStart',
65
        'workerStop',
66
        'packet',
67
        'bufferFull',
68
        'bufferEmpty',
69
        'task',
70
        'finish',
71
        'pipeMessage',
72
        'workerError',
73
        'managerStart',
74
        'managerStop',
75
        'request',
76
    ];
77
78
    /**
79
     * HTTP server manager constructor.
80
     *
81
     * @param \Illuminate\Contracts\Container\Container $container
82
     * @param string $framework
83
     * @param string $basePath
84
     *
85
     * @throws \Exception
86
     */
87
    public function __construct(Container $container, $framework, $basePath = null)
88
    {
89
        $this->container = $container;
90
        $this->setFramework($framework);
91
        $this->setBasepath($basePath);
92
        $this->initialize();
93
    }
94
95
    /**
96
     * Run swoole server.
97
     */
98
    public function run()
99
    {
100
        $this->container->make(Server::class)->start();
0 ignored issues
show
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

100
        $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...
101
    }
102
103
    /**
104
     * Stop swoole server.
105
     */
106
    public function stop()
107
    {
108
        $this->container->make(Server::class)->shutdown();
0 ignored issues
show
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

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

387
        $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...
388
    }
389
390
    /**
391
     * Indicates if it's in phpunit environment.
392
     *
393
     * @return bool
394
     */
395
    protected function isInTesting()
396
    {
397
        return defined('IN_PHPUNIT') && IN_PHPUNIT;
398
    }
399
400
    /**
401
     * Log server error.
402
     *
403
     * @param \Throwable|\Exception $e
404
     */
405
    public function logServerError(Throwable $e)
406
    {
407
        if ($this->isInTesting()) {
408
            return;
409
        }
410
411
        $exception = $this->normalizeException($e);
412
        $this->container->make(ConsoleOutput::class)
413
            ->writeln(sprintf("<error>%s</error>", $exception));
414
415
        $this->container->make(ExceptionHandler::class)
416
            ->report($exception);
417
    }
418
419
    /**
420
     * Normalize a throwable/exception to exception.
421
     *
422
     * @param \Throwable|\Exception $e
423
     */
424
    protected function normalizeException(Throwable $e)
425
    {
426
        if (! $e instanceof Exception) {
427
            if ($e instanceof \ParseError) {
428
                $severity = E_PARSE;
429
            } elseif ($e instanceof \TypeError) {
430
                $severity = E_RECOVERABLE_ERROR;
431
            } else {
432
                $severity = E_ERROR;
433
            }
434
435
            $error = [
436
                'type' => $severity,
437
                'message' => $e->getMessage(),
438
                'file' => $e->getFile(),
439
                'line' => $e->getLine(),
440
            ];
441
442
            $e = new FatalError($e->getMessage(), $e->getCode(), $error, null, true, $e->getTrace());
443
        }
444
445
        return $e;
446
    }
447
448
    /**
449
     * Indicates if the payload is async task.
450
     *
451
     * @param mixed $payload
452
     *
453
     * @return boolean
454
     */
455
    protected function isAsyncTaskPayload($payload): bool
456
    {
457
        $data = json_decode($payload, true);
458
459
        if (JSON_ERROR_NONE !== json_last_error()) {
460
            return false;
461
        }
462
463
        return isset($data['job']);
464
    }
465
}
466