diff --git a/Abstracts/AsyncServer.php b/Abstracts/AsyncServer.php index 5239d57..3c21d5d 100644 --- a/Abstracts/AsyncServer.php +++ b/Abstracts/AsyncServer.php @@ -67,6 +67,8 @@ class AsyncServer extends Component implements ServerInterface } $this->shuttingDown = true; + $this->terminateProcesses(); + if ($this->server !== null) { $this->server->shutdown(); } diff --git a/Abstracts/FileWatcher.php b/Abstracts/FileWatcher.php index 7439b6f..f0ea81c 100644 --- a/Abstracts/FileWatcher.php +++ b/Abstracts/FileWatcher.php @@ -13,6 +13,8 @@ use Kiri\Server\Processes\OnProcessInterface; use Psr\Log\LoggerInterface; use Swoole\Event; use Swoole\Process; +use const SIGKILL; +use const SIGTERM; use const SIGUSR1; class FileWatcher extends AbstractProcess implements OnProcessInterface @@ -35,6 +37,7 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface private array $extensions = ['php']; private string $strategy; private bool $running = false; + private bool $scanning = false; private $inotifyFd = null; private array $inotifyWatchMap = []; @@ -132,6 +135,10 @@ 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]); + } + if ($this->running) { return; } @@ -163,20 +170,14 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface $this->inotifyFd = null; } - if (isset($this->fswatchPipes[1]) && is_resource($this->fswatchPipes[1])) { - @Event::del($this->fswatchPipes[1]); - @fclose($this->fswatchPipes[1]); - } - - if (isset($this->fswatchPipes[2]) && is_resource($this->fswatchPipes[2])) { - @fclose($this->fswatchPipes[2]); - } - - if ($this->fswatchProcess && is_resource($this->fswatchProcess)) { - @proc_terminate($this->fswatchProcess); - @proc_close($this->fswatchProcess); - $this->fswatchProcess = null; + foreach ($this->fswatchPipes as $pipe) { + if (is_resource($pipe)) { + @Event::del($pipe); + @fclose($pipe); + } } + $this->fswatchPipes = []; + $this->stopFswatchProcess(); if ($this->pollTimer) { \Swoole\Timer::clear($this->pollTimer); @@ -191,6 +192,43 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface @Event::exit(); } + private function stopFswatchProcess(): void + { + if (!$this->fswatchProcess || !is_resource($this->fswatchProcess)) { + $this->fswatchProcess = null; + return; + } + + $status = @proc_get_status($this->fswatchProcess); + if (($status['running'] ?? false) === true) { + @proc_terminate($this->fswatchProcess, SIGTERM); + for ($i = 0; $i < 10; $i++) { + usleep(100000); + $status = @proc_get_status($this->fswatchProcess); + if (($status['running'] ?? false) !== true) { + break; + } + } + } + + $status = @proc_get_status($this->fswatchProcess); + if (($status['running'] ?? false) === true) { + @proc_terminate($this->fswatchProcess, SIGKILL); + for ($i = 0; $i < 5; $i++) { + usleep(100000); + $status = @proc_get_status($this->fswatchProcess); + if (($status['running'] ?? false) !== true) { + break; + } + } + } + + $status = @proc_get_status($this->fswatchProcess); + if (($status['running'] ?? false) !== true) { + @proc_close($this->fswatchProcess); + } + $this->fswatchProcess = null; + } private function isExcluded(string $path): bool { foreach ($this->excludePatterns as $pattern) { @@ -377,6 +415,10 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface } Event::add($this->inotifyFd, function ($fd) { + if (!$this->running) { + return; + } + $events = inotify_read($fd); if ($events === false) { return; @@ -466,6 +508,10 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface stream_set_blocking($this->fswatchPipes[1], false); Event::add($this->fswatchPipes[1], function ($pipe) { + if (!$this->running) { + return; + } + $data = fread($pipe, 8192); if ($data === false || $data === '') { return; @@ -486,13 +532,25 @@ class FileWatcher extends AbstractProcess implements OnProcessInterface $intervalMs = $this->pollInterval * 1000; $this->pollTimer = \Swoole\Timer::tick($intervalMs, function () { - $newSnapshot = []; - $changedFiles = $this->compareSnapshots($newSnapshot); - if (!empty($changedFiles)) { - $this->triggerCallback($changedFiles); + if (!$this->running || $this->scanning) { + return; } - $this->fileSnapshot = $newSnapshot; + $this->scanning = true; + try { + $newSnapshot = []; + $changedFiles = $this->compareSnapshots($newSnapshot); + if (!$this->running) { + return; + } + if (!empty($changedFiles)) { + $this->triggerCallback($changedFiles); + } + + $this->fileSnapshot = $newSnapshot; + } finally { + $this->scanning = false; + } }); } diff --git a/Abstracts/HotReload.php b/Abstracts/HotReload.php index aa4249e..d6faa43 100644 --- a/Abstracts/HotReload.php +++ b/Abstracts/HotReload.php @@ -76,10 +76,20 @@ class HotReload extends AbstractProcess */ public function onSigterm(): void { - // TODO: Implement onSigterm() method. $this->stop(); } + + 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 */ diff --git a/Abstracts/TraitServer.php b/Abstracts/TraitServer.php index b39fce3..7e8bbdb 100644 --- a/Abstracts/TraitServer.php +++ b/Abstracts/TraitServer.php @@ -49,9 +49,7 @@ trait TraitServer $handling = true; try { - $pidFile = function_exists('storage') ? storage('.swoole.pid') : ''; - $masterPid = is_file($pidFile) ? (int)trim((string)file_get_contents($pidFile)) : 0; - if ($masterPid > 1 && $masterPid !== getmypid()) { + if (!$this->shouldHandleSigint()) { return; } @@ -62,6 +60,81 @@ trait TraitServer } } + + private function shouldHandleSigint(): bool + { + $currentPid = getmypid(); + if ($this->isMasterProcess($currentPid)) { + return true; + } + + $masterPid = $this->getVerifiedPidFileMasterPid(); + return $masterPid !== null && $masterPid === $currentPid; + } + + + private function getVerifiedPidFileMasterPid(): ?int + { + $pidFile = function_exists('storage') ? storage('.swoole.pid') : ''; + if (!is_file($pidFile)) { + return null; + } + + $pid = (int)trim((string)file_get_contents($pidFile)); + if ($pid <= 1 || !$this->isProcessAlive($pid) || !$this->isSameAppProcess($pid) || !$this->isMasterProcess($pid)) { + return null; + } + + return $pid; + } + + + private function isMasterProcess(int $pid): bool + { + $command = $this->getProcessCommand($pid); + if ($command === '') { + return false; + } + + $prefix = '[' . config('site.id', 'system-service') . '].Master['; + return str_contains($command, $prefix); + } + + + private function getProcessCommand(int $pid): string + { + $command = @file_get_contents('/proc/' . $pid . '/cmdline'); + if (!is_string($command) || $command === '') { + return ''; + } + + return str_replace("\0", ' ', $command); + } + + + 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); + } + /** * @param $signal * @param $callback diff --git a/Processes/AbstractProcess.php b/Processes/AbstractProcess.php index 76844a3..1e2184c 100644 --- a/Processes/AbstractProcess.php +++ b/Processes/AbstractProcess.php @@ -17,6 +17,8 @@ abstract class AbstractProcess implements OnProcessInterface private bool $stop = false; + private bool $stopping = false; + public Process $process; @@ -132,10 +134,11 @@ abstract class AbstractProcess implements OnProcessInterface $array['deadlock_check_disable_trace'] = false; $array['exit_condition'] = [$this, 'exit_condition']; Coroutine::set($array); + $process::signal(SIGINT, static function (): void {}); Coroutine::create(fn() => $this->coroutineWaitSignal()); } else { $process::signal(SIGTERM, [$this, 'pointWaitSignal']); - $process::signal(SIGINT, [$this, 'pointWaitSignal']); + $process::signal(SIGINT, static function (): void {}); } return $this; } @@ -153,9 +156,7 @@ abstract class AbstractProcess implements OnProcessInterface */ public function pointWaitSignal($signal): void { - $this->stop = true; - - $this->onSigterm(); + $this->requestStop(); } @@ -164,10 +165,20 @@ abstract class AbstractProcess implements OnProcessInterface */ public function coroutineWaitSignal(): void { - $value = Coroutine::waitSignal(SIGTERM); - if ($value) { - $this->stop = true; + Coroutine::waitSignal(SIGTERM); + $this->requestStop(); + } + + + + private function requestStop(): void + { + if ($this->stopping) { + return; } + + $this->stopping = true; + $this->stop = true; $this->onSigterm(); } diff --git a/Processes/TraitProcess.php b/Processes/TraitProcess.php index 90a4a81..f050037 100644 --- a/Processes/TraitProcess.php +++ b/Processes/TraitProcess.php @@ -5,6 +5,8 @@ namespace Kiri\Server\Processes; use Exception; use Kiri; use Swoole\Process; +use const SIGKILL; +use const SIGTERM; trait TraitProcess { @@ -66,4 +68,39 @@ trait TraitProcess { return $this->_process; } -} + + /** + * @return void + */ + private function terminateProcesses(): void + { + $pids = []; + foreach ($this->_process as $process) { + $pid = (int)($process->pid ?? 0); + if ($pid > 1 && $pid !== getmypid()) { + $pids[] = $pid; + } + } + + $pids = array_values(array_unique($pids)); + foreach ($pids as $pid) { + if (@Process::kill($pid, 0)) { + @Process::kill($pid, SIGTERM); + } + } + + for ($i = 0; $i < 10; $i++) { + $alive = array_values(array_filter($pids, static fn(int $pid): bool => @Process::kill($pid, 0))); + if ($alive === []) { + return; + } + usleep(100000); + } + + foreach ($pids as $pid) { + if (@Process::kill($pid, 0)) { + @Process::kill($pid, SIGKILL); + } + } + } +} \ No newline at end of file