diff --git a/src/libraries/Console/Commands/Server/Serve.php b/src/libraries/Console/Commands/Server/Serve.php index 927d373..3ecdd5e 100644 --- a/src/libraries/Console/Commands/Server/Serve.php +++ b/src/libraries/Console/Commands/Server/Serve.php @@ -2,181 +2,230 @@ declare(strict_types=1); -/** - * Axm Framework PHP. - * - * @author Juan Cristobal - * @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'); } }