-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[FrameworkBundle] Additional helper commands to control PHP's built-in web server #11311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?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 (version_compare(phpversion(), '5.4.0', '<') || defined('HHVM_VERSION')) { | ||
return false; | ||
} | ||
|
||
if (!extension_loaded('pcntl')) { | ||
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'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
<?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\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:8000'), | ||
new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root', 'web/'), | ||
new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), | ||
)) | ||
->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>%command.full_name%</info> | ||
|
||
To change the default bind address and the default port use the <info>address</info> argument: | ||
|
||
<info>%command.full_name% 127.0.0.1:8080</info> | ||
|
||
To change the default document root directory use the <info>--docroot</info> option: | ||
|
||
<info>%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>%command.full_name% --router=app/config/router.php</info> | ||
|
||
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) | ||
{ | ||
$env = $this->getContainer()->getParameter('kernel.environment'); | ||
|
||
if ('prod' === $env) { | ||
$output->writeln('<error>Running PHP built-in server in production environment is NOT recommended!</error>'); | ||
} | ||
|
||
$pid = pcntl_fork(); | ||
|
||
if ($pid < 0) { | ||
$output->writeln('<error>Unable to start the server process</error>'); | ||
|
||
return 1; | ||
} | ||
|
||
if ($pid > 0) { | ||
$output->writeln('<info>Server started successfully</info>'); | ||
|
||
return; | ||
} | ||
|
||
if (posix_setsid() < 0) { | ||
$output->writeln('<error>Unable to set the child process as session leader</error>'); | ||
|
||
return 1; | ||
} | ||
|
||
$process = $this->createServerProcess( | ||
$input->getArgument('address'), | ||
$input->getOption('docroot'), | ||
$input->getOption('router'), | ||
$env, | ||
null | ||
); | ||
$process->disableOutput(); | ||
$process->start(); | ||
$lockFile = $this->getLockFile($input->getArgument('address')); | ||
touch($lockFile); | ||
|
||
if (!$process->isRunning()) { | ||
$output->writeln('<error>Unable to start the server process</error>'); | ||
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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the process is killed, let's remove the file if (file_exists($lockFile)) {
unlink($lockfile);
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it's one line above |
||
} | ||
|
||
/** | ||
* Creates a process to start PHP's built-in web server. | ||
* | ||
* @param string $address IP address and port to listen to | ||
* @param string $documentRoot The application's document root | ||
* @param string $router The router filename | ||
* @param string $env The application environment | ||
* @param int $timeout Process timeout | ||
* | ||
* @return Process The process | ||
*/ | ||
private function createServerProcess($address, $documentRoot, $router, $env, $timeout = null) | ||
{ | ||
$router = $router ?: $this | ||
->getContainer() | ||
->get('kernel') | ||
->locateResource(sprintf('@FrameworkBundle/Resources/config/router_%s.php', $env)) | ||
; | ||
$script = implode(' ', array_map(array('Symfony\Component\Process\ProcessUtils', 'escapeArgument'), array( | ||
PHP_BINARY, | ||
'-S', | ||
$address, | ||
$router, | ||
))); | ||
|
||
return new Process('exec '.$script, $documentRoot, null, null, $timeout); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?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\Output\OutputInterface; | ||
|
||
/** | ||
* 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'), | ||
)) | ||
->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) | ||
{ | ||
$address = $input->getArgument('address'); | ||
|
||
// remove an orphaned lock file | ||
if (file_exists($this->getLockFile($address)) && !$this->isServerRunning($address)) { | ||
unlink($this->getLockFile($address)); | ||
} | ||
|
||
if (file_exists($this->getLockFile($address))) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The server process could have crashed. You could check if the server sends HTTP responses. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. That could be done relatively easy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The lock file is now removed if the server process terminated before. |
||
$output->writeln(sprintf('<info>Web server still listening on %s</info>', $address)); | ||
} else { | ||
$output->writeln(sprintf('<error>No web server is listening on %s</error>', $address)); | ||
} | ||
} | ||
|
||
private function isServerRunning($address) | ||
{ | ||
list($hostname, $port) = explode(':', $address); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this would trigger an error in case the address is not in the right format (for example To avoid that, you can use: explode(':', $address) + array(8000); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or better, ensure the passed argument matches a regular expression There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really, there wouldn't be any server running with this address. So, there is no lock file and |
||
|
||
if (false !== $fp = @fsockopen($hostname, $port, $errno, $errstr, 1)) { | ||
fclose($fp); | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<?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\Output\OutputInterface; | ||
|
||
/** | ||
* Stops a background process running PHP's built-in web server. | ||
* | ||
* @author Christian Flothmann <christian.flothmann@xabbuh.de> | ||
*/ | ||
class ServerStopCommand extends ServerCommand | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function configure() | ||
{ | ||
$this | ||
->setDefinition(array( | ||
new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:8000'), | ||
)) | ||
->setName('server:stop') | ||
->setDescription('Stops PHP\'s built-in web server that was started with the server:start command') | ||
->setHelp(<<<EOF | ||
The <info>%command.name%</info> stops PHP's built-in web server: | ||
|
||
<info>%command.full_name%</info> | ||
|
||
To change the default bind address and the default port use the <info>address</info> argument: | ||
|
||
<info>%command.full_name% 127.0.0.1:8080</info> | ||
|
||
EOF | ||
) | ||
; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function execute(InputInterface $input, OutputInterface $output) | ||
{ | ||
$address = $input->getArgument('address'); | ||
$lockFile = $this->getLockFile($address); | ||
|
||
if (!file_exists($lockFile)) { | ||
$output->writeln(sprintf('<error>No web server is listening on %s</error>', $address)); | ||
|
||
return 1; | ||
} | ||
|
||
unlink($lockFile); | ||
$output->writeln(sprintf('<info>Stopped the web server listening on %s</info>', $address)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this command must be disabled when pcntl is not available
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. I added a check. I also added this check to the
server:stop
andserver:status
commands since they don't make any sense when theserver:start
command is not available.