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
![]() |
|||
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 |