diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b67a47742..5ea2e3ea5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,5 +62,5 @@ jobs: - name: Run tests with codeception run: | - php -S 127.0.0.1:8080 -t public > ./runtime/yii.log 2>&1 & - vendor/bin/codecept run + ./yii serve 127.0.0.1:8080 & + sleep 10s; vendor/bin/codecept run diff --git a/config/params.php b/config/params.php index 90de310c2..7f42ed9da 100644 --- a/config/params.php +++ b/config/params.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Command\Serve2; use App\Middleware\LocaleMiddleware; use App\ViewInjection\CommonViewInjection; use App\ViewInjection\LayoutViewInjection; @@ -93,6 +94,7 @@ 'autoExit' => false, 'commands' => [ 'serve' => Serve::class, + 'serve2' => Serve2::class, 'user/create' => App\User\Console\CreateCommand::class, 'user/assignRole' => App\User\Console\AssignRoleCommand::class, 'fixture/add' => App\Command\Fixture\AddCommand::class, diff --git a/src/Command/Serve2.php b/src/Command/Serve2.php new file mode 100644 index 000000000..c060a2bac --- /dev/null +++ b/src/Command/Serve2.php @@ -0,0 +1,132 @@ +setHelp('In order to access server from remote machines use 0.0.0.0:8000. That is especially useful when running server in a virtual machine.') + ->addArgument('address', InputArgument::OPTIONAL, 'Host to serve at', 'localhost') + ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Port to serve at', self::DEFAULT_PORT) + ->addOption('docroot', 't', InputOption::VALUE_OPTIONAL, 'Document root to serve from', self::DEFAULT_DOCROOT) + ->addOption('router', 'r', InputOption::VALUE_OPTIONAL, 'Path to router script', self::DEFAULT_ROUTER) + ->addOption('env', 'e', InputOption::VALUE_OPTIONAL, 'It is only used for testing.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + /** @var string $address */ + $address = $input->getArgument('address'); + + /** @var string $router */ + $router = $input->getOption('router'); + + /** @var string $port */ + $port = $input->getOption('port'); + + /** @var string $docroot */ + $docroot = $input->getOption('docroot'); + + if ($router === self::DEFAULT_ROUTER && !file_exists(self::DEFAULT_ROUTER)) { + $io->warning('Default router "' . self::DEFAULT_ROUTER . '" does not exist. Serving without router. URLs with dots may fail.'); + $router = null; + } + + /** @var string $env */ + $env = $input->getOption('env'); + + $documentRoot = getcwd() . '/' . $docroot; // TODO: can we do it better? + + if (strpos($address, ':') === false) { + $address .= ':' . $port; + } + + if (!is_dir($documentRoot)) { + $io->error("Document root \"$documentRoot\" does not exist."); + return self::EXIT_CODE_NO_DOCUMENT_ROOT; + } + + if ($this->isAddressTaken($address)) { + $io->error("http://$address is taken by another process."); + return self::EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS; + } + + if ($router !== null && !file_exists($router)) { + $io->error("Routing file \"$router\" does not exist."); + return self::EXIT_CODE_NO_ROUTING_FILE; + } + + $output->writeLn("Server started on http://$address/"); + $output->writeLn("Document root is \"$documentRoot\""); + + if ($router) { + $output->writeLn("Routing file is \"$router\""); + } + + $output->writeLn('Quit the server with CTRL-C or COMMAND-C.'); + + $output->writeln(empty($env) ? 'EMPTY' : $env); + + if ($env === 'test') { + return ExitCode::OK; + } + + $output->writeln('"' . PHP_BINARY . '"' . " -S $address -t \"$documentRoot\" $router"); + + passthru('"' . PHP_BINARY . '"' . " -S $address -t \"$documentRoot\" $router"); + + return ExitCode::OK; + } + + /** + * @param string $address The server address. + * + * @return bool If address is already in use. + */ + private function isAddressTaken(string $address): bool + { + [$hostname, $port] = explode(':', $address); + $fp = @fsockopen($hostname, (int)$port, $errno, $errstr, 3); + + if ($fp === false) { + return false; + } + + fclose($fp); + return true; + } +}