Skip to content

Commit

Permalink
Update Serve.php
Browse files Browse the repository at this point in the history
  • Loading branch information
juancristobalgd1 authored Aug 4, 2024
1 parent c7a8b9d commit 76bb726
Showing 1 changed file with 174 additions and 125 deletions.
299 changes: 174 additions & 125 deletions src/libraries/Console/Commands/Server/Serve.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,181 +2,230 @@

declare(strict_types=1);

/**
* Axm Framework PHP.
*
* @author Juan Cristobal <[email protected]>
* @link http://www.axm.com/
* @license http://www.axm.com/license/
* @package Console
*/

namespace Console\Commands\Server;

use Console\BaseCommand;
use Console\CLI;
use RuntimeException;

/**
* Class Serve
*
* Launch the Axm PHP Development Server
* @package Console\Commands\Server
*/
class Serve extends BaseCommand
{
/**
* Group
*/
protected string $group = 'Axm';

/**
* Name
*/
protected string $name = 'serve';

/**
* Description
*/
protected string $description = 'Launches the Axm PHP Development Server';

/**
* Usage
*/
protected string $usage = 'serve [--host] [--port]';

/**
* Options
*/
protected array $options = [
'--php' => 'The PHP Binary [default: "PHP_BINARY"]',
'--host' => 'The HTTP Host [default: "localhost"]',
'--port' => 'The HTTP Host Port [default: "8080"]',
];

/**
* The current port offset.
*/
protected int $portOffset = 0;

/**
* The max number of ports to attempt to serve from
*/
protected int $maxTries = 10;

/**
* Default port number
*/
protected int $defaultPort = 8080;

/**
*
*/
protected $process;
protected float $startTime;
protected bool $shouldShutdown = false;
protected int $serverPid;

/**
* Run the server
*/
public function run(array $params)
{
// Collect any user-supplied options and apply them.
$php = CLI::getOption('php', PHP_BINARY);
$host = CLI::getOption('host', 'localhost');
$port = (int) CLI::getOption('port', $this->defaultPort);

// Attempt alternative ports
// if (!$port = $this->findAvailablePort($host, $port)) {
// CLI::error('Could not bind to any port');
// exit;
// }

CLI::loading(1);
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, [$this, 'signalHandler']);
pcntl_signal(SIGTERM, [$this, 'signalHandler']);
}

// Server up
$this->startServer($php, $host, $port);
}

/**
* Find an available port
*/
protected function findAvailablePort(string $host, int $startPort): ?int
protected function startServer(string $php, string $host, int $port, bool $forceKill = false)
{
$fcroot = ROOT_PATH;
if (!is_dir($fcroot)) throw new RuntimeException("Invalid root directory: $fcroot");

if ($forceKill) $this->killExistingProcess($host, $port);

$command = sprintf('%s -S %s:%d -t %s', escapeshellarg($php), $host, $port, escapeshellarg($fcroot));

$this->printServerHeader();
CLI::write(" Command: " . CLI::color($command, 'cyan'), 'dark_gray');

if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, [$this, 'signalHandler']);
pcntl_signal(SIGTERM, [$this, 'signalHandler']);
}

$this->process = proc_open($command, [STDIN, STDOUT, STDERR], $pipes);

if (!is_resource($this->process)) throw new RuntimeException("Failed to start the server process.");

$status = proc_get_status($this->process);
$this->serverPid = $status['pid'];

$this->printServerInfo('http', $host, $port);

// Simplemente espera hasta que el proceso termine
while (proc_get_status($this->process)['running']) {
sleep(1);
if (function_exists('pcntl_signal_dispatch'))
pcntl_signal_dispatch();
}

$this->shutdown(true, true);
}

protected function printServerHeader()
{
CLI::newLine();
$header = " AXM DEVELOPMENT SERVER ";
$padding = str_repeat('=', strlen($header));
CLI::write($padding, 'green');
CLI::write($header, 'green');
CLI::write($padding, 'green');
CLI::newLine();
}

protected function printServerInfo(string $scheme, string $host, int $port)
{
$url = "{$scheme}://{$host}:{$port}";
CLI::write(" " . CLI::color('Server running at:', 'green'));
CLI::write(" " . CLI::color($url, 'yellow'));
CLI::newLine();
CLI::write(" " . CLI::color('Document root:', 'green') . " " . CLI::color(ROOT_PATH, 'dark_gray'));
CLI::write(" " . CLI::color('Environment:', 'green') . " " . CLI::color(getenv('AXM_ENV') ?: 'production', 'dark_gray'));
CLI::newLine();
CLI::write(" " . CLI::color('Press Ctrl+C to stop the server', 'cyan'));
CLI::newLine();
$this->printServerReadyMessage();
}

protected function printServerReadyMessage()
{
$maxTries = $this->maxTries;
for ($port = $startPort; $port < $startPort + $maxTries; $port++) {
if ($this->checkPort($host, $port)) {
return $port;
}
CLI::write(str_repeat('-', 50), 'dark_gray');
CLI::write(" " . CLI::color('Server is ready to handle requests!', 'green'));
CLI::write(str_repeat('-', 50), 'dark_gray');
CLI::newLine();
}

public function signalHandler($signo)
{
switch ($signo) {
case SIGINT:
case SIGTERM:
$this->shutdown(true, true);
exit;
}
}

return null;
public function shutdown(bool $exit = false, bool $message = true)
{
if ($message) {
CLI::newLine();
CLI::write(" " . CLI::color('Shutting down the server...', 'yellow'));
}

if (is_resource($this->process)) {
proc_terminate($this->process, SIGINT);
proc_close($this->process);
}

if ($message) {
CLI::write(" " . CLI::color('Server stopped successfully.', 'green'));
CLI::newLine();
}

if ($exit) exit(0);
}

/**
* Check if a port is available by attempting to connect to it.
*/
protected function checkPort(string $host, int $port): bool
protected function killExistingProcess(string $host, int $port)
{
try {
$url = "http://$host:$port";
$headers = @get_headers($url);
return !empty($headers);
} catch (\Throwable $th) {
return false;
if (PHP_OS_FAMILY === 'Windows')
exec("FOR /F \"usebackq tokens=5\" %a in (`netstat -ano ^| findstr :$port`) do taskkill /F /PID %a");
else
exec("lsof -ti tcp:$port | xargs kill -9");

sleep(1); // Dar tiempo para que el proceso se cierre completamente
}

protected function formatAndPrintOutput($output)
{
$lines = explode("\n", trim($output));

foreach ($lines as $line) {
if (preg_match('/^\[(.*?)\] (\[.*?\] )?(.*?)$/', $line, $matches)) {
$timestamp = $matches[1];
$clientInfo = $matches[2] ?? '';
$content = $matches[3];

$formattedLine = $this->formatTimestampAndClientInfo($timestamp, $clientInfo);
$formattedLine .= $this->formatHttpRequest($content);

CLI::write($formattedLine, 'light_gray');
} else
CLI::write(CLI::color($line, 'light_gray'));
}
}

/**
* Start the server
*/
protected function startServer(string $php, string $host, int $port)
protected function formatTimestampAndClientInfo($timestamp, $clientInfo)
{
$formattedTimestamp = CLI::color("[$timestamp]", 'light_gray');
$formattedClientInfo = CLI::color(" $clientInfo", 'light_gray');

return $formattedTimestamp . $formattedClientInfo;
}

protected function formatHttpRequest($content)
{
// Path Root.
$fcroot = getcwd();
if (is_dir($fcroot)) {
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => STDOUT, // stdout
2 => STDERR // stderr
];

$command = "{$php} -S {$host}:{$port} -t {$fcroot}";
$this->process = proc_open($command, $descriptors, $pipes);

if (is_resource($this->process)) {
while ($output = fgets($pipes[0])) {
if (strpos($output, 'SIGINT') !== false) {
$this->shutdown();
}
}

$this->printServerInfo('http', $host, $port);
}

$code = proc_close($this->process);
if ($code !== 0) {
throw new RuntimeException("Unknown error (code: $code)", $code);
}
if (preg_match('/(\[.*?\]) (\[(\d+)\]): ([A-Z]+) (.*)/', $content, $requestMatches)) {
$statusCode = $requestMatches[3];
$method = $requestMatches[4];
$path = $requestMatches[5];

$coloredMethod = $this->colorizeMethod($method);
$coloredPath = CLI::color($path, 'light_gray');
$coloredStatus = $this->colorizeStatusCode($statusCode);

return "{$coloredStatus}: {$coloredMethod} {$coloredPath}";
}

return CLI::color($content, 'light_gray');
}

/**
* Shutdown the server
*/
protected function shutdown()
protected function formatAndPrintError($error)
{
CLI::info('Shutting down the server...');
proc_terminate($this->process);
$lines = explode("\n", trim($error));
foreach ($lines as $line)
CLI::write(CLI::color('ERROR: ', 'red') . CLI::color($line, 'light_red'));
}

/**
* Print server information
*/
protected function printServerInfo(string $scheme, string $host, int $port)
protected function colorizeStatusCode($statusCode): string
{
CLI::info(self::ARROW_SYMBOL . 'Axm development server started on: ' . CLI::color("{$scheme}://{$host}:{$port}", 'green'));
$color = match (true) {
$statusCode >= 200 && $statusCode < 300 => 'green',
$statusCode >= 300 && $statusCode < 400 => 'yellow',
$statusCode >= 400 && $statusCode < 500 => 'light_red',
default => 'red',
};

return CLI::color("[$statusCode]", $color);
}

CLI::newLine();
CLI::write(self::ARROW_SYMBOL . 'Press Control-C to stop.', 'yellow');
CLI::newLine(2);
protected function colorizeMethod($method)
{
$colors = [
'GET' => 'green',
'POST' => 'yellow',
'PUT' => 'blue',
'DELETE' => 'red',
'PATCH' => 'purple',
'HEAD' => 'cyan',
'OPTIONS' => 'white'
];

return CLI::color($method, $colors[$method] ?? 'white');
}
}

0 comments on commit 76bb726

Please sign in to comment.