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\InputDefinition; |
20
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
21
|
|
|
use Symfony\Component\Console\Input\InputOption; |
22
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
23
|
|
|
use Symfony\Component\Filesystem\Exception\IOExceptionInterface; |
24
|
|
|
use Symfony\Component\Finder\Finder; |
25
|
|
|
use Symfony\Component\Process\Exception\ProcessFailedException; |
26
|
|
|
use Symfony\Component\Process\PhpExecutableFinder; |
27
|
|
|
use Symfony\Component\Process\Process; |
28
|
|
|
use Yosymfony\ResourceWatcher\Crc32ContentHash; |
29
|
|
|
use Yosymfony\ResourceWatcher\ResourceCacheMemory; |
30
|
|
|
use Yosymfony\ResourceWatcher\ResourceWatcher; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Starts the built-in server. |
34
|
|
|
*/ |
35
|
|
|
class Serve extends AbstractCommand |
36
|
|
|
{ |
37
|
|
|
/** |
38
|
|
|
* {@inheritdoc} |
39
|
|
|
*/ |
40
|
|
|
protected function configure() |
41
|
|
|
{ |
42
|
|
|
$this |
43
|
|
|
->setName('serve') |
44
|
|
|
->setDescription('Starts the built-in server') |
45
|
|
|
->setDefinition( |
46
|
|
|
new InputDefinition([ |
47
|
|
|
new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'), |
48
|
|
|
new InputOption('config', 'c', InputOption::VALUE_REQUIRED, 'Set the path to extra config files (comma-separated)'), |
49
|
|
|
new InputOption('drafts', 'd', InputOption::VALUE_NONE, 'Include drafts'), |
50
|
|
|
new InputOption('page', 'p', InputOption::VALUE_REQUIRED, 'Build a specific page'), |
51
|
|
|
new InputOption('open', 'o', InputOption::VALUE_NONE, 'Open web browser automatically'), |
52
|
|
|
new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Server host'), |
53
|
|
|
new InputOption('port', null, InputOption::VALUE_REQUIRED, 'Server port'), |
54
|
|
|
new InputOption('optimize', null, InputOption::VALUE_OPTIONAL, 'Optimize files (disable with "no")', false), |
55
|
|
|
new InputOption('clear-cache', null, InputOption::VALUE_OPTIONAL, 'Clear cache before build (optional cache key regular expression)', false), |
56
|
|
|
new InputOption('no-ignore-vcs', null, InputOption::VALUE_NONE, 'Changes watcher must not ignore VCS directories'), |
57
|
|
|
]) |
58
|
|
|
) |
59
|
|
|
->setHelp('Starts the live-reloading-built-in web server'); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* {@inheritdoc} |
64
|
|
|
* |
65
|
|
|
* @throws RuntimeException |
66
|
|
|
*/ |
67
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
68
|
|
|
{ |
69
|
|
|
$drafts = $input->getOption('drafts'); |
70
|
|
|
$open = $input->getOption('open'); |
71
|
|
|
$host = $input->getOption('host') ?? 'localhost'; |
72
|
|
|
$port = $input->getOption('port') ?? '8000'; |
73
|
|
|
$optimize = $input->getOption('optimize'); |
74
|
|
|
$clearcache = $input->getOption('clear-cache'); |
75
|
|
|
$verbose = $input->getOption('verbose'); |
76
|
|
|
$page = $input->getOption('page'); |
77
|
|
|
$noignorevcs = $input->getOption('no-ignore-vcs'); |
78
|
|
|
|
79
|
|
|
$this->setUpServer($host, $port); |
80
|
|
|
|
81
|
|
|
$phpFinder = new PhpExecutableFinder(); |
82
|
|
|
$php = $phpFinder->find(); |
83
|
|
|
if ($php === false) { |
84
|
|
|
throw new RuntimeException('Can\'t find a local PHP executable.'); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
$command = \sprintf( |
88
|
|
|
'"%s" -S %s:%d -t "%s" "%s"', |
89
|
|
|
$php, |
90
|
|
|
$host, |
91
|
|
|
$port, |
92
|
|
|
Util::joinFile($this->getPath(), (string) $this->getBuilder()->getConfig()->get('output.dir')), |
93
|
|
|
Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php') |
94
|
|
|
); |
95
|
|
|
$process = Process::fromShellCommandline($command); |
96
|
|
|
|
97
|
|
|
$buildProcessArguments = [ |
98
|
|
|
$php, |
99
|
|
|
$_SERVER['argv'][0], |
100
|
|
|
]; |
101
|
|
|
$buildProcessArguments[] = 'build'; |
102
|
|
|
$buildProcessArguments[] = $this->getPath(); |
103
|
|
|
if (!empty($this->getConfigFiles())) { |
104
|
|
|
$buildProcessArguments[] = '--config'; |
105
|
|
|
$buildProcessArguments[] = implode(',', $this->getConfigFiles()); |
106
|
|
|
} |
107
|
|
|
if ($drafts) { |
108
|
|
|
$buildProcessArguments[] = '--drafts'; |
109
|
|
|
} |
110
|
|
|
if ($optimize === null) { |
111
|
|
|
$buildProcessArguments[] = '--optimize'; |
112
|
|
|
} |
113
|
|
|
if (!empty($optimize)) { |
114
|
|
|
$buildProcessArguments[] = '--optimize'; |
115
|
|
|
$buildProcessArguments[] = $optimize; |
116
|
|
|
} |
117
|
|
|
if ($clearcache === null) { |
118
|
|
|
$buildProcessArguments[] = '--clear-cache'; |
119
|
|
|
} |
120
|
|
|
if (!empty($clearcache)) { |
121
|
|
|
$buildProcessArguments[] = '--clear-cache'; |
122
|
|
|
$buildProcessArguments[] = $clearcache; |
123
|
|
|
} |
124
|
|
|
if ($verbose) { |
125
|
|
|
$buildProcessArguments[] = '-' . str_repeat('v', $_SERVER['SHELL_VERBOSITY']); |
126
|
|
|
} |
127
|
|
|
if (!empty($page)) { |
128
|
|
|
$buildProcessArguments[] = '--page'; |
129
|
|
|
$buildProcessArguments[] = $page; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
$buildProcess = new Process( |
133
|
|
|
$buildProcessArguments, |
134
|
|
|
null, |
135
|
|
|
['BOX_REQUIREMENT_CHECKER' => '0'] // prevents double check (build then serve) |
136
|
|
|
); |
137
|
|
|
|
138
|
|
|
$buildProcess->setTty(Process::isTtySupported()); |
139
|
|
|
$buildProcess->setPty(Process::isPtySupported()); |
140
|
|
|
$buildProcess->setTimeout(3600 * 2); // timeout = 2 minutes |
141
|
|
|
|
142
|
|
|
$processOutputCallback = function ($type, $buffer) use ($output) { |
143
|
|
|
$output->write($buffer, false, OutputInterface::OUTPUT_RAW); |
144
|
|
|
}; |
145
|
|
|
|
146
|
|
|
// (re)builds before serve |
147
|
|
|
$output->writeln(\sprintf('<comment>Build process: %s</comment>', implode(' ', $buildProcessArguments)), OutputInterface::VERBOSITY_DEBUG); |
148
|
|
|
$buildProcess->run($processOutputCallback); |
149
|
|
|
if ($buildProcess->isSuccessful()) { |
150
|
|
|
$this->buildSuccess($output); |
151
|
|
|
} |
152
|
|
|
if ($buildProcess->getExitCode() !== 0) { |
153
|
|
|
return 1; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
// handles process |
157
|
|
|
if (!$process->isStarted()) { |
158
|
|
|
// set resource watcher |
159
|
|
|
$finder = new Finder(); |
160
|
|
|
$finder->files() |
161
|
|
|
->in($this->getPath()) |
162
|
|
|
->exclude((string) $this->getBuilder()->getConfig()->get('output.dir')); |
163
|
|
|
if (file_exists(Util::joinFile($this->getPath(), '.gitignore')) && $noignorevcs === false) { |
164
|
|
|
$finder->ignoreVCSIgnored(true); |
165
|
|
|
} |
166
|
|
|
$hashContent = new Crc32ContentHash(); |
167
|
|
|
$resourceCache = new ResourceCacheMemory(); |
168
|
|
|
$resourceWatcher = new ResourceWatcher($resourceCache, $finder, $hashContent); |
169
|
|
|
$resourceWatcher->initialize(); |
170
|
|
|
|
171
|
|
|
// starts server |
172
|
|
|
try { |
173
|
|
|
if (\function_exists('\pcntl_signal')) { |
174
|
|
|
pcntl_async_signals(true); |
175
|
|
|
pcntl_signal(SIGINT, [$this, 'tearDownServer']); |
176
|
|
|
pcntl_signal(SIGTERM, [$this, 'tearDownServer']); |
177
|
|
|
} |
178
|
|
|
$output->writeln(\sprintf('<comment>Server process: %s</comment>', $command), OutputInterface::VERBOSITY_DEBUG); |
179
|
|
|
$output->writeln(\sprintf('Starting server (<href=http://%s:%d>http://%s:%d</>)...', $host, $port, $host, $port)); |
180
|
|
|
$process->start(function ($type, $buffer) { |
181
|
|
|
if ($type === Process::ERR) { |
182
|
|
|
error_log($buffer, 3, Util::joinFile($this->getPath(), self::TMP_DIR, 'errors.log')); |
183
|
|
|
} |
184
|
|
|
}); |
185
|
|
|
if ($open) { |
186
|
|
|
$output->writeln('Opening web browser...'); |
187
|
|
|
Util\Plateform::openBrowser(\sprintf('http://%s:%s', $host, $port)); |
188
|
|
|
} |
189
|
|
|
while ($process->isRunning()) { |
190
|
|
|
sleep(1); // wait for server is ready |
191
|
|
|
if (!fsockopen($host, (int) $port)) { |
192
|
|
|
$output->writeln('<info>Server is not ready.</info>'); |
193
|
|
|
|
194
|
|
|
return 1; |
195
|
|
|
} |
196
|
|
|
$watcher = $resourceWatcher->findChanges(); |
197
|
|
|
if ($watcher->hasChanges()) { |
198
|
|
|
// prints deleted/new/updated files in debug mode |
199
|
|
|
$output->writeln('<comment>Changes detected.</comment>'); |
200
|
|
|
if (\count($watcher->getDeletedFiles()) > 0) { |
201
|
|
|
$output->writeln('<comment>Deleted files:</comment>', OutputInterface::VERBOSITY_DEBUG); |
202
|
|
|
foreach ($watcher->getDeletedFiles() as $file) { |
203
|
|
|
$output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG); |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
if (\count($watcher->getNewFiles()) > 0) { |
207
|
|
|
$output->writeln('<comment>New files:</comment>', OutputInterface::VERBOSITY_DEBUG); |
208
|
|
|
foreach ($watcher->getNewFiles() as $file) { |
209
|
|
|
$output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
if (\count($watcher->getUpdatedFiles()) > 0) { |
213
|
|
|
$output->writeln('<comment>Updated files:</comment>', OutputInterface::VERBOSITY_DEBUG); |
214
|
|
|
foreach ($watcher->getUpdatedFiles() as $file) { |
215
|
|
|
$output->writeln("<comment>- $file</comment>", OutputInterface::VERBOSITY_DEBUG); |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
$output->writeln(''); |
219
|
|
|
// re-builds |
220
|
|
|
$buildProcess->run($processOutputCallback); |
221
|
|
|
if ($buildProcess->isSuccessful()) { |
222
|
|
|
$this->buildSuccess($output); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
$output->writeln('<info>Server is runnning...</info>'); |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
if ($process->getExitCode() > 0) { |
229
|
|
|
$output->writeln(\sprintf('<comment>%s</comment>', trim($process->getErrorOutput()))); |
230
|
|
|
} |
231
|
|
|
} catch (ProcessFailedException $e) { |
232
|
|
|
$this->tearDownServer(); |
233
|
|
|
|
234
|
|
|
throw new RuntimeException(\sprintf($e->getMessage())); |
235
|
|
|
} |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
return 0; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Build success. |
243
|
|
|
*/ |
244
|
|
|
private function buildSuccess(OutputInterface $output): void |
245
|
|
|
{ |
246
|
|
|
// writes `changes.flag` file |
247
|
|
|
Util\File::getFS()->dumpFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'changes.flag'), time()); |
248
|
|
|
// writes `headers.ini` file |
249
|
|
|
if (null !== $headers = $this->getBuilder()->getConfig()->get('headers')) { |
250
|
|
|
$output->writeln('Writing headers file...'); |
251
|
|
|
foreach ($headers as $header) { |
252
|
|
|
Util\File::getFS()->appendToFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'headers.ini'), "[{$header['source']}]\n"); |
253
|
|
|
foreach ($header['headers'] as $h) { |
254
|
|
|
Util\File::getFS()->appendToFile(Util::joinFile($this->getPath(), self::TMP_DIR, 'headers.ini'), "{$h['key']} = \"{$h['value']}\"\n"); |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Prepares server's files. |
262
|
|
|
* |
263
|
|
|
* @throws RuntimeException |
264
|
|
|
*/ |
265
|
|
|
private function setUpServer(string $host, string $port): void |
266
|
|
|
{ |
267
|
|
|
try { |
268
|
|
|
$root = Util::joinFile(__DIR__, '../../'); |
269
|
|
|
if (Util\Plateform::isPhar()) { |
270
|
|
|
$root = Util\Plateform::getPharPath() . '/'; |
271
|
|
|
} |
272
|
|
|
// copying router |
273
|
|
|
Util\File::getFS()->copy( |
274
|
|
|
$root . '/resources/server/router.php', |
275
|
|
|
Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php'), |
276
|
|
|
true |
277
|
|
|
); |
278
|
|
|
// copying livereload JS |
279
|
|
|
Util\File::getFS()->copy( |
280
|
|
|
$root . '/resources/server/livereload.js', |
281
|
|
|
Util::joinFile($this->getPath(), self::TMP_DIR, 'livereload.js'), |
282
|
|
|
true |
283
|
|
|
); |
284
|
|
|
// copying baseurl text file |
285
|
|
|
Util\File::getFS()->dumpFile( |
286
|
|
|
Util::joinFile($this->getPath(), self::TMP_DIR, 'baseurl'), |
287
|
|
|
\sprintf( |
288
|
|
|
'%s;%s', |
289
|
|
|
(string) $this->getBuilder()->getConfig()->get('baseurl'), |
290
|
|
|
\sprintf('http://%s:%s/', $host, $port) |
291
|
|
|
) |
292
|
|
|
); |
293
|
|
|
} catch (IOExceptionInterface $e) { |
294
|
|
|
throw new RuntimeException(\sprintf('An error occurred while copying server\'s files to "%s".', $e->getPath())); |
295
|
|
|
} |
296
|
|
|
if (!is_file(Util::joinFile($this->getPath(), self::TMP_DIR, 'router.php'))) { |
297
|
|
|
throw new RuntimeException(\sprintf('Router not found: "%s".', Util::joinFile(self::TMP_DIR, 'router.php'))); |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Removes temporary directory. |
303
|
|
|
* |
304
|
|
|
* @throws RuntimeException |
305
|
|
|
*/ |
306
|
|
|
public function tearDownServer(): void |
307
|
|
|
{ |
308
|
|
|
$this->output->writeln(''); |
309
|
|
|
$this->output->writeln('<info>Server stopped.</info>'); |
310
|
|
|
|
311
|
|
|
try { |
312
|
|
|
Util\File::getFS()->remove(Util::joinFile($this->getPath(), self::TMP_DIR)); |
313
|
|
|
} catch (IOExceptionInterface $e) { |
314
|
|
|
throw new RuntimeException($e->getMessage()); |
315
|
|
|
} |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|