-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c7a8b9d
commit 76bb726
Showing
1 changed file
with
174 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'); | ||
} | ||
} |