|
2 | 2 |
|
3 | 3 | declare(strict_types=1); |
4 | 4 |
|
5 | | -/** |
6 | | - * Axm Framework PHP. |
7 | | - * |
8 | | - * @author Juan Cristobal <[email protected]> |
9 | | - * @link http://www.axm.com/ |
10 | | - * @license http://www.axm.com/license/ |
11 | | - * @package Console |
12 | | - */ |
13 | | - |
14 | 5 | namespace Console\Commands\Server; |
15 | 6 |
|
16 | 7 | use Console\BaseCommand; |
17 | 8 | use Console\CLI; |
18 | 9 | use RuntimeException; |
19 | 10 |
|
20 | | -/** |
21 | | - * Class Serve |
22 | | - * |
23 | | - * Launch the Axm PHP Development Server |
24 | | - * @package Console\Commands\Server |
25 | | - */ |
26 | 11 | class Serve extends BaseCommand |
27 | 12 | { |
28 | | - /** |
29 | | - * Group |
30 | | - */ |
31 | 13 | protected string $group = 'Axm'; |
32 | | - |
33 | | - /** |
34 | | - * Name |
35 | | - */ |
36 | 14 | protected string $name = 'serve'; |
37 | | - |
38 | | - /** |
39 | | - * Description |
40 | | - */ |
41 | 15 | protected string $description = 'Launches the Axm PHP Development Server'; |
42 | | - |
43 | | - /** |
44 | | - * Usage |
45 | | - */ |
46 | 16 | protected string $usage = 'serve [--host] [--port]'; |
47 | | - |
48 | | - /** |
49 | | - * Options |
50 | | - */ |
51 | 17 | protected array $options = [ |
52 | 18 | '--php' => 'The PHP Binary [default: "PHP_BINARY"]', |
53 | 19 | '--host' => 'The HTTP Host [default: "localhost"]', |
54 | 20 | '--port' => 'The HTTP Host Port [default: "8080"]', |
55 | 21 | ]; |
56 | 22 |
|
57 | | - /** |
58 | | - * The current port offset. |
59 | | - */ |
60 | 23 | protected int $portOffset = 0; |
61 | | - |
62 | | - /** |
63 | | - * The max number of ports to attempt to serve from |
64 | | - */ |
65 | 24 | protected int $maxTries = 10; |
66 | | - |
67 | | - /** |
68 | | - * Default port number |
69 | | - */ |
70 | 25 | protected int $defaultPort = 8080; |
71 | | - |
72 | | - /** |
73 | | - * |
74 | | - */ |
75 | 26 | protected $process; |
| 27 | + protected float $startTime; |
| 28 | + protected bool $shouldShutdown = false; |
| 29 | + protected int $serverPid; |
76 | 30 |
|
77 | | - /** |
78 | | - * Run the server |
79 | | - */ |
80 | 31 | public function run(array $params) |
81 | 32 | { |
82 | | - // Collect any user-supplied options and apply them. |
83 | 33 | $php = CLI::getOption('php', PHP_BINARY); |
84 | 34 | $host = CLI::getOption('host', 'localhost'); |
85 | 35 | $port = (int) CLI::getOption('port', $this->defaultPort); |
86 | 36 |
|
87 | | - // Attempt alternative ports |
88 | | - // if (!$port = $this->findAvailablePort($host, $port)) { |
89 | | - // CLI::error('Could not bind to any port'); |
90 | | - // exit; |
91 | | - // } |
92 | | - |
93 | | - CLI::loading(1); |
| 37 | + if (function_exists('pcntl_signal')) { |
| 38 | + pcntl_signal(SIGINT, [$this, 'signalHandler']); |
| 39 | + pcntl_signal(SIGTERM, [$this, 'signalHandler']); |
| 40 | + } |
94 | 41 |
|
95 | | - // Server up |
96 | 42 | $this->startServer($php, $host, $port); |
97 | 43 | } |
98 | 44 |
|
99 | | - /** |
100 | | - * Find an available port |
101 | | - */ |
102 | | - protected function findAvailablePort(string $host, int $startPort): ?int |
| 45 | + protected function startServer(string $php, string $host, int $port, bool $forceKill = false) |
| 46 | + { |
| 47 | + $fcroot = ROOT_PATH; |
| 48 | + if (!is_dir($fcroot)) throw new RuntimeException("Invalid root directory: $fcroot"); |
| 49 | + |
| 50 | + if ($forceKill) $this->killExistingProcess($host, $port); |
| 51 | + |
| 52 | + $command = sprintf('%s -S %s:%d -t %s', escapeshellarg($php), $host, $port, escapeshellarg($fcroot)); |
| 53 | + |
| 54 | + $this->printServerHeader(); |
| 55 | + CLI::write(" Command: " . CLI::color($command, 'cyan'), 'dark_gray'); |
| 56 | + |
| 57 | + if (function_exists('pcntl_signal')) { |
| 58 | + pcntl_signal(SIGINT, [$this, 'signalHandler']); |
| 59 | + pcntl_signal(SIGTERM, [$this, 'signalHandler']); |
| 60 | + } |
| 61 | + |
| 62 | + $this->process = proc_open($command, [STDIN, STDOUT, STDERR], $pipes); |
| 63 | + |
| 64 | + if (!is_resource($this->process)) throw new RuntimeException("Failed to start the server process."); |
| 65 | + |
| 66 | + $status = proc_get_status($this->process); |
| 67 | + $this->serverPid = $status['pid']; |
| 68 | + |
| 69 | + $this->printServerInfo('http', $host, $port); |
| 70 | + |
| 71 | + // Simplemente espera hasta que el proceso termine |
| 72 | + while (proc_get_status($this->process)['running']) { |
| 73 | + sleep(1); |
| 74 | + if (function_exists('pcntl_signal_dispatch')) |
| 75 | + pcntl_signal_dispatch(); |
| 76 | + } |
| 77 | + |
| 78 | + $this->shutdown(true, true); |
| 79 | + } |
| 80 | + |
| 81 | + protected function printServerHeader() |
| 82 | + { |
| 83 | + CLI::newLine(); |
| 84 | + $header = " AXM DEVELOPMENT SERVER "; |
| 85 | + $padding = str_repeat('=', strlen($header)); |
| 86 | + CLI::write($padding, 'green'); |
| 87 | + CLI::write($header, 'green'); |
| 88 | + CLI::write($padding, 'green'); |
| 89 | + CLI::newLine(); |
| 90 | + } |
| 91 | + |
| 92 | + protected function printServerInfo(string $scheme, string $host, int $port) |
| 93 | + { |
| 94 | + $url = "{$scheme}://{$host}:{$port}"; |
| 95 | + CLI::write(" " . CLI::color('Server running at:', 'green')); |
| 96 | + CLI::write(" " . CLI::color($url, 'yellow')); |
| 97 | + CLI::newLine(); |
| 98 | + CLI::write(" " . CLI::color('Document root:', 'green') . " " . CLI::color(ROOT_PATH, 'dark_gray')); |
| 99 | + CLI::write(" " . CLI::color('Environment:', 'green') . " " . CLI::color(getenv('AXM_ENV') ?: 'production', 'dark_gray')); |
| 100 | + CLI::newLine(); |
| 101 | + CLI::write(" " . CLI::color('Press Ctrl+C to stop the server', 'cyan')); |
| 102 | + CLI::newLine(); |
| 103 | + $this->printServerReadyMessage(); |
| 104 | + } |
| 105 | + |
| 106 | + protected function printServerReadyMessage() |
103 | 107 | { |
104 | | - $maxTries = $this->maxTries; |
105 | | - for ($port = $startPort; $port < $startPort + $maxTries; $port++) { |
106 | | - if ($this->checkPort($host, $port)) { |
107 | | - return $port; |
108 | | - } |
| 108 | + CLI::write(str_repeat('-', 50), 'dark_gray'); |
| 109 | + CLI::write(" " . CLI::color('Server is ready to handle requests!', 'green')); |
| 110 | + CLI::write(str_repeat('-', 50), 'dark_gray'); |
| 111 | + CLI::newLine(); |
| 112 | + } |
| 113 | + |
| 114 | + public function signalHandler($signo) |
| 115 | + { |
| 116 | + switch ($signo) { |
| 117 | + case SIGINT: |
| 118 | + case SIGTERM: |
| 119 | + $this->shutdown(true, true); |
| 120 | + exit; |
109 | 121 | } |
| 122 | + } |
110 | 123 |
|
111 | | - return null; |
| 124 | + public function shutdown(bool $exit = false, bool $message = true) |
| 125 | + { |
| 126 | + if ($message) { |
| 127 | + CLI::newLine(); |
| 128 | + CLI::write(" " . CLI::color('Shutting down the server...', 'yellow')); |
| 129 | + } |
| 130 | + |
| 131 | + if (is_resource($this->process)) { |
| 132 | + proc_terminate($this->process, SIGINT); |
| 133 | + proc_close($this->process); |
| 134 | + } |
| 135 | + |
| 136 | + if ($message) { |
| 137 | + CLI::write(" " . CLI::color('Server stopped successfully.', 'green')); |
| 138 | + CLI::newLine(); |
| 139 | + } |
| 140 | + |
| 141 | + if ($exit) exit(0); |
112 | 142 | } |
113 | 143 |
|
114 | | - /** |
115 | | - * Check if a port is available by attempting to connect to it. |
116 | | - */ |
117 | | - protected function checkPort(string $host, int $port): bool |
| 144 | + protected function killExistingProcess(string $host, int $port) |
118 | 145 | { |
119 | | - try { |
120 | | - $url = "http://$host:$port"; |
121 | | - $headers = @get_headers($url); |
122 | | - return !empty($headers); |
123 | | - } catch (\Throwable $th) { |
124 | | - return false; |
| 146 | + if (PHP_OS_FAMILY === 'Windows') |
| 147 | + exec("FOR /F \"usebackq tokens=5\" %a in (`netstat -ano ^| findstr :$port`) do taskkill /F /PID %a"); |
| 148 | + else |
| 149 | + exec("lsof -ti tcp:$port | xargs kill -9"); |
| 150 | + |
| 151 | + sleep(1); // Dar tiempo para que el proceso se cierre completamente |
| 152 | + } |
| 153 | + |
| 154 | + protected function formatAndPrintOutput($output) |
| 155 | + { |
| 156 | + $lines = explode("\n", trim($output)); |
| 157 | + |
| 158 | + foreach ($lines as $line) { |
| 159 | + if (preg_match('/^\[(.*?)\] (\[.*?\] )?(.*?)$/', $line, $matches)) { |
| 160 | + $timestamp = $matches[1]; |
| 161 | + $clientInfo = $matches[2] ?? ''; |
| 162 | + $content = $matches[3]; |
| 163 | + |
| 164 | + $formattedLine = $this->formatTimestampAndClientInfo($timestamp, $clientInfo); |
| 165 | + $formattedLine .= $this->formatHttpRequest($content); |
| 166 | + |
| 167 | + CLI::write($formattedLine, 'light_gray'); |
| 168 | + } else |
| 169 | + CLI::write(CLI::color($line, 'light_gray')); |
125 | 170 | } |
126 | 171 | } |
127 | 172 |
|
128 | | - /** |
129 | | - * Start the server |
130 | | - */ |
131 | | - protected function startServer(string $php, string $host, int $port) |
| 173 | + protected function formatTimestampAndClientInfo($timestamp, $clientInfo) |
| 174 | + { |
| 175 | + $formattedTimestamp = CLI::color("[$timestamp]", 'light_gray'); |
| 176 | + $formattedClientInfo = CLI::color(" $clientInfo", 'light_gray'); |
| 177 | + |
| 178 | + return $formattedTimestamp . $formattedClientInfo; |
| 179 | + } |
| 180 | + |
| 181 | + protected function formatHttpRequest($content) |
132 | 182 | { |
133 | | - // Path Root. |
134 | | - $fcroot = getcwd(); |
135 | | - if (is_dir($fcroot)) { |
136 | | - $descriptors = [ |
137 | | - 0 => ['pipe', 'r'], // stdin |
138 | | - 1 => STDOUT, // stdout |
139 | | - 2 => STDERR // stderr |
140 | | - ]; |
141 | | - |
142 | | - $command = "{$php} -S {$host}:{$port} -t {$fcroot}"; |
143 | | - $this->process = proc_open($command, $descriptors, $pipes); |
144 | | - |
145 | | - if (is_resource($this->process)) { |
146 | | - while ($output = fgets($pipes[0])) { |
147 | | - if (strpos($output, 'SIGINT') !== false) { |
148 | | - $this->shutdown(); |
149 | | - } |
150 | | - } |
151 | | - |
152 | | - $this->printServerInfo('http', $host, $port); |
153 | | - } |
154 | | - |
155 | | - $code = proc_close($this->process); |
156 | | - if ($code !== 0) { |
157 | | - throw new RuntimeException("Unknown error (code: $code)", $code); |
158 | | - } |
| 183 | + if (preg_match('/(\[.*?\]) (\[(\d+)\]): ([A-Z]+) (.*)/', $content, $requestMatches)) { |
| 184 | + $statusCode = $requestMatches[3]; |
| 185 | + $method = $requestMatches[4]; |
| 186 | + $path = $requestMatches[5]; |
| 187 | + |
| 188 | + $coloredMethod = $this->colorizeMethod($method); |
| 189 | + $coloredPath = CLI::color($path, 'light_gray'); |
| 190 | + $coloredStatus = $this->colorizeStatusCode($statusCode); |
| 191 | + |
| 192 | + return "{$coloredStatus}: {$coloredMethod} {$coloredPath}"; |
159 | 193 | } |
| 194 | + |
| 195 | + return CLI::color($content, 'light_gray'); |
160 | 196 | } |
161 | 197 |
|
162 | | - /** |
163 | | - * Shutdown the server |
164 | | - */ |
165 | | - protected function shutdown() |
| 198 | + protected function formatAndPrintError($error) |
166 | 199 | { |
167 | | - CLI::info('Shutting down the server...'); |
168 | | - proc_terminate($this->process); |
| 200 | + $lines = explode("\n", trim($error)); |
| 201 | + foreach ($lines as $line) |
| 202 | + CLI::write(CLI::color('ERROR: ', 'red') . CLI::color($line, 'light_red')); |
169 | 203 | } |
170 | 204 |
|
171 | | - /** |
172 | | - * Print server information |
173 | | - */ |
174 | | - protected function printServerInfo(string $scheme, string $host, int $port) |
| 205 | + protected function colorizeStatusCode($statusCode): string |
175 | 206 | { |
176 | | - CLI::info(self::ARROW_SYMBOL . 'Axm development server started on: ' . CLI::color("{$scheme}://{$host}:{$port}", 'green')); |
| 207 | + $color = match (true) { |
| 208 | + $statusCode >= 200 && $statusCode < 300 => 'green', |
| 209 | + $statusCode >= 300 && $statusCode < 400 => 'yellow', |
| 210 | + $statusCode >= 400 && $statusCode < 500 => 'light_red', |
| 211 | + default => 'red', |
| 212 | + }; |
| 213 | + |
| 214 | + return CLI::color("[$statusCode]", $color); |
| 215 | + } |
177 | 216 |
|
178 | | - CLI::newLine(); |
179 | | - CLI::write(self::ARROW_SYMBOL . 'Press Control-C to stop.', 'yellow'); |
180 | | - CLI::newLine(2); |
| 217 | + protected function colorizeMethod($method) |
| 218 | + { |
| 219 | + $colors = [ |
| 220 | + 'GET' => 'green', |
| 221 | + 'POST' => 'yellow', |
| 222 | + 'PUT' => 'blue', |
| 223 | + 'DELETE' => 'red', |
| 224 | + 'PATCH' => 'purple', |
| 225 | + 'HEAD' => 'cyan', |
| 226 | + 'OPTIONS' => 'white' |
| 227 | + ]; |
| 228 | + |
| 229 | + return CLI::color($method, $colors[$method] ?? 'white'); |
181 | 230 | } |
182 | 231 | } |
0 commit comments