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

HttpServerCommand::getPid()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 0
dl 0
loc 20
rs 9.9
c 0
b 0
f 0

1 Method

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

270
            $server->/** @scrutinizer ignore-call */ 
271
                     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...
271
        };
272
273
        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

273
        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

273
        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

273
        return (new FSProcess(/** @scrutinizer ignore-type */ $filter, $recursively, $directory))->make($cb);
Loading history...
274
    }
275
276
    /**
277
     * If Swoole process is running.
278
     *
279
     * @param int $pid
280
     *
281
     * @return bool
282
     */
283
    protected function isRunning($pid)
284
    {
285
        if (! $pid) {
286
            return false;
287
        }
288
289
        try {
290
            return Process::kill($pid, 0);
291
        } catch (Throwable $e) {
292
            return false;
293
        }
294
    }
295
296
    /**
297
     * Kill process.
298
     *
299
     * @param int $pid
300
     * @param int $sig
301
     * @param int $wait
302
     *
303
     * @return bool
304
     */
305
    protected function killProcess($pid, $sig, $wait = 0)
306
    {
307
        Process::kill($pid, $sig);
308
309
        if ($wait) {
310
            $start = time();
311
312
            do {
313
                if (! $this->isRunning($pid)) {
314
                    break;
315
                }
316
317
                usleep(100000);
318
            } while (time() < $start + $wait);
319
        }
320
321
        return $this->isRunning($pid);
322
    }
323
324
    /**
325
     * Get pid.
326
     *
327
     * @return int|null
328
     */
329
    protected function getCurrentPid()
330
    {
331
        if ($this->currentPid) {
332
            return $this->currentPid;
333
        }
334
335
        $path = $this->getPidPath();
336
337
        return $this->currentPid = file_exists($path)
338
            ? (int) file_get_contents($path) ?? $this->removePidFile()
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->removePidFile() targeting SwooleTW\Http\Commands\H...ommand::removePidFile() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
339
            : null;
340
    }
341
342
    /**
343
     * Get Pid file path.
344
     *
345
     * @return string
346
     */
347
    protected function getPidPath()
348
    {
349
        return Arr::get($this->config, 'server.options.pid_file', storage_path('logs/swoole.pid'));
350
    }
351
352
    /**
353
     * Remove Pid file.
354
     */
355
    protected function removePidFile()
356
    {
357
        if (file_exists($this->getPidPath())) {
358
            unlink($this->getPidPath());
359
        }
360
    }
361
362
    /**
363
     * Return daemonize config.
364
     */
365
    protected function isDaemon(): bool
366
    {
367
        return Arr::get($this->config, 'server.options.daemonize', false);
368
    }
369
370
    /**
371
     * Check running enironment.
372
     */
373
    protected function checkEnvironment()
374
    {
375
        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
376
            $this->error("Swoole extension doesn't support Windows OS yet.");
377
378
            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...
379
        }
380
381
        if (! extension_loaded('swoole')) {
382
            $this->error("Can't detect Swoole extension installed.");
383
384
            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...
385
        }
386
387
        if (! version_compare(swoole_version(), '4.0.0', 'ge')) {
388
            $this->error("Your Swoole version must be higher than 4.0 to use coroutine.");
389
390
            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...
391
        }
392
    }
393
394
    /**
395
     * Register access log services.
396
     */
397
    protected function registerAccessLog()
398
    {
399
        $this->laravel->singleton(OutputStyle::class, function () {
400
            return new OutputStyle($this->input, $this->output);
401
        });
402
403
        $this->laravel->singleton(AccessOutput::class, function () {
404
            return new AccessOutput(new ConsoleOutput);
405
        });
406
407
        $this->laravel->singleton(AccessLog::class, function (Container $container) {
408
            return new AccessLog($container->make(AccessOutput::class));
409
        });
410
    }
411
}
412