feature #21039 Web server bundle (fabpot)

This PR was squashed before being merged into the 3.3-dev branch (closes #21039).

Discussion
----------

Web server bundle

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #21040
| License       | MIT
| Doc PR        | n/a

Moved the `server:*` commands to a new bundle. It makes them more easily discoverable and more decoupled. Discoverability is important when not using symfony/symfony. In that case, the commands are not available unless you have the symfony/process component installed. With a dedicated bundle, installing the bundle also installs the dependency, making the whole process easier.

Usage is the same as the current commands for basic usage:

To start a web server in the foreground:

```
bin/console server:run
```

To manage a background server:

```
bin/console server:start
bin/console server:stop
bin/console server:status
```

The big difference is that port is auto-determined if something is already listening on port 8000.

Usage is **different** if you pass options:

```
bin/console server:start 127.0.0.1:8888
bin/console server:stop # no need to pass the address again
bin/console server:status # no need to pass the address again
```

That's possible as the web server now stores its address in a pid file stored in the current directory.

Commits
-------

f39b32735b [WebServerBundle] switched auto-run of server:start to off by default
961d1cea55 [WebServerBundle] fixed server:start when already running
126f4d9ec4 [WebServerBundle] added support for port auto-detection
6f689d6597 [WebServerBundle] changed the way we keep track of the web server
585d4451c8 [WebServerBundle] tweaked command docs
fa7ebc57de [WebServerBundle] moved most of the logic in a new class
951a1a227d [WebServerBundle] changed wording
ac1ba7700e made the router configurable via env vars
48dd2b0dbe removed obsolete check
132902c621 moved server:* command to a new bundle
This commit is contained in:
Fabien Potencier 2017-01-05 19:09:35 -08:00
commit be85fcc2b8
22 changed files with 744 additions and 656 deletions

View File

@ -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"
},

View File

@ -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()`

View File

@ -1,69 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@xabbuh.de>
*/
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;
}
}

View File

@ -1,175 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <michal.pipa.xsolve@gmail.com>
*/
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 <info>%command.name%</info> runs PHP built-in web server:
<info>%command.full_name%</info>
To change default bind address and port use the <info>address</info> argument:
<info>%command.full_name% 127.0.0.1:8080</info>
To change default docroot directory use the <info>--docroot</info> option:
<info>%command.full_name% --docroot=htdocs/</info>
If you have custom docroot directory layout, you can specify your own
router script using <info>--router</info> option:
<info>%command.full_name% --router=app/config/router.php</info>
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));
}
}

View File

@ -1,234 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@xabbuh.de>
*/
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 <info>%command.name%</info> runs PHP's built-in web server:
<info>php %command.full_name%</info>
To change the default bind address and the default port use the <info>address</info> argument:
<info>php %command.full_name% 127.0.0.1:8080</info>
To change the default document root directory use the <info>--docroot</info> option:
<info>php %command.full_name% --docroot=htdocs/</info>
If you have a custom document root directory layout, you can specify your own
router script using the <info>--router</info> option:
<info>php %command.full_name% --router=app/config/router.php</info>
Specifying a router script is required when the used environment is not <comment>"dev"</comment> or
<comment>"prod"</comment>.
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 <info>server:run</info> 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);
}
}

View File

@ -1,79 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@xabbuh.de>
*/
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;
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <michal.pipa.xsolve@gmail.com>
* @author: Albert Jessurum <ajessu@gmail.com>
*/
// 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);

View File

@ -1,31 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <michal.pipa.xsolve@gmail.com>
* @author: Albert Jessurum <ajessu@gmail.com>
*/
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';

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
3.3.0
-----
* Added bundle

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@xabbuh.de>
*/
abstract class ServerCommand extends ContainerAwareCommand
{
/**
* {@inheritdoc}
*/
public function isEnabled()
{
return !defined('HHVM_VERSION') && parent::isEnabled();
}
}

View File

@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <michal.pipa.xsolve@gmail.com>
*/
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 <info>%command.name%</info> runs a local web server:
<info>%command.full_name%</info>
Change the default address and port by passing them as an argument:
<info>%command.full_name% 127.0.0.1:8080</info>
Use the <info>--docroot</info> option to change the default docroot directory:
<info>%command.full_name% --docroot=htdocs/</info>
Specify your own router script via the <info>--router</info> option:
<info>%command.full_name% --router=app/config/router.php</info>
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;
}
}

View File

@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@xabbuh.de>
*/
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 <info>%command.name%</info> runs a local web server:
<info>php %command.full_name%</info>
Change the default address and port by passing them as an argument:
<info>php %command.full_name% 127.0.0.1:8080</info>
Use the <info>--docroot</info> option to change the default docroot directory:
<info>php %command.full_name% --docroot=htdocs/</info>
Specify your own router script via the <info>--router</info> option:
<info>php %command.full_name% --router=app/config/router.php</info>
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 <info>server:run</info> 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;
}
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@xabbuh.de>
*/
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.');
}
}
}

View File

@ -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 <christian.flothmann@xabbuh.de>
*/
@ -30,21 +30,19 @@ class ServerStopCommand extends ServerCommand
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'),
))
->setName('server:stop')
->setDescription('Stops PHP\'s built-in web server that was started with the server:start command')
->setDefinition(array(
new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'),
))
->setDescription('Stops the local web server that was started with the server:start command')
->setHelp(<<<'EOF'
The <info>%command.name%</info> stops PHP's built-in web server:
The <info>%command.name%</info> stops the local web server:
<info>php %command.full_name%</info>
To change the default bind address and the default port use the <info>address</info> argument:
<info>php %command.full_name% 127.0.0.1:8080</info>
EOF
)
;
@ -57,20 +55,14 @@ EOF
{
$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));
}
}

View File

@ -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.

View File

@ -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)

View File

@ -30,13 +30,15 @@ if (is_file($_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'
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);

View File

@ -0,0 +1,165 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
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';
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
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;
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony WebServerBundle Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>