1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
|
<?php
namespace Drupal\Core\Command;
use Drupal\Core\Database\ConnectionNotDefinedException;
use Drupal\Core\DrupalKernel;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Site\Settings;
use Drupal\user\Entity\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\PhpProcess;
use Symfony\Component\Process\Process;
/**
* Runs the PHP webserver for a Drupal site for local testing/development.
*
* @internal
* This command makes no guarantee of an API for Drupal extensions.
*/
class ServerCommand extends Command {
/**
* The class loader.
*
* @var object
*/
protected $classLoader;
/**
* Constructs a new ServerCommand command.
*
* @param object $class_loader
* The class loader.
*/
public function __construct($class_loader) {
parent::__construct('server');
$this->classLoader = $class_loader;
}
/**
* {@inheritdoc}
*/
protected function configure() {
$this->setDescription('Starts up a webserver for a site.')
->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on.', '127.0.0.1')
->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.')
->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
->addUsage('--host localhost --port 8080')
->addUsage('--host my-site.com --port 80');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$io = new SymfonyStyle($input, $output);
$host = $input->getOption('host');
$port = $input->getOption('port');
if (!$port) {
$port = $this->findAvailablePort($host);
}
if (!$port) {
$io->getErrorStyle()->error('Unable to automatically determine a port. Use the --port to hardcode an available port.');
}
try {
$kernel = $this->boot();
}
catch (ConnectionNotDefinedException) {
$io->getErrorStyle()->error("No installation found. Use the 'install' command.");
return 1;
}
return $this->start($host, $port, $kernel, $input, $io);
}
/**
* Boots up a Drupal environment.
*
* @return \Drupal\Core\DrupalKernelInterface
* The Drupal kernel.
*
* @throws \Exception
* Exception thrown if kernel does not boot.
*/
protected function boot() {
$kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
$kernel::bootEnvironment();
$kernel->setSitePath($this->getSitePath());
Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
$kernel->boot();
// Some services require a request to work. For example, CommentManager.
// This is needed as generating the URL fires up entity load hooks.
$kernel->getContainer()
->get('request_stack')
->push(Request::createFromGlobals());
return $kernel;
}
/**
* Finds an available port.
*
* @param string $host
* The host to find a port on.
*
* @return int|false
* The available port or FALSE, if no available port found,
*/
protected function findAvailablePort($host) {
$port = 8888;
while ($port >= 8888 && $port <= 9999) {
$connection = @fsockopen($host, $port);
if (is_resource($connection)) {
// Port is being used.
fclose($connection);
}
else {
// Port is available.
return $port;
}
$port++;
}
return FALSE;
}
/**
* Opens a URL in your system default browser.
*
* @param string $url
* The URL to browser to.
* @param \Symfony\Component\Console\Style\SymfonyStyle $io
* The IO.
*/
protected function openBrowser($url, SymfonyStyle $io) {
$is_windows = defined('PHP_WINDOWS_VERSION_BUILD');
if ($is_windows) {
// Handle escaping ourselves.
$cmd = 'start "web" "' . $url . '""';
}
else {
$url = escapeshellarg($url);
}
$is_linux = Process::fromShellCommandline('which xdg-open')->run();
$is_osx = Process::fromShellCommandline('which open')->run();
if ($is_linux === 0) {
$cmd = 'xdg-open ' . $url;
}
elseif ($is_osx === 0) {
$cmd = 'open ' . $url;
}
if (empty($cmd)) {
$io->getErrorStyle()
->error('No suitable browser opening command found, open yourself: ' . $url);
return;
}
if ($io->isVerbose()) {
$io->writeln("<info>Browser command:</info> $cmd");
}
// Need to escape double quotes in the command so the PHP will work.
$cmd = str_replace('"', '\"', $cmd);
// Sleep for 2 seconds before opening the browser. This allows the command
// to start up the PHP built-in webserver in the meantime. We use a
// PhpProcess so that Windows powershell users also get a browser opened
// for them.
$php = "<?php sleep(2); passthru(\"$cmd\"); ?>";
$process = new PhpProcess($php);
$process->start();
}
/**
* Gets a one time login URL for user 1.
*
* @return string
* The one time login URL for user 1.
*/
protected function getOneTimeLoginUrl() {
$user = User::load(1);
\Drupal::moduleHandler()->load('user');
return user_pass_reset_url($user);
}
/**
* Starts up a webserver with a running Drupal.
*
* @param string $host
* The hostname of the webserver.
* @param int $port
* The port to start the webserver on.
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The Drupal kernel.
* @param \Symfony\Component\Console\Input\InputInterface $input
* The input.
* @param \Symfony\Component\Console\Style\SymfonyStyle $io
* The IO.
*
* @return int
* The exit status of the PHP in-built webserver command.
*/
protected function start($host, $port, DrupalKernelInterface $kernel, InputInterface $input, SymfonyStyle $io) {
$finder = new PhpExecutableFinder();
$binary = $finder->find();
if ($binary === FALSE) {
throw new \RuntimeException('Unable to find the PHP binary.');
}
$io->writeln("<info>Drupal development server started:</info> <http://{$host}:{$port}>");
$io->writeln('<info>This server is not meant for production use.</info>');
$one_time_login = "http://$host:$port{$this->getOneTimeLoginUrl()}/login";
$io->writeln("<info>One time login url:</info> <$one_time_login>");
$io->writeln('Press Ctrl-C to quit the Drupal development server.');
if (!$input->getOption('suppress-login')) {
if ($this->openBrowser("$one_time_login?destination=" . urlencode("/"), $io) === 1) {
$io->error('Error while opening up a one time login URL');
}
}
// Use the Process object to construct an escaped command line.
$process = new Process([
$binary,
'-S',
$host . ':' . $port,
'.ht.router.php',
], $kernel->getAppRoot(), [], NULL, NULL);
if ($io->isVerbose()) {
$io->writeln("<info>Server command:</info> {$process->getCommandLine()}");
}
// Carefully manage output so we can display output only in verbose mode.
$descriptors = [];
$descriptors[0] = STDIN;
$descriptors[1] = ['pipe', 'w'];
$descriptors[2] = ['pipe', 'w'];
$server = proc_open($process->getCommandLine(), $descriptors, $pipes, $kernel->getAppRoot());
if (is_resource($server)) {
if ($io->isVerbose()) {
// Write a blank line so that server output and the useful information
// are visually separated.
$io->writeln('');
}
$server_status = proc_get_status($server);
while ($server_status['running']) {
if ($io->isVerbose()) {
fpassthru($pipes[2]);
}
sleep(1);
$server_status = proc_get_status($server);
}
}
return proc_close($server);
}
/**
* Gets the site path.
*
* Defaults to 'sites/default'. For testing purposes this can be overridden
* using the DRUPAL_DEV_SITE_PATH environment variable.
*
* @return string
* The site path to use.
*/
protected function getSitePath() {
return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
}
}
|