Passed
Push — watcher ( 4595cf...e2ba10 )
by Arnaud
03:54
created

Serve::execute()   F

Complexity

Conditions 32
Paths > 20000

Size

Total Lines 173
Code Lines 118

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 2 Features 0
Metric Value
cc 32
eloc 118
nc 2906113
nop 2
dl 0
loc 173
rs 0
c 6
b 2
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Command;
15
16
use Cecil\Exception\RuntimeException;
17
use Cecil\Util;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Input\InputOption;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
23
use Symfony\Component\Finder\Finder;
24
use Symfony\Component\Process\Exception\ProcessFailedException;
25
use Symfony\Component\Process\PhpExecutableFinder;
26
use Symfony\Component\Process\Process;
27
use Yosymfony\ResourceWatcher\Crc32ContentHash;
28
use Yosymfony\ResourceWatcher\ResourceCacheMemory;
29
use Yosymfony\ResourceWatcher\ResourceWatcher;
30
31
/**
32
 * Starts the built-in server.
33
 */
34
class Serve extends AbstractCommand
35
{
36
    /** @var boolean */
37
    protected $watcherEnabled;
38
39
    /**
40
     * {@inheritdoc}
41
     */
42
    protected function configure()
43
    {
44
        $this
45
            ->setName('serve')
46
            ->setDescription('Starts the built-in server')
47
            ->setDefinition([
48
                new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'),
49
                new InputOption('open', 'o', InputOption::VALUE_NONE, 'Open web browser automatically'),
50
                new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Server host', 'localhost'),
51
                new InputOption('port', null, InputOption::VALUE_REQUIRED, 'Server port', '8000'),
52
                new InputOption('watch', 'w', InputOption::VALUE_NEGATABLE, 'Enable (or disable --no-watch) changes watcher (enabled by default)', true),
53
                new InputOption('drafts', 'd', InputOption::VALUE_NONE, 'Include drafts'),
54
                new InputOption('optimize', null, InputOption::VALUE_NEGATABLE, 'Enable (or disable --no-optimize) optimization of generated files'),
55
                new InputOption('config', 'c', InputOption::VALUE_REQUIRED, 'Set the path to extra config files (comma-separated)'),
56
                new InputOption('clear-cache', null, InputOption::VALUE_OPTIONAL, 'Clear cache before build (optional cache key as regular expression)', false),
57
                new InputOption('page', 'p', InputOption::VALUE_REQUIRED, 'Build a specific page'),
58
                new InputOption('no-ignore-vcs', null, InputOption::VALUE_NONE, 'Changes watcher must not ignore VCS directories'),
59
                new InputOption('metrics', 'm', InputOption::VALUE_NONE, 'Show build metrics (duration and memory) of each step'),
60
                new InputOption('timeout', null, InputOption::VALUE_REQUIRED, 'Sets the process timeout (max. runtime) in seconds', 7200), // default is 2 hours
61
            ])
62
            ->setHelp(
63
                <<<'EOF'
64
The <info>%command.name%</> command starts the live-reloading-built-in web server.
65
66
  <info>%command.full_name%</>
67
  <info>%command.full_name% path/to/the/working/directory</>
68
  <info>%command.full_name% --open</>
69
  <info>%command.full_name% --drafts</>
70
  <info>%command.full_name% --no-watch</>
71
72
You can use a custom host and port by using the <info>--host</info> and <info>--port</info> options:
73
74
  <info>%command.full_name% --host=127.0.0.1 --port=8080</>
75
76
To build the website with an extra configuration file, you can use the <info>--config</info> option.
77
This is useful during local development to <comment>override some settings</comment> without modifying the main configuration:
78
79
  <info>%command.full_name% --config=config/dev.yml</>
80
81
To start the server with changes watcher <comment>not ignoring VCS</comment> directories, run:
82
83
  <info>%command.full_name% --no-ignore-vcs</>
84
85
To define the process <comment>timeout</comment> (in seconds), run:
86
87
  <info>%command.full_name% --timeout=7200</>
88
EOF
89
            );
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     *
95
     * @throws RuntimeException
96
     */
97
    protected function execute(InputInterface $input, OutputInterface $output)
98
    {
99
        $open = $input->getOption('open');
100
        $host = $input->getOption('host');
101
        $port = $input->getOption('port');
102
        $drafts = $input->getOption('drafts');
103
        $optimize = $input->getOption('optimize');
104
        $clearcache = $input->getOption('clear-cache');
105
        $page = $input->getOption('page');
106
        $noignorevcs = $input->getOption('no-ignore-vcs');
107
        $metrics = $input->getOption('metrics');
108
        $timeout = $input->getOption('timeout');
109
        $verbose = $input->getOption('verbose');
110
111
        $resourceWatcher = null;
112
        $this->watcherEnabled = $input->getOption('watch');
113
114
        // checks if PHP executable is available
115
        $phpFinder = new PhpExecutableFinder();
116
        $php = $phpFinder->find();
117
        if ($php === false) {
118
            throw new RuntimeException('Can\'t find a local PHP executable.');
119
        }
120
121
        // setup server
122
        $this->setUpServer($host, $port);
123
        $command = \sprintf(
124
            '"%s" -S %s:%d -t "%s" "%s"',
125
            $php,
126
            $host,
127
            $port,
128
            $this->getBuilder()->getConfig()->getOutputPath(),
129
            Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php')
130
        );
131
        $process = Process::fromShellCommandline($command);
132
133
        // setup build process
134
        $buildProcessArguments = [
135
            $php,
136
            $_SERVER['argv'][0],
137
        ];
138
        $buildProcessArguments[] = 'build';
139
        $buildProcessArguments[] = $this->getPath();
140
        if (!empty($this->getConfigFiles())) {
141
            $buildProcessArguments[] = '--config';
142
            $buildProcessArguments[] = implode(',', $this->getConfigFiles());
143
        }
144
        if ($drafts) {
145
            $buildProcessArguments[] = '--drafts';
146
        }
147
        if ($optimize === true) {
148
            $buildProcessArguments[] = '--optimize';
149
        }
150
        if ($optimize === false) {
151
            $buildProcessArguments[] = '--no-optimize';
152
        }
153
        if ($clearcache === null) {
154
            $buildProcessArguments[] = '--clear-cache';
155
        }
156
        if (!empty($clearcache)) {
157
            $buildProcessArguments[] = '--clear-cache';
158
            $buildProcessArguments[] = $clearcache;
159
        }
160
        if ($verbose) {
161
            $buildProcessArguments[] = '-' . str_repeat('v', $_SERVER['SHELL_VERBOSITY']);
162
        }
163
        if (!empty($page)) {
164
            $buildProcessArguments[] = '--page';
165
            $buildProcessArguments[] = $page;
166
        }
167
        if (!empty($metrics)) {
168
            $buildProcessArguments[] = '--metrics';
169
        }
170
        $buildProcess = new Process(
171
            $buildProcessArguments,
172
            null,
173
            ['BOX_REQUIREMENT_CHECKER' => '0'] // prevents double check (build then serve)
174
        );
175
        $buildProcess->setTty(Process::isTtySupported());
176
        $buildProcess->setPty(Process::isPtySupported());
177
        $buildProcess->setTimeout((float) $timeout);
178
        $processOutputCallback = function ($type, $buffer) use ($output) {
179
            $output->write($buffer, false, OutputInterface::OUTPUT_RAW);
180
        };
181
182
        // builds before serve
183
        $output->writeln(\sprintf('<comment>Build process: %s</comment>', implode(' ', $buildProcessArguments)), OutputInterface::VERBOSITY_DEBUG);
184
        $buildProcess->run($processOutputCallback);
185
        if ($buildProcess->isSuccessful()) {
186
            $this->buildSuccessActions($output);
187
        }
188
        if ($buildProcess->getExitCode() !== 0) {
189
            return 1;
190
        }
191
192
        // handles serve process
193
        if (!$process->isStarted()) {
194
            $messageSuffix = '';
195
            // setup resource watcher
196
            if ($this->watcherEnabled) {
197
                $resourceWatcher = $this->setupWatcher($noignorevcs);
198
                $resourceWatcher->initialize();
199
                $messageSuffix = ' with changes watcher';
200
            }
201
            // starts server
202
            try {
203
                if (\function_exists('\pcntl_signal')) {
204
                    pcntl_async_signals(true);
205
                    pcntl_signal(SIGINT, [$this, 'tearDownServer']);
206
                    pcntl_signal(SIGTERM, [$this, 'tearDownServer']);
207
                }
208
                $output->writeln(\sprintf('<comment>Server process: %s</comment>', $command), OutputInterface::VERBOSITY_DEBUG);
209
                $output->writeln(\sprintf('Starting server (<href=http://%s:%d>http://%s:%d</>)%s...', $host, $port, $host, $port, $messageSuffix));
210
                $process->start(function ($type, $buffer) {
211
                    if ($type === Process::ERR) {
212
                        error_log($buffer, 3, Util::joinFile($this->getPath(), self::TMP_DIR, 'errors.log'));
213
                    }
214
                });
215
                if ($open) {
216
                    $output->writeln('Opening web browser...');
217
                    Util\Platform::openBrowser(\sprintf('http://%s:%s', $host, $port));
218
                }
219
                while ($process->isRunning()) {
220
                    sleep(1); // wait for server is ready
221
                    if (!fsockopen($host, (int) $port)) {
222
                        $output->writeln('<info>Server is not ready.</info>');
223
224
                        return 1;
225
                    }
226
                    if ($this->watcherEnabled && $resourceWatcher instanceof ResourceWatcher) {
227
                        $watcher = $resourceWatcher->findChanges();
228
                        if ($watcher->hasChanges()) {
229
                            $output->writeln('<comment>Changes detected.</comment>');
230
                            // prints deleted/new/updated files in debug mode
231
                            if (\count($watcher->getDeletedFiles()) > 0) {
232
                                $output->writeln('<comment>Deleted files:</comment>', OutputInterface::VERBOSITY_DEBUG);
233
                                foreach ($watcher->getDeletedFiles() as $file) {
234
                                    $output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG);
235
                                }
236
                            }
237
                            if (\count($watcher->getNewFiles()) > 0) {
238
                                $output->writeln('<comment>New files:</comment>', OutputInterface::VERBOSITY_DEBUG);
239
                                foreach ($watcher->getNewFiles() as $file) {
240
                                    $output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG);
241
                                }
242
                            }
243
                            if (\count($watcher->getUpdatedFiles()) > 0) {
244
                                $output->writeln('<comment>Updated files:</comment>', OutputInterface::VERBOSITY_DEBUG);
245
                                foreach ($watcher->getUpdatedFiles() as $file) {
246
                                    $output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG);
247
                                }
248
                            }
249
                            $output->writeln('');
250
                            // re-builds
251
                            $buildProcess->run($processOutputCallback);
252
                            if ($buildProcess->isSuccessful()) {
253
                                $this->buildSuccessActions($output);
254
                            }
255
                            $output->writeln('<info>Server is runnning...</info>');
256
                        }
257
                    }
258
                }
259
                if ($process->getExitCode() > 0) {
260
                    $output->writeln(\sprintf('<comment>%s</comment>', trim($process->getErrorOutput())));
261
                }
262
            } catch (ProcessFailedException $e) {
263
                $this->tearDownServer();
264
265
                throw new RuntimeException(\sprintf($e->getMessage()));
266
            }
267
        }
268
269
        return 0;
270
    }
271
272
    /**
273
     * Build success actions.
274
     */
275
    private function buildSuccessActions(OutputInterface $output): void
276
    {
277
        // writes `changes.flag` file
278
        if ($this->watcherEnabled) {
279
            Util\File::getFS()->dumpFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'changes.flag'), time());
280
        }
281
        // writes `headers.ini` file
282
        $headers = $this->getBuilder()->getConfig()->get('server.headers');
283
        if (is_iterable($headers)) {
284
            $output->writeln('Writing headers file...');
285
            Util\File::getFS()->remove(Util::joinFile($this->getPath(), self::TMP_DIR, 'headers.ini'));
286
            foreach ($headers as $entry) {
287
                Util\File::getFS()->appendToFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'headers.ini'), "[{$entry['path']}]\n");
288
                foreach ($entry['headers'] ?? [] as $header) {
289
                    Util\File::getFS()->appendToFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'headers.ini'), "{$header['key']} = \"{$header['value']}\"\n");
290
                }
291
            }
292
        }
293
    }
294
295
    /**
296
     * Sets up the watcher.
297
     */
298
    private function setupWatcher(bool $noignorevcs = false): ResourceWatcher
299
    {
300
        $finder = new Finder();
301
        $finder->files()
302
            ->in($this->getPath())
303
            ->exclude((string) $this->getBuilder()->getConfig()->get('output.dir'));
304
        if (file_exists(Util::joinFile($this->getPath(), '.gitignore')) && $noignorevcs === false) {
305
            $finder->ignoreVCSIgnored(true);
306
        }
307
        return new ResourceWatcher(new ResourceCacheMemory(), $finder, new Crc32ContentHash());
308
    }
309
310
    /**
311
     * Prepares server's files.
312
     *
313
     * @throws RuntimeException
314
     */
315
    private function setUpServer(string $host, string $port): void
316
    {
317
        try {
318
            // define root path
319
            $root = Util\Platform::isPhar() ? Util\Platform::getPharPath() . '/' : realpath(Util::joinFile(__DIR__, '/../../'));
320
            // copying router
321
            Util\File::getFS()->copy(
322
                $root . '/resources/server/router.php',
323
                Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php'),
324
                true
325
            );
326
            // copying livereload JS for watcher
327
            $livereloadJs = Util::joinFile($this->getPath(), self::TMP_DIR, 'livereload.js');
328
            if (is_file($livereloadJs)) {
329
                Util\File::getFS()->remove($livereloadJs);
330
            }
331
            if ($this->watcherEnabled) {
332
                Util\File::getFS()->copy(
333
                    $root . '/resources/server/livereload.js',
334
                    $livereloadJs,
335
                    true
336
                );
337
            }
338
            // copying baseurl text file
339
            Util\File::getFS()->dumpFile(
340
                Util::joinFile($this->getPath(), self::TMP_DIR, 'baseurl'),
341
                \sprintf(
342
                    '%s;%s',
343
                    (string) $this->getBuilder()->getConfig()->get('baseurl'),
344
                    \sprintf('http://%s:%s/', $host, $port)
345
                )
346
            );
347
        } catch (IOExceptionInterface $e) {
348
            throw new RuntimeException(\sprintf('An error occurred while copying server\'s files to "%s".', $e->getPath()));
349
        }
350
        if (!is_file(Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php'))) {
351
            throw new RuntimeException(\sprintf('Router not found: "%s".', Util::joinFile(self::TMP_DIR, 'router.php')));
352
        }
353
    }
354
355
    /**
356
     * Removes temporary directory.
357
     *
358
     * @throws RuntimeException
359
     */
360
    public function tearDownServer(): void
361
    {
362
        $this->output->writeln('');
363
        $this->output->writeln('<info>Server stopped.</info>');
364
365
        try {
366
            Util\File::getFS()->remove(Util::joinFile($this->getPath(), self::TMP_DIR));
367
        } catch (IOExceptionInterface $e) {
368
            throw new RuntimeException($e->getMessage());
369
        }
370
    }
371
}
372