Passed
Pull Request — master (#452)
by
unknown
03:41
created

HttpServerCommand::start()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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

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

275
        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

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

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