Completed
Push — master ( 46edea...a51709 )
by Biao
04:28
created

Portal::kill()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 6
rs 10
1
<?php
2
3
namespace Hhxsv5\LaravelS\Console;
4
5
use Hhxsv5\LaravelS\Components\Apollo\Apollo;
6
use Hhxsv5\LaravelS\Illuminate\LogTrait;
7
use Hhxsv5\LaravelS\LaravelS;
8
use Swoole\Process;
9
use Symfony\Component\Console\Command\Command;
10
use Symfony\Component\Console\Input\InputArgument;
11
use Symfony\Component\Console\Input\InputInterface;
12
use Symfony\Component\Console\Input\InputOption;
13
use Symfony\Component\Console\Output\OutputInterface;
14
use Symfony\Component\Console\Style\SymfonyStyle;
15
16
class Portal extends Command
17
{
18
    use LogTrait;
19
20
21
    /**@var string */
22
    protected $basePath;
23
24
    /**@var InputInterface */
25
    protected $input;
26
27
    /**@var OutputInterface */
28
    protected $output;
29
30
    public function __construct($basePath)
31
    {
32
        parent::__construct('laravels');
33
        $this->basePath = $basePath;
34
    }
35
36
    protected function configure()
37
    {
38
        $this->setDescription('LaravelS console tool');
39
        $this->setHelp('LaravelS console tool');
40
41
        $this->addArgument('action', InputArgument::OPTIONAL, 'start|stop|restart|reload|info|help', 'help');
42
        $this->addOption('env', 'e', InputOption::VALUE_OPTIONAL, 'The environment the command should run under, this feature requires Laravel 5.2+');
43
        $this->addOption('daemonize', 'd', InputOption::VALUE_NONE, 'Whether run as a daemon for "start & restart"');
44
        $this->addOption('ignore', 'i', InputOption::VALUE_NONE, 'Whether ignore checking process pid for "start & restart"');
45
        $this->addOption('apollo', 'a', InputOption::VALUE_NONE, 'Whether to import Apollo configurations to .env for "start & restart"');
46
    }
47
48
    protected function execute(InputInterface $input, OutputInterface $output)
49
    {
50
        $this->input = $input;
51
        $this->output = $output;
52
        LaravelS::setOutputStyle(new SymfonyStyle($this->input, $this->output));
53
54
        try {
55
            $action = $input->getArgument('action');
56
            switch ($action) {
57
                case 'start':
58
                    return $this->start();
59
                case 'stop':
60
                    return $this->stop();
61
                case 'restart':
62
                    return $this->restart();
63
                case 'reload':
64
                    return $this->reload();
65
                case 'info':
66
                    return $this->showInfo();
67
                default:
68
                    $help = <<<EOS
69
70
Usage: 
71
  [%s] ./bin/laravels [options] <action>
72
73
Arguments:
74
  action                start|stop|restart|reload|info|help
75
76
Options:
77
  -e, --env             The environment the command should run under, this feature requires Laravel 5.2+
78
  -d, --daemonize       Whether run as a daemon for "start & restart"
79
  -i, --ignore          Whether ignore checking process pid for "start & restart"
80
  -a, --apollo          Whether to import Apollo configurations to .env for "start & restart"
81
EOS;
82
83
                    $this->info(sprintf($help, PHP_BINARY));
84
                    return 0;
85
            }
86
        } catch (\Exception $e) {
87
            $error = sprintf(
88
                'Uncaught exception "%s"([%d]%s) at %s:%s, %s%s',
89
                get_class($e),
90
                $e->getCode(),
91
                $e->getMessage(),
92
                $e->getFile(),
93
                $e->getLine(),
94
                PHP_EOL,
95
                $e->getTraceAsString()
96
            );
97
            $this->error($error);
98
            return 1;
99
        }
100
    }
101
102
    public function start()
103
    {
104
        if (!extension_loaded('swoole')) {
105
            $this->error('LaravelS requires swoole extension, try to `pecl install swoole` and `php --ri swoole`.');
106
            return 1;
107
        }
108
109
        // Generate configuration storage/laravels.json
110
        $options = array_filter($this->input->getOptions());
111
        if (isset($options['env'])) {
112
            putenv('LARAVELS_ENV=' . $options['env']);
113
        }
114
115
        if (!empty($options['apollo'])) {
116
            // Load Apollo configurations to .env file
117
            $this->loadApollo();
118
            unset($options['apollo']);
119
        }
120
        unset($options['env']); // Pass env parameter in makeArtisanCmd()
121
122
        $optionStr = '';
123
        foreach ($options as $key => $value) {
124
            $optionStr .= sprintf('--%s%s ', $key, is_bool($value) ? '' : ('=' . $value));
125
        }
126
        $statusCode = $this->runArtisanCommand(trim('laravels config ' . $optionStr));
127
        if ($statusCode !== 0) {
128
            return $statusCode;
129
        }
130
131
        // Here we go...
132
        $config = $this->getConfig();
133
134
        if (!$config['server']['ignore_check_pid'] && file_exists($config['server']['swoole']['pid_file'])) {
135
            $pid = (int)file_get_contents($config['server']['swoole']['pid_file']);
136
            if ($pid > 0 && self::kill($pid, 0)) {
137
                $this->warning(sprintf('Swoole[PID=%d] is already running.', $pid));
138
                return 1;
139
            }
140
        }
141
142
        if ($config['server']['swoole']['daemonize']) {
143
            $this->trace('Swoole is running in daemon mode, see "ps -ef|grep laravels".');
144
        } else {
145
            $this->trace('Swoole is running, press Ctrl+C to quit.');
146
        }
147
148
        (new LaravelS($config['server'], $config['laravel']))->run();
149
150
        return 0;
151
    }
152
153
    public function stop()
154
    {
155
        $config = $this->getConfig();
156
        $pidFile = $config['server']['swoole']['pid_file'];
157
        if (!file_exists($pidFile)) {
158
            $this->warning('It seems that Swoole is not running.');
159
            return 0;
160
        }
161
162
        $pid = file_get_contents($pidFile);
163
        if (self::kill($pid, 0)) {
164
            if (self::kill($pid, SIGTERM)) {
165
                // Make sure that master process quit
166
                $time = 1;
167
                $waitTime = isset($config['server']['swoole']['max_wait_time']) ? $config['server']['swoole']['max_wait_time'] : 60;
168
                $this->info("The max time of waiting to forcibly stop is {$waitTime}s.");
169
                while (self::kill($pid, 0)) {
170
                    if ($time > $waitTime) {
171
                        $this->warning("Swoole [PID={$pid}] cannot be stopped gracefully in {$waitTime}s, will be stopped forced right now.");
172
                        return 1;
173
                    }
174
                    $this->info("Waiting Swoole[PID={$pid}] to stop. [{$time}]");
175
                    sleep(1);
176
                    $time++;
177
                }
178
                $basePath = dirname($pidFile);
179
                $deleteFiles = [
180
                    $pidFile,
181
                    $basePath . '/laravels-custom-processes.pid',
182
                    $basePath . '/laravels-timer-process.pid',
183
                ];
184
                foreach ($deleteFiles as $deleteFile) {
185
                    if (file_exists($deleteFile)) {
186
                        unlink($deleteFile);
187
                    }
188
                }
189
                $this->info("Swoole [PID={$pid}] is stopped.");
190
                return 0;
191
            } else {
192
                $this->error("Swoole [PID={$pid}] is stopped failed.");
193
                return 1;
194
            }
195
        } else {
196
            $this->warning("Swoole [PID={$pid}] does not exist, or permission denied.");
197
            return 0;
198
        }
199
    }
200
201
    public function restart()
202
    {
203
        $code = $this->stop();
204
        if ($code !== 0) {
205
            return $code;
206
        }
207
        return $this->start();
208
    }
209
210
    public function reload()
211
    {
212
        $config = $this->getConfig();
213
        $pidFile = $config['server']['swoole']['pid_file'];
214
        if (!file_exists($pidFile)) {
215
            $this->error('It seems that Swoole is not running.');
216
            return 1;
217
        }
218
219
        // Reload worker processes
220
        $pid = file_get_contents($pidFile);
221
        if (!$pid || !self::kill($pid, 0)) {
222
            $this->error("Swoole [PID={$pid}] does not exist, or permission denied.");
223
            return 1;
224
        }
225
        if (self::kill($pid, SIGUSR1)) {
226
            $this->info("Swoole [PID={$pid}] is reloaded.");
227
        } else {
228
            $this->error("Swoole [PID={$pid}] is reloaded failed.");
229
        }
230
231
        // Reload custom processes
232
        $pidFile = dirname($pidFile) . '/laravels-custom-processes.pid';
233
        if (file_exists($pidFile)) {
234
            $pids = (array)explode("\n", trim(file_get_contents($pidFile)));
235
            unlink($pidFile);
236
            foreach ($pids as $pid) {
237
                if (!$pid || !self::kill($pid, 0)) {
238
                    $this->error("Custom process[PID={$pid}] does not exist, or permission denied.");
239
                    continue;
240
                }
241
242
                if (self::kill($pid, SIGUSR1)) {
243
                    $this->info("Custom process[PID={$pid}] is reloaded.");
244
                } else {
245
                    $this->error("Custom process[PID={$pid}] is reloaded failed.");
246
                }
247
            }
248
        }
249
250
        // Reload timer process
251
        if (!empty($config['server']['timer']['enable']) && !empty($config['server']['timer']['jobs'])) {
252
            $pidFile = dirname($pidFile) . '/laravels-timer-process.pid';
253
            $pid = file_get_contents($pidFile);
254
            if (!$pid || !self::kill($pid, 0)) {
255
                $this->error("Timer process[PID={$pid}] does not exist, or permission denied.");
256
                return 1;
257
            }
258
259
            if (self::kill($pid, SIGUSR1)) {
260
                $this->info("Timer process[PID={$pid}] is reloaded.");
261
            } else {
262
                $this->error("Timer process[PID={$pid}] is reloaded failed.");
263
            }
264
        }
265
        return 0;
266
    }
267
268
    public function showInfo()
269
    {
270
        return $this->runArtisanCommand('laravels info');
271
    }
272
273
    public function loadApollo()
274
    {
275
        $envFile = $this->basePath . '/.env';
276
        $env = getenv('LARAVELS_ENV');
277
        if ($env) {
278
            $envFile .= '.' . $env;
279
        }
280
        $keepOld = isset($envs['APOLLO_KEEP_OLD']) ? $envs['APOLLO_KEEP_OLD'] : false;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $envs does not exist. Did you maybe mean $env?
Loading history...
281
        Apollo::createFromEnv()->pullAllAndSave($envFile, $keepOld);
282
    }
283
284
    public function makeArtisanCmd($subCmd)
285
    {
286
        $phpCmd = sprintf('%s -c "%s"', PHP_BINARY, php_ini_loaded_file());
287
        $env = $this->input->getOption('env');
288
        $envs = $env ? "APP_ENV={$env}" : '';
289
        return trim(sprintf('%s %s %s/artisan %s', $envs, $phpCmd, $this->basePath, $subCmd));
290
    }
291
292
    public function runArtisanCommand($cmd)
293
    {
294
        $cmd = $this->makeArtisanCmd($cmd);
295
        return self::runCommand($cmd);
296
    }
297
298
    public function getConfig()
299
    {
300
        $json = file_get_contents($this->basePath . '/storage/laravels.json');
301
        return (array)json_decode($json, true);
302
    }
303
304
    public static function runCommand($cmd, $input = null)
305
    {
306
        $fp = popen($cmd, 'w');
307
        if ($fp === false) {
308
            return false;
309
        }
310
        if ($input !== null) {
311
            $bytes = fwrite($fp, $input);
312
            if ($bytes === false) {
313
                return 1;
314
            }
315
        }
316
        return pclose($fp);
317
    }
318
319
    public static function kill($pid, $sig)
320
    {
321
        try {
322
            return Process::kill((int)$pid, $sig);
323
        } catch (\Exception $e) {
324
            return false;
325
        }
326
    }
327
}
328