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: }