diff --git a/Abstracts/AsyncServer.php b/Abstracts/AsyncServer.php index abd59d0..5239d57 100644 --- a/Abstracts/AsyncServer.php +++ b/Abstracts/AsyncServer.php @@ -117,7 +117,7 @@ class AsyncServer extends Component implements ServerInterface $settings[Constant::OPTION_DAEMONIZE] = (bool)$daemon; $settings[Constant::OPTION_ENABLE_REUSE_PORT] = true; $settings[Constant::OPTION_PID_FILE] = storage('.swoole.pid'); - if (!isset($settings[Constant::OPTION_PID_FILE])) { + if (!isset($settings[Constant::OPTION_LOG_FILE])) { $settings[Constant::OPTION_LOG_FILE] = storage('system.log'); } return $settings; diff --git a/Abstracts/FileWatcher.php b/Abstracts/FileWatcher.php index e824748..676abc0 100644 --- a/Abstracts/FileWatcher.php +++ b/Abstracts/FileWatcher.php @@ -10,10 +10,10 @@ use Kiri\Router\Router; use Kiri\Server\Events\OnWorkerStart; use Kiri\Server\Processes\AbstractProcess; use Kiri\Server\Processes\OnProcessInterface; -use Kiri\Server\ServerInterface; use Psr\Log\LoggerInterface; use Swoole\Event; use Swoole\Process; +use const SIGUSR1; class FileWatcher extends AbstractProcess implements OnProcessInterface { @@ -45,6 +45,8 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface private $pollTimer = null; private $debounceTimer = null; private int $debounceMs = 200; + private int $minReloadIntervalMs = 1000; + private int $lastReloadAtMs = 0; private const STRATEGY_INOTIFY = 'inotify'; private const STRATEGY_FSWATCH = 'fswatch'; @@ -58,6 +60,8 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface di(EventProvider::class)->on(OnWorkerStart::class, [di(Router::class), 'scan_build_route']); + $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(); } @@ -222,7 +226,11 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface \Swoole\Timer::clear($this->debounceTimer); } - $this->debounceTimer = \Swoole\Timer::after($this->debounceMs, function () use ($changedFiles) { + $now = intdiv(hrtime(true), 1000000); + $elapsed = $this->lastReloadAtMs > 0 ? $now - $this->lastReloadAtMs : $this->minReloadIntervalMs; + $delay = max($this->debounceMs, $this->minReloadIntervalMs - $elapsed); + + $this->debounceTimer = \Swoole\Timer::after($delay, function () use ($changedFiles) { $this->debounceTimer = null; $this->reload($changedFiles); }); @@ -237,6 +245,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface $this->reloading = true; try { + $this->lastReloadAtMs = intdiv(hrtime(true), 1000000); $preview = implode(', ', array_slice($changedFiles, 0, 3)); if (count($changedFiles) > 3) { $preview .= ' ...'; @@ -255,9 +264,6 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface private function reloadWorkers(): bool { - $server = \Kiri::getDi()->get(ServerInterface::class); - return $server->reload(); - $pid = $this->getMasterPid(); if ($pid === null) { return false; @@ -268,17 +274,92 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface private function getMasterPid(): ?int { + $processes = $this->listProcesses(); + $candidates = []; + $pidFile = function_exists('storage') ? storage('.swoole.pid') : ''; - if (!is_file($pidFile)) { - return null; + if (is_file($pidFile)) { + $candidates[] = (int)trim((string)file_get_contents($pidFile)); } - $pid = (int)trim((string)file_get_contents($pidFile)); - if ($pid <= 1 || !Process::kill($pid, 0)) { - return null; + foreach ($processes as $process) { + if ($this->isMasterProcessCommand($process['command'])) { + $candidates[] = (int)$process['pid']; + } } - return $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); } private function startInotify(): void { diff --git a/Abstracts/HotReload.php b/Abstracts/HotReload.php index 8590ab2..aa4249e 100644 --- a/Abstracts/HotReload.php +++ b/Abstracts/HotReload.php @@ -139,17 +139,92 @@ class HotReload extends AbstractProcess private function getMasterPid(): ?int { + $processes = $this->listProcesses(); + $candidates = []; + $pidFile = function_exists('storage') ? storage('.swoole.pid') : ''; - if (!is_file($pidFile)) { - return null; + if (is_file($pidFile)) { + $candidates[] = (int)trim((string)file_get_contents($pidFile)); } - $pid = (int)trim((string)file_get_contents($pidFile)); - if ($pid <= 1 || !Process::kill($pid, 0)) { - return null; + foreach ($processes as $process) { + if ($this->isMasterProcessCommand($process['command'])) { + $candidates[] = (int)$process['pid']; + } } - return $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 { diff --git a/State.php b/State.php index 93ce976..c9ccd54 100644 --- a/State.php +++ b/State.php @@ -126,7 +126,7 @@ class State extends Component } $pid = (int)trim((string)file_get_contents($pidFile)); - if ($pid <= 1 || !$this->isProcessAlive($pid)) { + if ($pid <= 1 || !$this->isProcessAlive($pid) || !$this->isSameAppProcess($pid)) { return null; } @@ -168,6 +168,9 @@ class State extends Component if (!preg_match('/\\]\\..+\\[[0-9]+\\]/', $command)) { continue; } + if (!$this->isSameAppProcess((int)$process['pid'])) { + continue; + } $pids[] = (int)$process['pid']; } @@ -203,6 +206,22 @@ class State extends Component } + 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 isProcessAlive(int $pid): bool { return $pid > 1 && @Process::kill($pid, 0);