Passed
Push — master ( 4c5fe3...29c6bf )
by
unknown
04:49 queued 11s
created

HttpServerCommand::showInfos()   C

Complexity

Conditions 9
Paths 256

Size

Total Lines 46
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 2 Features 0
Metric Value
cc 9
eloc 35
c 6
b 2
f 0
nc 256
nop 0
dl 0
loc 46
rs 6.5222
1
<?php
2
3
namespace SwooleTW\Http\Commands;
4
5
use Throwable;
6
use Swoole\Process;
7
use Illuminate\Support\Arr;
8
use SwooleTW\Http\Helpers\OS;
9
use Illuminate\Console\Command;
10
use SwooleTW\Http\Server\Manager;
11
use Illuminate\Console\OutputStyle;
12
use SwooleTW\Http\HotReload\FSEvent;
13
use SwooleTW\Http\HotReload\FSOutput;
14
use SwooleTW\Http\HotReload\FSProcess;
15
use SwooleTW\Http\Server\AccessOutput;
16
use SwooleTW\Http\Server\PidManager;
17
use SwooleTW\Http\Middleware\AccessLog;
18
use SwooleTW\Http\Server\Facades\Server;
19
use Illuminate\Contracts\Container\Container;
20
use Symfony\Component\Console\Output\ConsoleOutput;
21
22
/**
23
 * @codeCoverageIgnore
24
 */
25
class HttpServerCommand extends Command
26
{
27
    /**
28
     * The name and signature of the console command.
29
     *
30
     * @var string
31
     */
32
    protected $signature = 'swoole:http {action : start|stop|restart|reload|infos}';
33
34
    /**
35
     * The console command description.
36
     *
37
     * @var string
38
     */
39
    protected $description = 'Swoole HTTP Server controller.';
40
41
    /**
42
     * The console command action. start|stop|restart|reload
43
     *
44
     * @var string
45
     */
46
    protected $action;
47
48
    /**
49
     *
50
     * The pid.
51
     *
52
     * @var int
53
     */
54
    protected $currentPid;
55
56
    /**
57
     * The configs for this package.
58
     *
59
     * @var array
60
     */
61
    protected $config;
62
63
    /**
64
     * Execute the console command.
65
     *
66
     * @return void
67
     */
68
    public function handle()
69
    {
70
        $this->checkEnvironment();
71
        $this->loadConfigs();
72
        $this->initAction();
73
        $this->hookAction();
74
        $this->runAction();
75
    }
76
77
    /**
78
     * Load configs.
79
     */
80
    protected function loadConfigs()
81
    {
82
        $this->config = $this->laravel->make('config')->get('swoole_http');
83
    }
84
    
85
    /**
86
     * Hook action
87
     */
88
    protected function hookAction()
89
    {
90
        // custom hook task before starting server
91
    }
92
93
    /**
94
     * Run action.
95
     */
96
    protected function runAction()
97
    {
98
        $this->{$this->action}();
99
    }
100
101
    /**
102
     * Run swoole_http_server.
103
     */
104
    protected function start()
105
    {
106
        if ($this->isRunning()) {
107
            $this->error('Failed! swoole_http_server process is already running.');
108
109
            return;
110
        }
111
112
        $host = Arr::get($this->config, 'server.host');
113
        $port = Arr::get($this->config, 'server.port');
114
        $hotReloadEnabled = Arr::get($this->config, 'hot_reload.enabled');
115
        $accessLogEnabled = Arr::get($this->config, 'server.access_log');
116
117
        $this->info('Starting swoole http server...');
118
        $this->info("Swoole http server started: <http://{$host}:{$port}>");
119
        if ($this->isDaemon()) {
120
            $this->info(
121
                '> (You can run this command to ensure the ' .
122
                'swoole_http_server process is running: ps aux|grep "swoole")'
123
            );
124
        }
125
126
        $manager = $this->laravel->make(Manager::class);
127
        $server = $this->laravel->make(Server::class);
128
129
        if ($accessLogEnabled) {
130
            $this->registerAccessLog();
131
        }
132
133
        if ($hotReloadEnabled) {
134
            $manager->addProcess($this->getHotReloadProcess($server));
135
        }
136
137
        $manager->run();
138
    }
139
140
    /**
141
     * Stop swoole_http_server.
142
     */
143
    protected function stop()
144
    {
145
        if (! $this->isRunning()) {
146
            $this->error("Failed! There is no swoole_http_server process running.");
147
148
            return;
149
        }
150
151
        $this->info('Stopping swoole http server...');
152
153
        $isRunning = $this->killProcess(SIGTERM, 15);
154
155
        if ($isRunning) {
156
            $this->error('Unable to stop the swoole_http_server process.');
157
158
            return;
159
        }
160
161
        // I don't known why Swoole didn't trigger "onShutdown" after sending SIGTERM.
162
        // So we should manually remove the pid file.
163
        $this->laravel->make(PidManager::class)->delete();
164
165
        $this->info('> success');
166
    }
167
168
    /**
169
     * Restart swoole http server.
170
     */
171
    protected function restart()
172
    {
173
        if ($this->isRunning()) {
174
            $this->stop();
175
        }
176
177
        $this->start();
178
    }
179
180
    /**
181
     * Reload.
182
     */
183
    protected function reload()
184
    {
185
        if (! $this->isRunning()) {
186
            $this->error("Failed! There is no swoole_http_server process running.");
187
188
            return;
189
        }
190
191
        $this->info('Reloading swoole_http_server...');
192
193
        if (! $this->killProcess(SIGUSR1)) {
194
            $this->error('> failure');
195
196
            return;
197
        }
198
199
        $this->info('> success');
200
    }
201
202
    /**
203
     * Display PHP and Swoole misc info.
204
     */
205
    protected function infos()
206
    {
207
        $this->showInfos();
208
    }
209
210
    /**
211
     * Display PHP and Swoole miscs infos.
212
     */
213
    protected function showInfos()
214
    {
215
        $isRunning = $this->isRunning();
216
        $host = Arr::get($this->config, 'server.host');
217
        $port = Arr::get($this->config, 'server.port');
218
        $reactorNum = Arr::get($this->config, 'server.options.reactor_num');
219
        $workerNum = Arr::get($this->config, 'server.options.worker_num');
220
        $taskWorkerNum = Arr::get($this->config, 'server.options.task_worker_num');
221
        $isWebsocket = Arr::get($this->config, 'websocket.enabled');
222
223
        $queueConfig = $this->laravel->make('config')->get('queue');
224
        
225
        // lookup for set swoole driver
226
        $isDefinedSwooleDriver = in_array(
227
            'swoole',
228
            array_column(
229
                $queueConfig['connections'] ?? [],
230
                'driver'
231
            ),
232
            true
233
        ) || ($queueConfig['default'] ?? null) === 'swoole';
234
235
        $hasTaskWorker = $isWebsocket || $isDefinedSwooleDriver;
236
237
        $logFile = Arr::get($this->config, 'server.options.log_file');
238
        $pids = $this->laravel->make(PidManager::class)->read();
239
        $masterPid = $pids['masterPid'] ?? null;
240
        $managerPid = $pids['managerPid'] ?? null;
241
242
        $table = [
243
            ['PHP Version', 'Version' => phpversion()],
244
            ['Swoole Version', 'Version' => swoole_version()],
245
            ['Laravel Version', $this->getApplication()->getVersion()],
246
            ['Listen IP', $host],
247
            ['Listen Port', $port],
248
            ['Server Status', $isRunning ? 'Online' : 'Offline'],
249
            ['Reactor Num', $reactorNum],
250
            ['Worker Num', $workerNum],
251
            ['Task Worker Num', $hasTaskWorker ? $taskWorkerNum : 0],
252
            ['Websocket Mode', $isWebsocket ? 'On' : 'Off'],
253
            ['Master PID', $isRunning ? $masterPid : 'None'],
254
            ['Manager PID', $isRunning && $managerPid ? $managerPid : 'None'],
255
            ['Log Path', $logFile],
256
        ];
257
258
        $this->table(['Name', 'Value'], $table);
259
    }
260
261
    /**
262
     * Initialize command action.
263
     */
264
    protected function initAction()
265
    {
266
        $this->action = $this->argument('action');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->argument('action') can also be of type array. However, the property $action is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
267
268
        if (! in_array($this->action, ['start', 'stop', 'restart', 'reload', 'infos'], true)) {
269
            $this->error(
270
                "Invalid argument '{$this->action}'. Expected 'start', 'stop', 'restart', 'reload' or 'infos'."
271
            );
272
273
            return;
274
        }
275
    }
276
277
    /**
278
     * @param \SwooleTW\Http\Server\Facades\Server $server
279
     *
280
     * @return \Swoole\Process
281
     */
282
    protected function getHotReloadProcess($server)
283
    {
284
        $recursively = Arr::get($this->config, 'hot_reload.recursively');
285
        $directory = Arr::get($this->config, 'hot_reload.directory');
286
        $filter = Arr::get($this->config, 'hot_reload.filter');
287
        $log = Arr::get($this->config, 'hot_reload.log');
288
289
        $cb = function (FSEvent $event) use ($server, $log) {
290
            $log ? $this->info(FSOutput::format($event)) : null;
291
            $server->reload();
0 ignored issues
show
Bug introduced by
The method reload() 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

291
            $server->/** @scrutinizer ignore-call */ 
292
                     reload();

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...
292
        };
293
294
        return (new FSProcess($filter, $recursively, $directory))->make($cb);
0 ignored issues
show
Bug introduced by
It seems like $filter can also be of type null; however, parameter $filter of SwooleTW\Http\HotReload\FSProcess::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

294
        return (new FSProcess(/** @scrutinizer ignore-type */ $filter, $recursively, $directory))->make($cb);
Loading history...
Bug introduced by
It seems like $recursively can also be of type null; however, parameter $recursively of SwooleTW\Http\HotReload\FSProcess::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

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

294
        return (new FSProcess($filter, /** @scrutinizer ignore-type */ $recursively, $directory))->make($cb);
Loading history...
Bug introduced by
It seems like $directory can also be of type null; however, parameter $directory of SwooleTW\Http\HotReload\FSProcess::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

294
        return (new FSProcess($filter, $recursively, /** @scrutinizer ignore-type */ $directory))->make($cb);
Loading history...
295
    }
296
297
    /**
298
     * If Swoole process is running.
299
     *
300
     * @param int $pid
301
     *
302
     * @return bool
303
     */
304
    public function isRunning()
305
    {
306
        $pids = $this->laravel->make(PidManager::class)->read();
307
308
        if (! count($pids)) {
309
            return false;
310
        }
311
312
        $masterPid = $pids['masterPid'] ?? null;
313
        $managerPid = $pids['managerPid'] ?? null;
314
315
        if ($managerPid) {
316
            // Swoole process mode
317
            return $masterPid && $managerPid && Process::kill((int) $managerPid, 0);
318
        }
319
320
        // Swoole base mode, no manager process
321
        return $masterPid && Process::kill((int) $masterPid, 0);
322
    }
323
324
    /**
325
     * Kill process.
326
     *
327
     * @param int $sig
328
     * @param int $wait
329
     *
330
     * @return bool
331
     */
332
    protected function killProcess($sig, $wait = 0)
333
    {
334
        Process::kill(
335
            Arr::first($this->laravel->make(PidManager::class)->read()),
336
            $sig
337
        );
338
339
        if ($wait) {
340
            $start = time();
341
342
            do {
343
                if (! $this->isRunning()) {
344
                    break;
345
                }
346
347
                usleep(100000);
348
            } while (time() < $start + $wait);
349
        }
350
351
        return $this->isRunning();
352
    }
353
354
    /**
355
     * Return daemonize config.
356
     */
357
    protected function isDaemon(): bool
358
    {
359
        return Arr::get($this->config, 'server.options.daemonize', false);
360
    }
361
362
    /**
363
     * Check running enironment.
364
     */
365
    protected function checkEnvironment()
366
    {
367
        if (OS::is(OS::WIN)) {
368
            $this->error('Swoole extension doesn\'t support Windows OS.');
369
370
            exit(1);
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...
371
        }
372
373
        if (! extension_loaded('swoole')) {
374
            $this->error('Can\'t detect Swoole extension installed.');
375
376
            exit(1);
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...
377
        }
378
379
        if (! version_compare(swoole_version(), '4.3.1', 'ge')) {
380
            $this->error('Your Swoole version must be higher than `4.3.1`.');
381
382
            exit(1);
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...
383
        }
384
    }
385
386
    /**
387
     * Register access log services.
388
     */
389
    protected function registerAccessLog()
390
    {
391
        $this->laravel->singleton(OutputStyle::class, function () {
392
            return new OutputStyle($this->input, $this->output);
393
        });
394
395
        $this->laravel->singleton(AccessOutput::class, function () {
396
            return new AccessOutput(new ConsoleOutput);
397
        });
398
399
        $this->laravel->singleton(AccessLog::class, function (Container $container) {
400
            return new AccessLog($container->make(AccessOutput::class));
401
        });
402
    }
403
}
404