diff --git a/Abstracts/FileWatcher.php b/Abstracts/FileWatcher.php index 1edb253..0f1939f 100644 --- a/Abstracts/FileWatcher.php +++ b/Abstracts/FileWatcher.php @@ -7,6 +7,7 @@ use Kiri\Di\Inject\Container; use Kiri\Error\StdoutLogger; use Kiri\Events\EventProvider; use Kiri\Router\Router; +use Kiri\Router\RouteArtifactState; use Kiri\Server\Events\OnWorkerStart; use Kiri\Server\Processes\AbstractProcess; use Kiri\Server\Processes\OnProcessInterface; @@ -298,7 +299,12 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface } di(HotReloadState::class)->store($changedFiles); - di(StdoutLogger::class)->println('detected file changes, reloading server: ' . $preview); + di(StdoutLogger::class)->println('detected file changes, building hot reload artifacts: ' . $preview); + + if (!$this->buildChangedFiles($changedFiles)) { + di(StdoutLogger::class)->println('hot reload build failed, keep old workers running'); + return; + } if (!$this->reloadWorkers()) { di(StdoutLogger::class)->println('reload server failed: master pid not found or signal failed'); @@ -308,6 +314,54 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface } } + private function buildChangedFiles(array $changedFiles): bool + { + $entry = $this->detectConsoleEntry(); + if ($entry === null) { + di(StdoutLogger::class)->println('hot reload build failed: entry script not found'); + return false; + } + + $artifactType = defined('ROUTER_TYPE_HTTP') ? ROUTER_TYPE_HTTP : 'http'; + $artifactState = di(RouteArtifactState::class); + $artifactState->clearBuild($artifactType); + + $php = PHP_BINARY ?: 'php'; + $command = escapeshellarg($php) . ' ' . escapeshellarg($entry) . ' sw:file-build'; + $output = []; + $returnCode = 0; + exec($command . ' 2>&1', $output, $returnCode); + + foreach ($output as $line) { + if ($line !== '') { + di(StdoutLogger::class)->println('[file-build] ' . $line); + } + } + + if ($returnCode !== 0) { + return false; + } + + return $artifactState->coversChangedFiles($artifactType, $changedFiles); + } + + private function detectConsoleEntry(): ?string + { + $candidates = [ + $_SERVER['SCRIPT_FILENAME'] ?? '', + defined('APP_PATH') ? APP_PATH . 'kiri.php' : '', + defined('APP_PATH') ? APP_PATH . 'bin/kiri' : '', + defined('APP_PATH') ? APP_PATH . 'artisan' : '', + ]; + + foreach ($candidates as $candidate) { + if (is_string($candidate) && $candidate !== '' && is_file($candidate)) { + return $candidate; + } + } + + return null; + } private function invalidateChangedFiles(array $changedFiles): void { if (!function_exists('opcache_invalidate')) { diff --git a/FileBuildCommand.php b/FileBuildCommand.php new file mode 100644 index 0000000..2b6177d --- /dev/null +++ b/FileBuildCommand.php @@ -0,0 +1,72 @@ +setName('sw:file-build') + ->setDescription('build hot reload artifacts for changed files'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + defined('ROUTER_TYPE_HTTP') or define('ROUTER_TYPE_HTTP', 'http'); + putenv('KIRI_FILE_BUILD=1'); + + $changedFiles = di(HotReloadState::class)->peek(); + $startedAt = microtime(true); + + try { + $syntaxError = $this->lintChangedFiles($changedFiles); + if ($syntaxError !== null) { + $output->writeln('file build failed: ' . $syntaxError . ''); + return Command::FAILURE; + } + + di(Router::class)->scan_build_route(); + di(HotReloadState::class)->store($changedFiles); + di(RouteArtifactState::class)->markBuild(ROUTER_TYPE_HTTP, $changedFiles); + + $elapsed = round((microtime(true) - $startedAt) * 1000, 2); + $output->writeln(sprintf('file build complete: %d changed files, %sms', count($changedFiles), $elapsed)); + return Command::SUCCESS; + } catch (Throwable $throwable) { + if (class_exists('Kiri')) { + \Kiri::getLogger()->json_log($throwable); + } + $output->writeln('file build failed: ' . $throwable->getMessage() . ''); + return Command::FAILURE; + } finally { + putenv('KIRI_FILE_BUILD'); + } + } + + private function lintChangedFiles(array $changedFiles): ?string + { + foreach ($changedFiles as $file) { + if (!is_string($file) || !str_ends_with($file, '.php') || !is_file($file)) { + continue; + } + + $output = []; + $returnCode = 0; + exec(escapeshellarg(PHP_BINARY ?: 'php') . ' -l ' . escapeshellarg($file) . ' 2>&1', $output, $returnCode); + if ($returnCode !== 0) { + return implode("\n", $output); + } + } + + return null; + } +} \ No newline at end of file diff --git a/ServerProviders.php b/ServerProviders.php index 1948bff..6193ae9 100644 --- a/ServerProviders.php +++ b/ServerProviders.php @@ -21,8 +21,10 @@ class ServerProviders extends Providers public function onImport(): void { $server = $this->container->get(ServerCommand::class); + $builder = $this->container->get(FileBuildCommand::class); $console = $this->container->get(Application::class); $console->addCommand($server); + $console->addCommand($builder); } }