Chris@17: classLoader = $class_loader; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function configure() { Chris@17: $this->setDescription('Starts up a webserver for a site.') Chris@17: ->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on.', '127.0.0.1') Chris@17: ->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.') Chris@17: ->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.') Chris@17: ->addUsage('--host localhost --port 8080') Chris@17: ->addUsage('--host my-site.com --port 80'); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function execute(InputInterface $input, OutputInterface $output) { Chris@17: $io = new SymfonyStyle($input, $output); Chris@17: Chris@17: $host = $input->getOption('host'); Chris@17: $port = $input->getOption('port'); Chris@17: if (!$port) { Chris@17: $port = $this->findAvailablePort($host); Chris@17: } Chris@17: if (!$port) { Chris@17: $io->getErrorStyle()->error('Unable to automatically determine a port. Use the --port to hardcode an available port.'); Chris@17: } Chris@17: Chris@17: try { Chris@17: $kernel = $this->boot(); Chris@17: } Chris@17: catch (ConnectionNotDefinedException $e) { Chris@17: $io->getErrorStyle()->error("No installation found. Use the 'install' command."); Chris@17: return 1; Chris@17: } Chris@17: return $this->start($host, $port, $kernel, $input, $io); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Boots up a Drupal environment. Chris@17: * Chris@17: * @return \Drupal\Core\DrupalKernelInterface Chris@17: * The Drupal kernel. Chris@17: * Chris@17: * @throws \Exception Chris@17: * Exception thrown if kernel does not boot. Chris@17: */ Chris@17: protected function boot() { Chris@17: $kernel = new DrupalKernel('prod', $this->classLoader, FALSE); Chris@17: $kernel::bootEnvironment(); Chris@17: $kernel->setSitePath($this->getSitePath()); Chris@17: Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader); Chris@17: $kernel->boot(); Chris@17: // Some services require a request to work. For example, CommentManager. Chris@17: // This is needed as generating the URL fires up entity load hooks. Chris@17: $kernel->getContainer() Chris@17: ->get('request_stack') Chris@17: ->push(Request::createFromGlobals()); Chris@17: Chris@17: return $kernel; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Finds an available port. Chris@17: * Chris@17: * @param string $host Chris@17: * The host to find a port on. Chris@17: * Chris@17: * @return int|false Chris@17: * The available port or FALSE, if no available port found, Chris@17: */ Chris@17: protected function findAvailablePort($host) { Chris@17: $port = 8888; Chris@17: while ($port >= 8888 && $port <= 9999) { Chris@17: $connection = @fsockopen($host, $port); Chris@17: if (is_resource($connection)) { Chris@17: // Port is being used. Chris@17: fclose($connection); Chris@17: } Chris@17: else { Chris@17: // Port is available. Chris@17: return $port; Chris@17: } Chris@17: $port++; Chris@17: } Chris@17: return FALSE; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Opens a URL in your system default browser. Chris@17: * Chris@17: * @param string $url Chris@17: * The URL to browser to. Chris@17: * @param \Symfony\Component\Console\Style\SymfonyStyle $io Chris@17: * The IO. Chris@17: */ Chris@17: protected function openBrowser($url, SymfonyStyle $io) { Chris@17: $is_windows = defined('PHP_WINDOWS_VERSION_BUILD'); Chris@17: if ($is_windows) { Chris@17: // Handle escaping ourselves. Chris@17: $cmd = 'start "web" "' . $url . '""'; Chris@17: } Chris@17: else { Chris@17: $url = escapeshellarg($url); Chris@17: } Chris@17: Chris@17: $is_linux = (new Process('which xdg-open'))->run(); Chris@17: $is_osx = (new Process('which open'))->run(); Chris@17: if ($is_linux === 0) { Chris@17: $cmd = 'xdg-open ' . $url; Chris@17: } Chris@17: elseif ($is_osx === 0) { Chris@17: $cmd = 'open ' . $url; Chris@17: } Chris@17: Chris@17: if (empty($cmd)) { Chris@17: $io->getErrorStyle() Chris@17: ->error('No suitable browser opening command found, open yourself: ' . $url); Chris@17: return; Chris@17: } Chris@17: Chris@17: if ($io->isVerbose()) { Chris@17: $io->writeln("Browser command: $cmd"); Chris@17: } Chris@17: Chris@17: // Need to escape double quotes in the command so the PHP will work. Chris@17: $cmd = str_replace('"', '\"', $cmd); Chris@17: // Sleep for 2 seconds before opening the browser. This allows the command Chris@17: // to start up the PHP built-in webserver in the meantime. We use a Chris@17: // PhpProcess so that Windows powershell users also get a browser opened Chris@17: // for them. Chris@17: $php = ""; Chris@17: $process = new PhpProcess($php); Chris@17: $process->start(); Chris@17: return; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Gets a one time login URL for user 1. Chris@17: * Chris@17: * @return string Chris@17: * The one time login URL for user 1. Chris@17: */ Chris@17: protected function getOneTimeLoginUrl() { Chris@17: $user = User::load(1); Chris@17: \Drupal::moduleHandler()->load('user'); Chris@17: return user_pass_reset_url($user); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Starts up a webserver with a running Drupal. Chris@17: * Chris@17: * @param string $host Chris@17: * The hostname of the webserver. Chris@17: * @param int $port Chris@17: * The port to start the webserver on. Chris@17: * @param \Drupal\Core\DrupalKernelInterface $kernel Chris@17: * The Drupal kernel. Chris@17: * @param \Symfony\Component\Console\Input\InputInterface $input Chris@17: * The input. Chris@17: * @param \Symfony\Component\Console\Style\SymfonyStyle $io Chris@17: * The IO. Chris@17: * Chris@17: * @return int Chris@17: * The exit status of the PHP in-built webserver command. Chris@17: */ Chris@17: protected function start($host, $port, DrupalKernelInterface $kernel, InputInterface $input, SymfonyStyle $io) { Chris@17: $finder = new PhpExecutableFinder(); Chris@17: $binary = $finder->find(); Chris@17: if ($binary === FALSE) { Chris@17: throw new \RuntimeException('Unable to find the PHP binary.'); Chris@17: } Chris@17: Chris@17: $io->writeln("Drupal development server started: "); Chris@17: $io->writeln('This server is not meant for production use.'); Chris@17: $one_time_login = "http://$host:$port{$this->getOneTimeLoginUrl()}/login"; Chris@17: $io->writeln("One time login url: <$one_time_login>"); Chris@17: $io->writeln('Press Ctrl-C to quit the Drupal development server.'); Chris@17: Chris@17: if (!$input->getOption('suppress-login')) { Chris@17: if ($this->openBrowser("$one_time_login?destination=" . urlencode("/"), $io) === 1) { Chris@17: $io->error('Error while opening up a one time login URL'); Chris@17: } Chris@17: } Chris@17: Chris@17: // Use the Process object to construct an escaped command line. Chris@17: $process = new Process([ Chris@17: $binary, Chris@17: '-S', Chris@17: $host . ':' . $port, Chris@17: '.ht.router.php', Chris@17: ], $kernel->getAppRoot(), [], NULL, NULL); Chris@17: if ($io->isVerbose()) { Chris@17: $io->writeln("Server command: {$process->getCommandLine()}"); Chris@17: } Chris@17: Chris@17: // Carefully manage output so we can display output only in verbose mode. Chris@17: $descriptors = []; Chris@17: $descriptors[0] = STDIN; Chris@17: $descriptors[1] = ['pipe', 'w']; Chris@17: $descriptors[2] = ['pipe', 'w']; Chris@17: $server = proc_open($process->getCommandLine(), $descriptors, $pipes, $kernel->getAppRoot()); Chris@17: if (is_resource($server)) { Chris@17: if ($io->isVerbose()) { Chris@17: // Write a blank line so that server output and the useful information are Chris@17: // visually separated. Chris@17: $io->writeln(''); Chris@17: } Chris@17: $server_status = proc_get_status($server); Chris@17: while ($server_status['running']) { Chris@17: if ($io->isVerbose()) { Chris@17: fpassthru($pipes[2]); Chris@17: } Chris@17: sleep(1); Chris@17: $server_status = proc_get_status($server); Chris@17: } Chris@17: } Chris@17: return proc_close($server); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Gets the site path. Chris@17: * Chris@17: * Defaults to 'sites/default'. For testing purposes this can be overridden Chris@17: * using the DRUPAL_DEV_SITE_PATH environment variable. Chris@17: * Chris@17: * @return string Chris@17: * The site path to use. Chris@17: */ Chris@17: protected function getSitePath() { Chris@17: return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default'; Chris@17: } Chris@17: Chris@17: }