diff --git a/HotReload.php b/HotReload.php new file mode 100644 index 0000000..2685ce4 --- /dev/null +++ b/HotReload.php @@ -0,0 +1,350 @@ +setName('hot:load'); + parent::configure(); // TODO: Change the autogenerated stub + } + + + private array $dirs = [APP_PATH . 'app/', APP_PATH . 'config/', APP_PATH . 'router/']; + + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->startProcess(); + if (extension_loaded('inotify')) { + $this->onInotifyReload(); + } else { + $this->onCrontabReload(); + } + } + + + /** + * @return void + */ + private function startProcess(): void + { + $this->process = new Process(function (Process $process) { + $process->exec(PHP_BINARY . ' ' . APP_PATH . 'kiri.php', ['sw:server', 'start']); + }); + $this->process->start(); + } + + + /** + * @return void + * @throws Exception + */ + private function onCrontabReload(): void + { + $this->loadDirs(); + $this->tick(); + } + + + /** + * @return void + * @throws Exception + */ + private function onInotifyReload(): void + { + $init = inotify_init(); + foreach ([APP_PATH . 'app/'] as $dir) { + if (!is_dir($dir)) { + continue; + } + $this->watch($init, $dir); + } + Event::add($init, fn() => $this->check($init)); + Event::wait(); + } + + + /** + * @param bool $isReload + * @throws Exception + */ + private function loadDirs(bool $isReload = false): void + { + foreach ($this->dirs as $value) { + if (is_bool($path = realpath($value))) { + continue; + } + + if (!is_dir($path)) continue; + + $this->loadByDir($path, $isReload); + } + } + + + /** + * @throws Exception + */ + public function tick(): void + { + $isReloading = Context::get('isReloading', false); + if ($isReloading) { + return; + } + + $this->loadDirs(true); + + sleep(2); + + $this->tick(); + } + + + /** + * @param $path + * @param bool $isReload + * @return void + * @throws Exception + */ + private function loadByDir($path, $isReload = false): void + { + if (!is_string($path)) { + return; + } + $path = rtrim($path, '/'); + foreach (glob(realpath($path) . '/*') as $value) { + if (is_dir($value)) { + $this->loadByDir($value, $isReload); + } + if (is_file($value)) { + if ($this->checkFile($value, $isReload)) { + $this->timerReload(); + break; + } + } + } + } + + + /** + * @param $value + * @param $isReload + * @return bool + */ + private function checkFile($value, $isReload): bool + { + $md5 = md5($value); + $mTime = filectime($value); + if (!isset($this->md5Map[$md5])) { + if ($isReload) { + return true; + } + $this->md5Map[$md5] = $mTime; + } else { + if ($this->md5Map[$md5] != $mTime) { + if ($isReload) { + return true; + } + $this->md5Map[$md5] = $mTime; + } + } + return false; + } + + + /** + * 开始监听 + */ + public function check($inotify) + { + if (!($events = inotify_read($inotify))) { + return; + } + $isReloading = Context::get('isReloading', false); + if ($isReloading) { + return; + } + + $eventList = [IN_CREATE, IN_DELETE, IN_MODIFY, IN_MOVED_TO, IN_MOVED_FROM]; + foreach ($events as $ev) { + if (empty($ev['name'])) { + continue; + } + if ($ev['mask'] == IN_IGNORED) { + continue; + } + if (!in_array($ev['mask'], $eventList)) { + continue; + } + $fileType = strstr($ev['name'], '.'); + //非重启类型 + if ($fileType !== '.php') { + continue; + } + if (Context::get('swoole_timer_after') !== -1) { + return; + } + $int = @swoole_timer_after(2000, fn() => $this->reload($inotify)); + Context::set('swoole_timer_after', $int); + Context::set('isReloading', true); + } + } + + /** + * @throws Exception + */ + public function reload($inotify): void + { + Context::set('isReloading', true); + $this->trigger_reload(); + + $this->clearWatch($inotify); + foreach ($this->dirs as $root) { + $this->watch($inotify, $root); + } + Context::set('swoole_timer_after', -1); + Context::set('isReloading', false); + $this->md5Map = []; + } + + /** + * @throws Exception + */ + public function timerReload() + { + Context::set('isReloading', true); + $this->trigger_reload(); + + Context::set('swoole_timer_after', -1); + + $this->loadDirs(); + + Context::set('isReloading', false); + + $this->tick(); + } + + + /** + * 重启 + * @throws Exception + */ + public function trigger_reload(): void + { + $this->process->exit(0); + $this->startProcess(); + } + + + /** + * @throws Exception + */ + public function clearWatch($inotify) + { + foreach ($this->watchFiles as $wd) { + try { + inotify_rm_watch($inotify, $wd); + } catch (\Throwable $exception) { + logger()->addError($exception, 'throwable'); + } + } + $this->watchFiles = []; + } + + + /** + * @param $code + * @param $message + * @param $file + * @param $line + * @throws Exception + */ + protected function onErrorHandler($code, $message, $file, $line) + { + if (str_contains($message, 'The file descriptor is not an inotify instance')) { + return; + } + debug('Error:' . $message . ' at ' . $file . ':' . $line); + } + + + /** + * @param $inotify + * @param $dir + * @return bool + * @throws Exception + */ + public function watch($inotify, $dir): bool + { + //目录不存在 + if (!is_dir($dir)) { + return logger()->addError("[$dir] is not a directory."); + } + //避免重复监听 + if (isset($this->watchFiles[$dir])) { + return FALSE; + } + + if (in_array($dir, [APP_PATH . 'commands', APP_PATH . '.git', APP_PATH . '.gitee'])) { + return FALSE; + } + + $wd = @inotify_add_watch($inotify, $dir, IN_MODIFY | IN_DELETE | IN_CREATE | IN_MOVE); + $this->watchFiles[$dir] = $wd; + + $files = scandir($dir); + foreach ($files as $f) { + if ($f == '.' or $f == '..' or $f == 'runtime' or preg_match('/\.txt/', $f) or preg_match('/\.sql/', $f) or preg_match('/\.log/', $f)) { + continue; + } + $path = $dir . '/' . $f; + //递归目录 + if (is_dir($path)) { + $this->watch($inotify, $path); + } + + //检测文件类型 + if (strstr($f, '.') == '.php') { + $wd = @inotify_add_watch($inotify, $path, IN_MODIFY | IN_DELETE | IN_CREATE | IN_MOVE); + $this->watchFiles[$path] = $wd; + } + } + return TRUE; + } + +} diff --git a/ServerProviders.php b/ServerProviders.php index 0818613..7f42dce 100644 --- a/ServerProviders.php +++ b/ServerProviders.php @@ -30,5 +30,6 @@ class ServerProviders extends Providers $console = $this->container->get(Application::class); $console->add($server); + $console->add(new HotReload()); } }