_getSpoke()->getPath(); $format = "%s | %s\n"; fwrite(STDOUT, sprintf($format, $spokePath, $message)); return $this; } public function setSpoke(Spoke $spoke): Logger { if ($this->_spoke === null) { $this->_spoke = $spoke; }else { throw new \LogicException('Spoke is already set.'); } return $this; } protected function _getSpoke(): Spoke { if ($this->_spoke === null) { throw new \LogicException('Spoke is not set.'); } return $this->_spoke; } } class Wheel { const SIGNAL_NUMBER = 'signo'; const MAX_NUMBER_OF_SPOKES = 3; protected $_spokes = []; protected $_waitSignals = [ SIGCHLD, SIGALRM, SIGINT, SIGTERM, ]; protected $_info = []; protected $_spoke; protected $_logger; public function start(): Wheel { $this->_initialize(); while ($this->getNumberOfSpokes() < self::MAX_NUMBER_OF_SPOKES) { $spoke = new Spoke(); $spoke->appendPath($this->_getSpoke()->getPath()); $spoke->start(); $this->_getLogger()->log("Initialized spoke with PID [{$spoke->getProcessId()}]."); $this->_spokes[$spoke->getProcessId()] = $spoke; } pcntl_sigprocmask(SIG_BLOCK, $this->_waitSignals); while (true) { $this->_info = []; $this->_getLogger()->log("Waiting for signal."); pcntl_sigwaitinfo($this->_waitSignals, $this->_info); switch ($this->_info[self::SIGNAL_NUMBER]) { case SIGCHLD: $this->_getLogger()->log("Received SIGCHLD."); while ($processId = pcntl_wait($status, WNOHANG)) { $this->_getLogger()->log("Spoke with PID[$processId] exited."); unset($this->_spokes[$processId]); $spoke = new Spoke(); $spoke->appendPath($this->_getSpoke()->getPath()); $spoke->start(); $this->_getLogger()->log("Added new spoke with PID[{$spoke->getProcessId()}]."); $this->_spokes[$spoke->getProcessId()] = $spoke; } break; case SIGALRM: $this->_getLogger()->log("Received SIGALRM."); pcntl_alarm(8); break; case SIGINT: case SIGTERM: $this->_getLogger()->log("Received SIGINT or SIGTERM."); $this->terminateChildProcesses(); break 2; default: throw new \UnexpectedValueException('Unexpected blocked signal.'); } } } public function setSpoke(Spoke $spoke): Wheel { $this->_spoke = $spoke; return $this; } protected function _getSpoke(): Spoke { if ($this->_spoke === null) { throw new \LogicException('Spoke is not set.'); } return $this->_spoke; } protected function _initialize(): Wheel { register_shutdown_function([$this, 'terminateChildProcesses']); pcntl_signal(SIGTERM, [$this, 'terminateChildProcesses']); pcntl_signal(SIGINT, [$this, 'terminateChildProcesses']); return $this; } public function terminateChildProcesses(): Wheel { if (!empty($this->_processes)) { /** @var Spoke $spoke */ foreach ($this->_spokes as $spoke) { $processId = $spoke->getProcessId(); if (rand(1, 0)) { posix_kill($processId, SIGKILL); }else { posix_kill($processId, SIGTERM); } unset($this->_processes[$processId]); } } return $this; } public function getNumberOfSpokes(): int { return count($this->_spokes); } protected function _getLogger(): Logger { if ($this->_logger === null) { $this->_logger = new Logger(); $this->_logger->setSpoke($this->_getSpoke()); } return $this->_logger; } } class Spoke { const FORK_FAILURE_CODE = -1; protected $_parentProcessId; protected $_processId; protected $_wheel; protected $_path = ''; protected $_logger; public function start(): Spoke { $processId = pcntl_fork(); if ($processId === self::FORK_FAILURE_CODE) { throw new \RuntimeException('Failed to fork a new spoke.'); }elseif ($processId > 0) { $this->_initialize($processId); }else { $this->_initialize(); sleep(random_int(10, 3600)); if (substr_count($this->getPath(), '/') <= 3) { $this->_startNewWheel(); } exit(0); } return $this; } public function getProcessId(): int { if ($this->_processId === null) { throw new \LogicException('Process ID is not set.'); } return $this->_processId; } protected function _startNewWheel(): Spoke { $this->_wheel = new Wheel(); $this->_wheel->setSpoke($this); $this->_wheel->start(); return $this; } protected function _initialize(int $processId = null): Spoke { if ($processId === null) { $this->_parentProcessId = posix_getppid(); $this->_processId = posix_getpid(); $this->appendPath((string)$this->getProcessId()); }else { $this->_processId = $processId; $this->appendPath((string)$this->getProcessId()); } return $this; } public function getPath(): string { return $this->_path; } public function appendPath(string $path): Spoke { $this->_path .= $path . '/'; return $this; } protected function _getLogger(): Logger { if ($this->_logger === null) { $this->_logger = new Logger(); $this->_logger->setSpoke($this); } return $this->_logger; } } $spoke = new Spoke(); $spoke->start(); return;