on(OnWorkerStart::class, [di(Router::class), 'scan_build_route']); } /** * @return string */ public function getName(): string { return 'hotReload'; } /** * @return void */ public function onSigterm(): void { // TODO: Implement onSigterm() method. $this->stop(); } /** * @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 reload(): void { if ($this->reloading) { return; } $this->reloading = true; 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; } /** * @return void */ 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; } 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); } } /** * @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); } } } }