pradosoft /
prado
| 1 | <?php |
||
| 2 | |||
| 3 | /** |
||
| 4 | * TSignalsDispatcher class file. |
||
| 5 | * |
||
| 6 | * @author Brad Anderson <[email protected]> |
||
| 7 | * @link https://github.com/pradosoft/prado |
||
| 8 | * @license https://github.com/pradosoft/prado/blob/master/LICENSE |
||
| 9 | */ |
||
| 10 | |||
| 11 | namespace Prado\Util; |
||
| 12 | |||
| 13 | use Prado\Collections\TPriorityPropertyTrait; |
||
| 14 | use Prado\Collections\TWeakCallableCollection; |
||
| 15 | use Prado\Exceptions\{TExitException}; |
||
| 16 | use Prado\Exceptions\{TInvalidOperationException, TInvalidDataValueException}; |
||
| 17 | use Prado\TComponent; |
||
| 18 | use Prado\Util\Helpers\TProcessHelper; |
||
| 19 | |||
| 20 | /** |
||
| 21 | * TSignalsDispatcher class. |
||
| 22 | * |
||
| 23 | * This class handles linux process signals. It translates the signals into global |
||
| 24 | * events in PRADO. There are special handlers for SIGALRM to handle time based |
||
| 25 | * alarm callbacks and for SIGCHLD to handle specific Process ID. |
||
| 26 | * |
||
| 27 | * The signals translate to global events as such: |
||
| 28 | * SIGALRM => `fxSignalAlarm` ~ |
||
| 29 | * SIGHUP => `fxSignalHangUp` ^ |
||
| 30 | * SIGINT => `fxSignalInterrupt` ^ |
||
| 31 | * SIGQUIT => `fxSignalQuit` ^ |
||
| 32 | * SIGTRAP => `fxSignalTrap` |
||
| 33 | * SIGABRT => `fxSignalAbort` ^ |
||
| 34 | * SIGUSR1 => `fxSignalUser1` |
||
| 35 | * SIGUSR2 => `fxSignalUser2` |
||
| 36 | * SIGTERM => `fxSignalTerminate` ^ |
||
| 37 | * SIGCHLD => `fxSignalChild` @ |
||
| 38 | * SIGCONT => `fxSignalContinue` |
||
| 39 | * SIGTSTP => `fxSignalTerminalStop` |
||
| 40 | * SIGTTIN => `fxSignalBackgroundRead` |
||
| 41 | * SIGTTOU => `fxSignalBackgroundWrite` |
||
| 42 | * SIGURG => `fxSignalUrgent` |
||
| 43 | * ^ Designates an exiting signal-event unless changed in the TSignalParameter. |
||
| 44 | * ~ SIGALRM has a special handler {@see self::ring()} to process and manage the time |
||
| 45 | * queue of callbacks and reset the alarm. |
||
| 46 | * @ SIGCHLD has a special handler {@see self::delegateChild()} to delegate to process |
||
| 47 | * specific handlers. |
||
| 48 | * |
||
| 49 | * The signal handlers are stored and restored on {@see self::attach()}ing and {@see |
||
| 50 | * self::detach()}ing, respectively. This installs and uninstalls the class as |
||
| 51 | * the signals' handler through the {@see self::__invoke()} method. |
||
| 52 | * |
||
| 53 | * Alarms may be added with (@see self::alarm()) and removed with {@see self::disarm()}. |
||
| 54 | * Alarms can be added without callbacks and will raise the `fxSignalAlarm` event |
||
| 55 | * without a time callback. By calling alarm() without parameters, the next alarm |
||
| 56 | * time is returned. |
||
| 57 | * |
||
| 58 | * The methods {@see self::hasEvent()}, {@see self::hasEventHandler}, and {@see self::getEventHandlers} |
||
| 59 | * will accept process signals (int) as event names and translate it into the associated |
||
| 60 | * PRADO signal global event. These methods will also return the proper values |
||
| 61 | * for PID handlers, as well, by providing the event name in the format "pid:####" |
||
| 62 | * (where #### is the PID). hasEvent and getEventHandlers checks if the PID is running. |
||
| 63 | * To get the PID handler without validation use {@see self::getPidHandlers()}. |
||
| 64 | * |
||
| 65 | * Child PID handlers can be checked for with {@see self::hasPidHandler()}. The PID |
||
| 66 | * handlers can be retrieved with {@see self::getPidHandlers()} (besides using getEventHandlers |
||
| 67 | * with a special formatted event name [above]). Handlers can be attached to a specific |
||
| 68 | * PID with {@see self::attachPidHandler()} and detached with {@see self::detachPidHandler()}. |
||
| 69 | * Child PID handlers can be cleared with {@see self::clearPidHandlers()} |
||
| 70 | * |
||
| 71 | * @author Brad Anderson <[email protected]> |
||
| 72 | * @since 4.3.0 |
||
| 73 | */ |
||
| 74 | class TSignalsDispatcher extends TComponent implements \Prado\ISingleton |
||
| 75 | { |
||
| 76 | use TPriorityPropertyTrait; |
||
| 77 | |||
| 78 | public const FX_SIGNAL_ALARM = 'fxSignalAlarm'; |
||
| 79 | public const FX_SIGNAL_HANG_UP = 'fxSignalHangUp'; |
||
| 80 | public const FX_SIGNAL_INTERRUPT = 'fxSignalInterrupt'; |
||
| 81 | public const FX_SIGNAL_QUIT = 'fxSignalQuit'; |
||
| 82 | public const FX_SIGNAL_TRAP = 'fxSignalTrap'; |
||
| 83 | public const FX_SIGNAL_ABORT = 'fxSignalAbort'; |
||
| 84 | public const FX_SIGNAL_USER1 = 'fxSignalUser1'; |
||
| 85 | public const FX_SIGNAL_USER2 = 'fxSignalUser2'; |
||
| 86 | public const FX_SIGNAL_TERMINATE = 'fxSignalTerminate'; |
||
| 87 | public const FX_SIGNAL_CHILD = 'fxSignalChild'; |
||
| 88 | public const FX_SIGNAL_CONTINUE = 'fxSignalContinue'; |
||
| 89 | public const FX_SIGNAL_TERMINAL_STOP = 'fxSignalTerminalStop'; |
||
| 90 | public const FX_SIGNAL_BACKGROUND_READ = 'fxSignalBackgroundRead'; |
||
| 91 | public const FX_SIGNAL_BACKGROUND_WRITE = 'fxSignalBackgroundWrite'; |
||
| 92 | public const FX_SIGNAL_URGENT = 'fxSignalUrgent'; |
||
| 93 | |||
| 94 | public const SIGNAL_MAP = [ |
||
| 95 | SIGALRM => self::FX_SIGNAL_ALARM, // Alarm signal. Sent by pcntl_alarm when the time is over. |
||
| 96 | SIGHUP => self::FX_SIGNAL_HANG_UP, // Hangup signal. Sent to a process when its controlling terminal is closed. |
||
| 97 | SIGINT => self::FX_SIGNAL_INTERRUPT, // Interrupt signal. Typically generated by pressing Ctrl+C. |
||
| 98 | SIGQUIT => self::FX_SIGNAL_QUIT, // Quit signal. Similar to SIGINT but produces a core dump when received by the process. |
||
| 99 | SIGTRAP => self::FX_SIGNAL_TRAP, // Trace/breakpoint trap signal. Used by debuggers to catch trace and breakpoint conditions. |
||
| 100 | SIGABRT => self::FX_SIGNAL_ABORT, // Abort signal. Sent by the process itself to terminate due to a critical error condition. |
||
| 101 | SIGUSR1 => self::FX_SIGNAL_USER1, // User-defined signal 1. |
||
| 102 | SIGUSR2 => self::FX_SIGNAL_USER2, // User-defined signal 2. |
||
| 103 | SIGTERM => self::FX_SIGNAL_TERMINATE, // Termination signal. Typically used to request graceful termination of a process. |
||
| 104 | SIGCHLD => self::FX_SIGNAL_CHILD, // Child signal. Sent to a parent process when a child process terminates. |
||
| 105 | SIGCONT => self::FX_SIGNAL_CONTINUE, // Continue signal. Sent to resume a process that has been stopped. |
||
| 106 | SIGTSTP => self::FX_SIGNAL_TERMINAL_STOP, // Terminal stop signal. Sent by pressing Ctrl+Z to suspend the process. |
||
| 107 | SIGTTIN => self::FX_SIGNAL_BACKGROUND_READ, // Background read signal. Sent to a process when it attempts to read from the terminal while in the background. |
||
| 108 | SIGTTOU => self::FX_SIGNAL_BACKGROUND_WRITE, // Background write signal. Sent to a process when it attempts to write to the terminal while in the background. |
||
| 109 | SIGURG => self::FX_SIGNAL_URGENT, // Urgent condition signal. Indicates the arrival of out-of-band data on a socket. |
||
| 110 | ]; |
||
| 111 | |||
| 112 | /** The signals that exit by default. */ |
||
| 113 | public const EXIT_SIGNALS = [ |
||
| 114 | SIGABRT => true, |
||
| 115 | SIGBUS => true, |
||
| 116 | SIGFPE => true, |
||
| 117 | SIGHUP => true, |
||
| 118 | SIGILL => true, |
||
| 119 | SIGINT => true, |
||
| 120 | SIGPIPE => true, |
||
| 121 | SIGQUIT => true, |
||
| 122 | SIGSEGV => true, |
||
| 123 | SIGSYS => true, |
||
| 124 | SIGTERM => true, |
||
| 125 | ]; |
||
| 126 | |||
| 127 | /** @var callable The alarm when no callback is provided. */ |
||
| 128 | public const NULL_ALARM = [self::class, 'nullAlarm']; |
||
| 129 | |||
| 130 | /** @var ?TSignalsDispatcher The Singleton instance of the class. This is the class |
||
| 131 | * that gets installed as the signals handler. |
||
| 132 | */ |
||
| 133 | private static ?TSignalsDispatcher $_singleton = null; |
||
| 134 | |||
| 135 | /** @var ?bool Are the signals async. */ |
||
| 136 | private static ?bool $_asyncSignals = null; |
||
| 137 | |||
| 138 | /** @var array The handlers of the signals prior to attaching. |
||
| 139 | * Format [0 => original value, 1 => closure for the event handler to call the original callable]. |
||
| 140 | */ |
||
| 141 | private static array $_priorHandlers = []; |
||
| 142 | |||
| 143 | /** @var ?float Any signal handlers are installed into PRADO at this priority. */ |
||
| 144 | private static ?float $_priorHandlerPriority = null; |
||
| 145 | |||
| 146 | /** @var ?bool Were the signals async before TSignalsDispatcher. */ |
||
| 147 | private static ?bool $_priorAsync = null; |
||
| 148 | |||
| 149 | /** @var array The integer alarm times and handlers */ |
||
| 150 | protected static array $_alarms = []; |
||
| 151 | |||
| 152 | /** @var bool Is the $_alarms array ordered by time. */ |
||
| 153 | protected static bool $_alarmsOrdered = true; |
||
| 154 | |||
| 155 | /** @var ?int The next alarm time in the _alarms array. */ |
||
| 156 | protected static ?int $_nextAlarmTime = null; |
||
| 157 | |||
| 158 | /** @var array The pid handlers. */ |
||
| 159 | private static array $_pidHandlers = []; |
||
| 160 | |||
| 161 | /** |
||
| 162 | * Returns the singleton of the class. The singleton is created if/when $create |
||
| 163 | * is true. |
||
| 164 | * @param bool $create Should the singleton be created if not existing, default true. |
||
| 165 | * @return ?object The singleton of the class, null where none is available. |
||
| 166 | */ |
||
| 167 | public static function singleton(bool $create = true): ?object |
||
| 168 | { |
||
| 169 | if ($create && static::hasSignals() && !self::$_singleton) { |
||
| 170 | $instance = new (static::class)(); |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 171 | } |
||
| 172 | |||
| 173 | return self::$_singleton; |
||
| 174 | } |
||
| 175 | |||
| 176 | /** |
||
| 177 | * Constructs the TSignalsDispatcher. |
||
| 178 | * The first instance is attached and set as the singleton. |
||
| 179 | */ |
||
| 180 | public function __construct() |
||
| 181 | { |
||
| 182 | parent::__construct(); |
||
| 183 | $this->attach(); |
||
| 184 | } |
||
| 185 | |||
| 186 | /** |
||
| 187 | * This translates the global event into the signal that raises the event. |
||
| 188 | * @param string $event The event name to look up its signal. |
||
| 189 | * @return ?int The signal for an event name. |
||
| 190 | */ |
||
| 191 | public static function getSignalFromEvent(string $event): ?int |
||
| 192 | { |
||
| 193 | static $eventMap = null; |
||
| 194 | |||
| 195 | if ($eventMap === null) { |
||
| 196 | $eventMap = array_flip(static::SIGNAL_MAP); |
||
| 197 | } |
||
| 198 | return $eventMap[$event] ?? null; |
||
| 199 | } |
||
| 200 | |||
| 201 | /** |
||
| 202 | * @return bool Does the system support Process Signals (pcntl_signal) |
||
| 203 | */ |
||
| 204 | public static function hasSignals(): bool |
||
| 205 | { |
||
| 206 | return function_exists('pcntl_signal'); |
||
| 207 | } |
||
| 208 | |||
| 209 | /** |
||
| 210 | * This sets the signals' handlers to this object, attaches the original handlers |
||
| 211 | * to the PRADO global events at the {@see self::getPriorHandlerPriority()}. |
||
| 212 | * The alarm handler and Child handler is installed for routing. |
||
| 213 | * @return bool Is the instance attached as the singleton. |
||
| 214 | */ |
||
| 215 | public function attach(): bool |
||
| 216 | { |
||
| 217 | if (!static::hasSignals() || self::$_singleton !== null) { |
||
| 218 | return false; |
||
| 219 | } |
||
| 220 | |||
| 221 | self::$_singleton = $this; |
||
| 222 | |||
| 223 | if (self::$_asyncSignals === null) { |
||
| 224 | static::setAsyncSignals(true); |
||
| 225 | } |
||
| 226 | |||
| 227 | foreach (static::SIGNAL_MAP as $signal => $event) { |
||
| 228 | $handler = pcntl_signal_get_handler($signal); |
||
| 229 | |||
| 230 | if ($handler instanceof self) { |
||
| 231 | continue; |
||
| 232 | } |
||
| 233 | self::$_priorHandlers[$signal] = [$handler]; |
||
| 234 | |||
| 235 | $callable = is_callable($handler); |
||
| 236 | if ($callable) { |
||
| 237 | $handler = function ($sender, $param) use ($handler) { |
||
| 238 | return $handler($param->getSignal(), $param->getParameter()); |
||
| 239 | }; |
||
| 240 | self::$_priorHandlers[$signal][1] = $handler; |
||
| 241 | } |
||
| 242 | |||
| 243 | $installHandler = true; |
||
| 244 | switch ($signal) { |
||
| 245 | case SIGALRM: |
||
| 246 | parent::attachEventHandler($event, [$this, 'ring'], $this->getPriority()); |
||
| 247 | if ($nextAlarm = pcntl_alarm(0)) { |
||
| 248 | self::$_nextAlarmTime = $nextAlarm + time(); |
||
| 249 | if ($callable) { |
||
| 250 | static::$_alarms[self::$_nextAlarmTime][] = $handler; |
||
| 251 | } |
||
| 252 | pcntl_alarm($nextAlarm); |
||
| 253 | } |
||
| 254 | $installHandler = false; |
||
| 255 | break; |
||
| 256 | case SIGCHLD: |
||
| 257 | parent::attachEventHandler($event, [$this, 'delegateChild'], $this->getPriority()); |
||
| 258 | break; |
||
| 259 | } |
||
| 260 | |||
| 261 | if ($installHandler && $callable) { |
||
| 262 | parent::attachEventHandler($event, $handler, static::getPriorHandlerPriority()); |
||
| 263 | } |
||
| 264 | |||
| 265 | pcntl_signal($signal, $this); |
||
| 266 | } |
||
| 267 | return true; |
||
| 268 | } |
||
| 269 | |||
| 270 | /** |
||
| 271 | * Detaches the singleton when it is the singleton. Prior signal handlers are |
||
| 272 | * restored. |
||
| 273 | * @return bool Is the instance detached from singleton. |
||
| 274 | */ |
||
| 275 | public function detach(): bool |
||
| 276 | { |
||
| 277 | if (self::$_singleton !== $this) { |
||
| 278 | return false; |
||
| 279 | } |
||
| 280 | |||
| 281 | foreach (self::$_priorHandlers as $signal => $originalCallback) { |
||
| 282 | pcntl_signal($signal, $originalCallback[0]); |
||
| 283 | $uninstallHandler = true; |
||
| 284 | switch ($signal) { |
||
| 285 | case SIGALRM: |
||
| 286 | parent::detachEventHandler(static::SIGNAL_MAP[$signal], [$this, 'ring']); |
||
| 287 | pcntl_alarm(0); |
||
| 288 | $uninstallHandler = false; |
||
| 289 | break; |
||
| 290 | case SIGCHLD: |
||
| 291 | parent::detachEventHandler(static::SIGNAL_MAP[$signal], [$this, 'delegateChild']); |
||
| 292 | break; |
||
| 293 | } |
||
| 294 | if ($uninstallHandler && isset($originalCallback[1])) { |
||
| 295 | parent::detachEventHandler(static::SIGNAL_MAP[$signal], $originalCallback[1]); |
||
| 296 | } |
||
| 297 | } |
||
| 298 | |||
| 299 | self::$_priorHandlers = []; |
||
| 300 | self::$_pidHandlers = []; |
||
| 301 | static::$_alarms = []; |
||
| 302 | self::$_singleton = null; |
||
| 303 | |||
| 304 | static::setAsyncSignals(self::$_priorAsync); |
||
| 305 | self::$_priorAsync = null; |
||
| 306 | self::$_asyncSignals = null; |
||
| 307 | |||
| 308 | return true; |
||
| 309 | } |
||
| 310 | |||
| 311 | /** |
||
| 312 | * Determines whether an event is defined. |
||
| 313 | * The event name can be an integer Signal or a pid. Pid handlers have a prefix |
||
| 314 | * of 'pid:'. |
||
| 315 | * An event is defined if the class has a method whose name is the event name |
||
| 316 | * prefixed with 'on', 'fx', or 'dy'. |
||
| 317 | * Every object responds to every 'fx' and 'dy' event as they are in a universally |
||
| 318 | * accepted event space. 'on' event must be declared by the object. |
||
| 319 | * When enabled, this will loop through all active behaviors for 'on' events |
||
| 320 | * defined by the behavior. |
||
| 321 | * Note, event name is case-insensitive. |
||
| 322 | * @param mixed $name the event name |
||
| 323 | * @return bool Is the event, signal event, or PID event available. |
||
| 324 | */ |
||
| 325 | public function hasEvent($name) |
||
| 326 | { |
||
| 327 | if (isset(static::SIGNAL_MAP[$name])) { |
||
| 328 | $name = static::SIGNAL_MAP[$name]; |
||
| 329 | } elseif (strncasecmp('pid:', $name, 4) === 0) { |
||
| 330 | if (is_numeric($pid = trim(substr($name, 4)))) { |
||
| 331 | return TProcessHelper::isRunning((int) $pid); |
||
| 332 | } |
||
| 333 | return false; |
||
| 334 | } |
||
| 335 | return parent::hasEvent($name); |
||
| 336 | } |
||
| 337 | |||
| 338 | /** |
||
| 339 | * Checks if an event has any handlers. This function also checks through all |
||
| 340 | * the behaviors for 'on' events when behaviors are enabled. |
||
| 341 | * The event name can be an integer Signal or a pid. Pid handlers have a prefix |
||
| 342 | * of 'pid:'. |
||
| 343 | * 'dy' dynamic events are not handled by this function. |
||
| 344 | * @param string $name the event name |
||
| 345 | * @return bool whether an event has been attached one or several handlers |
||
| 346 | */ |
||
| 347 | public function hasEventHandler($name) |
||
| 348 | { |
||
| 349 | if (isset(static::SIGNAL_MAP[$name])) { |
||
| 350 | $name = static::SIGNAL_MAP[$name]; |
||
| 351 | } elseif (strncasecmp('pid:', $name, 4) === 0) { |
||
| 352 | if (is_numeric($pid = trim(substr($name, 4)))) { |
||
| 353 | $pid = (int) $pid; |
||
| 354 | return isset(self::$_pidHandlers[$pid]) && self::$_pidHandlers[$pid]->getCount() > 0; |
||
| 355 | } |
||
| 356 | return false; |
||
| 357 | } |
||
| 358 | return parent::hasEventHandler($name); |
||
| 359 | } |
||
| 360 | |||
| 361 | /** |
||
| 362 | * Returns the list of attached event handlers for an 'on' or 'fx' event. This function also |
||
| 363 | * checks through all the behaviors for 'on' event lists when behaviors are enabled. |
||
| 364 | * The event name can be an integer Signal or a pid. Pid handlers have a prefix |
||
| 365 | * of 'pid:'. |
||
| 366 | * @param mixed $name |
||
| 367 | * @throws TInvalidOperationException if the event is not defined or PID not a valid numeric. |
||
| 368 | * @return TWeakCallableCollection list of attached event handlers for an event |
||
| 369 | */ |
||
| 370 | public function getEventHandlers($name) |
||
| 371 | { |
||
| 372 | if (isset(static::SIGNAL_MAP[$name])) { |
||
| 373 | $name = static::SIGNAL_MAP[$name]; |
||
| 374 | } elseif (strncasecmp('pid:', $name, 4) === 0) { |
||
| 375 | if (!is_numeric($pid = trim(substr($name, 4)))) { |
||
| 376 | throw new TInvalidOperationException('signalsdispatcher_bad_pid', $pid); |
||
| 377 | } |
||
| 378 | $pid = (int) $pid; |
||
| 379 | if (!isset(self::$_pidHandlers[$pid]) && TProcessHelper::isRunning($pid)) { |
||
| 380 | self::$_pidHandlers[$pid] = new TWeakCallableCollection(); |
||
| 381 | } elseif (isset(self::$_pidHandlers[$pid]) && !TProcessHelper::isRunning($pid)) { |
||
| 382 | unset(self::$_pidHandlers[$pid]); |
||
| 383 | } |
||
| 384 | return self::$_pidHandlers[$pid] ?? null; |
||
| 385 | } |
||
| 386 | return parent::getEventHandlers($name); |
||
| 387 | } |
||
| 388 | |||
| 389 | /** |
||
| 390 | * @param int $pid The PID to check for handlers. |
||
| 391 | * @return bool Does the PID have handlers. |
||
| 392 | */ |
||
| 393 | public function hasPidHandler(int $pid): bool |
||
| 394 | { |
||
| 395 | return isset(self::$_pidHandlers[$pid]); |
||
| 396 | } |
||
| 397 | |||
| 398 | /** |
||
| 399 | * Returns the Handlers for a specific PID. |
||
| 400 | * @param int $pid The PID to get the handlers of. |
||
| 401 | * @param bool $validate Ensure that the PID is running before providing its handlers. |
||
| 402 | * Default false. |
||
| 403 | * @return ?TWeakCallableCollection The handlers for a pid or null if validating |
||
| 404 | * and the PID is not running. |
||
| 405 | */ |
||
| 406 | public function getPidHandlers(int $pid, bool $validate = false) |
||
| 407 | { |
||
| 408 | if ($validate && !TProcessHelper::isRunning($pid)) { |
||
| 409 | return null; |
||
| 410 | } |
||
| 411 | if (!isset(self::$_pidHandlers[$pid])) { |
||
| 412 | self::$_pidHandlers[$pid] = new TWeakCallableCollection(); |
||
| 413 | } |
||
| 414 | return self::$_pidHandlers[$pid]; |
||
| 415 | } |
||
| 416 | |||
| 417 | /** |
||
| 418 | * Attaches a handler to a child PID at a priority. Optionally validates the process |
||
| 419 | * before attaching. |
||
| 420 | * @param int $pid The PID to install the handler. |
||
| 421 | * @param mixed $handler The handler to attach to the process. |
||
| 422 | * @param null|numeric $priority The priority of the handler, default null for the |
||
| 423 | * default |
||
| 424 | * @param bool $validate Should the PID be validated before attaching. |
||
| 425 | * @return bool Is the handler attached? this can only be false if $validate = true |
||
| 426 | * and the PID is not running any more. |
||
| 427 | */ |
||
| 428 | public function attachPidHandler(int $pid, mixed $handler, mixed $priority = null, bool $validate = false) |
||
| 429 | { |
||
| 430 | if ($validate && !TProcessHelper::isRunning($pid)) { |
||
| 431 | return false; |
||
| 432 | } |
||
| 433 | if (!isset(self::$_pidHandlers[$pid])) { |
||
| 434 | self::$_pidHandlers[$pid] = new TWeakCallableCollection(); |
||
| 435 | } |
||
| 436 | self::$_pidHandlers[$pid]->add($handler, $priority); |
||
| 437 | return true; |
||
| 438 | } |
||
| 439 | |||
| 440 | /** |
||
| 441 | * Detaches a handler from a child PID at a priority. |
||
| 442 | * @param int $pid The PID to detach the handler from. |
||
| 443 | * @param mixed $handler The handler to remove. |
||
| 444 | * @param mixed $priority The priority of the handler to remove. default false for |
||
| 445 | * any priority. |
||
| 446 | */ |
||
| 447 | public function detachPidHandler(int $pid, mixed $handler, mixed $priority = false) |
||
| 448 | { |
||
| 449 | if (isset(self::$_pidHandlers[$pid])) { |
||
| 450 | try { |
||
| 451 | self::$_pidHandlers[$pid]->remove($handler, $priority); |
||
| 452 | } catch (\Exception $e) { |
||
| 453 | } |
||
| 454 | if (self::$_pidHandlers[$pid]->getCount() === 0) { |
||
| 455 | $this->clearPidHandlers($pid); |
||
| 456 | } |
||
| 457 | return true; |
||
| 458 | } |
||
| 459 | return false; |
||
| 460 | } |
||
| 461 | |||
| 462 | /** |
||
| 463 | * Clears the Handlers for a specific PID. |
||
| 464 | * @param int $pid The pid to clear the handlers. |
||
| 465 | * @return bool Were there any handlers for the PID that were cleared. |
||
| 466 | */ |
||
| 467 | public function clearPidHandlers(int $pid): bool |
||
| 468 | { |
||
| 469 | $return = isset(self::$_pidHandlers[$pid]); |
||
| 470 | unset(self::$_pidHandlers[$pid]); |
||
| 471 | return $return; |
||
| 472 | } |
||
| 473 | |||
| 474 | /** |
||
| 475 | * The common SIGCHLD callback to delegate per PID. If there are specific PID |
||
| 476 | * handlers for a child, those PID callbacks are called. On an exit event, the PID |
||
| 477 | * handlers are cleared. |
||
| 478 | * @param TSignalsDispatcher $sender The object raising the event. |
||
| 479 | * @param TSignalParameter $param The signal parameters. |
||
| 480 | */ |
||
| 481 | public function delegateChild($sender, $param) |
||
| 482 | { |
||
| 483 | if (!static::hasSignals()) { |
||
| 484 | return; |
||
| 485 | } |
||
| 486 | |||
| 487 | if (!$param || ($pid = $param->getParameterPid()) === null) { |
||
| 488 | if (($pid = pcntl_waitpid(-1, $status, WNOHANG)) < 1) { |
||
| 489 | return; |
||
| 490 | } |
||
| 491 | if (!$param) { |
||
| 492 | $param = new TSignalParameter(SIGCHLD); |
||
| 493 | } |
||
| 494 | $sigInfo = $param->getParameter() ?? []; |
||
| 495 | $sigInfo['pid'] = $pid; |
||
| 496 | $sigInfo['status'] = $status; |
||
| 497 | $param->setParameter($sigInfo); |
||
| 498 | } |
||
| 499 | if (!isset(self::$_pidHandlers[$pid])) { |
||
| 500 | return; |
||
| 501 | } |
||
| 502 | |||
| 503 | array_map(fn ($child) => $child($sender, $param), self::$_pidHandlers[$pid]->toArray()); |
||
| 504 | |||
| 505 | if (in_array($param->getParameterCode(), [1, 2])) { // 1 = normal exit, 2 = kill |
||
| 506 | unset(self::$_pidHandlers[$pid]); |
||
| 507 | } |
||
| 508 | } |
||
| 509 | |||
| 510 | /** |
||
| 511 | * {@inheritDoc} |
||
| 512 | * |
||
| 513 | * Raises signal events by converting the $name as a Signal to the PRADO event |
||
| 514 | * for the signal. |
||
| 515 | * @param mixed $name The event name or linux signal to raise. |
||
| 516 | * @param mixed $sender The sender raising the event. |
||
| 517 | * @param mixed $param The parameter for the event. |
||
| 518 | * @param null|mixed $responsetype The response type. |
||
| 519 | * @param null|mixed $postfunction The results post filter. |
||
| 520 | */ |
||
| 521 | public function raiseEvent($name, $sender, $param, $responsetype = null, $postfunction = null) |
||
| 522 | { |
||
| 523 | if (isset(static::SIGNAL_MAP[$name])) { |
||
| 524 | $name = static::SIGNAL_MAP[$name]; |
||
| 525 | } |
||
| 526 | return parent::raiseEvent($name, $sender, $param, $responsetype, $postfunction); |
||
| 527 | } |
||
| 528 | |||
| 529 | /** |
||
| 530 | * This is called when receiving a system process signal. The global event |
||
| 531 | * for the signal is raised when the signal is received. |
||
| 532 | * @param int $signal The signal being sent. |
||
| 533 | * @param null|mixed $signalInfo The signal information. |
||
| 534 | * @throws TExitException When the signal needs to exit the application. |
||
| 535 | */ |
||
| 536 | public function __invoke(int $signal, mixed $signalInfo = null) |
||
| 537 | { |
||
| 538 | if (!isset(static::SIGNAL_MAP[$signal])) { |
||
| 539 | return; |
||
| 540 | } |
||
| 541 | |||
| 542 | $parameter = new TSignalParameter($signal, isset(self::EXIT_SIGNALS[$signal]), 128 + $signal, $signalInfo); |
||
| 543 | |||
| 544 | parent::raiseEvent(static::SIGNAL_MAP[$signal], $this, $parameter); |
||
| 545 | |||
| 546 | if ($parameter->getIsExiting()) { |
||
| 547 | throw new TExitException($parameter->getExitCode()); |
||
| 548 | } |
||
| 549 | } |
||
| 550 | |||
| 551 | /** |
||
| 552 | * Creates a new alarm callback at a specific time from now. If no callback is |
||
| 553 | * provided, then the alarm will raise `fxSignalAlarm` without a time-based callback. |
||
| 554 | * When calling alarm() without parameters, it will return the next alarm time. |
||
| 555 | * @param int $seconds Seconds from now to trigger the alarm. Default 0 for returning |
||
| 556 | * the next alarm time and not adding any callback. |
||
| 557 | * @param mixed $callback The alarm callback. Default null. |
||
| 558 | * @return ?int The time of the alarm for the callback or the next alarm time when |
||
| 559 | * $seconds = 0. |
||
| 560 | */ |
||
| 561 | public static function alarm(int $seconds = 0, mixed $callback = null): ?int |
||
| 562 | { |
||
| 563 | if (!static::hasSignals()) { |
||
| 564 | return null; |
||
| 565 | } |
||
| 566 | |||
| 567 | if ($seconds > 0) { |
||
| 568 | static::singleton(); |
||
| 569 | $alarmTime = time() + $seconds; |
||
| 570 | if ($callback === null) { |
||
| 571 | $callback = static::NULL_ALARM; |
||
| 572 | } |
||
| 573 | if (!isset(static::$_alarms[$alarmTime])) { |
||
| 574 | static::$_alarmsOrdered = false; |
||
| 575 | } |
||
| 576 | static::$_alarms[$alarmTime][] = $callback; |
||
| 577 | |||
| 578 | if (self::$_nextAlarmTime === null || $alarmTime < self::$_nextAlarmTime) { |
||
| 579 | self::$_nextAlarmTime = $alarmTime; |
||
| 580 | pcntl_alarm($seconds); |
||
| 581 | } |
||
| 582 | return $alarmTime; |
||
| 583 | } elseif ($callback === null) { |
||
| 584 | return self::$_nextAlarmTime; |
||
| 585 | } |
||
| 586 | |||
| 587 | return null; |
||
| 588 | } |
||
| 589 | |||
| 590 | /** |
||
| 591 | * Disarms an alarm time-callback, at the optional time. |
||
| 592 | * @param ?int $alarmTime |
||
| 593 | * @param callable $callback |
||
| 594 | */ |
||
| 595 | public static function disarm(mixed $alarmTime = null, mixed $callback = null): ?int |
||
| 596 | { |
||
| 597 | if (!static::hasSignals()) { |
||
| 598 | return null; |
||
| 599 | } |
||
| 600 | |||
| 601 | if ($alarmTime !== null && !is_int($alarmTime)) { |
||
| 602 | $tmp = $callback; |
||
| 603 | $callback = $alarmTime; |
||
| 604 | $alarmTime = $tmp; |
||
| 605 | } |
||
| 606 | |||
| 607 | // If alarmTime but has no handlers for the time. |
||
| 608 | if ($alarmTime !== null && !isset(static::$_alarms[$alarmTime])) { |
||
| 609 | return null; |
||
| 610 | } |
||
| 611 | |||
| 612 | if ($callback === null) { |
||
| 613 | $callback = static::NULL_ALARM; |
||
| 614 | } |
||
| 615 | |||
| 616 | if (!static::$_alarmsOrdered) { |
||
| 617 | ksort(static::$_alarms, SORT_NUMERIC); |
||
| 618 | static::$_alarmsOrdered = true; |
||
| 619 | } |
||
| 620 | if ($callback === self::$_priorHandlers[SIGALRM][0]) { |
||
| 621 | $callback = self::$_priorHandlers[SIGALRM][1]; |
||
| 622 | } |
||
| 623 | |||
| 624 | foreach ($alarmTime !== null ? [$alarmTime] : array_keys(static::$_alarms) as $time) { |
||
| 625 | if (($key = array_search($callback, static::$_alarms[$time] ?? [], true)) !== false) { |
||
| 626 | unset(static::$_alarms[$time][$key]); |
||
| 627 | if (is_array(static::$_alarms[$time] ?? false)) { |
||
| 628 | unset(static::$_alarms[$time]); |
||
| 629 | if ($time === self::$_nextAlarmTime) { |
||
| 630 | self::$_nextAlarmTime = array_key_first(static::$_alarms); |
||
| 631 | if (self::$_nextAlarmTime !== null) { |
||
| 632 | $seconds = min(1, self::$_nextAlarmTime - time()); |
||
| 633 | } else { |
||
| 634 | $seconds = 0; |
||
| 635 | } |
||
| 636 | pcntl_alarm($seconds); |
||
| 637 | } |
||
| 638 | } |
||
| 639 | return $time; |
||
| 640 | } |
||
| 641 | } |
||
| 642 | return null; |
||
| 643 | } |
||
| 644 | |||
| 645 | /** |
||
| 646 | * The common SIGALRM callback time-processing handler raised by `fxSignalAlarm`. |
||
| 647 | * All alarm callbacks before or at `time()` are called. The next alarm time is |
||
| 648 | * found and the next signal alarm is set. |
||
| 649 | * @param TSignalsDispatcher $sender The object raising the event. |
||
| 650 | * @param TSignalParameter $signalParam The signal parameters. |
||
| 651 | */ |
||
| 652 | public function ring($sender, $signalParam) |
||
| 653 | { |
||
| 654 | if (!static::hasSignals()) { |
||
| 655 | return null; |
||
| 656 | } |
||
| 657 | if (!static::$_alarmsOrdered) { |
||
| 658 | ksort(static::$_alarms, SORT_NUMERIC); |
||
| 659 | static::$_alarmsOrdered = true; |
||
| 660 | } |
||
| 661 | do { |
||
| 662 | $nextTime = null; |
||
| 663 | $startTime = time(); |
||
| 664 | $signalParam->setAlarmTime($startTime); |
||
| 665 | foreach (static::$_alarms as $alarmTime => $alarms) { |
||
| 666 | if ($alarmTime <= $startTime) { |
||
| 667 | array_map(fn ($alarm) => $alarm($this, $signalParam), static::$_alarms[$alarmTime]); |
||
| 668 | unset(static::$_alarms[$alarmTime]); |
||
| 669 | } elseif ($nextTime === null) { |
||
| 670 | $nextTime = $alarmTime; |
||
| 671 | break; |
||
| 672 | } |
||
| 673 | } |
||
| 674 | $now = time(); |
||
| 675 | } while ($startTime !== $now); |
||
| 676 | |||
| 677 | if ($nextTime !== null) { |
||
| 678 | pcntl_alarm($nextTime - $now); |
||
| 679 | } |
||
| 680 | self::$_nextAlarmTime = $nextTime; |
||
| 681 | } |
||
| 682 | |||
| 683 | /** |
||
| 684 | * The null alarm to simply trigger an alarm without callback |
||
| 685 | * @param TSignalsDispatcher $sender The object raising the event. |
||
| 686 | * @param TSignalParameter $param The signal parameters. |
||
| 687 | */ |
||
| 688 | public static function nullAlarm($sender, $param) |
||
| 689 | { |
||
| 690 | } |
||
| 691 | |||
| 692 | /** |
||
| 693 | * When PHP Signals are not in asynchronous mode, this must be called to dispatch |
||
| 694 | * the pending events. To change the async mode, use {@see self::setAsyncSignals()}. |
||
| 695 | * @return ?bool Returns true on success or false on failure. |
||
| 696 | */ |
||
| 697 | public static function syncDispatch(): ?bool |
||
| 698 | { |
||
| 699 | if (!static::hasSignals()) { |
||
| 700 | return null; |
||
| 701 | } |
||
| 702 | return pcntl_signal_dispatch(); |
||
| 703 | } |
||
| 704 | |||
| 705 | /** |
||
| 706 | * Gets whether the system is in async signals mode. |
||
| 707 | * @return ?bool Is the system set to handle async signals. null when there are |
||
| 708 | * no Process Signals in the PHP instance. |
||
| 709 | */ |
||
| 710 | public static function getAsyncSignals(): ?bool |
||
| 711 | { |
||
| 712 | if (!static::hasSignals()) { |
||
| 713 | return null; |
||
| 714 | } |
||
| 715 | return pcntl_async_signals(); |
||
| 716 | } |
||
| 717 | |||
| 718 | /** |
||
| 719 | * Sets whether the system is in async signals mode. This is set to true on instancing. |
||
| 720 | * If this is set to false, then {@see self::syncDispatch()} must be called for |
||
| 721 | * signals to be processed. Any pending signals are dispatched when setting async to true. |
||
| 722 | * @param bool $value Should signals be processed asynchronously. |
||
| 723 | * @return ?bool The prior value AsyncSignals before setting. |
||
| 724 | * @link https://www.php.net/manual/en/function.pcntl-signal-dispatch.php |
||
| 725 | */ |
||
| 726 | public static function setAsyncSignals(bool $value): ?bool |
||
| 727 | { |
||
| 728 | if (!static::hasSignals()) { |
||
| 729 | return null; |
||
| 730 | } |
||
| 731 | self::$_asyncSignals = $value; |
||
| 732 | |||
| 733 | $return = pcntl_async_signals($value); |
||
| 734 | |||
| 735 | if (self::$_priorAsync === null) { |
||
| 736 | self::$_priorAsync = $return; |
||
| 737 | } |
||
| 738 | |||
| 739 | if ($value === true && $return === false) { |
||
| 740 | pcntl_signal_dispatch(); |
||
| 741 | } |
||
| 742 | |||
| 743 | return $return; |
||
| 744 | } |
||
| 745 | |||
| 746 | /** |
||
| 747 | * This returns the priority of the signal handlers when they are installed as |
||
| 748 | * event handlers. |
||
| 749 | * @return ?float The priority for prior signal handlers when TSignalsDispatcher |
||
| 750 | * is attached. |
||
| 751 | */ |
||
| 752 | public static function getPriorHandlerPriority(): ?float |
||
| 753 | { |
||
| 754 | return self::$_priorHandlerPriority; |
||
| 755 | } |
||
| 756 | |||
| 757 | /** |
||
| 758 | * Sets the priority of the signal handlers when they are installed as |
||
| 759 | * event handlers. |
||
| 760 | * @param ?float $value The priority for prior signal handlers when TSignalsDispatcher |
||
| 761 | * is attached. |
||
| 762 | * @throws TInvalidOperationException When TSignalsDispatcher is already installed. |
||
| 763 | */ |
||
| 764 | public static function setPriorHandlerPriority(?float $value): bool |
||
| 765 | { |
||
| 766 | if (static::singleton(false)) { |
||
| 767 | throw new TInvalidOperationException('signalsdispatcher_no_change', 'PriorHandlerPriority'); |
||
| 768 | } |
||
| 769 | |||
| 770 | self::$_priorHandlerPriority = $value; |
||
| 771 | return true; |
||
| 772 | } |
||
| 773 | |||
| 774 | /** |
||
| 775 | * This gets the signal handlers that were installed prior to the TSignalsDispatcher |
||
| 776 | * being attached. |
||
| 777 | * @param int $signal The signal to get the prior handler value. |
||
| 778 | * @param bool $original Return the original handler, default false for the signal |
||
| 779 | * closure handler that wraps the original handler with a PRADO signal event handler. |
||
| 780 | */ |
||
| 781 | public static function getPriorHandler(int $signal, bool $original = false): mixed |
||
| 782 | { |
||
| 783 | return self::$_priorHandlers[$signal][$original ? 0 : 1] ?? null; |
||
| 784 | } |
||
| 785 | } |
||
| 786 |