Completed
Push — master ( 5e80d2...67203c )
by
unknown
15s queued 13s
created

HttpServerCommand::hookAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 0
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 2
rs 10
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
        $hasTaskWorker = $isWebsocket || Arr::get($this->config, 'queue.default') === 'swoole';
223
        $logFile = Arr::get($this->config, 'server.options.log_file');
224
        $pids = $this->laravel->make(PidManager::class)->read();
225
        $masterPid = $pids['masterPid'] ?? null;
226
        $managerPid = $pids['managerPid'] ?? null;
227
228
        $table = [
229
            ['PHP Version', 'Version' => phpversion()],
230
            ['Swoole Version', 'Version' => swoole_version()],
231
            ['Laravel Version', $this->getApplication()->getVersion()],
232
            ['Listen IP', $host],
233
            ['Listen Port', $port],
234
            ['Server Status', $isRunning ? 'Online' : 'Offline'],
235
            ['Reactor Num', $reactorNum],
236
            ['Worker Num', $workerNum],
237
            ['Task Worker Num', $hasTaskWorker ? $taskWorkerNum : 0],
238
            ['Websocket Mode', $isWebsocket ? 'On' : 'Off'],
239
            ['Master PID', $isRunning ? $masterPid : 'None'],
240
            ['Manager PID', $isRunning && $managerPid ? $managerPid : 'None'],
241
            ['Log Path', $logFile],
242
        ];
243
244
        $this->table(['Name', 'Value'], $table);
245
    }
246
247
    /**
248
     * Initialize command action.
249
     */
250
    protected function initAction()
251
    {
252
        $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...
253
254
        if (! in_array($this->action, ['start', 'stop', 'restart', 'reload', 'infos'], true)) {
255
            $this->error(
256
                "Invalid argument '{$this->action}'. Expected 'start', 'stop', 'restart', 'reload' or 'infos'."
257
            );
258
259
            return;
260
        }
261
    }
262
263
    /**
264
     * @param \SwooleTW\Http\Server\Facades\Server $server
265
     *
266
     * @return \Swoole\Process
267
     */
268
    protected function getHotReloadProcess($server)
269
    {
270
        $recursively = Arr::get($this->config, 'hot_reload.recursively');
271
        $directory = Arr::get($this->config, 'hot_reload.directory');
272
        $filter = Arr::get($this->config, 'hot_reload.filter');
273
        $log = Arr::get($this->config, 'hot_reload.log');
274
275
        $cb = function (FSEvent $event) use ($server, $log) {
276
            $log ? $this->info(FSOutput::format($event)) : null;
277
            $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

277
            $server->/** @scrutinizer ignore-call */ 
278
                     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...
278
        };
279
280
        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

280
        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

280
        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

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