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

HttpServerCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
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
     * A manager to handle pid about the application.
65
     *
66
     * @var PidManager
67
     */
68
    protected $pidManager;
69
70
    /**
71
     * Create a an new HttpServerCommand instance.
72
     *
73
     * @param PidManager $pidManager
74
     */
75
    public function __construct(PidManager $pidManager)
76
    {
77
        parent::__construct();
78
79
        $this->pidManager = $pidManager;
80
    }
81
82
    /**
83
     * Execute the console command.
84
     *
85
     * @return void
86
     */
87
    public function handle()
88
    {
89
        $this->checkEnvironment();
90
        $this->loadConfigs();
91
        $this->initAction();
92
        $this->runAction();
93
    }
94
95
    /**
96
     * Load configs.
97
     */
98
    protected function loadConfigs()
99
    {
100
        $this->config = $this->laravel->make('config')->get('swoole_http');
101
    }
102
103
    /**
104
     * Run action.
105
     */
106
    protected function runAction()
107
    {
108
        $this->{$this->action}();
109
    }
110
111
    /**
112
     * Run swoole_http_server.
113
     */
114
    protected function start()
115
    {
116
        if ($this->isRunning()) {
117
            $this->error('Failed! swoole_http_server process is already running.');
118
119
            return;
120
        }
121
122
        $host = Arr::get($this->config, 'server.host');
123
        $port = Arr::get($this->config, 'server.port');
124
        $hotReloadEnabled = Arr::get($this->config, 'hot_reload.enabled');
125
        $accessLogEnabled = Arr::get($this->config, 'server.access_log');
126
127
        $this->info('Starting swoole http server...');
128
        $this->info("Swoole http server started: <http://{$host}:{$port}>");
129
        if ($this->isDaemon()) {
130
            $this->info(
131
                '> (You can run this command to ensure the ' .
132
                'swoole_http_server process is running: ps aux|grep "swoole")'
133
            );
134
        }
135
136
        $manager = $this->laravel->make(Manager::class);
137
        $server = $this->laravel->make(Server::class);
138
139
        if ($accessLogEnabled) {
140
            $this->registerAccessLog();
141
        }
142
143
        if ($hotReloadEnabled) {
144
            $manager->addProcess($this->getHotReloadProcess($server));
145
        }
146
147
        $manager->run();
148
    }
149
150
    /**
151
     * Stop swoole_http_server.
152
     */
153
    protected function stop()
154
    {
155
        if (! $this->isRunning()) {
156
            $this->error("Failed! There is no swoole_http_server process running.");
157
158
            return;
159
        }
160
161
        $this->info('Stopping swoole http server...');
162
163
        $isRunning = $this->killProcess(SIGTERM, 15);
164
165
        if ($isRunning) {
166
            $this->error('Unable to stop the swoole_http_server process.');
167
168
            return;
169
        }
170
171
        // I don't known why Swoole didn't trigger "onShutdown" after sending SIGTERM.
172
        // So we should manually remove the pid file.
173
        $this->pidManager->delete();
174
175
        $this->info('> success');
176
    }
177
178
    /**
179
     * Restart swoole http server.
180
     */
181
    protected function restart()
182
    {
183
        if ($this->isRunning()) {
184
            $this->stop();
185
        }
186
187
        $this->start();
188
    }
189
190
    /**
191
     * Reload.
192
     */
193
    protected function reload()
194
    {
195
        if (! $this->isRunning()) {
196
            $this->error("Failed! There is no swoole_http_server process running.");
197
198
            return;
199
        }
200
201
        $this->info('Reloading swoole_http_server...');
202
203
        if (! $this->killProcess(SIGUSR1)) {
204
            $this->error('> failure');
205
206
            return;
207
        }
208
209
        $this->info('> success');
210
    }
211
212
    /**
213
     * Display PHP and Swoole misc info.
214
     */
215
    protected function infos()
216
    {
217
        $this->showInfos();
218
    }
219
220
    /**
221
     * Display PHP and Swoole miscs infos.
222
     */
223
    protected function showInfos()
224
    {
225
        $isRunning = $this->isRunning();
226
        $host = Arr::get($this->config, 'server.host');
227
        $port = Arr::get($this->config, 'server.port');
228
        $reactorNum = Arr::get($this->config, 'server.options.reactor_num');
229
        $workerNum = Arr::get($this->config, 'server.options.worker_num');
230
        $taskWorkerNum = Arr::get($this->config, 'server.options.task_worker_num');
231
        $isWebsocket = Arr::get($this->config, 'websocket.enabled');
232
        $logFile = Arr::get($this->config, 'server.options.log_file');
233
234
        $table = [
235
            ['PHP Version', 'Version' => phpversion()],
236
            ['Swoole Version', 'Version' => swoole_version()],
237
            ['Laravel Version', $this->getApplication()->getVersion()],
238
            ['Listen IP', $host],
239
            ['Listen Port', $port],
240
            ['Server Status', $isRunning ? 'Online' : 'Offline'],
241
            ['Reactor Num', $reactorNum],
242
            ['Worker Num', $workerNum],
243
            ['Task Worker Num', $isWebsocket ? $taskWorkerNum : 0],
244
            ['Websocket Mode', $isWebsocket ? 'On' : 'Off'],
245
            ['PID', $isRunning ? implode(', ', $this->pidManager->read()) : 'None'],
246
            ['Log Path', $logFile],
247
        ];
248
249
        $this->table(['Name', 'Value'], $table);
250
    }
251
252
    /**
253
     * Initialize command action.
254
     */
255
    protected function initAction()
256
    {
257
        $this->action = $this->argument('action');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->argument('action') can also be of type string[]. 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...
258
259
        if (! in_array($this->action, ['start', 'stop', 'restart', 'reload', 'infos'], true)) {
260
            $this->error(
261
                "Invalid argument '{$this->action}'. Expected 'start', 'stop', 'restart', 'reload' or 'infos'."
262
            );
263
264
            return;
265
        }
266
    }
267
268
    /**
269
     * @param \SwooleTW\Http\Server\Facades\Server $server
270
     *
271
     * @return \Swoole\Process
272
     */
273
    protected function getHotReloadProcess($server)
274
    {
275
        $recursively = Arr::get($this->config, 'hot_reload.recursively');
276
        $directory = Arr::get($this->config, 'hot_reload.directory');
277
        $filter = Arr::get($this->config, 'hot_reload.filter');
278
        $log = Arr::get($this->config, 'hot_reload.log');
279
280
        $cb = function (FSEvent $event) use ($server, $log) {
281
            $log ? $this->info(FSOutput::format($event)) : null;
282
            $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

282
            $server->/** @scrutinizer ignore-call */ 
283
                     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...
283
        };
284
285
        return (new FSProcess($filter, $recursively, $directory))->make($cb);
0 ignored issues
show
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

285
        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

285
        return (new FSProcess($filter, $recursively, /** @scrutinizer ignore-type */ $directory))->make($cb);
Loading history...
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

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