annotate core/lib/Drupal/Core/Command/ServerCommand.php @ 19:fa3358dc1485 tip

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