diff --git a/Abstracts/FileWatcher.php b/Abstracts/FileWatcher.php index f0ea81c..700758f 100644 --- a/Abstracts/FileWatcher.php +++ b/Abstracts/FileWatcher.php @@ -11,14 +11,18 @@ use Kiri\Server\Events\OnWorkerStart; use Kiri\Server\Processes\AbstractProcess; use Kiri\Server\Processes\OnProcessInterface; use Psr\Log\LoggerInterface; +use Swoole\Coroutine; use Swoole\Event; use Swoole\Process; +use Swoole\Timer; use const SIGKILL; use const SIGTERM; use const SIGUSR1; class FileWatcher extends AbstractProcess implements OnProcessInterface { + use ReloadWorkers; + protected mixed $pipe; #[Container(LoggerInterface::class)] @@ -32,40 +36,40 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface protected bool $reloading = false; - private array $watchPaths = []; - private array $excludePatterns = []; - private array $extensions = ['php']; + private array $watchPaths = []; + private array $excludePatterns = []; + private array $extensions = ['php']; private string $strategy; - private bool $running = false; - private bool $scanning = false; + private bool $running = false; + private bool $scanning = false; - private $inotifyFd = null; - private array $inotifyWatchMap = []; - private $fswatchProcess = null; - private array $fswatchPipes = []; - private int $pollInterval = 2; - private array $fileSnapshot = []; - private $pollTimer = null; - private $debounceTimer = null; - private int $debounceMs = 200; - private int $minReloadIntervalMs = 1000; - private int $lastReloadAtMs = 0; + private mixed $inotifyFd = null; + private array $inotifyWatchMap = []; + private mixed $fswatchProcess = null; + private array $fswatchPipes = []; + private int $pollInterval = 2; + private array $fileSnapshot = []; + private int $pollTimer = -1; + private int $debounceTimer = -1; + private int $debounceMs = 200; + private int $minReloadIntervalMs = 1000; + private int $lastReloadAtMs = 0; - private const STRATEGY_INOTIFY = 'inotify'; - private const STRATEGY_FSWATCH = 'fswatch'; - private const STRATEGY_POLL = 'poll'; + private const string STRATEGY_INOTIFY = 'inotify'; + private const string STRATEGY_FSWATCH = 'fswatch'; + private const string STRATEGY_POLL = 'poll'; public function __construct() { - $this->watchPaths = config('servers.reload.listen', []); + $this->watchPaths = config('servers.reload.listen', []); $this->excludePatterns = config('servers.reload.scan.skip_patterns', []); - $this->extensions = config('servers.reload.scan.extensions', ['php']); + $this->extensions = config('servers.reload.scan.extensions', ['php']); di(EventProvider::class)->on(OnWorkerStart::class, [di(Router::class), 'scan_build_route']); - $this->debounceMs = (int)config('servers.reload.debounce_ms', $this->debounceMs); + $this->debounceMs = (int)config('servers.reload.debounce_ms', $this->debounceMs); $this->minReloadIntervalMs = (int)config('servers.reload.min_interval_ms', $this->minReloadIntervalMs); - $this->strategy = $this->detectBestStrategy(); + $this->strategy = $this->detectBestStrategy(); } public function getName(): string @@ -115,7 +119,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface private function isFswatchAvailable(): bool { - $output = []; + $output = []; $returnVar = 0; exec('which fswatch 2>/dev/null', $output, $returnVar); return $returnVar === 0 && !empty($output[0]); @@ -136,7 +140,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface public function process(Process|null $process): void { if (class_exists('\\Swoole\\Coroutine')) { - \Swoole\Coroutine::set(['enable_deadlock_check' => false]); + Coroutine::set(['enable_deadlock_check' => false]); } if ($this->running) { @@ -180,13 +184,13 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface $this->stopFswatchProcess(); if ($this->pollTimer) { - \Swoole\Timer::clear($this->pollTimer); - $this->pollTimer = null; + Timer::clear($this->pollTimer); + $this->pollTimer = -1; } if ($this->debounceTimer) { - \Swoole\Timer::clear($this->debounceTimer); - $this->debounceTimer = null; + Timer::clear($this->debounceTimer); + $this->debounceTimer = -1; } @Event::exit(); @@ -229,6 +233,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface } $this->fswatchProcess = null; } + private function isExcluded(string $path): bool { foreach ($this->excludePatterns as $pattern) { @@ -252,23 +257,25 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface private function triggerCallback(array $changedFiles): void { - $changedFiles = array_values(array_unique(array_filter($changedFiles, function ($file) { - return !$this->isExcluded($file) && $this->hasValidExtension($file); - }))); + $changedFiles = array_filter($changedFiles, function ($file) { + return !$this->isExcluded($file) && $this->hasValidExtension($file); + }) + |> array_unique(...) + |> array_values(...); if (empty($changedFiles)) { return; } if ($this->debounceTimer) { - \Swoole\Timer::clear($this->debounceTimer); + Timer::clear($this->debounceTimer); } - $now = intdiv(hrtime(true), 1000000); + $now = intdiv(hrtime(true), 1000000); $elapsed = $this->lastReloadAtMs > 0 ? $now - $this->lastReloadAtMs : $this->minReloadIntervalMs; - $delay = max($this->debounceMs, $this->minReloadIntervalMs - $elapsed); + $delay = max($this->debounceMs, $this->minReloadIntervalMs - $elapsed); - $this->debounceTimer = \Swoole\Timer::after($delay, function () use ($changedFiles) { + $this->debounceTimer = Timer::after($delay, function () use ($changedFiles) { $this->debounceTimer = null; $this->reload($changedFiles); }); @@ -284,7 +291,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface try { $this->lastReloadAtMs = intdiv(hrtime(true), 1000000); - $preview = implode(', ', array_slice($changedFiles, 0, 3)); + $preview = implode(', ', array_slice($changedFiles, 0, 3)); if (count($changedFiles) > 3) { $preview .= ' ...'; } @@ -300,58 +307,6 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface } } - private function reloadWorkers(): bool - { - $pid = $this->getMasterPid(); - if ($pid === null) { - return false; - } - - return Process::kill($pid, SIGUSR1); - } - - private function getMasterPid(): ?int - { - $processes = $this->listProcesses(); - $candidates = []; - - $pidFile = function_exists('storage') ? storage('.swoole.pid') : ''; - if (is_file($pidFile)) { - $candidates[] = (int)trim((string)file_get_contents($pidFile)); - } - - foreach ($processes as $process) { - if ($this->isMasterProcessCommand($process['command'])) { - $candidates[] = (int)$process['pid']; - } - } - - foreach (array_values(array_unique(array_filter($candidates))) as $pid) { - if ($this->isProcessAlive($pid) && $this->isMasterPid($pid, $processes)) { - return $pid; - } - } - - return null; - } - - - /** - * @param int $pid - * @param array $processes - * @return bool - */ - private function isMasterPid(int $pid, array $processes): bool - { - foreach ($processes as $process) { - if ((int)$process['pid'] === $pid) { - return $this->isMasterProcessCommand($process['command']) && $this->isSameAppProcess($pid); - } - } - - return false; - } - private function isMasterProcessCommand(string $command): bool { $prefix = '[' . config('site.id', 'system-service') . '].Master['; @@ -374,37 +329,12 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface return rtrim(str_replace('\\', '/', $real), '/'); } - private function listProcesses(): array - { - $lines = []; - exec('ps -eo pid=,ppid=,args=', $lines); - - $processes = []; - foreach ($lines as $line) { - $line = trim($line); - if ($line === '') { - continue; - } - - $parts = preg_split('/\s+/', $line, 3); - if (count($parts) < 3) { - continue; - } - - $processes[] = [ - 'pid' => (int)$parts[0], - 'ppid' => (int)$parts[1], - 'command' => $parts[2], - ]; - } - - return $processes; - } private function isProcessAlive(int $pid): bool { return $pid > 1 && @Process::kill($pid, 0); } + private function startInotify(): void { $this->inotifyFd = inotify_init(); @@ -426,9 +356,9 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface $changedFiles = []; foreach ($events as $event) { - $wd = $event['wd']; - $dir = $this->inotifyWatchMap[$wd] ?? ''; - $name = $event['name'] ?? ''; + $wd = $event['wd']; + $dir = $this->inotifyWatchMap[$wd] ?? ''; + $name = $event['name'] ?? ''; $filePath = $name === '' ? $dir : $dir . '/' . $name; if (($event['mask'] & IN_CREATE) && $filePath !== '' && is_dir($filePath) && !$this->isExcluded($filePath)) { @@ -452,7 +382,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface if (is_file($path)) { if ($this->hasValidExtension($path)) { - $wd = inotify_add_watch($this->inotifyFd, $path, IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVE | IN_CLOSE_WRITE); + $wd = inotify_add_watch($this->inotifyFd, $path, IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVE | IN_CLOSE_WRITE); $this->inotifyWatchMap[$wd] = dirname($path); } return; @@ -462,7 +392,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface return; } - $wd = inotify_add_watch($this->inotifyFd, $path, IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVE | IN_CLOSE_WRITE); + $wd = inotify_add_watch($this->inotifyFd, $path, IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVE | IN_CLOSE_WRITE); $this->inotifyWatchMap[$wd] = $path; $items = scandir($path); @@ -517,7 +447,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface return; } - $files = explode("\0", trim($data, "\0")); + $files = explode("\0", trim($data, "\0")); $changedFiles = array_values(array_filter($files, function ($file) { return $file !== ''; })); @@ -530,15 +460,15 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface { $this->buildFileSnapshot(); - $intervalMs = $this->pollInterval * 1000; - $this->pollTimer = \Swoole\Timer::tick($intervalMs, function () { + $intervalMs = $this->pollInterval * 1000; + $this->pollTimer = Timer::tick($intervalMs, function () { if (!$this->running || $this->scanning) { return; } $this->scanning = true; try { - $newSnapshot = []; + $newSnapshot = []; $changedFiles = $this->compareSnapshots($newSnapshot); if (!$this->running) { return; diff --git a/Abstracts/HotReload.php b/Abstracts/HotReload.php index d6faa43..ef46db4 100644 --- a/Abstracts/HotReload.php +++ b/Abstracts/HotReload.php @@ -15,267 +15,195 @@ use Kiri\Server\Processes\AbstractProcess; class HotReload extends AbstractProcess { - - /** - * @var mixed - */ - protected mixed $pipe; + use ReloadWorkers; - /** - * @var LoggerInterface|StdoutLogger - */ - #[Container(LoggerInterface::class)] - public StdoutLogger|LoggerInterface $logger; + /** + * @var mixed + */ + protected mixed $pipe; - /** - * @var bool - */ - protected bool $enable_coroutine = false; + /** + * @var LoggerInterface|StdoutLogger + */ + #[Container(LoggerInterface::class)] + public StdoutLogger|LoggerInterface $logger; - /** - * @var array - */ - protected array $watches = []; + /** + * @var bool + */ + protected bool $enable_coroutine = false; - /** - * @var bool - */ - protected bool $enable_queue = false; + /** + * @var array + */ + protected array $watches = []; - /** - * @var bool - */ - protected bool $reloading = false; + /** + * @var bool + */ + protected bool $enable_queue = false; - /** - * @throws \Exception - */ - public function __construct() - { - di(EventProvider::class)->on(OnWorkerStart::class, [di(Router::class), 'scan_build_route']); - } + /** + * @var bool + */ + protected bool $reloading = false; - /** - * @return string - */ - public function getName(): string - { - return 'hotReload'; - } + /** + * @throws \Exception + */ + public function __construct() + { + di(EventProvider::class)->on(OnWorkerStart::class, [di(Router::class), 'scan_build_route']); + } - /** - * @return void - */ - public function onSigterm(): void - { - $this->stop(); - } + /** + * @return string + */ + public function getName(): string + { + return 'hotReload'; + } - public function stop(): void - { - parent::stop(); - if (isset($this->pipe) && is_resource($this->pipe)) { - @Event::del($this->pipe); - @fclose($this->pipe); - } - @Event::exit(); - } - - /** - * @param ?Process $process - */ - public function process(Process|null $process): void - { - $this->pipe = inotify_init(); - $this->addListen(); - Event::add($this->pipe, function () use ($process) { - $read = inotify_read($this->pipe); - if (count($read) > 0) { - $this->reload(); - } - }); - Event::cycle(function (): void { - if ($this->isStop()) { - Event::exit(); - } - }); - Event::wait(); - } + /** + * @return void + */ + public function onSigterm(): void + { + $this->stop(); + } - /** - * @return void - */ - public function reload(): void - { - if ($this->reloading) { - return; - } - $this->reloading = true; + public function stop(): void + { + parent::stop(); + if (isset($this->pipe) && is_resource($this->pipe)) { + @Event::del($this->pipe); + @fclose($this->pipe); + } + @Event::exit(); + } - di(StdoutLogger::class)->println('reloading server[' . \config('site.id', 'system-service') . '], please waite.'); - - $this->clear(); - if (!$this->reloadWorkers()) { - di(StdoutLogger::class)->println('reload server failed: master pid not found or signal failed'); - } - $this->addListen(); - - $this->reloading = false; - } + /** + * @param ?Process $process + */ + public function process(Process|null $process): void + { + $this->pipe = inotify_init(); + $this->addListen(); + Event::add($this->pipe, function () use ($process) { + $read = inotify_read($this->pipe); + if (count($read) > 0) { + $this->reload(); + } + }); + Event::cycle(function (): void { + if ($this->isStop()) { + Event::exit(); + } + }); + Event::wait(); + } - /** - * @return void - */ - private function reloadWorkers(): bool - { - $pid = $this->getMasterPid(); - if ($pid === null) { - return false; - } + /** + * @return void + */ + public function reload(): void + { + if ($this->reloading) { + return; + } + $this->reloading = true; - return Process::kill($pid, SIGUSR1); - } + di(StdoutLogger::class)->println('reloading server[' . \config('site.id', 'system-service') . '], please waite.'); - private function getMasterPid(): ?int - { - $processes = $this->listProcesses(); - $candidates = []; + $this->clear(); + if (!$this->reloadWorkers()) { + di(StdoutLogger::class)->println('reload server failed: master pid not found or signal failed'); + } + $this->addListen(); - $pidFile = function_exists('storage') ? storage('.swoole.pid') : ''; - if (is_file($pidFile)) { - $candidates[] = (int)trim((string)file_get_contents($pidFile)); - } - - foreach ($processes as $process) { - if ($this->isMasterProcessCommand($process['command'])) { - $candidates[] = (int)$process['pid']; - } - } - - foreach (array_values(array_unique(array_filter($candidates))) as $pid) { - if ($this->isProcessAlive($pid) && $this->isMasterPid($pid, $processes)) { - return $pid; - } - } - - return null; - } - - private function isMasterPid(int $pid, array $processes): bool - { - foreach ($processes as $process) { - if ((int)$process['pid'] === $pid) { - return $this->isMasterProcessCommand($process['command']) && $this->isSameAppProcess($pid); - } - } - - return false; - } - - private function isMasterProcessCommand(string $command): bool - { - $prefix = '[' . config('site.id', 'system-service') . '].Master['; - return str_contains($command, $prefix); - } - - private function isSameAppProcess(int $pid): bool - { - $cwd = @readlink('/proc/' . $pid . '/cwd'); - if ($cwd === false || $cwd === '') { - return false; - } - - return $this->normalizePath($cwd) === $this->normalizePath(APP_PATH); - } - - private function normalizePath(string $path): string - { - $real = realpath($path) ?: $path; - return rtrim(str_replace('\\', '/', $real), '/'); - } - - private function listProcesses(): array - { - $lines = []; - exec('ps -eo pid=,ppid=,args=', $lines); - - $processes = []; - foreach ($lines as $line) { - $line = trim($line); - if ($line === '') { - continue; - } - - $parts = preg_split('/\s+/', $line, 3); - if (count($parts) < 3) { - continue; - } - - $processes[] = [ - 'pid' => (int)$parts[0], - 'ppid' => (int)$parts[1], - 'command' => $parts[2], - ]; - } - - return $processes; - } - - private function isProcessAlive(int $pid): bool - { - return $pid > 1 && @Process::kill($pid, 0); - } - protected function addListen(): void - { - foreach (config('servers.reload.listen') as $value) { - $this->readDirectory($value); - } - } + $this->reloading = false; + } - /** - * @return void - */ - protected function clear(): void - { - $this->watches = []; - } + private function isMasterProcessCommand(string $command): bool + { + $prefix = '[' . config('site.id', 'system-service') . '].Master['; + return str_contains($command, $prefix); + } - /** - * @param string $directory - * @return void - */ - public function readFile(string $directory): void - { - if (str_ends_with($directory, '.php') === true) { - inotify_add_watch($this->pipe, $directory, IN_MODIFY | IN_MOVE | IN_CREATE | IN_DELETE); - } - } + private function isSameAppProcess(int $pid): bool + { + $cwd = @readlink('/proc/' . $pid . '/cwd'); + if ($cwd === false || $cwd === '') { + return false; + } + + return $this->normalizePath($cwd) === $this->normalizePath(APP_PATH); + } + + private function normalizePath(string $path): string + { + $real = realpath($path) ?: $path; + return rtrim(str_replace('\\', '/', $real), '/'); + } - /** - * @param string $directory - * @return void - */ - public function readDirectory(string $directory): void - { - foreach (glob($directory . '/*') as $data) { - if (is_dir($data)) { - $this->readDirectory($data); - } else { - $this->readFile($data); - } - } - } + private function isProcessAlive(int $pid): bool + { + return $pid > 1 && @Process::kill($pid, 0); + } + + protected function addListen(): void + { + foreach (config('servers.reload.listen') as $value) { + $this->readDirectory($value); + } + } + + + /** + * @return void + */ + protected function clear(): void + { + $this->watches = []; + } + + /** + * @param string $directory + * @return void + */ + public function readFile(string $directory): void + { + if (str_ends_with($directory, '.php') === true) { + inotify_add_watch($this->pipe, $directory, IN_MODIFY | IN_MOVE | IN_CREATE | IN_DELETE); + } + } + + + /** + * @param string $directory + * @return void + */ + public function readDirectory(string $directory): void + { + foreach (glob($directory . '/*') as $data) { + if (is_dir($data)) { + $this->readDirectory($data); + } else { + $this->readFile($data); + } + } + } } diff --git a/Abstracts/ReloadWorkers.php b/Abstracts/ReloadWorkers.php new file mode 100644 index 0000000..09e46f6 --- /dev/null +++ b/Abstracts/ReloadWorkers.php @@ -0,0 +1,87 @@ +getMasterPid(); + if ($pid === null) { + return false; + } + + return Process::kill($pid, SIGUSR1); + } + + private function getMasterPid(): ?int + { + $processes = $this->listProcesses(); + $candidates = []; + + $pidFile = function_exists('storage') ? storage('.swoole.pid') : ''; + if (is_file($pidFile)) { + $candidates[] = (int)trim((string)file_get_contents($pidFile)); + } + + foreach ($processes as $process) { + if ($this->isMasterProcessCommand($process['command'])) { + $candidates[] = (int)$process['pid']; + } + } + + foreach ($candidates + |> array_filter(...) + |> array_unique(...) + |> array_values(...) as $pid) { + if ($this->isProcessAlive($pid) && $this->isMasterPid($pid, $processes)) { + return $pid; + } + } + + return null; + } + + private function isMasterPid(int $pid, array $processes): bool + { + foreach ($processes as $process) { + if ((int)$process['pid'] === $pid) { + return $this->isMasterProcessCommand($process['command']) && $this->isSameAppProcess($pid); + } + } + + return false; + } + private function listProcesses(): array + { + $lines = []; + exec('ps -eo pid=,ppid=,args=', $lines); + + $processes = []; + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + + $parts = preg_split('/\s+/', $line, 3); + if (count($parts) < 3) { + continue; + } + + $processes[] = [ + 'pid' => (int)$parts[0], + 'ppid' => (int)$parts[1], + 'command' => $parts[2], + ]; + } + + return $processes; + } +}