diff --git a/server b/server index dbd009e..328ab9d 100644 --- a/server +++ b/server @@ -13,15 +13,78 @@ if (php_sapi_name() !== 'cli') { exit; } +class Output +{ + public const RESET = "\033[0m"; + public const BOLD = "\033[1m"; + + // Text colors + public const RED = "\033[31m"; + public const GREEN = "\033[32m"; + public const YELLOW = "\033[33m"; + public const BLUE = "\033[34m"; + public const MAGENTA = "\033[35m"; + public const CYAN = "\033[36m"; + public const WHITE = "\033[37m"; + public const GRAY = "\033[90m"; + + // Background colors + public const BG_RED = "\033[41m"; + public const BG_GREEN = "\033[42m"; + public const BG_YELLOW = "\033[43m"; + + public static function colorize(string $text, string $color): string + { + return $color . $text . self::RESET; + } + + public static function success(string $text): string + { + return self::colorize("🚀 " . $text, self::GREEN . self::BOLD); + } + + public static function error(string $text): string + { + return self::colorize("❌ " . $text, self::RED . self::BOLD); + } + + public static function warning(string $text): string + { + return self::colorize("âš ī¸ " . $text, self::YELLOW . self::BOLD); + } + + public static function info(string $text): string + { + return self::colorize("â„šī¸ " . $text, self::CYAN . self::BOLD); + } + + public static function debug(string $text): string + { + return self::colorize($text, self::GRAY); + } +} + $config = Dotenv::createArrayBacked(__DIR__, '.env')->load(); class Watcher extends Watch { protected string $host; + protected int $port; + protected int|null $pid; + protected Process $serverProcess; + protected int $consecutiveErrors = 0; + + protected int $maxConsecutiveErrors = 5; + + protected int $lastHealthCheck = 0; + + protected bool $stopAttempts = false; + + public function __construct( array $paths, array $config = [] @@ -38,20 +101,13 @@ class Watcher extends Watch $watcher = $this->getWatchProcess(); while (true) { - if (! $watcher->isRunning()) { - throw CouldNotStartWatcher::make($watcher); - } + $this->ensureWatcherIsRunning($watcher); - if ($output = $watcher->getIncrementalOutput()) { - $this->actOnOutput($output); - } + $this->handleWatcherOutput($watcher); - if ($this->serverProcess->isRunning()) { - echo $this->serverProcess->getIncrementalOutput(); - echo $this->serverProcess->getIncrementalErrorOutput(); - } + $this->handleServerProcess(); - if (! ($this->shouldContinue)()) { + if (!($this->shouldContinue)()) { break; } @@ -59,15 +115,15 @@ class Watcher extends Watch } } - public function watch(): void { - echo "Watching for changes..." . PHP_EOL . PHP_EOL; + public function watch(): void + { + echo Output::info("Watching for changes...") . PHP_EOL . PHP_EOL; $this->onAnyChange(function (): void { $this->killExistingProcess(); $this->runServer(); - }) - ->start(); + })->start(); } public function systemIsReady(): bool @@ -81,18 +137,18 @@ class Watcher extends Watch if ($process->isSuccessful() && strpos($process->getOutput(), $packageName) !== false) { return true; } else { - echo "Chokidar is not installed. Installing..." . PHP_EOL; + echo Output::warning("Chokidar is not installed. Installing...") . PHP_EOL; $installCommand = 'npm install ' . escapeshellarg($packageName); $installProcess = Process::fromShellCommandline($installCommand); $installProcess->run(); if ($installProcess->isSuccessful()) { - echo "Chokidar installed successfully." . PHP_EOL; + echo Output::success("Chokidar installed successfully.") . PHP_EOL; return true; } else { - echo "Failed to install chokidar. Please check your npm configuration." . PHP_EOL; + echo Output::error("Failed to install chokidar. Please check your npm configuration.") . PHP_EOL; echo $installProcess->getErrorOutput(); return false; @@ -100,27 +156,281 @@ class Watcher extends Watch } } - public function runServer(): void { - $this->serverProcess = Process::fromShellCommandline("php public/index.php"); + public function runServer(bool $incrementErrorCounter = true): void + { + if ($this->consecutiveErrors >= $this->maxConsecutiveErrors || $this->stopAttempts) { + if (!$this->stopAttempts) { + echo Output::error("Too many consecutive errors ({$this->consecutiveErrors}). Stopping server attempts.") . PHP_EOL; + + $this->stopAttempts = true; + } + + return; + } + + $command = "XDEBUG_MODE=off php public/index.php"; + $this->serverProcess = Process::fromShellCommandline($command); $this->serverProcess->setTimeout(null); - $this->serverProcess->start(); - $this->pid = $this->serverProcess->getPid(); + try { + $this->serverProcess->start(); + + usleep(100000); // 100ms + + if (!$this->serverProcess->isRunning()) { + $exitCode = $this->serverProcess->getExitCode(); + + if ($incrementErrorCounter) { + $this->consecutiveErrors++; + } + + echo Output::error("Server failed to start. Exit code: {$exitCode}") . PHP_EOL; + echo Output::debug("Error: " . $this->serverProcess->getErrorOutput()) . PHP_EOL; + echo Output::warning("Consecutive errors: {$this->consecutiveErrors}") . PHP_EOL . PHP_EOL; + + if ($this->consecutiveErrors < $this->maxConsecutiveErrors) { + sleep(2); + + $this->runServer($incrementErrorCounter); + } + + return; + } + + if ($incrementErrorCounter) { + $this->consecutiveErrors = 0; + } + $this->pid = $this->serverProcess->getPid(); + + echo Output::success("Server started on {$this->host}:{$this->port}") . PHP_EOL; + echo Output::info("PID: {$this->pid}") . PHP_EOL . PHP_EOL; + + } catch (Exception $e) { + if ($incrementErrorCounter) { + $this->consecutiveErrors++; + } + + echo Output::error("Failed to start server: " . $e->getMessage()) . PHP_EOL; + echo Output::debug("Command: {$command}") . PHP_EOL; + echo Output::warning("Consecutive errors: {$this->consecutiveErrors}") . PHP_EOL . PHP_EOL; - echo "Server started on {$this->host}:{$this->port}" . PHP_EOL; - echo "PID: {$this->pid}" . PHP_EOL . PHP_EOL; + if ($this->consecutiveErrors < $this->maxConsecutiveErrors) { + sleep(2); + + $this->runServer($incrementErrorCounter); + } + } } - protected function killExistingProcess() + private function ensureWatcherIsRunning(Process $watcher): void { - if ($this->pid) { - echo "Restarting server..." . PHP_EOL . PHP_EOL; + if (!$watcher->isRunning()) { + throw CouldNotStartWatcher::make($watcher); + } + } + + private function handleWatcherOutput(Process $watcher): void + { + if ($output = $watcher->getIncrementalOutput()) { + $this->actOnOutput($output); + } + } + + private function handleServerProcess(): void + { + if (!isset($this->serverProcess)) { + return; + } + + if ($this->serverProcess->isRunning()) { + $this->outputServerProcess(); + + $this->periodicHealthCheck(); + } else { + $this->handleServerExit(); + } + } - $killProcess = Process::fromShellCommandline('kill ' . escapeshellarg($this->pid)); + private function outputServerProcess(): void + { + $output = $this->serverProcess->getIncrementalOutput(); + $errorOutput = $this->serverProcess->getIncrementalErrorOutput(); + + if ($output) { + echo $this->colorizeServerOutput($output); + } + + if ($errorOutput) { + echo Output::colorize($errorOutput, Output::RED); + } + } + + private function colorizeServerOutput(string $output): string + { + $lines = explode("\n", $output); + $colorizedLines = []; + + foreach ($lines as $line) { + $colorizedLines[] = $this->colorizeLine($line); + } + + return implode("\n", $colorizedLines); + } + + private function colorizeLine(string $line): string + { + $trimmed = trim($line); + $result = $line; + + if ($trimmed === '') { + // leave $result as $line + } elseif (strpos($line, '.NOTICE:') !== false) { + $result = Output::colorize($line, Output::CYAN); + } elseif (strpos($line, '.WARNING:') !== false) { + $result = Output::colorize($line, Output::YELLOW); + } elseif (strpos($line, '.ERROR:') !== false) { + $result = Output::colorize($line, Output::RED); + } elseif (strpos($line, '.DEBUG:') !== false) { + $result = Output::colorize($line, Output::GRAY); + } elseif (strpos($line, '.INFO:') !== false) { + $result = Output::colorize($line, Output::GREEN); + } elseif (strpos($line, 'Started server') !== false || strpos($line, 'Listening on') !== false) { + $result = Output::colorize($line, Output::GREEN . Output::BOLD); + } elseif ($this->isHttpMethodLine($line)) { + $result = $this->colorizeHttpStatusLine($line); + } + + return $result; + } + + private function isHttpMethodLine(string $line): bool + { + return strpos($line, 'GET') !== false + || strpos($line, 'POST') !== false + || strpos($line, 'PUT') !== false; + } + + private function colorizeHttpStatusLine(string $line): string + { + $colorized = Output::colorize($line, Output::BLUE); + + if (strpos($line, ' 200 ') !== false) { + $colorized = Output::colorize($line, Output::GREEN); + } elseif (strpos($line, ' 404 ') !== false) { + $colorized = Output::colorize($line, Output::YELLOW); + } elseif (strpos($line, ' 500 ') !== false) { + $colorized = Output::colorize($line, Output::RED); + } + + return $colorized; + } + + private function periodicHealthCheck(): void + { + if ($this->stopAttempts) { + return; + } + + if (time() - $this->lastHealthCheck > 10) { + if (!$this->isServerHealthy()) { + echo Output::warning("Server health check failed. Restarting...") . PHP_EOL; + + $this->consecutiveErrors++; + echo Output::warning("Consecutive errors: {$this->consecutiveErrors}") . PHP_EOL; + + $this->killExistingProcess(); + + if ($this->consecutiveErrors < $this->maxConsecutiveErrors) { + $this->runServer(false); + } else { + echo Output::error("Maximum consecutive errors reached. Server will not restart automatically.") . PHP_EOL; + + $this->stopAttempts = true; + } + } else { + if ($this->consecutiveErrors > 0) { + echo Output::success("Server health check passed. Resetting error counter.") . PHP_EOL; + + $this->consecutiveErrors = 0; + $this->stopAttempts = false; // Allow attempts again + } + } + + $this->lastHealthCheck = time(); + } + } + + private function handleServerExit(): void + { + if ($this->stopAttempts) { + return; + } + + $exitCode = $this->serverProcess->getExitCode(); + + if ($exitCode !== null && $exitCode !== 0) { + echo Output::error("Server process exited with error code: {$exitCode}") . PHP_EOL; + echo Output::debug("Error output: " . $this->serverProcess->getErrorOutput()) . PHP_EOL; + echo Output::info("Restarting server...") . PHP_EOL . PHP_EOL; + + $this->consecutiveErrors++; + echo Output::warning("Consecutive errors: {$this->consecutiveErrors}") . PHP_EOL; + + if ($this->consecutiveErrors < $this->maxConsecutiveErrors) { + $this->runServer(false); + } else { + echo Output::error("Maximum consecutive errors reached. Server will not restart automatically.") . PHP_EOL; + + $this->stopAttempts = true; + } + } + } + + private function killExistingProcess(): void + { + if (!isset($this->serverProcess) || !$this->pid) { + return; + } + + echo Output::info("Restarting server...") . PHP_EOL . PHP_EOL; + + try { + $this->serverProcess->stop(3); // 3 second timeout + + if ($this->serverProcess->isRunning()) { + $killProcess = Process::fromShellCommandline('kill -9 ' . escapeshellarg($this->pid)); + $killProcess->run(); + } + + echo Output::success("Server was stopped (PID {$this->pid})") . PHP_EOL . PHP_EOL; + + } catch (Exception $e) { + echo Output::error("Error stopping server: " . $e->getMessage()) . PHP_EOL; + + $killProcess = Process::fromShellCommandline('kill -9 ' . escapeshellarg($this->pid)); $killProcess->run(); + } + + $this->pid = null; + } - echo "Server was stopped (PID {$this->pid})" . PHP_EOL . PHP_EOL; + private function isServerHealthy(): bool + { + if (!isset($this->serverProcess) || !$this->serverProcess->isRunning()) { + return false; } + + $context = stream_context_create([ + 'http' => [ + 'timeout' => 1, + 'ignore_errors' => true + ] + ]); + + $url = str_replace(['http://', 'https://'], '', $this->host) . ':' . $this->port; + $result = @file_get_contents("http://{$url}", false, $context); + + return $result !== false; } } @@ -139,7 +449,7 @@ try { $watcher->watch(); } else { - echo "System is not ready. Exiting..." . PHP_EOL . PHP_EOL; + echo Output::error("System is not ready. Exiting...") . PHP_EOL . PHP_EOL; } } catch (Throwable $th) { echo $th->getMessage();