Passed
Pull Request — master (#503)
by
unknown
05:15
created

HttpServerCommand::killProcess()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 3
nop 2
dl 0
loc 20
rs 9.9
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
     * 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
        // lookup for settled swoole driver
224
        $isDefinedSwooleDriver = in_array(
225
            'swoole',
226
            array_column(
227
                $queueConfig['connections'] ?? [],
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $queueConfig seems to never exist and therefore isset should always be false.
Loading history...
228
                'driver'
229
            ),
230
            true
231
        ) || ($queueConfig['default'] ?? null) === 'swoole';
232
233
        $hasTaskWorker = $isWebsocket || $isDefinedSwooleDriver;
234
235
        $logFile = Arr::get($this->config, 'server.options.log_file');
236
        $pids = $this->laravel->make(PidManager::class)->read();
237
        $masterPid = $pids['masterPid'] ?? null;
238
        $managerPid = $pids['managerPid'] ?? null;
239
240
        $table = [
241
            ['PHP Version', 'Version' => phpversion()],
242
            ['Swoole Version', 'Version' => swoole_version()],
243
            ['Laravel Version', $this->getApplication()->getVersion()],
244
            ['Listen IP', $host],
245
            ['Listen Port', $port],
246
            ['Server Status', $isRunning ? 'Online' : 'Offline'],
247
            ['Reactor Num', $reactorNum],
248
            ['Worker Num', $workerNum],
249
            ['Task Worker Num', $hasTaskWorker ? $taskWorkerNum : 0],
250
            ['Websocket Mode', $isWebsocket ? 'On' : 'Off'],
251
            ['Master PID', $isRunning ? $masterPid : 'None'],
252
            ['Manager PID', $isRunning && $managerPid ? $managerPid : 'None'],
253
            ['Log Path', $logFile],
254
        ];
255
256
        $this->table(['Name', 'Value'], $table);
257
    }
258
259
    /**
260
     * Initialize command action.
261
     */
262
    protected function initAction()
263
    {
264
        $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...
265
266
        if (! in_array($this->action, ['start', 'stop', 'restart', 'reload', 'infos'], true)) {
267
            $this->error(
268
                "Invalid argument '{$this->action}'. Expected 'start', 'stop', 'restart', 'reload' or 'infos'."
269
            );
270
271
            return;
272
        }
273
    }
274
275
    /**
276
     * @param \SwooleTW\Http\Server\Facades\Server $server
277
     *
278
     * @return \Swoole\Process
279
     */
280
    protected function getHotReloadProcess($server)
281
    {
282
        $recursively = Arr::get($this->config, 'hot_reload.recursively');
283
        $directory = Arr::get($this->config, 'hot_reload.directory');
284
        $filter = Arr::get($this->config, 'hot_reload.filter');
285
        $log = Arr::get($this->config, 'hot_reload.log');
286
287
        $cb = function (FSEvent $event) use ($server, $log) {
288
            $log ? $this->info(FSOutput::format($event)) : null;
289
            $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

289
            $server->/** @scrutinizer ignore-call */ 
290
                     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...
290
        };
291
292
        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

292
        return (new FSProcess($filter, /** @scrutinizer ignore-type */ $recursively, $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

292
        return (new FSProcess(/** @scrutinizer ignore-type */ $filter, $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

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