ServerCommand.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. <?php
  2. /**
  3. * @package Grav\Console\Cli
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Console\Cli;
  9. use Grav\Common\Utils;
  10. use Grav\Console\GravCommand;
  11. use Symfony\Component\Console\Input\InputOption;
  12. use Symfony\Component\Process\PhpExecutableFinder;
  13. use Symfony\Component\Process\Process;
  14. /**
  15. * Class ServerCommand
  16. * @package Grav\Console\Cli
  17. */
  18. class ServerCommand extends GravCommand
  19. {
  20. const SYMFONY_SERVER = 'Symfony Server';
  21. const PHP_SERVER = 'Built-in PHP Server';
  22. /** @var string */
  23. protected $ip;
  24. /** @var int */
  25. protected $port;
  26. /**
  27. * @return void
  28. */
  29. protected function configure(): void
  30. {
  31. $this
  32. ->setName('server')
  33. ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Preferred HTTP port rather than auto-find (default is 8000-9000')
  34. ->addOption('symfony', null, InputOption::VALUE_NONE, 'Force using Symfony server')
  35. ->addOption('php', null, InputOption::VALUE_NONE, 'Force using built-in PHP server')
  36. ->setDescription("Runs built-in web-server, Symfony first, then tries PHP's")
  37. ->setHelp("Runs built-in web-server, Symfony first, then tries PHP's");
  38. }
  39. /**
  40. * @return int
  41. */
  42. protected function serve(): int
  43. {
  44. $input = $this->getInput();
  45. $io = $this->getIO();
  46. $io->title('Grav Web Server');
  47. // Ensure CLI colors are on
  48. ini_set('cli_server.color', 'on');
  49. // Options
  50. $force_symfony = $input->getOption('symfony');
  51. $force_php = $input->getOption('php');
  52. // Find PHP
  53. $executableFinder = new PhpExecutableFinder();
  54. $php = $executableFinder->find(false);
  55. $this->ip = '127.0.0.1';
  56. $this->port = (int)($input->getOption('port') ?? 8000);
  57. // Get an open port
  58. while (!$this->portAvailable($this->ip, $this->port)) {
  59. $this->port++;
  60. }
  61. // Setup the commands
  62. $symfony_cmd = ['symfony', 'server:start', '--ansi', '--port=' . $this->port];
  63. $php_cmd = [$php, '-S', $this->ip.':'.$this->port, 'system/router.php'];
  64. $commands = [
  65. self::SYMFONY_SERVER => $symfony_cmd,
  66. self::PHP_SERVER => $php_cmd
  67. ];
  68. if ($force_symfony) {
  69. unset($commands[self::PHP_SERVER]);
  70. } elseif ($force_php) {
  71. unset($commands[self::SYMFONY_SERVER]);
  72. }
  73. $error = 0;
  74. foreach ($commands as $name => $command) {
  75. $process = $this->runProcess($name, $command);
  76. if (!$process) {
  77. $io->note('Starting ' . $name . '...');
  78. }
  79. // Should only get here if there's an error running
  80. if (!$process->isRunning() && (($name === self::SYMFONY_SERVER && $force_symfony) || ($name === self::PHP_SERVER))) {
  81. $error = 1;
  82. $io->error('Could not start ' . $name);
  83. }
  84. }
  85. return $error;
  86. }
  87. /**
  88. * @param string $name
  89. * @param array $cmd
  90. * @return Process
  91. */
  92. protected function runProcess(string $name, array $cmd): Process
  93. {
  94. $io = $this->getIO();
  95. $process = new Process($cmd);
  96. $process->setTimeout(0);
  97. $process->start();
  98. if ($name === self::SYMFONY_SERVER && Utils::contains($process->getErrorOutput(), 'symfony: not found')) {
  99. $io->error('The symfony binary could not be found, please install the CLI tools: https://symfony.com/download');
  100. $io->warning('Falling back to PHP web server...');
  101. }
  102. if ($name === self::PHP_SERVER) {
  103. $io->success('Built-in PHP web server listening on http://' . $this->ip . ':' . $this->port . ' (PHP v' . PHP_VERSION . ')');
  104. }
  105. $process->wait(function ($type, $buffer) {
  106. $this->getIO()->write($buffer);
  107. });
  108. return $process;
  109. }
  110. /**
  111. * Simple function test the port
  112. *
  113. * @param string $ip
  114. * @param int $port
  115. * @return bool
  116. */
  117. protected function portAvailable(string $ip, int $port): bool
  118. {
  119. $fp = @fsockopen($ip, $port, $errno, $errstr, 0.1);
  120. if (!$fp) {
  121. return true;
  122. }
  123. fclose($fp);
  124. return false;
  125. }
  126. }