Issues (1474)

framework/Util/TSignalsDispatcher.php (1 issue)

Labels
Severity
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
A parse error occurred: Syntax error, unexpected '(' on line 170 at column 19
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