diff --git a/composer.json b/composer.json index 99850ef99849c..acfbd0d3c29d1 100644 --- a/composer.json +++ b/composer.json @@ -73,6 +73,7 @@ "symfony/validator": "self.version", "symfony/var-dumper": "self.version", "symfony/web-profiler-bundle": "self.version", + "symfony/web-server-bundle": "self.version", "symfony/workflow": "self.version", "symfony/yaml": "self.version" }, diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 61f90c06c5111..41e1c2453a36a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 3.3.0 ----- + * The server:* commands and their associated router files were moved to WebServerBundle * Translation related services are not loaded anymore when the `framework.translator` option is disabled. * Added `GlobalVariables::getToken()` diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerCommand.php deleted file mode 100644 index ccfa5dde6b7e8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerCommand.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Command; - -/** - * Base methods for commands related to PHP's built-in web server. - * - * @author Christian Flothmann - */ -abstract class ServerCommand extends ContainerAwareCommand -{ - /** - * {@inheritdoc} - */ - public function isEnabled() - { - if (defined('HHVM_VERSION')) { - return false; - } - - if (!class_exists('Symfony\Component\Process\Process')) { - return false; - } - - return parent::isEnabled(); - } - - /** - * Determines the name of the lock file for a particular PHP web server process. - * - * @param string $address An address/port tuple - * - * @return string The filename - */ - protected function getLockFile($address) - { - return sys_get_temp_dir().'/'.strtr($address, '.:', '--').'.pid'; - } - - protected function isOtherServerProcessRunning($address) - { - $lockFile = $this->getLockFile($address); - - if (file_exists($lockFile)) { - return true; - } - - list($hostname, $port) = explode(':', $address); - - $fp = @fsockopen($hostname, $port, $errno, $errstr, 5); - - if (false !== $fp) { - fclose($fp); - - return true; - } - - return false; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php deleted file mode 100644 index e7dec312f99ba..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php +++ /dev/null @@ -1,175 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Command; - -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Process\PhpExecutableFinder; -use Symfony\Component\Process\Process; -use Symfony\Component\Process\ProcessBuilder; -use Symfony\Component\Process\Exception\RuntimeException; - -/** - * Runs Symfony application using PHP built-in web server. - * - * @author Michał Pipa - */ -class ServerRunCommand extends ServerCommand -{ - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setDefinition(array( - new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1'), - new InputOption('port', 'p', InputOption::VALUE_REQUIRED, 'Address port number', '8000'), - new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root', null), - new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), - )) - ->setName('server:run') - ->setDescription('Runs PHP built-in web server') - ->setHelp(<<<'EOF' -The %command.name% runs PHP built-in web server: - - %command.full_name% - -To change default bind address and port use the address argument: - - %command.full_name% 127.0.0.1:8080 - -To change default docroot directory use the --docroot option: - - %command.full_name% --docroot=htdocs/ - -If you have custom docroot directory layout, you can specify your own -router script using --router option: - - %command.full_name% --router=app/config/router.php - -Specifing a router script is required when the used environment is not "dev", -"prod", or "test". - -See also: http://www.php.net/manual/en/features.commandline.webserver.php - -EOF - ) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $io = new SymfonyStyle($input, $output); - $documentRoot = $input->getOption('docroot'); - - if (null === $documentRoot) { - $documentRoot = $this->getContainer()->getParameter('kernel.root_dir').'/../web'; - } - - if (!is_dir($documentRoot)) { - $io->error(sprintf('The given document root directory "%s" does not exist', $documentRoot)); - - return 1; - } - - $env = $this->getContainer()->getParameter('kernel.environment'); - $address = $input->getArgument('address'); - - if (false === strpos($address, ':')) { - $address = $address.':'.$input->getOption('port'); - } - - if ($this->isOtherServerProcessRunning($address)) { - $io->error(sprintf('A process is already listening on http://%s.', $address)); - - return 1; - } - - if ('prod' === $env) { - $io->error('Running PHP built-in server in production environment is NOT recommended!'); - } - - $io->success(sprintf('Server running on http://%s', $address)); - $io->comment('Quit the server with CONTROL-C.'); - - if (null === $builder = $this->createPhpProcessBuilder($io, $address, $input->getOption('router'), $env)) { - return 1; - } - - $builder->setWorkingDirectory($documentRoot); - $builder->setTimeout(null); - $process = $builder->getProcess(); - $callback = null; - - if (OutputInterface::VERBOSITY_NORMAL > $output->getVerbosity()) { - $process->disableOutput(); - } else { - try { - $process->setTty(true); - } catch (RuntimeException $e) { - $callback = function ($type, $buffer) use ($output) { - if (Process::ERR === $type && $output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - $output->write($buffer, false, OutputInterface::OUTPUT_RAW); - }; - } - } - $process->run($callback); - - if (!$process->isSuccessful()) { - $errorMessages = array('Built-in server terminated unexpectedly.'); - - if ($process->isOutputDisabled()) { - $errorMessages[] = 'Run the command again with -v option for more details.'; - } - - $io->error($errorMessages); - } - - return $process->getExitCode(); - } - - private function createPhpProcessBuilder(SymfonyStyle $io, $address, $router, $env) - { - $router = $router ?: $this - ->getContainer() - ->get('kernel') - ->locateResource(sprintf('@FrameworkBundle/Resources/config/router_%s.php', $env)) - ; - - if (!file_exists($router)) { - $io->error(sprintf('The given router script "%s" does not exist.', $router)); - - return; - } - - $router = realpath($router); - $finder = new PhpExecutableFinder(); - - if (false === $binary = $finder->find()) { - $io->error('Unable to find PHP binary to run server.'); - - return; - } - - return new ProcessBuilder(array($binary, '-S', $address, $router)); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php deleted file mode 100644 index 5e2f273ac8784..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php +++ /dev/null @@ -1,234 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Command; - -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Process\PhpExecutableFinder; -use Symfony\Component\Process\Process; - -/** - * Runs PHP's built-in web server in a background process. - * - * @author Christian Flothmann - */ -class ServerStartCommand extends ServerCommand -{ - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setDefinition(array( - new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1'), - new InputOption('port', 'p', InputOption::VALUE_REQUIRED, 'Address port number', '8000'), - new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root', null), - new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), - new InputOption('force', 'f', InputOption::VALUE_NONE, 'Force web server startup'), - )) - ->setName('server:start') - ->setDescription('Starts PHP built-in web server in the background') - ->setHelp(<<<'EOF' -The %command.name% runs PHP's built-in web server: - - php %command.full_name% - -To change the default bind address and the default port use the address argument: - - php %command.full_name% 127.0.0.1:8080 - -To change the default document root directory use the --docroot option: - - php %command.full_name% --docroot=htdocs/ - -If you have a custom document root directory layout, you can specify your own -router script using the --router option: - - php %command.full_name% --router=app/config/router.php - -Specifying a router script is required when the used environment is not "dev" or -"prod". - -See also: http://www.php.net/manual/en/features.commandline.webserver.php - -EOF - ) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $io = new SymfonyStyle($input, $cliOutput = $output); - - if (!extension_loaded('pcntl')) { - $io->error(array( - 'This command needs the pcntl extension to run.', - 'You can either install it or use the "server:run" command instead to run the built-in web server.', - )); - - if ($io->ask('Do you want to execute server:run immediately? [Yn] ', true)) { - $command = $this->getApplication()->find('server:run'); - - return $command->run($input, $cliOutput); - } - - return 1; - } - - $documentRoot = $input->getOption('docroot'); - - if (null === $documentRoot) { - $documentRoot = $this->getContainer()->getParameter('kernel.root_dir').'/../web'; - } - - if (!is_dir($documentRoot)) { - $io->error(sprintf('The given document root directory "%s" does not exist.', $documentRoot)); - - return 1; - } - - $env = $this->getContainer()->getParameter('kernel.environment'); - - if (false === $router = $this->determineRouterScript($input->getOption('router'), $env, $io)) { - return 1; - } - - $address = $input->getArgument('address'); - - if (false === strpos($address, ':')) { - $address = $address.':'.$input->getOption('port'); - } - - if (!$input->getOption('force') && $this->isOtherServerProcessRunning($address)) { - $io->error(array( - sprintf('A process is already listening on http://%s.', $address), - 'Use the --force option if the server process terminated unexpectedly to start a new web server process.', - )); - - return 1; - } - - if ('prod' === $env) { - $io->error('Running PHP built-in server in production environment is NOT recommended!'); - } - - $pid = pcntl_fork(); - - if ($pid < 0) { - $io->error('Unable to start the server process.'); - - return 1; - } - - if ($pid > 0) { - $io->success(sprintf('Web server listening on http://%s', $address)); - - return; - } - - if (posix_setsid() < 0) { - $io->error('Unable to set the child process as session leader'); - - return 1; - } - - if (null === $process = $this->createServerProcess($io, $address, $documentRoot, $router)) { - return 1; - } - - $process->disableOutput(); - $process->start(); - $lockFile = $this->getLockFile($address); - touch($lockFile); - - if (!$process->isRunning()) { - $io->error('Unable to start the server process'); - unlink($lockFile); - - return 1; - } - - // stop the web server when the lock file is removed - while ($process->isRunning()) { - if (!file_exists($lockFile)) { - $process->stop(); - } - - sleep(1); - } - } - - /** - * Determine the absolute file path for the router script, using the environment to choose a standard script - * if no custom router script is specified. - * - * @param string|null $router File path of the custom router script, if set by the user; otherwise null - * @param string $env The application environment - * @param SymfonyStyle $io An SymfonyStyle instance - * - * @return string|bool The absolute file path of the router script, or false on failure - */ - private function determineRouterScript($router, $env, SymfonyStyle $io) - { - if (null === $router) { - $router = $this - ->getContainer() - ->get('kernel') - ->locateResource(sprintf('@FrameworkBundle/Resources/config/router_%s.php', $env)) - ; - } - - if (false === $path = realpath($router)) { - $io->error(sprintf('The given router script "%s" does not exist.', $router)); - - return false; - } - - return $path; - } - - /** - * Creates a process to start PHP's built-in web server. - * - * @param SymfonyStyle $io A SymfonyStyle instance - * @param string $address IP address and port to listen to - * @param string $documentRoot The application's document root - * @param string $router The router filename - * - * @return Process The process - */ - private function createServerProcess(SymfonyStyle $io, $address, $documentRoot, $router) - { - $finder = new PhpExecutableFinder(); - if (false === $binary = $finder->find()) { - $io->error('Unable to find PHP binary to start server.'); - - return; - } - - $script = implode(' ', array_map(array('Symfony\Component\Process\ProcessUtils', 'escapeArgument'), array( - $binary, - '-S', - $address, - $router, - ))); - - return new Process('exec '.$script, $documentRoot, null, null, null); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStatusCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStatusCommand.php deleted file mode 100644 index d7cb9e7d08197..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStatusCommand.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Command; - -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; - -/** - * Shows the status of a process that is running PHP's built-in web server in - * the background. - * - * @author Christian Flothmann - */ -class ServerStatusCommand extends ServerCommand -{ - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setDefinition(array( - new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:8000'), - new InputOption('port', 'p', InputOption::VALUE_REQUIRED, 'Address port number', '8000'), - )) - ->setName('server:status') - ->setDescription('Outputs the status of the built-in web server for the given address') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $io = new SymfonyStyle($input, $output); - $address = $input->getArgument('address'); - - if (false === strpos($address, ':')) { - $address = $address.':'.$input->getOption('port'); - } - - // remove an orphaned lock file - if (file_exists($this->getLockFile($address)) && !$this->isServerRunning($address)) { - unlink($this->getLockFile($address)); - } - - if (file_exists($this->getLockFile($address))) { - $io->success(sprintf('Web server still listening on http://%s', $address)); - } else { - $io->warning(sprintf('No web server is listening on http://%s', $address)); - } - } - - private function isServerRunning($address) - { - list($hostname, $port) = explode(':', $address); - - if (false !== $fp = @fsockopen($hostname, $port, $errno, $errstr, 1)) { - fclose($fp); - - return true; - } - - return false; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_prod.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_prod.php deleted file mode 100644 index 97613a6248f71..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_prod.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/* - * This file implements rewrite rules for PHP built-in web server. - * - * See: http://www.php.net/manual/en/features.commandline.webserver.php - * - * If you have custom directory layout, then you have to write your own router - * and pass it as a value to 'router' option of server:run command. - * - * @author: Michał Pipa - * @author: Albert Jessurum - */ - -// Workaround https://bugs.php.net/64566 -if (ini_get('auto_prepend_file') && !in_array(realpath(ini_get('auto_prepend_file')), get_included_files(), true)) { - require ini_get('auto_prepend_file'); -} - -if (is_file($_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { - return false; -} - -$_SERVER = array_merge($_SERVER, $_ENV); -$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'app.php'; - -// Since we are rewriting to app.php, adjust SCRIPT_NAME and PHP_SELF accordingly -$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR.'app.php'; -$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR.'app.php'; - -require 'app.php'; - -error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_test.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_test.php deleted file mode 100644 index 5b020d5d22977..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_test.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/* - * This file implements rewrite rules for PHP built-in web server. - * - * See: http://www.php.net/manual/en/features.commandline.webserver.php - * - * If you have custom directory layout, then you have to write your own router - * and pass it as a value to 'router' option of server:run command. - * - * @author: Michał Pipa - * @author: Albert Jessurum - */ - -if (is_file($_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { - return false; -} - -$_SERVER = array_merge($_SERVER, $_ENV); -$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'app_test.php'; - -require 'app_test.php'; diff --git a/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md new file mode 100644 index 0000000000000..5fb759ca9729b --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +3.3.0 +----- + + * Added bundle diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerCommand.php new file mode 100644 index 0000000000000..ac661ac56bd20 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerCommand.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; + +/** + * Base methods for commands related to a local web server. + * + * @author Christian Flothmann + */ +abstract class ServerCommand extends ContainerAwareCommand +{ + /** + * {@inheritdoc} + */ + public function isEnabled() + { + return !defined('HHVM_VERSION') && parent::isEnabled(); + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php new file mode 100644 index 0000000000000..ca91f259539a0 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bundle\WebServerBundle\WebServer; +use Symfony\Bundle\WebServerBundle\WebServerConfig; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Runs Symfony application using a local web server. + * + * @author Michał Pipa + */ +class ServerRunCommand extends ServerCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('addressport', InputArgument::OPTIONAL, 'The address to listen to (can be address:port, address, or port)'), + new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root'), + new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), + )) + ->setName('server:run') + ->setDescription('Runs a local web server') + ->setHelp(<<<'EOF' +The %command.name% runs a local web server: + + %command.full_name% + +Change the default address and port by passing them as an argument: + + %command.full_name% 127.0.0.1:8080 + +Use the --docroot option to change the default docroot directory: + + %command.full_name% --docroot=htdocs/ + +Specify your own router script via the --router option: + + %command.full_name% --router=app/config/router.php + +See also: http://www.php.net/manual/en/features.commandline.webserver.php +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + if (null === $documentRoot = $input->getOption('docroot')) { + $documentRoot = $this->getContainer()->getParameter('kernel.root_dir').'/../web'; + } + + if (!is_dir($documentRoot)) { + $io->error(sprintf('The document root directory "%s" does not exist.', $documentRoot)); + + return 1; + } + + $env = $this->getContainer()->getParameter('kernel.environment'); + if ('prod' === $env) { + $io->error('Running this server in production environment is NOT recommended!'); + } + + $callback = null; + $disableOutput = false; + if ($output->isQuiet()) { + $disableOutput = true; + } else { + $callback = function ($type, $buffer) use ($output) { + if (Process::ERR === $type && $output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + $output->write($buffer, false, OutputInterface::OUTPUT_RAW); + }; + } + + try { + $server = new WebServer(); + $config = new WebServerConfig($documentRoot, $env, $input->getArgument('addressport'), $input->getOption('router')); + + $io->success(sprintf('Server listening on http://%s', $config->getAddress())); + $io->comment('Quit the server with CONTROL-C.'); + + $exitCode = $server->run($config, $disableOutput, $callback); + } catch (\Exception $e) { + $io->error($e->getMessage()); + + return 1; + } + + return $exitCode; + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php new file mode 100644 index 0000000000000..ba5f29d452efd --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bundle\WebServerBundle\WebServer; +use Symfony\Bundle\WebServerBundle\WebServerConfig; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Runs a local web server in a background process. + * + * @author Christian Flothmann + */ +class ServerStartCommand extends ServerCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('server:start') + ->setDefinition(array( + new InputArgument('addressport', InputArgument::OPTIONAL, 'The address to listen to (can be address:port, address, or port)'), + new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root'), + new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), + new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), + )) + ->setDescription('Starts a local web server in the background') + ->setHelp(<<<'EOF' +The %command.name% runs a local web server: + + php %command.full_name% + +Change the default address and port by passing them as an argument: + + php %command.full_name% 127.0.0.1:8080 + +Use the --docroot option to change the default docroot directory: + + php %command.full_name% --docroot=htdocs/ + +Specify your own router script via the --router option: + + php %command.full_name% --router=app/config/router.php + +See also: http://www.php.net/manual/en/features.commandline.webserver.php +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $cliOutput = $output); + + if (!extension_loaded('pcntl')) { + $io->error(array( + 'This command needs the pcntl extension to run.', + 'You can either install it or use the "server:run" command instead.', + )); + + if ($io->ask('Do you want to execute server:run immediately? [yN] ', false)) { + return $this->getApplication()->find('server:run')->run($input, $cliOutput); + } + + return 1; + } + + if (null === $documentRoot = $input->getOption('docroot')) { + $documentRoot = $this->getContainer()->getParameter('kernel.root_dir').'/../web'; + } + + if (!is_dir($documentRoot)) { + $io->error(sprintf('The document root directory "%s" does not exist.', $documentRoot)); + + return 1; + } + + $env = $this->getContainer()->getParameter('kernel.environment'); + if ('prod' === $env) { + $io->error('Running this server in production environment is NOT recommended!'); + } + + try { + $server = new WebServer(); + if ($server->isRunning($input->getOption('pidfile'))) { + $io->error(sprintf('The web server is already running (listening on http://%s).', $server->getAddress($input->getOption('pidfile')))); + + return 1; + } + + $config = new WebServerConfig($documentRoot, $env, $input->getArgument('addressport'), $input->getOption('router')); + + if (WebServer::STARTED === $server->start($config, $input->getOption('pidfile'))) { + $io->success(sprintf('Server listening on http://%s', $config->getAddress())); + } + } catch (\Exception $e) { + $io->error($e->getMessage()); + + return 1; + } + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php new file mode 100644 index 0000000000000..c77f2f755f646 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bundle\WebServerBundle\WebServer; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Shows the status of a process that is running PHP's built-in web server in + * the background. + * + * @author Christian Flothmann + */ +class ServerStatusCommand extends ServerCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('server:status') + ->setDefinition(array( + new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), + )) + ->setDescription('Outputs the status of the local web server for the given address') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $server = new WebServer(); + if ($server->isRunning($input->getOption('pidfile'))) { + $io->success(sprintf('Web server still listening on http://%s', $server->getAddress($input->getOption('pidfile')))); + } else { + $io->warning('No web server is listening.'); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStopCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php similarity index 54% rename from src/Symfony/Bundle/FrameworkBundle/Command/ServerStopCommand.php rename to src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php index 8f79978a9a845..e461b7ca6b46c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStopCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php @@ -9,16 +9,16 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\Command; +namespace Symfony\Bundle\WebServerBundle\Command; -use Symfony\Component\Console\Input\InputArgument; +use Symfony\Bundle\WebServerBundle\WebServer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Style\SymfonyStyle; /** - * Stops a background process running PHP's built-in web server. + * Stops a background process running a local web server. * * @author Christian Flothmann */ @@ -30,21 +30,19 @@ class ServerStopCommand extends ServerCommand protected function configure() { $this + ->setName('server:stop') ->setDefinition(array( - new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1'), - new InputOption('port', 'p', InputOption::VALUE_REQUIRED, 'Address port number', '8000'), + new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), )) - ->setName('server:stop') - ->setDescription('Stops PHP\'s built-in web server that was started with the server:start command') + ->setDescription('Stops the local web server that was started with the server:start command') ->setHelp(<<<'EOF' -The %command.name% stops PHP's built-in web server: +The %command.name% stops the local web server: php %command.full_name% To change the default bind address and the default port use the address argument: php %command.full_name% 127.0.0.1:8080 - EOF ) ; @@ -57,20 +55,14 @@ protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); - $address = $input->getArgument('address'); - if (false === strpos($address, ':')) { - $address = $address.':'.$input->getOption('port'); - } - - $lockFile = $this->getLockFile($address); - - if (!file_exists($lockFile)) { - $io->error(sprintf('No web server is listening on http://%s', $address)); + try { + $server = new WebServer(); + $server->stop($input->getOption('pidfile')); + $io->success('Stopped the web server.'); + } catch (\Exception $e) { + $io->error($e->getMessage()); return 1; } - - unlink($lockFile); - $io->success(sprintf('Stopped the web server listening on http://%s', $address)); } } diff --git a/src/Symfony/Bundle/WebServerBundle/LICENSE b/src/Symfony/Bundle/WebServerBundle/LICENSE new file mode 100644 index 0000000000000..17d16a13367dd --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2017 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Bundle/WebServerBundle/README.md b/src/Symfony/Bundle/WebServerBundle/README.md new file mode 100644 index 0000000000000..15c45b1a1a431 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/README.md @@ -0,0 +1,10 @@ +WebServerBundle +=============== + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php b/src/Symfony/Bundle/WebServerBundle/Resources/router.php similarity index 86% rename from src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php rename to src/Symfony/Bundle/WebServerBundle/Resources/router.php index 432ccff9204f1..187be0b8366ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php +++ b/src/Symfony/Bundle/WebServerBundle/Resources/router.php @@ -30,13 +30,15 @@ return false; } +$script = getenv('APP_FRONT_CONTROLLER') ?: 'index.php'; + $_SERVER = array_merge($_SERVER, $_ENV); -$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'app_dev.php'; +$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$script; // Since we are rewriting to app_dev.php, adjust SCRIPT_NAME and PHP_SELF accordingly -$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR.'app_dev.php'; -$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR.'app_dev.php'; +$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR.$script; +$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR.$script; -require 'app_dev.php'; +require $script; error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4); diff --git a/src/Symfony/Bundle/WebServerBundle/WebServer.php b/src/Symfony/Bundle/WebServerBundle/WebServer.php new file mode 100644 index 0000000000000..a7ddcd7ab9737 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/WebServer.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle; + +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessBuilder; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * Manages a local HTTP web server. + * + * @author Fabien Potencier + */ +class WebServer +{ + const STARTED = 0; + const STOPPED = 1; + + public function run(WebServerConfig $config, $disableOutput = true, callable $callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException(sprintf('A process is already listening on http://%s.', $config->getAddress())); + } + + $process = $this->createServerProcess($config); + if ($disableOutput) { + $process->disableOutput(); + $callback = null; + } else { + try { + $process->setTty(true); + $callback = null; + } catch (RuntimeException $e) { + } + } + + $process->run($callback); + + if (!$process->isSuccessful()) { + $error = 'Server terminated unexpectedly.'; + if ($process->isOutputDisabled()) { + $error .= ' Run the command again with -v option for more details.'; + } + + throw new \RuntimeException($error); + } + } + + public function start(WebServerConfig $config, $pidFile = null) + { + if ($this->isRunning()) { + throw new \RuntimeException(sprintf('A process is already listening on http://%s.', $config->getAddress())); + } + + $pid = pcntl_fork(); + + if ($pid < 0) { + throw new \RuntimeException('Unable to start the server process.'); + } + + if ($pid > 0) { + return self::STARTED; + } + + if (posix_setsid() < 0) { + throw new \RuntimeException('Unable to set the child process as session leader.'); + } + + $process = $this->createServerProcess($config); + $process->disableOutput(); + $process->start(); + + if (!$process->isRunning()) { + throw new \RuntimeException('Unable to start the server process.'); + } + + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + file_put_contents($pidFile, $config->getAddress()); + + // stop the web server when the lock file is removed + while ($process->isRunning()) { + if (!file_exists($pidFile)) { + $process->stop(); + } + + sleep(1); + } + + return self::STOPPED; + } + + public function stop($pidFile = null) + { + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + if (!file_exists($pidFile)) { + throw new \RuntimeException('No web server is listening.'); + } + + unlink($pidFile); + } + + public function getAddress($pidFile = null) + { + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + if (!file_exists($pidFile)) { + return false; + } + + return file_get_contents($pidFile); + } + + public function isRunning($pidFile = null) + { + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + if (!file_exists($pidFile)) { + return false; + } + + $address = file_get_contents($pidFile); + $pos = strrpos($address, ':'); + $hostname = substr($address, 0, $pos); + $port = substr($address, $pos + 1); + if (false !== $fp = @fsockopen($hostname, $port, $errno, $errstr, 1)) { + fclose($fp); + + return true; + } + + unlink($pidFile); + + return false; + } + + /** + * @return Process The process + */ + private function createServerProcess(WebServerConfig $config) + { + $finder = new PhpExecutableFinder(); + if (false === $binary = $finder->find()) { + throw new \RuntimeException('Unable to find the PHP binary.'); + } + + $builder = new ProcessBuilder(array($binary, '-S', $config->getAddress(), $config->getRouter())); + $builder->setWorkingDirectory($config->getDocumentRoot()); + $builder->setTimeout(null); + + return $builder->getProcess(); + } + + private function getDefaultPidFile() + { + return getcwd().'/.web-server-pid'; + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php b/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php new file mode 100644 index 0000000000000..3e3f41f45c978 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class WebServerBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php new file mode 100644 index 0000000000000..80eed60a4a032 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle; + +/** + * @author Fabien Potencier + */ +class WebServerConfig +{ + private $hostname; + private $port; + private $documentRoot; + private $env; + private $router; + + public function __construct($documentRoot, $env, $address = null, $router = null) + { + if (!is_dir($documentRoot)) { + throw new \InvalidArgumentException(sprintf('The document root directory "%s" does not exist.', $documentRoot)); + } + + if (null === $file = $this->guessFrontController($documentRoot, $env)) { + throw new \InvalidArgumentException(sprintf('Unable to guess the front controller under "%s".', $documentRoot)); + } + + putenv('APP_FRONT_CONTROLLER='.$file); + + $this->documentRoot = $documentRoot; + $this->env = $env; + $this->router = $router ?: __DIR__.'/Resources/router.php'; + + if (null === $address) { + $this->hostname = '127.0.0.1'; + $this->port = $this->findBestPort(); + } elseif (false !== $pos = strrpos($address, ':')) { + $this->hostname = substr($address, 0, $pos); + $this->port = substr($address, $pos + 1); + } elseif (ctype_digit($address)) { + $this->hostname = '127.0.0.1'; + $this->port = $address; + } else { + $this->hostname = $address; + $this->port = $this->findBestPort(); + } + + if (!ctype_digit($this->port)) { + throw new \InvalidArgumentException(sprintf('Port "%s" is not valid.', $this->port)); + } + } + + public function getDocumentRoot() + { + return $this->documentRoot; + } + + public function getEnv() + { + return $this->env; + } + + public function getRouter() + { + return $this->router; + } + + public function getHostname() + { + return $this->hostname; + } + + public function getPort() + { + return $this->port; + } + + public function getAddress() + { + return $this->hostname.':'.$this->port; + } + + private function guessFrontController($documentRoot, $env) + { + foreach (array('app', 'index') as $prefix) { + $file = sprintf('%s_%s.php', $prefix, $env); + if (file_exists($documentRoot.'/'.$file)) { + return $file; + } + + $file = sprintf('%s.php', $prefix); + if (file_exists($documentRoot.'/'.$file)) { + return $file; + } + } + } + + private function findBestPort() + { + $port = 8000; + while (false !== $fp = @fsockopen('127.0.0.1', $port, $errno, $errstr, 1)) { + fclose($fp); + if ($port++ >= 8100) { + throw new \RuntimeException('Unable to find a port available to run the web server.'); + } + } + + return $port; + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/composer.json b/src/Symfony/Bundle/WebServerBundle/composer.json new file mode 100644 index 0000000000000..3e5c3b94608a6 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/web-server-bundle", + "type": "web-server-bundle", + "description": "Symfony WebServerBundle", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9", + "symfony/console": "~2.8.8|~3.0.8|~3.1.2|~3.2", + "symfony/process": "~2.8|~3.0" + }, + "autoload": { + "psr-4": { "Symfony\\Bundle\\WebServerBundle\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/phpunit.xml.dist b/src/Symfony/Bundle/WebServerBundle/phpunit.xml.dist new file mode 100644 index 0000000000000..6bfaf3004a1f9 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + +