From b43eaddec3e3f28124b184703cdf6e2098a8c59f Mon Sep 17 00:00:00 2001 From: Belisoful Date: Mon, 24 Jul 2023 12:35:08 -0700 Subject: [PATCH] #972 TProcessHelper, TSignalsDispatcher, and related classes & functions - TProcessHelper for process related functions like forking (pcntl_fork) and isSystemWindows(). - TSignalsDispatcher for raising signal related global events from signals for multiple handlers per signal, alarm and disarm callbacks at a specific time (1 second precision), and for callbacks per child processes (proc_open, pcntl_fork, etc) when they end (or "stop and start"?). - ISingleton is a new interface for application singleton objects - TShellWriter is updated to use the TProcessHelper::isSystemWindows function -TComponent only hasMethod of global event handlers that do actually exist. As a global event it exists always, but not the method. - TEventSubscription example is updated to reflect TSignalsDispatcher and TSignalParameter - TApplicationSignals behavior for configuration and attaching of TSignalsDispatcher - TCaptureForkLog behavior for capturing the logs of forked child processes in the main log. Effectively a ForkLogRouter but registering as a behavior on TApplication. - TForkable behavior for attaching the owner's ::fxPrepareForFork() and ::fxRestoreAfterFork() handlers to their respective global events. -TGlobalClassAware behavior for attaching the owner's ::fxAttachClassBehavior() and ::fxDetachClassBehavior() to their respective global events so they change with global class changes without listening. - TProcessWindowsPriority specifies the numeric priorities that windows uses for each respective priority level. - TProcessWindowsPriorityName specifies the text priorities that windows uses for each respective priority level. --- HISTORY.md | 4 +- framework/Exceptions/messages/messages.txt | 9 + framework/ISingleton.php | 27 + framework/Shell/TShellWriter.php | 14 +- framework/TComponent.php | 4 +- framework/TEventSubscription.php | 5 +- .../Util/Behaviors/TApplicationSignals.php | 166 ++++ framework/Util/Behaviors/TCaptureForkLog.php | 304 +++++++ framework/Util/Behaviors/TForkable.php | 67 ++ .../Util/Behaviors/TGlobalClassAware.php | 63 ++ framework/Util/Helpers/TProcessHelper.php | 357 ++++++++ .../Util/Helpers/TProcessWindowsPriority.php | 28 + .../Helpers/TProcessWindowsPriorityName.php | 28 + framework/Util/TLogger.php | 18 +- framework/Util/TSignalParameter.php | 186 +++++ framework/Util/TSignalsDispatcher.php | 784 ++++++++++++++++++ framework/classes.php | 10 + tests/unit/TComponentTest.php | 4 +- .../Util/Behaviors/TApplicationSignalTest.php | 116 +++ .../TBehaviorParameterLoaderTest.php | 5 + .../Util/Behaviors/TCaptureForkLogTest.php | 66 ++ tests/unit/Util/Behaviors/TForkableTest.php | 115 +++ .../Util/Behaviors/TGlobalClassAwareTest.php | 44 + .../unit/Util/Helpers/TProcessHelperTest.php | 185 +++++ tests/unit/Util/TLoggerTest.php | 9 +- tests/unit/Util/TSignalParameterTest.php | 101 +++ tests/unit/Util/TSignalsDispatcherTest.php | 582 +++++++++++++ tests/unit/Util/TSysLogRouteTest.php | 3 +- 28 files changed, 3277 insertions(+), 27 deletions(-) create mode 100644 framework/ISingleton.php create mode 100644 framework/Util/Behaviors/TApplicationSignals.php create mode 100644 framework/Util/Behaviors/TCaptureForkLog.php create mode 100644 framework/Util/Behaviors/TForkable.php create mode 100644 framework/Util/Behaviors/TGlobalClassAware.php create mode 100644 framework/Util/Helpers/TProcessHelper.php create mode 100644 framework/Util/Helpers/TProcessWindowsPriority.php create mode 100644 framework/Util/Helpers/TProcessWindowsPriorityName.php create mode 100644 framework/Util/TSignalParameter.php create mode 100644 framework/Util/TSignalsDispatcher.php create mode 100644 tests/unit/Util/Behaviors/TApplicationSignalTest.php create mode 100644 tests/unit/Util/Behaviors/TCaptureForkLogTest.php create mode 100644 tests/unit/Util/Behaviors/TForkableTest.php create mode 100644 tests/unit/Util/Behaviors/TGlobalClassAwareTest.php create mode 100644 tests/unit/Util/Helpers/TProcessHelperTest.php create mode 100644 tests/unit/Util/TSignalParameterTest.php create mode 100644 tests/unit/Util/TSignalsDispatcherTest.php diff --git a/HISTORY.md b/HISTORY.md index 31fc021a8..94e1ac78d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,7 +13,9 @@ ENH: Issue #944 - TExitException for gracefully exiting the application anywhere ENH: Issue #979 - TComponent::raiseEvent optionally execute handlers in reverse; asa() and getBehaviors() searches for behaviors based on class after failing on name. (belisoful) ENH: Issue #975 - Prado base object methods for each log type and automatic discovery of calling object for log category. (belisoful) ENH: Issue #977 - THttpRequest::onResolveRequest for custom service resolution and TRequestConnectionUpgrade behavior for selecting service on http headers for "websocket". (belisoful) -ENH: Issue #982 General Logging update: Profiling, Flushing large logs for long running processes, optional Tracing, tracks PID for multi-threaded logging, TBrowserLogRoute colorizes the time delta, TDbLogRoute adds a new DB field 'prefix' and functions for getting the DB log, DB log count, and deleting DB logs, TDBLogRoute also adds a RetainPeriod for automatically removing old logs, Adds an event TLogger::OnFlushLogs and flushes as a register_shutdown_function, adds the TSysLogRoute, and adds unit tests for logging +ENH: Issue #982 - General Logging update: Profiling, Flushing large logs for long running processes, optional Tracing, tracks PID for multi-threaded logging, TBrowserLogRoute colorizes the time delta, TDbLogRoute adds a new DB field 'prefix' and functions for getting the DB log, DB log count, and deleting DB logs, TDBLogRoute also adds a RetainPeriod for automatically removing old logs, Adds an event TLogger::OnFlushLogs and flushes as a register_shutdown_function, adds the TSysLogRoute, and adds unit tests for logging. (belisoful) +ENH: Issue #984 - TEventSubscription for temporary event handlers. (belisoful) +ENG: Issue #972 - TProcessHelper (isSystemWindows, forking, kill, priority) and TSignalsDispatcher for delegating signals to respective global events, alarm interrupt callbacks at specific times, and per child PIDs callbacks. TEventSubscription can subscribe to a PHP process signal, an integer, as an event "name" (in TSignalsDispatcher). (belisoful) ## Version 4.2.2 - April 6, 2023 diff --git a/framework/Exceptions/messages/messages.txt b/framework/Exceptions/messages/messages.txt index bac8ec559..465cc7bf9 100644 --- a/framework/Exceptions/messages/messages.txt +++ b/framework/Exceptions/messages/messages.txt @@ -199,6 +199,15 @@ parameterizebehavior_cannot_set_defaultValue_after_attach = TParameterizeBehavio parameterizebehavior_cannot_set_localize_after_attach = TParameterizeBehavior cannot set Localize after being attached. parameterizebehavior_cannot_set_routeBehaviorName_after_attach = TParameterizeBehavior cannot set RouteBehaviorName after being attached. +processhelper_no_forking = Process Forking is not supported. +processhelper_no_signals = Process signals cannot be sent on Windows. + +appsignals_no_change = TApplicationSignals::{0} cannot be changed after attaching. +appsignals_not_a_dispatcher = '{0}' is not a TSignalsDispatcher. + +signalsdispatcher_bad_pid = '{0}' is not a valid PID. +signalsdispatcher_no_change = TSignalsDispatcher::{0} cannot be changed after attaching. + template_closingtag_unexpected = Unexpected closing tag '{0}' is found. template_closingtag_expected = Closing tag '{0}' is expected, found '{1}'. template_directive_nonunique = Directive '<%@ ... %>' must appear at the beginning of the template and can appear at most once. diff --git a/framework/ISingleton.php b/framework/ISingleton.php new file mode 100644 index 000000000..ebd9f0af7 --- /dev/null +++ b/framework/ISingleton.php @@ -0,0 +1,27 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado; + +/** + * ISingleton interface. + * + * This interface is for getting specific class (application) singletons. + * + * @author Brad Anderson + * @since 4.2.3 + */ +interface ISingleton +{ + /** + * @param bool $create Should the singleton be created if it doesn't exist. + * @return ?object The singleton instance of the class + */ + public static function singleton(bool $create = true): ?object; +} diff --git a/framework/Shell/TShellWriter.php b/framework/Shell/TShellWriter.php index 0a3375a05..8f551419a 100644 --- a/framework/Shell/TShellWriter.php +++ b/framework/Shell/TShellWriter.php @@ -10,6 +10,7 @@ namespace Prado\Shell; use Prado\TPropertyValue; +use Prado\Util\Helpers\TProcessHelper; /** * TShellWriter class. @@ -308,7 +309,7 @@ public function unformat($str) */ protected function isColorSupported() { - if (static::isRunningOnWindows()) { + if (TProcessHelper::isSystemWindows()) { return getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON'; } @@ -519,7 +520,7 @@ public static function getScreenSize($refresh = false) return $size; } - if (static::isRunningOnWindows()) { + if (TProcessHelper::isSystemWindows()) { $output = []; exec('mode con', $output); if (isset($output[1]) && strpos($output[1], 'CON') !== false) { @@ -595,13 +596,4 @@ public function wrapText($text, $indent = 0, $refresh = false) return implode("\n", $lines); } - - /** - * Returns true if the console is running on windows. - * @return bool - */ - public static function isRunningOnWindows() - { - return DIRECTORY_SEPARATOR === '\\'; - } } diff --git a/framework/TComponent.php b/framework/TComponent.php index c922d55ae..745eec722 100644 --- a/framework/TComponent.php +++ b/framework/TComponent.php @@ -5,7 +5,7 @@ * @author Qiang Xue * * Global Events, intra-object events, Class behaviors, expanded behaviors - * @author Brad Anderson + * @author Brad Anderson * * @author Qiang Xue * @link https://github.com/pradosoft/prado @@ -1136,7 +1136,7 @@ protected function getCallChain($method, ...$args): ?TCallChain */ public function hasMethod($name) { - if (Prado::method_visible($this, $name) || strncasecmp($name, 'fx', 2) === 0 || strncasecmp($name, 'dy', 2) === 0) { + if (Prado::method_visible($this, $name) || strncasecmp($name, 'dy', 2) === 0) { return true; } elseif ($this->_m !== null && $this->getBehaviorsEnabled()) { foreach ($this->_m->toArray() as $behavior) { diff --git a/framework/TEventSubscription.php b/framework/TEventSubscription.php index 0c4294cd4..8843b31c5 100644 --- a/framework/TEventSubscription.php +++ b/framework/TEventSubscription.php @@ -24,7 +24,10 @@ * { * $exitLoop = false; * $subscription = new TEventSubscription($dispatcher, 'fxSignalInterrupt', - * function ($sender, $param) use (&$exitLoop){$exitLoop = true;}); + * function ($sender, $param) use (&$exitLoop){ + * $exitLoop = true; + * $param->setExit(false); + * }); * ... * } // dereference unsubscribes * ``` diff --git a/framework/Util/Behaviors/TApplicationSignals.php b/framework/Util/Behaviors/TApplicationSignals.php new file mode 100644 index 000000000..1bcffb768 --- /dev/null +++ b/framework/Util/Behaviors/TApplicationSignals.php @@ -0,0 +1,166 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util\Behaviors; + +use Prado\Exceptions\TInvalidDataValueException; +use Prado\Exceptions\TInvalidOperationException; +use Prado\TComponent; +use Prado\TPropertyValue; +use Prado\Util\TBehavior; +use Prado\Util\TSignalsDispatcher; + +/** + * TApplicationSignals class. + * + * This behavior installs the {@see \Prado\Util\TSignalsDispatcher} (or subclass) for + * the application when PHP pcntl_* is available. The signals dispatcher class can + * be specified with {@see self::setSignalsClass()} and is installed when the TApplicationSignals + * behavior is attached to the TApplication owner. + * + * There is a TSignalsDispatcher getter {@see self::getSignalsDispatcher} added to + * the owner (TApplication) for retrieving the dispatcher. + * + * There are two properties of TApplicationSignals for TSignalsDispatcher. {@see + * self::setAsyncSignals} changes how signals are handled. When synchronous, + * {@see \Prado\Util\TSignalsDispatcher::syncDispatch()} must be called for signals + * to be processed. When asynchronous, the signals will be handled by atomic interrupt. + * + * ```xml + * + * ``` + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TApplicationSignals extends TBehavior +{ + /** @var string the signals class. */ + protected ?string $_signalsClass = null; + + /** + * Attaches the TSignalsDispatcher to handle the process signals. + * @param TComponent $component The owner. + * @return bool Should the behavior's event handlers be attached. + */ + protected function attachEventHandlers(TComponent $component): bool + { + if ($return = parent::attachEventHandlers($component)) { + ($this->getSignalsClass())::singleton(); + } + return $return; + } + + /** + * Detaches the TSignalsDispatcher from handling the process signals. + * @param TComponent $component The owner. + * @return bool Should the behavior's event handlers be detached. + */ + protected function detachEventHandlers(TComponent $component): bool + { + if ($return = parent::detachEventHandlers($component)) { + if ($dispatcher = ($this->getSignalsClass())::singleton(false)) { + $dispatcher->detach(); + unset($dispatcher); + } + } + return $return; + } + + /** + * @return ?object The Signal Dispatcher. + */ + public function getSignalsDispatcher(): ?object + { + return ($this->getSignalsClass())::singleton(false); + } + + /** + * @return ?string The class of the Signals Dispatcher. + */ + public function getSignalsClass(): ?string + { + if ($this->_signalsClass === null) { + $this->_signalsClass = TSignalsDispatcher::class; + } + + return $this->_signalsClass; + } + + /** + * @param ?string $value The class of the Signals Dispatcher. + * @throws TInvalidOperationException When already attached, this cannot be changed. + * @throws TInvalidDataValueException When the class is not a TSignalsDispatcher. + * @return static The current object. + */ + public function setSignalsClass($value): static + { + if ($this->getOwner()) { + throw new TInvalidOperationException('appsignals_no_change', 'SignalsClass'); + } + if ($value === '') { + $value = null; + } + + if ($value !== null) { + $value = TPropertyValue::ensureString($value); + if (!is_a($value, TSignalsDispatcher::class, true)) { + throw new TInvalidDataValueException('appsignals_not_a_dispatcher', $value); + } + } + + $this->_signalsClass = $value; + + return $this; + } + + /** + * @return bool Is the system executing signal handlers asynchronously. + */ + public function getAsyncSignals(): bool + { + return ($this->getSignalsClass())::getAsyncSignals(); + } + + /** + * @param mixed $value Set the system to execute signal handlers asynchronously (or synchronously on false). + * @return bool Was the system executing signal handlers asynchronously. + */ + public function setAsyncSignals($value): bool + { + return ($this->getSignalsClass())::setAsyncSignals(TPropertyValue::ensureBoolean($value)); + } + + /** + * When the original signal handlers are placed into the Signals Events this is the + * priority of original signal handlers. + * @return ?float The priority of the signal handlers that were installed before + * the TSignalsDispatcher attaches. + */ + public function getPriorHandlerPriority(): ?float + { + return ($this->getSignalsClass())::getPriorHandlerPriority(); + } + + /** + * @param null|float|string $value The priority of the signal handlers that were installed before + * the TSignalsDispatcher attaches. + * @return bool Is the Prior Handler Priority changed. + */ + public function setPriorHandlerPriority($value): bool + { + if ($value === '') { + $value = null; + } + if ($value !== null) { + $value = TPropertyValue::ensureFloat($value); + } + return ($this->getSignalsClass())::setPriorHandlerPriority($value); + } +} diff --git a/framework/Util/Behaviors/TCaptureForkLog.php b/framework/Util/Behaviors/TCaptureForkLog.php new file mode 100644 index 000000000..850efd284 --- /dev/null +++ b/framework/Util/Behaviors/TCaptureForkLog.php @@ -0,0 +1,304 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util\Behaviors; + +use Prado\Prado; +use Prado\Util\{TBehavior, IBaseBehavior}; +use Prado\Util\TCallChain; +use Prado\Util\TLogger; +use Prado\Util\Helpers\TProcessHelper; + +/** + * TCaptureForkLog class. + * + * This captures the log of a child fork and sends it back to the parent thread. + * + * When {@see \Prado\Util\TProcessHelper::fork()} is called, `fxPrepareForFork` + * is raised before the fore and `fxRestoreAfterFork` after the fork. + * On `fxPrepareForFork`, this class creates a socket pair. On `fxRestoreAfterFork`, + * The parent stores all child connections, and the child sends all log data to + * the parent. The parent receives logs when processing logs. + * + * Before sending logs, a child will receive any logs for its own child processes. + * When sending the final logs from the child, the socket is closed and the parent + * is flagged to close the connection. + * + * When not the final processing of the logs, only the pending logs will be sent + * and received before returning. When Final, the parent will wait until all children + * processes have returned their logs. + * + * Due to this class adding logs during the flushing of the logs + * + * Attach this behavior to the TApplication class or object. + * ```xml + * + * ``` + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TCaptureForkLog extends \Prado\Util\TBehavior +{ + public const BEHAVIOR_NAME = 'captureforklog'; + + /** @var bool Is the master receiver (of child logs) installed. */ + private bool $_receiverInstalled = false; + + /** @var ?array The parent connections to each child fork receiving logs from. */ + protected ?array $_parentConnections = []; + + /** @var mixed The child connection to the parent */ + protected mixed $_childConnection = null; + + /** + * Installs {@see self::generateConnection()} on fxPrepareForFork and + * {@see self::configureForChildLogs()} on fxRestoreAfterFork. + * @return array Event callbacks for the behavior. + */ + public function events() + { + return [TProcessHelper::FX_PREPARE_FOR_FORK => 'generateConnection', + TProcessHelper::FX_RESTORE_AFTER_FORK => 'configureForChildLogs']; + } + + /** + * + * @return ?float The priority of the behavior, default -10 and not + * the normal "null". + */ + public function getPriority(): ?float + { + if (($priority = parent::getPriority()) === null) { + $priority = -10; + } + return $priority; + } + + /** + * This is the singleton behavior. Only one instance of itself can be a behavior + * of the owner. + * @param string $name The name of teh behavior being added to the owner. + * @param IBaseBehavior $behavior The behavior being added to the owner. + * @param TCallChain $chain The chain of event handlers. + */ + public function dyAttachBehavior($name, $behavior, TCallChain $chain) + { + $owner = $this->getOwner(); + if (count($owner->getBehaviors(self::class)) > 1) { + $owner->detachBehavior($name); + } + return $chain->dyAttachBehavior($name, $behavior); + } + + /** + * The behavior callback for fxPrepareForFork that creates a socket pair connection + * between the parent process and child process before forking. + * @param mixed $sender The TApplication doing the fork. + * @param mixed $param The parameter of fxPrepareForFork. + * @return ?array Any data to be passed back to the restore function. + * eg. `return ['key', 'data'];` will be passed in the restore function as + * `['key' => 'data', 'pid' => ###, ...]`. + */ + public function generateConnection(mixed $sender, mixed $param) + { + $domain = TProcessHelper::isSystemWindows() ? AF_INET : AF_UNIX; + if (!socket_create_pair($domain, SOCK_STREAM, 0, $this->_childConnection)) { + $this->_childConnection = null; + return; + } + $this->_childConnection[0] = socket_export_stream($this->_childConnection[0]); + $this->_childConnection[1] = socket_export_stream($this->_childConnection[1]); + return null; + } + + /** + * The behavior call back for fxRestoreAfterFork that cleans the log and resets + * the logger to send the log to the parent process. The Parent process stores + * the child stream and pid and installs the onEndRequest handler to receive the + * logs from the children forks. + * @param mixed $sender + * @param array $data the + */ + public function configureForChildLogs(mixed $sender, mixed $data) + { + if (!$this->_childConnection) { + return; + } + $pid = $data['pid']; + if ($pid === -1) { //fail + if ($this->_childConnection) { + stream_socket_shutdown($this->_childConnection[0], STREAM_SHUT_RDWR); + stream_socket_shutdown($this->_childConnection[1], STREAM_SHUT_RDWR); + $this->_childConnection = null; + } + } elseif ($pid === 0) { // Child Process + $this->_parentConnections = []; + $this->_childConnection = $this->_childConnection[1]; + $logger = Prado::getLogger(); + $logger->deleteLogs(); + $logs = $logger->deleteProfileLogs(); + $pid = getmypid(); + foreach(array_keys($logs) as $key) { // Reset PROFILE_BEGIN to pid. + $logs[$key][TLogger::LOG_LEVEL] &= ~TLogger::LOGGED; + $logs[$key][TLogger::LOG_PID] = $pid; + } + $logger->mergeLogs($logs); // Profiler logs with child pid + $logger->getEventHandlers('onFlushLogs')->clear(); + $logger->attachEventHandler('onFlushLogs', [$this, 'sendLogsToParent'], $this->getPriority()); + $this->_receiverInstalled = true; + } else { // Parent Process + $this->_parentConnections[$pid] = $this->_childConnection[0]; + $this->_childConnection = null; + if (!$this->_receiverInstalled) { + $logger = Prado::getLogger(); + $logger->attachEventHandler('onCollectLogs', [$this, 'receiveLogsFromChildren'], $this->getPriority()); + $this->_receiverInstalled = true; + } + } + } + + /** + * Receives logs from children forked processes and merges the logs with the current + * application TLogger. + * @param ?int $pid The process ID to receive the logs from, default null for all. + * @param bool $wait Wait for results until complete, default true. When false, + * this will process the pending logs and not wait for further logs. + */ + public function receiveLogsFromChildren(?int $pid = null, bool $wait = true) + { + if (!$this->_parentConnections) { + return; + } + if($pid && !isset($this->_parentConnections[$pid])) { + return; + } + + $completeLogs = []; + $write = $except = []; + $connections = $this->_parentConnections; + $childLogs = []; + do { + $read = $connections; + if (stream_select($read, $write, $except, ($wait || count($childLogs)) ? 1 : 0, 0)) { + foreach($read as $pid => $socket) { + $data = fread($socket, 8192); + do { + $iterate = false; + if($data !== false) { + if (array_key_exists($pid, $childLogs)) { + $childLogs[$pid][0] .= $data; + } else { + $length = substr($data, 0, 4); + if (strlen($length) >= 4) { + $length = unpack('N', $length)[1]; + $final = ($length & 0x80000000) != 0; + $length &= 0x7FFFFFFF; + if ($length) { + $data = substr($data, 4); + $childLogs[$pid] = [$data, abs($length), $final]; + } else { + $childLogs[$pid] = ['', 0, $final]; + } + } else { + $childLogs[$pid] = ['', 0, true]; + } + } + } else { + $childLogs[$pid] = ['', 0, true]; + } + if (isset($childLogs[$pid]) && strlen($childLogs[$pid][0]) >= $childLogs[$pid][1]) { + if ($childLogs[$pid][2]) { + stream_socket_shutdown($socket, STREAM_SHUT_RDWR); + unset($this->_parentConnections[$pid]); + unset($connections[$pid]); + } + if ($childLogs[$pid][1]) { + $completeLogs[$pid][] = substr($childLogs[$pid][0], 0, $childLogs[$pid][1]); + } + $data = substr($childLogs[$pid][0], $childLogs[$pid][1]); + unset($childLogs[$pid]); + if (!strlen($data)) { + $data = false; + } else { + $iterate = true; + } + } + + } while ($iterate); + } + } + } while(count($childLogs) || $wait && ($pid && isset($connections[$pid]) || $pid === null && $connections)); + + if (!$completeLogs) { + return; + } + foreach(array_merge(...$completeLogs) as $pid => $logs) { + Prado::getLogger()->mergeLogs(unserialize($logs)); + } + } + + /** + * First, Receives any logs from the children. If this instance is a child fork + * then send the log to the parent process. If the call is final then the connection + * to the parent is shutdown. + * @param mixed $logger The TLogger that raised the onFlushLogs event. + * @param mixed $final Is this the last and final call. + */ + public function sendLogsToParent($logger, $final) + { + if (!$this->_childConnection) { + return; + } + + if(!($logger instanceof TLogger)) { + $logger = Prado::getLogger(); + } + + $logs = $logger->getLogs(); + + { // clear logs already logged. + $reset = false; + foreach ($logs as $key => $log) { + if ($log[TLogger::LOG_LEVEL] & TLogger::LOGGED) { + unset($logs[$key]); + $reset = true; + } + } + if ($reset) { + $logs = array_values($logs); + } + } + + $data = serialize($logs); + + if (!$logs) { + $data = ''; + } + + $data = pack('N', ($final ? 0x80000000 : 0) | ($length = strlen($data))) . $data; + $count = null; + $read = $except = []; + + do { + $write = [$this->_childConnection]; + if (stream_select($read, $write, $except, 1, 0)) { + $count = fwrite($this->_childConnection, $data); + if ($count > 0) { + $data = substr($data, $count); + } + } + } while($count !== false && strlen($data) > 0); + + if ($final) { + stream_socket_shutdown($this->_childConnection, STREAM_SHUT_RDWR); + $this->_childConnection = null; + } + } +} diff --git a/framework/Util/Behaviors/TForkable.php b/framework/Util/Behaviors/TForkable.php new file mode 100644 index 000000000..3e30fa5b1 --- /dev/null +++ b/framework/Util/Behaviors/TForkable.php @@ -0,0 +1,67 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util\Behaviors; + +use Prado\TComponent; +use Prado\Util\Helpers\TProcessHelper; + +/** + * TForkable class. + * + * This attaches the Owner Component `fxPrepareForFork` and `fxRestoreAfterFork` + * methods as the event handlers for the PRADO global event `fxPrepareForFork` and + * `fxRestoreAfterFork`, respectively. + * + * This should only be used when the TComponent is not {@see \Prado\TComponent::listen()}ing + * or the handlers will be double added. + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TForkable extends \Prado\Util\TBehavior +{ + private int $_methods = 0; + + /** + * Attaches the component event handlers: + * {@see \Prado\TComponent::fxAttachClassBehavior()} and {@see \Prado\TComponent::fxDetachClassBehavior()} + * @param TComponent $component + * @return bool Attaching handlers. + */ + protected function attachEventHandlers(TComponent $component): bool + { + if ($return = parent::attachEventHandlers($component)) { + if ($component->hasMethod(TProcessHelper::FX_PREPARE_FOR_FORK)) { + $this->_methods |= 1; + $component->attachEventHandler(TProcessHelper::FX_PREPARE_FOR_FORK, [$component, TProcessHelper::FX_PREPARE_FOR_FORK], $this->getPriority()); + } + if ($component->hasMethod(TProcessHelper::FX_RESTORE_AFTER_FORK)) { + $this->_methods |= 2; + $component->attachEventHandler(TProcessHelper::FX_RESTORE_AFTER_FORK, [$component, TProcessHelper::FX_RESTORE_AFTER_FORK], $this->getPriority()); + } + } + return $return; + } + + protected function detachEventHandlers(TComponent $component): bool + { + if ($return = parent::detachEventHandlers($component)) { + if ($this->_methods & 1) { + $component->detachEventHandler(TProcessHelper::FX_PREPARE_FOR_FORK, [$component, TProcessHelper::FX_PREPARE_FOR_FORK]); + } + + if ($this->_methods & 2) { + $component->detachEventHandler(TProcessHelper::FX_RESTORE_AFTER_FORK, [$component, TProcessHelper::FX_RESTORE_AFTER_FORK]); + } + $this->_methods = 0; + } + return $return; + } +} diff --git a/framework/Util/Behaviors/TGlobalClassAware.php b/framework/Util/Behaviors/TGlobalClassAware.php new file mode 100644 index 000000000..65693d785 --- /dev/null +++ b/framework/Util/Behaviors/TGlobalClassAware.php @@ -0,0 +1,63 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util\Behaviors; + +use Prado\TComponent; + +/** + * TGlobalClassAware class. + * + * This behavior registers the handlers `fxAttachClassBehavior` and `fxDetachClassBehavior` + * of the owner to listen for dynamic changes to its class behaviors (after it is + * instanced). + * + * This should only be used when the TComponent is not {@see \Prado\TComponent::listen()}ing + * or the handlers will be double added. Listening is turned on automatically with + * {@see \Prado\TComponent::getAutoGlobalListen()} (returning true) for select classes. + * + * Without this behavior (or listen), an instanced TComponent will not update its + * class behaviors when there is a change in the global class behaviors. + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TGlobalClassAware extends \Prado\Util\TBehavior +{ + /** + * Attaches the component event handlers: + * {@see \Prado\TComponent::fxAttachClassBehavior()} and {@see \Prado\TComponent::fxDetachClassBehavior()} + * @param TComponent $component + * @return bool Attaching handlers. + */ + protected function attachEventHandlers(TComponent $component): bool + { + if ($return = parent::attachEventHandlers($component)) { + $component->attachEventHandler('fxAttachClassBehavior', [$component, 'fxAttachClassBehavior'], $this->getPriority()); + $component->attachEventHandler('fxDetachClassBehavior', [$component, 'fxDetachClassBehavior'], $this->getPriority()); + } + return $return; + } + + /** + * Detaches the component event handlers: + * {@see \Prado\TComponent::fxAttachClassBehavior()} and {@see \Prado\TComponent::fxDetachClassBehavior()} + * @param TComponent $component + * @return bool Detaching handlers. + */ + protected function detachEventHandlers(TComponent $component): bool + { + if ($return = parent::detachEventHandlers($component)) { + $component->detachEventHandler('fxAttachClassBehavior', [$component, 'fxAttachClassBehavior']); + $component->detachEventHandler('fxDetachClassBehavior', [$component, 'fxDetachClassBehavior']); + } + return $return; + } +} diff --git a/framework/Util/Helpers/TProcessHelper.php b/framework/Util/Helpers/TProcessHelper.php new file mode 100644 index 000000000..db87b8f35 --- /dev/null +++ b/framework/Util/Helpers/TProcessHelper.php @@ -0,0 +1,357 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util\Helpers; + +use Prado\Exceptions\TNotSupportedException; +use Prado\Prado; +use Prado\TComponent; +use Prado\TEventParameter; +use Prado\Util\Behaviors\TCaptureForkLog; +use Prado\Util\TSignalsDispatcher; + +/** + * TProcessHelper class. + * + * This class handles process related functions. + * + * {@see self::isSystemWindows()} is used to determine if the PHP system is Windows + * or not. + * + * {@see self::isForkable()} can be used if the system supports forking of the current + * process. {@see self::fork()} is used to fork the current process, where supported. + * When forking, `fxPrepareForFork` {@see self::FX_PREPARE_FOR_FORK} is raised before + * forking and `fxRestoreAfterFork` {@see self::FX_RESTORE_AFTER_FORK} is raised after + * forking. When $captureForkLog (fork parameter) is true, a {@see \Prado\Util\Behaviors\TCaptureForkLog} + * behavior is attached to the {@see \Prado\TApplication} object. All forked child + * processes ensure the {@see \Prado\Util\TSignalsDispatcher} behavior is attached + * to the TApplication object to allow for graceful termination on exiting signals. + * + * When filtering commands for popen and proc_open, {@see self::filterCommand()} will + * replace '@php' with PHP_BINARY and wrap Windows commands with double quotes. + * Individual arguments can be properly shell escaped with {@see self::escapeShellArg()}. + * + * Linux Process signals can be sent with {@see self::sendSignal()} to the current + * pid or child pid. To kill a child pid, call {@see self::kill()}. {@see self::isRunning} + * can determine if a child process is still running. + * + * System Process priority can be retrieved and set with {@see self::getProcessPriority()} + * and {@see self::setProcessPriority()}, respectively. + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TProcessHelper +{ + /** @var string When running a Pipe or Process, this is replaced with PHP_BINARY */ + public const PHP_COMMAND = "@php"; + + /** @var string The global event prior to forking a process. */ + public const FX_PREPARE_FOR_FORK = 'fxPrepareForFork'; + + /** @var string The global event after forking a process. */ + public const FX_RESTORE_AFTER_FORK = 'fxRestoreAfterFork'; + + /** + * The WINDOWS_*_PRIORITY is what the windows priority would map into the PRADO + * and linux priority numbering. Windows will only have these priorities. + */ + public const WINDOWS_IDLE_PRIORITY = 20; + public const WINDOWS_BELOW_NORMAL_PRIORITY = 8; + public const WINDOWS_NORMAL_PRIORITY = 0; + public const WINDOWS_ABOVE_NORMAL_PRIORITY = -5; + public const WINDOWS_HIGH_PRIORITY = -10; + public const WINDOWS_REALTIME_PRIORITY = -17; + + /** + * Checks if the system that PHP is run on is Windows. + * @return bool Is the system Windows. + */ + public static function isSystemWindows(): bool + { + static $isWindows = null; + if ($isWindows === null) { + $isWindows = strncasecmp(php_uname('s'), 'win', 3) === 0; + } + return $isWindows; + } + + /** + * @return bool Can PHP fork the process. + */ + public static function isForkable(): bool + { + return function_exists('pcntl_fork'); + } + + /** + * This forks the current process. When specified, it will install a {@see \Prado\Util\Behaviors\TCaptureForkLog}. + * Before forking, `fxPrepareForFork` is raised and after forking `fxRestoreAfterFork` is raised. + * + * `fxPrepareForFork` handlers should return null or an array of data that it will + * receive in `fxRestoreAfterFork`. + * ```php + * public function fxPrepareForFork ($sender, $param) { + * return ['mydata' => 'value']; + * } + * + * public function fxRestoreAfterFork ($sender, $param) { + * $param['mydata'] === 'value'; + * $param['pid']; + * } + * ``` + * @param bool $captureForkLog Installs {@see \Prado\Util\Behaviors\TCaptureForkLog} behavior on the application + * so the fork log is stored by the forking process. Default false. + * @throws TNotSupportedException When PHP Forking `pcntl_fork` is not supporting + * @return int The Child Process ID. For children processes, they receive 0. Failure is -1. + */ + public static function fork(bool $captureForkLog = false): int + { + if (!static::isForkable()) { + throw new TNotSupportedException('processhelper_no_forking'); + } + $app = Prado::getApplication(); + if ($captureForkLog && !$app->asa(TCaptureForkLog::class)) { + $app->attachBehavior(TCaptureForkLog::BEHAVIOR_NAME, TCaptureForkLog::class); + } + $responses = $app->raiseEvent(static::FX_PREPARE_FOR_FORK, $app, null); + $restore = array_merge(...$responses); + $restore['pid'] = $pid = pcntl_fork(); + $app->raiseEvent(static::FX_RESTORE_AFTER_FORK, $app, $restore); + if ($pid > 0) { + Prado::info("Fork child: $pid", static::class); + } elseif ($pid === 0) { + Prado::info("Executing child fork", static::class); + TSignalsDispatcher::singleton(); + } elseif ($pid === -1) { + Prado::notice("failed fork", static::class); + } + return $pid; + } + + /** + * If the exitCode is an exit code, returns the exit Status. + * @param int $exitCode + * @return int The exit Status + */ + public static function exitStatus(int $exitCode): int + { + if (function_exists('pcntl_wifexited') && pcntl_wifexited($exitCode)) { + $exitCode = pcntl_wexitstatus($exitCode); + } + return $exitCode; + } + + /** + * Filters a {@see popen} or {@see proc_open} command. + * The string "@php" is replaced by {@see PHP_BINARY} and in Windows the string + * is surrounded by double quotes. + * + * @param mixed $command + */ + public static function filterCommand($command) + { + $command = str_replace(static::PHP_COMMAND, PHP_BINARY, $command); + + if (TProcessHelper::isSystemWindows()) { + if (is_string($command)) { + $command = '"' . $command . '"'; //Windows, better command support + } + } + return $command; + } + + /** + * Sends a process signal on posix or linux systems. + * @param int $signal The signal to be sent. + * @param ?int $pid The process to send the signal, default null for the current + * process. + * @throws TNotSupportedException When running on Windows. + */ + public static function sendSignal(int $signal, ?int $pid = null): bool + { + if (static::isSystemWindows()) { + throw new TNotSupportedException('processhelper_no_signals'); + } + if ($pid === null) { + $pid = getmypid(); + } + if (function_exists("posix_kill")) { + return posix_kill($pid, $signal); + } + exec("/usr/bin/kill -s $signal $pid 2>&1", $output, $return_code); + return !$return_code; + } + + /** + * Kills a process. + * @param int $pid The PID to kill. + * @return bool Was the signal successfully sent. + */ + public static function kill(int $pid): bool + { + if (static::isSystemWindows()) { + return shell_exec("taskkill /F /PID $pid") !== null; + } + return static::sendSignal(SIGKILL, $pid); + } + + /** + * @param int $pid The Process ID to check if it is running. + * @return bool Is the PID running. + */ + public static function isRunning(int $pid): bool + { + if (static::isSystemWindows()) { + $out = []; + exec("TASKLIST /FO LIST /FI \"PID eq $pid\"", $out); + return count($out) > 1; + } + + return static::sendSignal(0, $pid); + } + + /** + * @param ?int $pid The process id to get the priority of, default null for current + * process. + * @return ?int The priority of the process. + */ + public static function getProcessPriority(?int $pid = null): ?int + { + if ($pid === null) { + $pid = getmypid(); + } + if (static::isSystemWindows()) { + $output = shell_exec("wmic process where ProcessId={$pid} get priority"); + preg_match('/^\s*Priority\s*\r?\n\s*(\d+)/m', $output, $matches); + if (isset($matches[1])) { + $priorityValues = [ // Map Windows Priority Numbers to Linux style Numbers + TProcessWindowsPriority::Idle => static::WINDOWS_IDLE_PRIORITY, + TProcessWindowsPriority::BelowNormal => static::WINDOWS_BELOW_NORMAL_PRIORITY, + TProcessWindowsPriority::Normal => static::WINDOWS_NORMAL_PRIORITY, + TProcessWindowsPriority::AboveNormal => static::WINDOWS_ABOVE_NORMAL_PRIORITY, + TProcessWindowsPriority::HighPriority => static::WINDOWS_HIGH_PRIORITY, + TProcessWindowsPriority::Realtime => static::WINDOWS_REALTIME_PRIORITY, + ]; + return $priorityValues[$matches[1]] ?? null; + } else { + return false; + } + } else { + if (strlen($priority = trim(shell_exec('exec ps -o nice= -p ' . $pid)))) { + return (int) $priority; + } + return null; + } + } + + /** + * In linux systems, the priority can only go up (and have less priority). + * @param int $priority The priority of the PID. + * @param ?int $pid The PID to change the priority, default null for current process. + * @return bool Was successful. + */ + public static function setProcessPriority(int $priority, ?int $pid = null): bool + { + if ($pid === null) { + $pid = getmypid(); + } + if (static::isSystemWindows()) { + $priorityValues = [ // The priority cap to windows text priority. + -15 => TProcessWindowsPriorityName::Realtime, + -10 => TProcessWindowsPriorityName::HighPriority, + -5 => TProcessWindowsPriorityName::AboveNormal, + 4 => TProcessWindowsPriorityName::Normal, + 9 => TProcessWindowsPriorityName::BelowNormal, + PHP_INT_MAX => TProcessWindowsPriorityName::Idle, + ]; + foreach($priorityValues as $keyPriority => $priorityName) { + if ($priority <= $keyPriority) { + break; + } + } + $command = "wmic process where ProcessId={$pid} CALL setpriority \"$priorityName\""; + $result = shell_exec($command); + if (strpos($result, 'successful') !== false) { + return true; + } + if (!preg_match('/ReturnValue\s*=\s*(\d+);/m', $result, $matches)) { + return false; + } + return $matches[1] === 0; + } else { + if (($pp = static::getProcessPriority($pid)) === null) { + return false; + } + $priority -= $pp; + $result = shell_exec("exec renice -n $priority -p $pid"); + if (is_string($result) && strlen($result) > 1) { + return false; + } + return true; + } + } + + /** + * Escapes a string to be used as a shell argument. + * @param string $argument + * @return string + */ + public static function escapeShellArg(string $argument): string + { + // Fix for PHP bug #43784 escapeshellarg removes % from given string + // Fix for PHP bug #49446 escapeshellarg doesn't work on Windows + // @see https://bugs.php.net/bug.php?id=43784 + // @see https://bugs.php.net/bug.php?id=49446 + if (static::isSystemWindows()) { + if ($argument === '') { + return '""'; + } + + $escapedArgument = ''; + $addQuote = false; + + foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ($part === '"') { + $escapedArgument .= '\\"'; + } elseif (static::isSurroundedBy($part, '%')) { + // environment variables + $escapedArgument .= '^%"' . substr($part, 1, -1) . '"^%'; + } else { + // escape trailing backslash + if (str_ends_with($part, '\\')) { + $part .= '\\'; + } + $addQuote = true; + $escapedArgument .= $part; + } + } + + if ($addQuote) { + $escapedArgument = '"' . $escapedArgument . '"'; + } + + return $escapedArgument; + } + + return "'" . str_replace("'", "'\\''", $argument) . "'"; + } + + /** + * Is the string surrounded by the prefix and reversed in appendix. + * @param string $string + * @param string $prefix + * @return bool Is the string surrounded by the string + */ + public static function isSurroundedBy(string $string, string $prefix): bool + { + $len = strlen($prefix); + return strlen($string) >= 2 * $len && str_starts_with($string, $prefix) && str_ends_with($string, strrev($prefix)); + } +} diff --git a/framework/Util/Helpers/TProcessWindowsPriority.php b/framework/Util/Helpers/TProcessWindowsPriority.php new file mode 100644 index 000000000..f63e5c3bf --- /dev/null +++ b/framework/Util/Helpers/TProcessWindowsPriority.php @@ -0,0 +1,28 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util\Helpers; + +/** + * TProcessWindowsPriority class + * + * Windows indicates process priority with these specified priority numbers. + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TProcessWindowsPriority extends \Prado\TEnumerable +{ + public const Idle = 4; + public const BelowNormal = 6; + public const Normal = 8; + public const AboveNormal = 10; + public const HighPriority = 13; + public const Realtime = 24; +} diff --git a/framework/Util/Helpers/TProcessWindowsPriorityName.php b/framework/Util/Helpers/TProcessWindowsPriorityName.php new file mode 100644 index 000000000..094b4295c --- /dev/null +++ b/framework/Util/Helpers/TProcessWindowsPriorityName.php @@ -0,0 +1,28 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util\Helpers; + +/** + * TProcessWindowsPriorityName class + * + * Windows uses these priority names to specify the priorities of processes. + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TProcessWindowsPriorityName extends \Prado\TEnumerable +{ + public const Idle = 'idle'; + public const BelowNormal = 'below normal'; + public const Normal = 'normal'; + public const AboveNormal = 'above normal'; + public const HighPriority = 'high priority'; + public const Realtime = 'realtime'; +} diff --git a/framework/Util/TLogger.php b/framework/Util/TLogger.php index 90a208834..846c43087 100644 --- a/framework/Util/TLogger.php +++ b/framework/Util/TLogger.php @@ -248,7 +248,6 @@ protected function ensureFlushing() if ($app = Prado::getApplication()) { $app->attachEventHandler('onEndRequest', [$this, 'onFlushLogs'], 20); - //$app->attachEventHandler(TProcessHelper::FX_TERMINATE_PROCESS, [$this, 'onFlushLogs'], 20); $this->_registered = true; } } @@ -384,6 +383,17 @@ public function log(mixed $token, int $level, ?string $category = null, mixed $c } + /** + * This event collects any logs from other aspects of the application that may + * not be able to directly log to the TLogger. + * @param bool $final Is the final collection, default false. + * @since 4.2.3 + */ + public function onCollectLogs(bool $final = false) + { + return $this->raiseEvent('onCollectLogs', $this, $final); + } + /** * This is an Event and an event handler. When {@see Application::onEndRequest()} * is raised and as a `register_shutdown_function` function, this is called. @@ -395,7 +405,7 @@ public function log(mixed $token, int $level, ?string $category = null, mixed $c */ public function onFlushLogs(mixed $sender = null, mixed $final = null) { - if ($this->_flushing || !$final && !count($this->_logs)) { + if ($this->_flushing) { return; } @@ -403,12 +413,10 @@ public function onFlushLogs(mixed $sender = null, mixed $final = null) $final = $sender; $sender = null; } - //if (($final instanceof TEventParameter) && $final->getEventName() === TProcessHelper::FX_TERMINATE_PROCESS) { - // $final = true; - //} else if (!is_bool($final)) { $final = false; } + $this->onCollectLogs($final); $this->_flushing = true; $this->_flushingLog = []; diff --git a/framework/Util/TSignalParameter.php b/framework/Util/TSignalParameter.php new file mode 100644 index 000000000..ff71a5d33 --- /dev/null +++ b/framework/Util/TSignalParameter.php @@ -0,0 +1,186 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util; + +/** + * TSignalParameter class. + * + * This is the parameter for signal events. The {@see \Prado\TEventParameter::getParameter()} + * property is the array of $signalInfo from the Signal handler {@see \Prado\Util\TSignalsDispatcher::__invoke()}; + * when available on the PHP system. + * + * There is also the Signal {@see self::getSignal()}, whether to exit {@see self::getIsExit()}, + * the exit code when exiting {@see self::getExitCode()}, and the alarm time {@see self::getAlarmTime()} + * if the signal is SIGALRM. + * + * When there is Signal Info in the Parameter property, there are inspection methods + * {@see self::getParameterErrorNumber()} for accessing the Signal Error Number, + * {@see self::getParameterCode()} for accessing the Signal Code, {@see self::getParameterStatus()} + * for accessing the Signal Status, {@see self::getParameterPID()} for accessing + * the Signal PID, and {@see self::getParameterUID()} for accessing the Signal PID UID. + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TSignalParameter extends \Prado\TEventParameter +{ + /** @var int Signal being sent. */ + private int $_signal; + + /** @var bool Should the Signal exit. */ + private bool $_isExit; + + /** @var int The exit code when exiting. */ + private int $_exitCode; + + /** @var ?int The time of the alarm signal. */ + private ?int $_alarmTime; + + /** + * Constructor. + * @param mixed $parameter parameter of the event + * @param bool $isExiting + * @param int $exitCode + * @param int $signal + */ + public function __construct(int $signal = 0, bool $isExiting = false, int $exitCode = 0, mixed $parameter = null) + { + $this->_signal = $signal; + $this->_isExit = $isExiting; + $this->_exitCode = $exitCode; + parent::__construct($parameter); + } + + /** + * @return int The signal being raised. + */ + public function getSignal(): int + { + return $this->_signal; + } + + /** + * @param int $value The signal being raised. + * @return static The current object. + */ + public function setSignal(int $value): static + { + $this->_signal = $value; + + return $this; + } + + /** + * @return bool Should the signal exit. + */ + public function getIsExiting(): bool + { + return $this->_isExit; + } + + /** + * @param bool $value Should the signal exit. + * @return static The current object. + */ + public function setIsExiting(bool $value): static + { + $this->_isExit = $value; + + return $this; + } + + /** + * @return int The exit code when exiting. + */ + public function getExitCode(): int + { + return $this->_exitCode; + } + + /** + * @param int $value The exit code when exiting. + * @return static The current object. + */ + public function setExitCode(int $value): static + { + $this->_exitCode = $value; + + return $this; + } + + /** + * @return ?int The alarm time. + */ + public function getAlarmTime(): ?int + { + return $this->_alarmTime; + } + + /** + * @param ?int $value The alarm time. + * @return static The current object. + */ + public function setAlarmTime(?int $value): static + { + $this->_alarmTime = $value; + + return $this; + } + + /** + * @return ?int The Parameter Error Number. + */ + public function getParameterErrorNumber(): ?int + { + $param = $this->getParameter(); + + return $param['errno'] ?? null; + } + + /** + * @return ?int The Parameter Code. + */ + public function getParameterCode(): ?int + { + $param = $this->getParameter(); + + return $param['code'] ?? null; + } + + /** + * @return ?int The Parameter Status. + */ + public function getParameterStatus(): ?int + { + $param = $this->getParameter(); + + return $param['status'] ?? null; + } + + /** + * @return ?int The Parameter PID. + */ + public function getParameterPID(): ?int + { + $param = $this->getParameter(); + + return $param['pid'] ?? null; + } + + /** + * @return ?int The Parameter UID. + */ + public function getParameterUID(): ?int + { + $param = $this->getParameter(); + + return $param['uid'] ?? null; + } +} diff --git a/framework/Util/TSignalsDispatcher.php b/framework/Util/TSignalsDispatcher.php new file mode 100644 index 000000000..ce4d413c7 --- /dev/null +++ b/framework/Util/TSignalsDispatcher.php @@ -0,0 +1,784 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util; + +use Prado\Collections\TPriorityPropertyTrait; +use Prado\Collections\TWeakCallableCollection; +use Prado\Exceptions\{TExitException}; +use Prado\Exceptions\{TInvalidOperationException, TInvalidDataValueException}; +use Prado\TComponent; +use Prado\Util\Helpers\TProcessHelper; + +/** + * TSignalsDispatcher class. + * + * This class handles linux process signals. It translates the signals into global + * events in PRADO. There are special handlers for SIGALRM to handle time based + * alarm callbacks and for SIGCHLD to handle specific Process ID. + * + * The signals translate to global events as such: + * SIGALRM => `fxSignalAlarm` ~ + * SIGHUP => `fxSignalHangUp` ^ + * SIGINT => `fxSignalInterrupt` ^ + * SIGQUIT => `fxSignalQuit` ^ + * SIGTRAP => `fxSignalTrap` + * SIGABRT => `fxSignalAbort` ^ + * SIGUSR1 => `fxSignalUser1` + * SIGUSR2 => `fxSignalUser2` + * SIGTERM => `fxSignalTerminate` ^ + * SIGCHLD => `fxSignalChild` @ + * SIGCONT => `fxSignalContinue` + * SIGTSTP => `fxSignalTerminalStop` + * SIGTTIN => `fxSignalBackgroundRead` + * SIGTTOU => `fxSignalBackgroundWrite` + * SIGURG => `fxSignalUrgent` + * ^ Designates an exiting signal-event unless changed in the TSignalParameter. + * ~ SIGALRM has a special handler {@see self::ring()} to process and manage the time + * queue of callbacks and reset the alarm. + * @ SIGCHLD has a special handler {@see self::delegateChild()} to delegate to process + * specific handlers. + * + * The signal handlers are stored and restored on {@see self::attach()}ing and {@see + * self::detach()}ing, respectively. This installs and uninstalls the class as + * the signals' handler through the {@see self::__invoke()} method. + * + * Alarms may be added with (@see self::alarm()) and removed with {@see self::disarm()}. + * Alarms can be added without callbacks and will raise the `fxSignalAlarm` event + * without a time callback. By calling alarm() without parameters, the next alarm + * time is returned. + * + * The methods {@see self::hasEvent()}, {@see self::hasEventHandler}, and {@see self::getEventHandlers} + * will accept process signals (int) as event names and translate it into the associated + * PRADO signal global event. These methods will also return the proper values + * for PID handlers, as well, by providing the event name in the format "pid:####" + * (where #### is the PID). hasEvent and getEventHandlers checks if the PID is running. + * To get the PID handler without validation use {@see self::getPidHandlers()}. + * + * Child PID handlers can be checked for with {@see self::hasPidHandler()}. The PID + * handlers can be retrieved with {@see self::getPidHandlers()} (besides using getEventHandlers + * with a special formatted event name [above]). Handlers can be attached to a specific + * PID with {@see self::attachPidHandler()} and detached with {@see self::detachPidHandler()}. + * Child PID handlers can be cleared with {@see self::clearPidHandlers()} + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TSignalsDispatcher extends TComponent implements \Prado\ISingleton +{ + use TPriorityPropertyTrait; + + public const FX_SIGNAL_ALARM = 'fxSignalAlarm'; + public const FX_SIGNAL_HANG_UP = 'fxSignalHangUp'; + public const FX_SIGNAL_INTERRUPT = 'fxSignalInterrupt'; + public const FX_SIGNAL_QUIT = 'fxSignalQuit'; + public const FX_SIGNAL_TRAP = 'fxSignalTrap'; + public const FX_SIGNAL_ABORT = 'fxSignalAbort'; + public const FX_SIGNAL_USER1 = 'fxSignalUser1'; + public const FX_SIGNAL_USER2 = 'fxSignalUser2'; + public const FX_SIGNAL_TERMINATE = 'fxSignalTerminate'; + public const FX_SIGNAL_CHILD = 'fxSignalChild'; + public const FX_SIGNAL_CONTINUE = 'fxSignalContinue'; + public const FX_SIGNAL_TERMINAL_STOP = 'fxSignalTerminalStop'; + public const FX_SIGNAL_BACKGROUND_READ = 'fxSignalBackgroundRead'; + public const FX_SIGNAL_BACKGROUND_WRITE = 'fxSignalBackgroundWrite'; + public const FX_SIGNAL_URGENT = 'fxSignalUrgent'; + + public const SIGNAL_MAP = [ + SIGALRM => self::FX_SIGNAL_ALARM, // Alarm signal. Sent by pcntl_alarm when the time is over. + SIGHUP => self::FX_SIGNAL_HANG_UP, // Hangup signal. Sent to a process when its controlling terminal is closed. + SIGINT => self::FX_SIGNAL_INTERRUPT, // Interrupt signal. Typically generated by pressing Ctrl+C. + SIGQUIT => self::FX_SIGNAL_QUIT, // Quit signal. Similar to SIGINT but produces a core dump when received by the process. + SIGTRAP => self::FX_SIGNAL_TRAP, // Trace/breakpoint trap signal. Used by debuggers to catch trace and breakpoint conditions. + SIGABRT => self::FX_SIGNAL_ABORT, // Abort signal. Sent by the process itself to terminate due to a critical error condition. + SIGUSR1 => self::FX_SIGNAL_USER1, // User-defined signal 1. + SIGUSR2 => self::FX_SIGNAL_USER2, // User-defined signal 2. + SIGTERM => self::FX_SIGNAL_TERMINATE, // Termination signal. Typically used to request graceful termination of a process. + SIGCHLD => self::FX_SIGNAL_CHILD, // Child signal. Sent to a parent process when a child process terminates. + SIGCONT => self::FX_SIGNAL_CONTINUE, // Continue signal. Sent to resume a process that has been stopped. + SIGTSTP => self::FX_SIGNAL_TERMINAL_STOP, // Terminal stop signal. Sent by pressing Ctrl+Z to suspend the process. + 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. + 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. + SIGURG => self::FX_SIGNAL_URGENT, // Urgent condition signal. Indicates the arrival of out-of-band data on a socket. + ]; + + /** The signals that exit by default. */ + public const EXIT_SIGNALS = [ + SIGABRT => true, + SIGBUS => true, + SIGFPE => true, + SIGHUP => true, + SIGILL => true, + SIGINT => true, + SIGPIPE => true, + SIGQUIT => true, + SIGSEGV => true, + SIGSYS => true, + SIGTERM => true, + ]; + + /** @var callable The alarm when no callback is provided. */ + public const NULL_ALARM = [self::class, 'nullAlarm']; + + /** @var ?TSignalsDispatcher The Singleton instance of the class. This is the class + * that gets installed as the signals handler. + */ + private static ?TSignalsDispatcher $_singleton = null; + + /** @var ?bool Are the signals async. */ + private static ?bool $_asyncSignals = null; + + /** @var array The handlers of the signals prior to attaching. + * Format [0 => original value, 1 => closure for the event handler to call the original callable]. + */ + private static array $_priorHandlers = []; + + /** @var ?float Any signal handlers are installed into PRADO at this priority. */ + private static ?float $_priorHandlerPriority = null; + + /** @var ?bool Were the signals async before TSignalsDispatcher. */ + private static ?bool $_priorAsync = null; + + /** @var array The integer alarm times and handlers */ + protected static array $_alarms = []; + + /** @var bool Is the $_alarms array ordered by time. */ + protected static bool $_alarmsOrdered = true; + + /** @var ?int The next alarm time in the _alarms array. */ + protected static ?int $_nextAlarmTime = null; + + /** @var array The pid handlers. */ + private static array $_pidHandlers = []; + + /** + * Returns the singleton of the class. The singleton is created if/when $create + * is true. + * @param bool $create Should the singleton be created if not existing, default true. + * @return ?object The singleton of the class, null where none is available. + */ + public static function singleton(bool $create = true): ?object + { + if ($create && static::hasSignals() && !self::$_singleton) { + $instance = new (static::class)(); + } + + return self::$_singleton; + } + + /** + * Constructs the TSignalsDispatcher. + * The first instance is attached and set as the singleton. + */ + public function __construct() + { + parent::__construct(); + $this->attach(); + } + + /** + * This translates the global event into the signal that raises the event. + * @param string $event The event name to look up its signal. + * @return ?int The signal for an event name. + */ + public static function getSignalFromEvent(string $event): ?int + { + static $eventMap = null; + + if ($eventMap === null) { + $eventMap = array_flip(static::SIGNAL_MAP); + } + return $eventMap[$event] ?? null; + } + + /** + * @return bool Does the system support Process Signals (pcntl_signal) + */ + public static function hasSignals(): bool + { + return function_exists('pcntl_signal'); + } + + /** + * This sets the signals' handlers to this object, attaches the original handlers + * to the PRADO global events at the {@see self::getPriorHandlerPriority()}. + * The alarm handler and Child handler is installed for routing. + * @return bool Is the instance attached as the singleton. + */ + public function attach(): bool + { + if (!static::hasSignals() || self::$_singleton !== null) { + return false; + } + + self::$_singleton = $this; + + if (self::$_asyncSignals === null) { + static::setAsyncSignals(true); + } + + foreach(static::SIGNAL_MAP as $signal => $event) { + $handler = pcntl_signal_get_handler($signal); + + if ($handler instanceof self) { + continue; + } + self::$_priorHandlers[$signal] = [$handler]; + + $callable = is_callable($handler); + if ($callable) { + $handler = function ($sender, $param) use ($handler) { + return $handler($param->getSignal(), $param->getParameter()); + }; + self::$_priorHandlers[$signal][1] = $handler; + } + + $installHandler = true; + switch ($signal) { + case SIGALRM: + parent::attachEventHandler($event, [$this, 'ring'], $this->getPriority()); + if ($nextAlarm = pcntl_alarm(0)) { + self::$_nextAlarmTime = $nextAlarm + time(); + if ($callable) { + static::$_alarms[self::$_nextAlarmTime][] = $handler; + } + pcntl_alarm($nextAlarm); + } + $installHandler = false; + break; + case SIGCHLD: + parent::attachEventHandler($event, [$this, 'delegateChild'], $this->getPriority()); + break; + } + + if ($installHandler && $callable) { + parent::attachEventHandler($event, $handler, static::getPriorHandlerPriority()); + } + + pcntl_signal($signal, $this); + } + return true; + } + + /** + * Detaches the singleton when it is the singleton. Prior signal handlers are + * restored. + * @return bool Is the instance detached from singleton. + */ + public function detach(): bool + { + if (self::$_singleton !== $this) { + return false; + } + + foreach(self::$_priorHandlers as $signal => $originalCallback) { + pcntl_signal($signal, $originalCallback[0]); + $uninstallHandler = true; + switch ($signal) { + case SIGALRM: + parent::detachEventHandler(static::SIGNAL_MAP[$signal], [$this, 'ring']); + pcntl_alarm(0); + $uninstallHandler = false; + break; + case SIGCHLD: + parent::detachEventHandler(static::SIGNAL_MAP[$signal], [$this, 'delegateChild']); + break; + } + if ($uninstallHandler && isset($originalCallback[1])) { + parent::detachEventHandler(static::SIGNAL_MAP[$signal], $originalCallback[1]); + } + } + + self::$_priorHandlers = []; + self::$_pidHandlers = []; + static::$_alarms = []; + self::$_singleton = null; + + static::setAsyncSignals(self::$_priorAsync); + self::$_priorAsync = null; + self::$_asyncSignals = null; + + return true; + } + + /** + * Determines whether an event is defined. + * The event name can be an integer Signal or a pid. Pid handlers have a prefix + * of 'pid:'. + * An event is defined if the class has a method whose name is the event name + * prefixed with 'on', 'fx', or 'dy'. + * Every object responds to every 'fx' and 'dy' event as they are in a universally + * accepted event space. 'on' event must be declared by the object. + * When enabled, this will loop through all active behaviors for 'on' events + * defined by the behavior. + * Note, event name is case-insensitive. + * @param mixed $name the event name + * @return bool Is the event, signal event, or PID event available. + */ + public function hasEvent($name) + { + if (isset(static::SIGNAL_MAP[$name])) { + $name = static::SIGNAL_MAP[$name]; + } elseif(strncasecmp('pid:', $name, 4) === 0) { + if (is_numeric($pid = trim(substr($name, 4)))) { + return TProcessHelper::isRunning((int) $pid); + } + return false; + } + return parent::hasEvent($name); + } + + /** + * Checks if an event has any handlers. This function also checks through all + * the behaviors for 'on' events when behaviors are enabled. + * The event name can be an integer Signal or a pid. Pid handlers have a prefix + * of 'pid:'. + * 'dy' dynamic events are not handled by this function. + * @param string $name the event name + * @return bool whether an event has been attached one or several handlers + */ + public function hasEventHandler($name) + { + if (isset(static::SIGNAL_MAP[$name])) { + $name = static::SIGNAL_MAP[$name]; + } elseif(strncasecmp('pid:', $name, 4) === 0) { + if (is_numeric($pid = trim(substr($name, 4)))) { + $pid = (int) $pid; + return isset(self::$_pidHandlers[$pid]) && self::$_pidHandlers[$pid]->getCount() > 0; + } + return false; + } + return parent::hasEventHandler($name); + } + + /** + * Returns the list of attached event handlers for an 'on' or 'fx' event. This function also + * checks through all the behaviors for 'on' event lists when behaviors are enabled. + * The event name can be an integer Signal or a pid. Pid handlers have a prefix + * of 'pid:'. + * @param mixed $name + * @throws TInvalidOperationException if the event is not defined or PID not a valid numeric. + * @return TWeakCallableCollection list of attached event handlers for an event + */ + public function getEventHandlers($name) + { + if (isset(static::SIGNAL_MAP[$name])) { + $name = static::SIGNAL_MAP[$name]; + } elseif(strncasecmp('pid:', $name, 4) === 0) { + if (!is_numeric($pid = trim(substr($name, 4)))) { + throw new TInvalidOperationException('signalsdispatcher_bad_pid', $pid); + } + $pid = (int) $pid; + if (!isset(self::$_pidHandlers[$pid]) && TProcessHelper::isRunning($pid)) { + self::$_pidHandlers[$pid] = new TWeakCallableCollection(); + } elseif (isset(self::$_pidHandlers[$pid]) && !TProcessHelper::isRunning($pid)) { + unset(self::$_pidHandlers[$pid]); + } + return self::$_pidHandlers[$pid] ?? null; + } + return parent::getEventHandlers($name); + } + + /** + * @param int $pid The PID to check for handlers. + * @return bool Does the PID have handlers. + */ + public function hasPidHandler(int $pid): bool + { + return isset(self::$_pidHandlers[$pid]); + } + + /** + * Returns the Handlers for a specific PID. + * @param int $pid The PID to get the handlers of. + * @param bool $validate Ensure that the PID is running before providing its handlers. + * Default false. + * @return ?TWeakCallableCollection The handlers for a pid or null if validating + * and the PID is not running. + */ + public function getPidHandlers(int $pid, bool $validate = false) + { + if ($validate && !TProcessHelper::isRunning($pid)) { + return null; + } + if (!isset(self::$_pidHandlers[$pid])) { + self::$_pidHandlers[$pid] = new TWeakCallableCollection(); + } + return self::$_pidHandlers[$pid]; + } + + /** + * Attaches a handler to a child PID at a priority. Optionally validates the process + * before attaching. + * @param int $pid The PID to install the handler. + * @param mixed $handler The handler to attach to the process. + * @param null|numeric $priority The priority of the handler, default null for the + * default + * @param bool $validate Should the PID be validated before attaching. + * @return bool Is the handler attached? this can only be false if $validate = true + * and the PID is not running any more. + */ + public function attachPidHandler(int $pid, mixed $handler, mixed $priority = null, bool $validate = false) + { + if ($validate && !TProcessHelper::isRunning($pid)) { + return false; + } + if (!isset(self::$_pidHandlers[$pid])) { + self::$_pidHandlers[$pid] = new TWeakCallableCollection(); + } + self::$_pidHandlers[$pid]->add($handler, $priority); + return true; + } + + /** + * Detaches a handler from a child PID at a priority. + * @param int $pid The PID to detach the handler from. + * @param mixed $handler The handler to remove. + * @param mixed $priority The priority of the handler to remove. default false for + * any priority. + */ + public function detachPidHandler(int $pid, mixed $handler, mixed $priority = false) + { + if (isset(self::$_pidHandlers[$pid])) { + try { + self::$_pidHandlers[$pid]->remove($handler, $priority); + } catch (\Exception $e) { + } + if (self::$_pidHandlers[$pid]->getCount() === 0) { + $this->clearPidHandlers($pid); + } + return true; + } + return false; + } + + /** + * Clears the Handlers for a specific PID. + * @param int $pid The pid to clear the handlers. + * @return bool Were there any handlers for the PID that were cleared. + */ + public function clearPidHandlers(int $pid): bool + { + $return = isset(self::$_pidHandlers[$pid]); + unset(self::$_pidHandlers[$pid]); + return $return; + } + + /** + * The common SIGCHLD callback to delegate per PID. If there are specific PID + * handlers for a child, those PID callbacks are called. On an exit event, the PID + * handlers are cleared. + * @param TSignalsDispatcher $sender The object raising the event. + * @param TSignalParameter $param The signal parameters. + */ + public function delegateChild($sender, $param) + { + if (!static::hasSignals()) { + return; + } + + if (!$param || ($pid = $param->getParameterPid()) === null) { + if (($pid = pcntl_waitpid(-1, $status, WNOHANG)) < 1) { + return; + } + if (!$param) { + $param = new TSignalParameter(SIGCHLD); + } + $sigInfo = $param->getParameter() ?? []; + $sigInfo['pid'] = $pid; + $sigInfo['status'] = $status; + $param->setParameter($sigInfo); + } + if (!isset(self::$_pidHandlers[$pid])) { + return; + } + + array_map(fn ($child) => $child($sender, $param), self::$_pidHandlers[$pid]->toArray()); + + if (in_array($param->getParameterCode(), [1, 2])) { // 1 = normal exit, 2 = kill + unset(self::$_pidHandlers[$pid]); + } + } + + /** + * {@inheritDoc} + * + * Raises signal events by converting the $name as a Signal to the PRADO event + * for the signal. + * @param mixed $name The event name or linux signal to raise. + * @param mixed $sender The sender raising the event. + * @param mixed $param The parameter for the event. + * @param null|mixed $responsetype The response type. + * @param null|mixed $postfunction The results post filter. + */ + public function raiseEvent($name, $sender, $param, $responsetype = null, $postfunction = null) + { + if (isset(static::SIGNAL_MAP[$name])) { + $name = static::SIGNAL_MAP[$name]; + } + return parent::raiseEvent($name, $sender, $param, $responsetype, $postfunction); + } + + /** + * This is called when receiving a system process signal. The global event + * for the signal is raised when the signal is received. + * @param int $signal The signal being sent. + * @param null|mixed $signalInfo The signal information. + * @throws TExitException When the signal needs to exit the application. + */ + public function __invoke(int $signal, mixed $signalInfo = null) + { + if (!isset(static::SIGNAL_MAP[$signal])) { + return; + } + + $parameter = new TSignalParameter($signal, isset(self::EXIT_SIGNALS[$signal]), 128 + $signal, $signalInfo); + + parent::raiseEvent(static::SIGNAL_MAP[$signal], $this, $parameter); + + if ($parameter->getIsExiting()) { + throw new TExitException($parameter->getExitCode()); + } + } + + /** + * Creates a new alarm callback at a specific time from now. If no callback is + * provided, then the alarm will raise `fxSignalAlarm` without a time-based callback. + * When calling alarm() without parameters, it will return the next alarm time. + * @param int $seconds Seconds from now to trigger the alarm. Default 0 for returning + * the next alarm time and not adding any callback. + * @param mixed $callback The alarm callback. Default null. + * @return ?int The time of the alarm for the callback or the next alarm time when + * $seconds = 0. + */ + public static function alarm(int $seconds = 0, mixed $callback = null): ?int + { + if (!static::hasSignals()) { + return null; + } + + if ($seconds > 0) { + static::singleton(); + $alarmTime = time() + $seconds; + if ($callback === null) { + $callback = static::NULL_ALARM; + } + if (!isset(static::$_alarms[$alarmTime])) { + static::$_alarmsOrdered = false; + } + static::$_alarms[$alarmTime][] = $callback; + + if (self::$_nextAlarmTime === null || $alarmTime < self::$_nextAlarmTime) { + self::$_nextAlarmTime = $alarmTime; + pcntl_alarm($seconds); + } + return $alarmTime; + } elseif ($callback === null) { + return self::$_nextAlarmTime; + } + + return null; + } + + /** + * Disarms an alarm time-callback, at the optional time. + * @param ?int $alarmTime + * @param callable $callback + */ + public static function disarm(mixed $alarmTime = null, mixed $callback = null): ?int + { + if (!static::hasSignals()) { + return null; + } + + if ($alarmTime !== null && !is_int($alarmTime)) { + $tmp = $callback; + $callback = $alarmTime; + $alarmTime = $tmp; + } + + // If alarmTime but has no handlers for the time. + if ($alarmTime !== null && !isset(static::$_alarms[$alarmTime])) { + return null; + } + + if ($callback === null) { + $callback = static::NULL_ALARM; + } + + if (!static::$_alarmsOrdered) { + ksort(static::$_alarms, SORT_NUMERIC); + static::$_alarmsOrdered = true; + } + if ($callback === self::$_priorHandlers[SIGALRM][0]) { + $callback = self::$_priorHandlers[SIGALRM][1]; + } + + foreach($alarmTime !== null ? [$alarmTime] : array_keys(static::$_alarms) as $time) { + if (($key = array_search($callback, static::$_alarms[$time] ?? [], true)) !== false) { + unset(static::$_alarms[$time][$key]); + if (is_array(static::$_alarms[$time] ?? false)) { + unset(static::$_alarms[$time]); + if ($time === self::$_nextAlarmTime) { + self::$_nextAlarmTime = array_key_first(static::$_alarms); + if (self::$_nextAlarmTime !== null) { + $seconds = min(1, self::$_nextAlarmTime - time()); + } else { + $seconds = 0; + } + pcntl_alarm($seconds); + } + } + return $time; + } + } + return null; + } + + /** + * The common SIGALRM callback time-processing handler raised by `fxSignalAlarm`. + * All alarm callbacks before or at `time()` are called. The next alarm time is + * found and the next signal alarm is set. + * @param TSignalsDispatcher $sender The object raising the event. + * @param TSignalParameter $signalParam The signal parameters. + */ + public function ring($sender, $signalParam) + { + if (!static::hasSignals()) { + return null; + } + if (!static::$_alarmsOrdered) { + ksort(static::$_alarms, SORT_NUMERIC); + static::$_alarmsOrdered = true; + } + do { + $nextTime = null; + $startTime = time(); + $signalParam->setAlarmTime($startTime); + foreach(static::$_alarms as $alarmTime => $alarms) { + if ($alarmTime <= $startTime) { + array_map(fn ($alarm) => $alarm($this, $signalParam), static::$_alarms[$alarmTime]); + unset(static::$_alarms[$alarmTime]); + } elseif ($nextTime === null) { + $nextTime = $alarmTime; + break; + } + } + $now = time(); + } while ($startTime !== $now); + + if ($nextTime !== null) { + pcntl_alarm($nextTime - $now); + } + self::$_nextAlarmTime = $nextTime; + } + + /** + * The null alarm to simply trigger an alarm without callback + * @param TSignalsDispatcher $sender The object raising the event. + * @param TSignalParameter $param The signal parameters. + */ + public static function nullAlarm($sender, $param) + { + } + + /** + * When PHP Signals are not in asynchronous mode, this must be called to dispatch + * the pending events. To change the async mode, use {@see self::setAsyncSignals()}. + * @return ?bool Returns true on success or false on failure. + */ + public static function syncDispatch(): ?bool + { + if (!static::hasSignals()) { + return null; + } + return pcntl_signal_dispatch(); + } + + /** + * Gets whether the system is in async signals mode. + * @return ?bool Is the system set to handle async signals. null when there are + * no Process Signals in the PHP instance. + */ + public static function getAsyncSignals(): ?bool + { + if (!static::hasSignals()) { + return null; + } + return pcntl_async_signals(); + } + + /** + * Sets whether the system is in async signals mode. This is set to true on instancing. + * If this is set to false, then {@see self::syncDispatch()} must be called for + * signals to be processed. Any pending signals are dispatched when setting async to true. + * @param bool $value Should signals be processed asynchronously. + * @return ?bool The prior value AsyncSignals before setting. + * @link https://www.php.net/manual/en/function.pcntl-signal-dispatch.php + */ + public static function setAsyncSignals(bool $value): ?bool + { + if (!static::hasSignals()) { + return null; + } + self::$_asyncSignals = $value; + + $return = pcntl_async_signals($value); + + if (self::$_priorAsync === null) { + self::$_priorAsync = $return; + } + + if ($value === true && $return === false) { + pcntl_signal_dispatch(); + } + + return $return; + } + + /** + * This returns the priority of the signal handlers when they are installed as + * event handlers. + * @return ?float The priority for prior signal handlers when TSignalsDispatcher + * is attached. + */ + public static function getPriorHandlerPriority(): ?float + { + return self::$_priorHandlerPriority; + } + + /** + * Sets the priority of the signal handlers when they are installed as + * event handlers. + * @param ?float $value The priority for prior signal handlers when TSignalsDispatcher + * is attached. + * @throws TInvalidOperationException When TSignalsDispatcher is already installed. + */ + public static function setPriorHandlerPriority(?float $value): bool + { + if (static::singleton(false)) { + throw new TInvalidOperationException('signalsdispatcher_no_change', 'PriorHandlerPriority'); + } + + self::$_priorHandlerPriority = $value; + return true; + } + + /** + * This gets the signal handlers that were installed prior to the TSignalsDispatcher + * being attached. + * @param int $signal The signal to get the prior handler value. + * @param bool $original Return the original handler, default false for the signal + * closure handler that wraps the original handler with a PRADO signal event handler. + */ + public static function getPriorHandler(int $signal, bool $original = false): mixed + { + return self::$_priorHandlers[$signal][$original ? 0 : 1] ?? null; + } +} diff --git a/framework/classes.php b/framework/classes.php index b218d53d0..b4efa9df9 100644 --- a/framework/classes.php +++ b/framework/classes.php @@ -228,6 +228,7 @@ 'TTarFileExtractor' => 'Prado\IO\TTarFileExtractor', 'TTextWriter' => 'Prado\IO\TTextWriter', 'IService' => 'Prado\IService', +'ISingleton' => 'Prado\ISingleton', 'IStatePersister' => 'Prado\IStatePersister', 'Prado' => 'Prado\Prado', 'PradoBase' => 'Prado\PradoBase', @@ -276,7 +277,11 @@ 'TModule' => 'Prado\TModule', 'TPropertyValue' => 'Prado\TPropertyValue', 'TService' => 'Prado\TService', +'TApplicationSignals' => 'Prado\Util\Behaviors\TApplicationSignals', 'TBehaviorParameterLoader' => 'Prado\Util\Behaviors\TBehaviorParameterLoader', +'TCaptureForkLog' => 'Prado\Util\Behaviors\TCaptureForkLog', +'Forkable' => 'Prado\Util\Behaviors\Forkable', +'TGlobalClassAware' => 'Prado\Util\Behaviors\TGlobalClassAware', 'TMapLazyLoadBehavior' => 'Prado\Util\Behaviors\TMapLazyLoadBehavior', 'TMapRouteBehavior' => 'Prado\Util\Behaviors\TMapRouteBehavior', 'TPageGlobalizationCharsetBehavior' => 'Prado\Util\Behaviors\TPageGlobalizationCharsetBehavior', @@ -297,6 +302,9 @@ 'TArrayHelper' => 'Prado\Util\Helpers\TArrayHelper', 'TBitHelper' => 'Prado\Util\Helpers\TBitHelper', 'TEscCharsetConverter' => 'Prado\Util\Helpers\TEscCharsetConverter', +'TProcessHelper' => 'Prado\Util\Helpers\TProcessHelper', +'TProcessWindowsPriority' => 'Prado\Util\Helpers\TProcessWindowsPriority', +'TProcessWindowsPriorityName' => 'Prado\Util\Helpers\TProcessWindowsPriorityName', 'IBaseBehavior' => 'Prado\Util\IBaseBehavior', 'IBehavior' => 'Prado\Util\IBehavior', 'IClassBehavior' => 'Prado\Util\IClassBehavior', @@ -332,6 +340,8 @@ 'TRpcClientRequestException' => 'Prado\Util\TRpcClientRequestException', 'TRpcClientResponseException' => 'Prado\Util\TRpcClientResponseException', 'TRpcClientTypesEnumerable' => 'Prado\Util\TRpcClientTypesEnumerable', +'TSignalsDispatcher' => 'Prado\Util\TSignalsDispatcher', +'TSignalsParameter' => 'Prado\Util\TSignalsParameter', 'TSimpleDateFormatter' => 'Prado\Util\TSimpleDateFormatter', 'TSysLogRoute' => 'Prado\Util\TSysLogRoute', 'TUtf8Converter' => 'Prado\Util\TUtf8Converter', diff --git a/tests/unit/TComponentTest.php b/tests/unit/TComponentTest.php index e26d7bf29..761491287 100755 --- a/tests/unit/TComponentTest.php +++ b/tests/unit/TComponentTest.php @@ -2278,8 +2278,8 @@ public function testHasMethod() $this->assertTrue($this->component->hasMethod('fxAttachClassBehavior')); $this->assertTrue($this->component->hasMethod('fxattachclassbehavior')); - $this->assertTrue($this->component->hasMethod('fxNonExistantGlobalEvent')); - $this->assertTrue($this->component->hasMethod('fxnonexistantglobalevent')); + $this->assertFalse($this->component->hasMethod('fxNonExistantGlobalEvent')); + $this->assertFalse($this->component->hasMethod('fxnonexistantglobalevent')); $this->assertTrue($this->component->hasMethod('dyNonExistantLocalEvent')); $this->assertTrue($this->component->hasMethod('dynonexistantlocalevent')); diff --git a/tests/unit/Util/Behaviors/TApplicationSignalTest.php b/tests/unit/Util/Behaviors/TApplicationSignalTest.php new file mode 100644 index 000000000..da56ea012 --- /dev/null +++ b/tests/unit/Util/Behaviors/TApplicationSignalTest.php @@ -0,0 +1,116 @@ +behavior = new TApplicationSignals(); + } + + protected function tearDown(): void + { + $this->behavior = null; + } + + public function testAttachDetach() + { + $app = Prado::getApplication(); + + self::assertNull(TSignalsDispatcher::singleton(false)); + $this->behavior->setSignalsClass(TTestAppSignalsDispatcher::class); + + $app->attachBehavior($name = 'appSignals', $this->behavior); + self::assertInstanceOf(TTestAppSignalsDispatcher::class, TSignalsDispatcher::singleton(false)); + + try { + $this->behavior->setSignalsClass(null); + self::fail("TInvalidOperationException not thrown when behavior already attached."); + } catch(TInvalidOperationException $e) { + } + + try { + $this->behavior->setPriorHandlerPriority(20); + self::fail("TInvalidOperationException not thrown when behavior already instanced."); + } catch(TInvalidOperationException $e) { + } + + self::assertEquals(TSignalsDispatcher::singleton(), $app->getSignalsDispatcher()); + + $app->detachBehavior($name); + + self::assertNull(TSignalsDispatcher::singleton(false)); + } + + public function testSignalsClass() + { + self::assertEquals(TSignalsDispatcher::class, $this->behavior->getSignalsClass()); + + $this->behavior->setSignalsClass(TTestAppSignalsDispatcher::class); + + self::assertEquals(TTestAppSignalsDispatcher::class, $this->behavior->getSignalsClass()); + + $this->behavior->setSignalsClass(null); + + self::assertEquals(TSignalsDispatcher::class, $this->behavior->getSignalsClass()); + + self::expectException(TInvalidDataValueException::class); + $this->behavior->setSignalsClass(TComponent::class); + } + + public function testAsyncSignals() + { + if (!TSignalsDispatcher::hasSignals()) { + $this->markTestSkipped("skipping " . TSignalsDispatcher::class . "::alarm and ::disarm."); + return; + } + + $ogAsyncSignal = $this->behavior->getAsyncSignals(); + + self::assertEquals($ogAsyncSignal, $this->behavior->setAsyncSignals(true)); + self::assertTrue($this->behavior->getAsyncSignals()); + self::assertTrue($this->behavior->setAsyncSignals(false)); + self::assertFalse($this->behavior->getAsyncSignals()); + self::assertFalse($this->behavior->setAsyncSignals("true")); + self::assertTrue($this->behavior->getAsyncSignals()); + self::assertTrue($this->behavior->setAsyncSignals("false")); + self::assertFalse($this->behavior->getAsyncSignals()); + + $this->behavior->setAsyncSignals(true); + pcntl_async_signals($ogAsyncSignal); + self::assertEquals($ogAsyncSignal, $this->behavior->getAsyncSignals()); + } + + public function testPriorHandlerPriority() + { + $ogHandlerPriority = $this->behavior->getPriorHandlerPriority(); + + self::assertTrue($this->behavior->setPriorHandlerPriority(3)); + self::assertEquals(3, $this->behavior->getPriorHandlerPriority()); + self::assertTrue($this->behavior->setPriorHandlerPriority("11")); + self::assertEquals(11, $this->behavior->getPriorHandlerPriority()); + self::assertTrue($this->behavior->setPriorHandlerPriority(5)); + self::assertEquals(5, $this->behavior->getPriorHandlerPriority()); + + self::assertTrue($this->behavior->setPriorHandlerPriority(null)); + self::assertNull($this->behavior->getPriorHandlerPriority()); + self::assertTrue($this->behavior->setPriorHandlerPriority($ogHandlerPriority)); + self::assertEquals($ogHandlerPriority, $this->behavior->getPriorHandlerPriority()); + } +} diff --git a/tests/unit/Util/Behaviors/TBehaviorParameterLoaderTest.php b/tests/unit/Util/Behaviors/TBehaviorParameterLoaderTest.php index 134e2c7ee..50df443bc 100644 --- a/tests/unit/Util/Behaviors/TBehaviorParameterLoaderTest.php +++ b/tests/unit/Util/Behaviors/TBehaviorParameterLoaderTest.php @@ -127,6 +127,8 @@ public function testDyInit() $this->assertEquals(['data123'], $app->asa('testBehavior1')->config); $app->detachBehavior('testBehavior1'); } + $mod->unlisten(); + $modB->unlisten(); } public function testAttachModuleBehaviors() @@ -153,6 +155,7 @@ public function testAttachModuleBehaviors() $this->assertInstanceOf('TestModuleBehaviorLoader1', $module->asa($behaviorName)); $this->assertEquals('value1', $module->asa($behaviorName)->PropertyA); $module->detachBehavior($behaviorName); + $module->unlisten(); } public function testAttachModuleBehaviors_anonymous() @@ -197,6 +200,7 @@ public function testAttachTPageBehaviors() $this->assertInstanceOf('TestModuleBehaviorLoader2', $page->asa($behaviorName)); $this->assertEquals('value', $page->asa($behaviorName)->PropertyA); $page->detachBehavior($behaviorName); + $page->unlisten(); } public function testAttachTPageBehaviors_anonymous() @@ -217,6 +221,7 @@ public function testAttachTPageBehaviors_anonymous() $this->assertInstanceOf('TestModuleBehaviorLoader2', $page->asa(0)); $this->assertEquals('value', $page->asa(0)->PropertyA); $page->detachBehavior(0); + $page->unlisten(); } public function testBehaviorName() diff --git a/tests/unit/Util/Behaviors/TCaptureForkLogTest.php b/tests/unit/Util/Behaviors/TCaptureForkLogTest.php new file mode 100644 index 000000000..c194d4fd7 --- /dev/null +++ b/tests/unit/Util/Behaviors/TCaptureForkLogTest.php @@ -0,0 +1,66 @@ +asa(TCaptureForkLog::BEHAVIOR_NAME)) + $app->detachBehavior(TCaptureForkLog::BEHAVIOR_NAME); + } + + public function testFork() + { + $logger = Prado::getLogger(); + + $logger->deleteLogs(); + + Prado::notice('parent notice'); + Prado::profileBegin('token1'); + + $withCaptureLog = true; + $pid = TProcessHelper::fork($withCaptureLog); + + if ($pid === -1) { + self::fail("failed to fork."); + } elseif ($pid === 0) { + Prado::profileBegin('token2'); + Prado::profileBegin('token3'); + self::assertLessThan(1, Prado::profileEnd('token1')); + Prado::profileEnd('token2'); + Prado::profileEnd('token4'); + Prado::warning('child warning'); + exit(); + } else { + $app = Prado::getApplication(); + Prado::profileEnd('token2'); + $app->receiveLogsFromChildren(); + self::assertEquals(4, count($logger->getLogs(pid: getMyPid()))); + self::assertEquals(8, count($logs = $logger->getLogs(pid: $pid))); + self::assertEquals('token1', $logs[0][0]); + //$logs[1][0] = 'Executing child fork...' + self::assertEquals('token2', $logs[2][0]); + self::assertEquals('token3', $logs[3][0]); + self::assertEquals('token1', $logs[4][0]); + self::assertEquals('token2', $logs[5][0]); + self::assertEquals('token4', $logs[6][0]); + self::assertEquals('child warning', $logs[7][0]); + + self::assertEquals(12, count($logger->getLogs())); + } + } + +} diff --git a/tests/unit/Util/Behaviors/TForkableTest.php b/tests/unit/Util/Behaviors/TForkableTest.php new file mode 100644 index 000000000..5a6135307 --- /dev/null +++ b/tests/unit/Util/Behaviors/TForkableTest.php @@ -0,0 +1,115 @@ +behavior = new TForkable(); + } + + + protected function tearDown(): void + { + $this->behavior = null; + } + + public function testAttachDetach() + { + $name = 'forkable'; + $component = new TComponent(); + + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + $component->attachBehavior($name, $this->behavior); + + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + $component->detachBehavior($name); + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + + $component = new TTestForkablePrepare(); + + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + $component->attachBehavior($name, $this->behavior); + self::assertTrue($component->hasEventHandler('fxPrepareForFork')); + self::assertEquals([[$component, 'fxPrepareForFork']], $component->getEventHandlers('fxPrepareForFork')->toArray()); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + $component->detachBehavior($name); + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + + $component = new TTestForkableRestore(); + + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + $component->attachBehavior($name, $this->behavior); + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertEquals([[$component, 'fxRestoreAfterFork']], $component->getEventHandlers('fxRestoreAfterFork')->toArray()); + self::assertTrue($component->hasEventHandler('fxRestoreAfterFork')); + + $component->detachBehavior($name); + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + + $component = new TTestForkableBehavior(); + + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + + $component->attachBehavior($name, $this->behavior); + self::assertTrue($component->hasEventHandler('fxPrepareForFork')); + self::assertEquals([[$component, 'fxRestoreAfterFork']], $component->getEventHandlers('fxRestoreAfterFork')->toArray()); + self::assertEquals([[$component, 'fxPrepareForFork']], $component->getEventHandlers('fxPrepareForFork')->toArray()); + self::assertTrue($component->hasEventHandler('fxRestoreAfterFork')); + + $component->detachBehavior($name); + self::assertFalse($component->hasEventHandler('fxPrepareForFork')); + self::assertFalse($component->hasEventHandler('fxRestoreAfterFork')); + } + +} diff --git a/tests/unit/Util/Behaviors/TGlobalClassAwareTest.php b/tests/unit/Util/Behaviors/TGlobalClassAwareTest.php new file mode 100644 index 000000000..9f87fd1ca --- /dev/null +++ b/tests/unit/Util/Behaviors/TGlobalClassAwareTest.php @@ -0,0 +1,44 @@ +behavior = new TGlobalClassAware(); + } + + + protected function tearDown(): void + { + $this->behavior = null; + } + + public function testAttachDetach() + { + $name = 'globalclassaware'; + $component = new TComponent(); + + self::assertFalse($component->hasEventHandler('fxAttachClassBehavior')); + self::assertFalse($component->hasEventHandler('fxDetachClassBehavior')); + + $component->attachBehavior($name, $this->behavior); + + self::assertTrue($component->hasEventHandler('fxAttachClassBehavior')); + self::assertEquals([[$component, 'fxAttachClassBehavior']], $component->getEventHandlers('fxAttachClassBehavior')->toArray()); + self::assertTrue($component->hasEventHandler('fxDetachClassBehavior')); + self::assertEquals([[$component, 'fxDetachClassBehavior']], $component->getEventHandlers('fxDetachClassBehavior')->toArray()); + + $component->detachBehavior($name); + self::assertFalse($component->hasEventHandler('fxAttachClassBehavior')); + self::assertFalse($component->hasEventHandler('fxDetachClassBehavior')); + + + } + +} diff --git a/tests/unit/Util/Helpers/TProcessHelperTest.php b/tests/unit/Util/Helpers/TProcessHelperTest.php new file mode 100644 index 000000000..49fcb70ff --- /dev/null +++ b/tests/unit/Util/Helpers/TProcessHelperTest.php @@ -0,0 +1,185 @@ +dispatcher) { + $this->dispatcher->detach(); + $this->dispatcher = null; + } + TTestSignalsDispatcher::setPriorHandlerPriority(null); + } + + public function testIsSystemWindows() + { + self::assertEquals(strncasecmp(php_uname('s'), 'win', 3) === 0, TProcessHelper::isSystemWindows()); + } + + public function testIsForkable() + { + self::assertEquals(function_exists('pcntl_fork'), TProcessHelper::isForkable()); + } + + public function testFork() + { + if (!TProcessHelper::isForkable()) { + //$this->markTestSkipped("skipping " . TProcessHelper::class . "::sendSignal on Windows."); + self::expectException(TNotSupportedException::class); + TProcessHelper::fork(); + return; + } + + $dispatcher = TSignalsDispatcher::singleton(); + $restoreCalled = $prepareCalled = false; + + + $prepareSub = new TEventSubscription($dispatcher, TProcessHelper::FX_PREPARE_FOR_FORK, + function($sender, $param) use (&$prepareCalled) { + $prepareCalled = true; + return ['datakey' => 'value']; + } + ); + $restoreParam = null; + $restoreSub = new TEventSubscription($dispatcher, TProcessHelper::FX_RESTORE_AFTER_FORK, + function($sender, $param) use (&$restoreCalled, &$restoreParam) { + $restoreCalled = true; + $restoreParam = $param; + } + ); + + $pid = TProcessHelper::fork(); + + if ($pid === 0) { + usleep(50_000); // time for isRunning by parent. delay of start is usually enough. + exit(); + } else if ($pid === -1) { + self::fail('Failed to fork with pid = -1'); + return; + } + + self::assertTrue(TProcessHelper::isRunning($pid)); + self::assertTrue($prepareCalled); + self::assertTrue($restoreCalled); + self::assertEquals($pid, $restoreParam['pid']); + self::assertEquals('value', $restoreParam['datakey']); + + $dispatcher->detach(); + } + + public function testSendSignal() + { + if (TProcessHelper::isSystemWindows()) { + self::expectException(TNotSupportedException::class); + TProcessHelper::sendSignal(0); + return; + } + $app = Prado::getApplication(); + $this->dispatcher = TSignalsDispatcher::singleton(); + + self::assertTrue($this->dispatcher->getAsyncSignals()); + + $done = false; + $signal = SIGUSR1; + $this->dispatcher->attachEventHandler($signal, $handler = function($sender, $param) use (&$done) {$done = true;}); + self::assertTrue(TProcessHelper::sendSignal($signal)); + self::assertTrue($done, 'did not send the signal'); + self::assertTrue($this->dispatcher->getEventHandlers('fxSignalUser1')->contains($handler)); + self::assertTrue($this->dispatcher->getEventHandlers($signal)->contains($handler)); + self::assertEquals($this->dispatcher, pcntl_signal_get_handler($signal)); + + $this->dispatcher->detachEventHandler($signal, $handler); + $this->dispatcher->detach(); + } + + public function testIsRunning_Kill_Priority() + { + self::assertTrue(TProcessHelper::isRunning(getmypid())); + + $command = TProcessHelper::filterCommand(['@php', '-r', 'sleep(5);']); + + $descriptorspec = []; + + $process = proc_open($command, $descriptorspec, $pipes); + $info = proc_get_status($process); + $pid = $info['pid']; + self::assertTrue(TProcessHelper::isRunning($pid)); + + $processPriority = TProcessHelper::getProcessPriority($pid); + self::assertTrue(TProcessHelper::setProcessPriority(TProcessHelper::WINDOWS_BELOW_NORMAL_PRIORITY, $pid)); + self::assertEquals(TProcessHelper::WINDOWS_BELOW_NORMAL_PRIORITY, TProcessHelper::getProcessPriority($pid)); + + self::assertTrue(TProcessHelper::setProcessPriority(TProcessHelper::WINDOWS_IDLE_PRIORITY, $pid)); + self::assertEquals(TProcessHelper::WINDOWS_IDLE_PRIORITY, TProcessHelper::getProcessPriority($pid)); + + self::assertTrue(TProcessHelper::kill($pid)); // kill pid despite sleeping. + proc_close($process); + self::assertFalse(TProcessHelper::isRunning($pid)); + } + + public function testEscapeShellArg() + { + if (TProcessHelper::isSystemWindows()) { + $argument = ''; + $expected = '""'; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + $argument = 'test'; + $expected = '"test"'; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + $argument = 'test argument'; + $expected = '"test argument"'; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + $argument = '"quoted"'; + $expected = '\\"quoted\\"'; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + $argument = '%ENVIRONMENT%'; + $expected = '^%"ENVIRONMENT"^%'; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + } else { + + $argument = ''; + $expected = "''"; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + $argument = 'test'; + $expected = "'test'"; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + $argument = 'test argument'; + $expected = "'test argument'"; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + $argument = "'quoted'"; + $expected = "''\\''quoted'\\'''"; + $this->assertEquals($expected, TProcessHelper::escapeShellArg($argument)); + + } + } + + public function testisSurroundedBy() + { + self::assertFalse(TProcessHelper::isSurroundedBy('text', '-')); + self::assertTrue(TProcessHelper::isSurroundedBy('-text-', '-')); + self::assertFalse(TProcessHelper::isSurroundedBy('-text', '-')); + self::assertTrue(TProcessHelper::isSurroundedBy('-text-', '-t')); + self::assertFalse(TProcessHelper::isSurroundedBy('-ext-', '-t')); + self::assertFalse(TProcessHelper::isSurroundedBy('-tex-', '-t')); + self::assertFalse(TProcessHelper::isSurroundedBy('-ex-', '-t')); + } +} \ No newline at end of file diff --git a/tests/unit/Util/TLoggerTest.php b/tests/unit/Util/TLoggerTest.php index 12734a5d7..1173770bb 100644 --- a/tests/unit/Util/TLoggerTest.php +++ b/tests/unit/Util/TLoggerTest.php @@ -291,9 +291,12 @@ public function testOnFlushLogs() } }; - $logger->onFlushLogs(); // Not calling due to no log items - $this->assertEquals(0, $called); - $this->assertNull($final); + $logger->onFlushLogs(); // Calling without log items + $this->assertEquals(1, $called); + $this->assertFalse($final); + + $called = 0; + $final = null; $logger->log('token1', TLogger::INFO); $logger->log('token2', TLogger::PROFILE_BEGIN); diff --git a/tests/unit/Util/TSignalParameterTest.php b/tests/unit/Util/TSignalParameterTest.php new file mode 100644 index 000000000..f686fcce6 --- /dev/null +++ b/tests/unit/Util/TSignalParameterTest.php @@ -0,0 +1,101 @@ + 20, 'errno' => $errno = 2, 'code' => $code = 1, 'status' => $status = 3, 'pid' => $pid = 1810, 'uid' => $uid = 501]); + + self::assertEquals($signal, $param->getSignal()); + self::assertEquals($exit, $param->getIsExiting()); + self::assertEquals($exitCode, $param->getExitCode()); + self::assertEquals($errno, $param->getParameterErrorNumber()); + self::assertEquals($code, $param->getParameterCode()); + self::assertEquals($status, $param->getParameterStatus()); + self::assertEquals($pid, $param->getParameterPID()); + self::assertEquals($uid, $param->getParameterUID()); + + $param = new TTestSignalParameter(); + + self::assertEquals(0, $param->getSignal()); + self::assertFalse($param->getIsExiting()); + self::assertEquals(0, $param->getExitCode()); + self::assertNull($param->getParameterErrorNumber()); + self::assertNull($param->getParameterCode()); + self::assertNull($param->getParameterStatus()); + self::assertNull($param->getParameterPID()); + self::assertNull($param->getParameterUID()); + } + + public function testSignal() + { + $param = new TTestSignalParameter(); + + $param->setSignal($ref = 3); + self::assertEquals($ref, $param->getSignal()); + $param->setSignal($ref = 5); + self::assertEquals($ref, $param->getSignal()); + } + + public function testIsExiting() + { + $param = new TTestSignalParameter(); + + $param->setIsExiting($ref = true); + self::assertEquals($ref, $param->getIsExiting()); + $param->setIsExiting($ref = false); + self::assertEquals($ref, $param->getIsExiting()); + $param->setIsExiting($ref = true); + self::assertEquals($ref, $param->getIsExiting()); + } + + public function testExitCode() + { + $param = new TTestSignalParameter(); + + $param->setExitCode($ref = 3); + self::assertEquals($ref, $param->getExitCode()); + $param->setExitCode($ref = 5); + self::assertEquals($ref, $param->getExitCode()); + $param->setExitCode($ref = 0); + self::assertEquals($ref, $param->getExitCode()); + } + + public function testAlarmTime() + { + $param = new TTestSignalParameter(); + + $param->setAlarmTime($ref = time()); + self::assertEquals($ref, $param->getAlarmTime()); + $param->setAlarmTime($ref = time() - 2); + self::assertEquals($ref, $param->getAlarmTime()); + $param->setAlarmTime($ref = time() + 10); + self::assertEquals($ref, $param->getAlarmTime()); + } + + public function testParameterValues() + { + $param = new TTestSignalParameter(); + + $param->setParameter(['signo' => 20, 'errno' => $errno = 2, 'code' => $code = 1, 'status' => $status = 3, 'pid' => $pid = 1810, 'uid' => $uid = 501]); + self::assertEquals($errno, $param->getParameterErrorNumber()); + self::assertEquals($code, $param->getParameterCode()); + self::assertEquals($status, $param->getParameterStatus()); + self::assertEquals($pid, $param->getParameterPID()); + self::assertEquals($uid, $param->getParameterUID()); + } +} + diff --git a/tests/unit/Util/TSignalsDispatcherTest.php b/tests/unit/Util/TSignalsDispatcherTest.php new file mode 100644 index 000000000..14dbcc73b --- /dev/null +++ b/tests/unit/Util/TSignalsDispatcherTest.php @@ -0,0 +1,582 @@ +data = $data; + } + public function __invoke($signal, $sigInfo) + { + $this->signal = $signal; + $this->sigInfo = $sigInfo; + } +} + +class TSignalsDispatcherTest extends PHPUnit\Framework\TestCase +{ + public $dispatcher = null; + protected function setUp(): void + { + } + + protected function tearDown(): void + { + if ($this->dispatcher) { + $this->dispatcher->detach(); + $this->dispatcher = null; + } + TTestSignalsDispatcher::setPriorHandlerPriority(null); + } + + public function testConstruct() + { + if (TSignalsDispatcher::hasSignals()) { + $this->markTestSkipped("skipping " . TSignalsDispatcher::class . "::__construct without signals."); + return; + } + self::assertNull(TTestSignalsDispatcher::singleton(false)); + $this->dispatcher = new TTestSignalsDispatcher(); + self::assertEquals($this->dispatcher, TTestSignalsDispatcher::singleton(false)); + } + + public function testSingleton() + { + self::assertNull(TTestSignalsDispatcher::singleton(false)); + if (!TSignalsDispatcher::hasSignals()) { + self::assertNull(TTestSignalsDispatcher::singleton()); + } else { + self::assertInstanceOf(TTestSignalsDispatcher::class, $this->dispatcher = TTestSignalsDispatcher::singleton()); + } + } + + public function testGetSignalFromEvent() + { + self::assertNull(TTestSignalsDispatcher::singleton(false)); + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + self::assertEquals($signal, TTestSignalsDispatcher::getSignalFromEvent($event)); + } + self::assertNull(TTestSignalsDispatcher::singleton(false)); + } + + public function testAttachDetach() + { + if (!TSignalsDispatcher::hasSignals()) { + $this->markTestSkipped("skipping " . TSignalsDispatcher::class . "::attach."); + return; + } + + // Preset custom handlers, stored on attach, to be restored on detach + $ogHandler = new TTestSignalInvokable(1); + + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + pcntl_signal($signal, $ogHandler); + } + self::assertNull(TTestSignalsDispatcher::singleton(false)); + self::assertNull(TTestSignalsDispatcher::singleton(false)); + $priorAsync = TTestSignalsDispatcher::getAsyncSignals(); + self::assertNull(TTestSignalsDispatcher::getPriorHandlerPriority()); + + // attach new signals dispatcher. + self::assertTrue(TTestSignalsDispatcher::setPriorHandlerPriority(5)); + $this->dispatcher = new TTestSignalsDispatcher(); + + // attach state + { // cannot set PriorHandlerPriority after attach + try { + self::assertFalse(TTestSignalsDispatcher::setPriorHandlerPriority(8)); + self::fail("failed to throw TInvalidOperationException when setting prior handler priority when already used."); + } catch(TInvalidOperationException $e) { + } + self::assertEquals(5, TTestSignalsDispatcher::getPriorHandlerPriority()); + } + self::assertTrue($this->dispatcher->getAsyncSignals()); + + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + self::assertEquals($this->dispatcher, pcntl_signal_get_handler($signal)); + + $handlers = $this->dispatcher->getEventHandlers($event); + $c = $handlers->getCount(); + $handler = $handlers[0]; + if ($signal !== SIGALRM) + self::assertEquals(5, $handlers->priorityOf($handler)); + if ($signal === SIGALRM) { + self::assertTrue($handlers->contains([$this->dispatcher, 'ring'])); + } elseif ($signal === SIGCHLD) { + self::assertTrue($handlers->contains([$this->dispatcher, 'delegateChild'])); + } + } + + $this->dispatcher->detach(); + + // original handlers restored + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + self::assertEquals($ogHandler, pcntl_signal_get_handler($signal), "$event signal does not match the original"); + + $handlers = $this->dispatcher->getEventHandlers($event); + self::assertFalse($handlers->contains($handler)); + if ($signal === SIGALRM) { + self::assertFalse($handlers->contains([$this->dispatcher, 'ring'])); + } elseif ($signal === SIGCHLD) { + self::assertFalse($handlers->contains([$this->dispatcher, 'delegateChild'])); + } + } + self::assertEquals($priorAsync, $this->dispatcher->getAsyncSignals()); + TTestSignalsDispatcher::setPriorHandlerPriority(null); + + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + pcntl_signal($signal, SIG_DFL); + } + } + + public function testHasEvent_Handler_Get() + { + if (!TSignalsDispatcher::hasSignals()) { + $this->markTestSkipped("skipping " . TSignalsDispatcher::class . "::getEventHandler."); + return; + } + + // Preset custom handlers, stored on attach, to be restored on detach + $handler = new TTestSignalInvokable(2); + + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + pcntl_signal($signal, $handler); + } + + // New PID + $command = TProcessHelper::filterCommand(['@php', '-r', 'sleep(5);']); + + $descriptorspec = []; + $this->dispatcher = new TTestSignalsDispatcher(); + + $process = proc_open($command, $descriptorspec, $pipes); + $info = proc_get_status($process); + $pid = $info['pid']; + + { // With valid pid + // Signal rather than event name: hasEvent, hasEventHandler, getEventHandlers + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + self::assertTrue($this->dispatcher->hasEvent($signal)); + self::assertTrue($this->dispatcher->hasEventHandler($signal)); + self::assertEquals(in_array($signal, [SIGCHLD]) ? 2 : 1, $this->dispatcher->getEventHandlers($signal)->count(), 'failed on '. $event); + } + + // handles pids + self::assertTrue($this->dispatcher->hasEvent('Pid: ' . $pid)); + self::assertTrue($this->dispatcher->hasEvent('pid:' . $pid)); + + self::assertFalse($this->dispatcher->hasEventHandler('pid:' . $pid)); + + self::assertInstanceOf(TWeakCallableCollection::class, $pidHandlers = $this->dispatcher->getEventHandlers('pid:' . $pid)); + + $sub = new TEventSubscription($this->dispatcher, 'pid:' . $pid, function($sender, $param) {}); + + self::assertTrue($this->dispatcher->hasEventHandler('pid:' . $pid)); + + $sub->unsubscribe(); + + self::assertFalse($this->dispatcher->hasEventHandler('pid:' . $pid)); + + self::assertInstanceOf(TWeakCallableCollection::class, $pidHandlers = $this->dispatcher->getEventHandlers('pid:' . $pid)); + } + + // End the PID + self::assertTrue(TProcessHelper::kill($pid)); // kill pid despite sleeping. + proc_close($process); + + self::assertNull($this->dispatcher->getEventHandlers('pid:' . $pid)); + + // detach dispatcher + $this->dispatcher->detach(); + + // reset signal handlers + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + pcntl_signal($signal, SIG_DFL); + } + + self::assertFalse($this->dispatcher->hasEvent('pid:' . $pid)); + } + + public function testTEventSubscription() + { + $this->dispatcher = TSignalsDispatcher::singleton(); + $this->subscription = new TEventSubscription($this->dispatcher, SIGTERM, $handler = function($sender, $param) {return true;}, 5); + self::assertTrue($this->subscription->getIsSubscribed()); + $handlers = $this->dispatcher->getEventHandlers('fxSignalTerminate'); + self::assertEquals($handlers, $this->subscription->getCollection()); + self::assertTrue($handlers->contains($handler)); + $this->subscription->unsubscribe(); + self::assertFalse($this->subscription->getIsSubscribed()); + self::assertFalse($handlers->contains($handler)); + } + + public function testPidHandlers() + { + // New PID + $command = TProcessHelper::filterCommand(['@php', '-r', 'sleep(5);']); + + $descriptorspec = []; + $this->dispatcher = new TTestSignalsDispatcher(); + + $process = proc_open($command, $descriptorspec, $pipes); + $info = proc_get_status($process); + $pid = $info['pid']; + + { // With valid pid + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + $handlers = $this->dispatcher->getPidHandlers($pid); + self::assertTrue($this->dispatcher->hasPidHandler($pid)); + $handlers = $this->dispatcher->getPidHandlers($pid, true); + self::assertTrue($this->dispatcher->hasPidHandler($pid)); + + $this->dispatcher->clearPidHandlers($pid); + + $handler = function ($sender, $param) { + + }; + self::assertTrue($this->dispatcher->attachPidHandler($pid, $handler, 5, true)); + $handlers = $this->dispatcher->getPidHandlers($pid); + self::assertTrue($handlers->contains($handler)); + self::assertEquals(5, $handlers->priorityOf($handler)); + + $this->dispatcher->detachPidHandler($pid, $handler, 5); + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + } + + // End the PID + self::assertTrue(TProcessHelper::kill($pid)); // kill pid despite sleeping. + proc_close($process); + + + // detach dispatcher + $this->dispatcher->detach(); + + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + self::assertNull($this->dispatcher->getPidHandlers($pid, true)); + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + + self::assertInstanceOf(TWeakCallableCollection::class, $handlers = $this->dispatcher->getPidHandlers($pid)); + self::assertTrue($this->dispatcher->hasPidHandler($pid)); + self::assertTrue($this->dispatcher->clearPidHandlers($pid)); + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + + self::assertFalse($this->dispatcher->attachPidHandler($pid, $handler, 5, true)); + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + self::assertTrue($this->dispatcher->attachPidHandler($pid, $handler)); + self::assertTrue($this->dispatcher->clearPidHandlers($pid)); + self::assertFalse($this->dispatcher->clearPidHandlers($pid)); + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + } + + public function testRaiseEvent() + { + $this->dispatcher = new TTestSignalsDispatcher(); + + $sentParam = null; + $handler = function ($sender, $param) use (&$sentParam) { + $sentParam = $param; + }; + $param = new TSignalParameter(); + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + $subscription = new TEventSubscription($this->dispatcher, $event, $handler); + $this->dispatcher->raiseEvent($signal, $this->dispatcher, $param); + self::assertEquals(strtolower($event), $sentParam->getEventName()); + } + } + + public function testInvoke() + { + // Preset custom handlers, stored on attach, to be restored on detach + $ogHandler = new TTestSignalInvokable(8); + + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + pcntl_signal($signal, $ogHandler); + } + + $this->dispatcher = new TTestSignalsDispatcher(); + + self::assertNull(($this->dispatcher)(-555)); + + $sentSender = null; + $sentParam = null; + $handler = function ($sender, $param) use (&$sentSender, &$sentParam) { + $sentSender = $sender; + $sentParam = $param; + }; + $param = new TSignalParameter(); + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + $exception = false; + $subscription = new TEventSubscription($this->dispatcher, $signal, $handler); + try { + ($this->dispatcher)($signal, $ref = ['uid' => 13]); + } catch(TExitException $e) { + $exception = true; + } + if ($exception || isset(TSignalsDispatcher::EXIT_SIGNALS[$signal])) { + if (isset(TSignalsDispatcher::EXIT_SIGNALS[$signal]) && !$exception) { + self::fail("TExitException not thrown on exiting signal"); + } elseif (!isset(TSignalsDispatcher::EXIT_SIGNALS[$signal]) && $exception) { + self::fail("TExitException thrown when not exiting during $event"); + } + } + self::assertEquals($this->dispatcher, $sentSender); + self::assertEquals(strtolower($event), $sentParam->getEventName()); + + if ($signal !== SIGALRM) { + self::assertEquals($signal, $ogHandler->signal, "$event did not signal"); + self::assertEquals($ref, $ogHandler->sigInfo); + } + } + + $this->dispatcher->detach(); + + // reset signal handlers + foreach(TSignalsDispatcher::SIGNAL_MAP as $signal => $event) { + pcntl_signal($signal, SIG_DFL); + } + } + + public function testInitialAttachAlarm() + { + if (!TSignalsDispatcher::hasSignals()) { + $this->markTestSkipped("skipping " . TSignalsDispatcher::class . "::alarm and ::disarm."); + return; + } + + $originalAlarm = pcntl_signal_get_handler(SIGALRM); + + // Preset custom handlers, stored on attach, to be restored on detach + $ogHandler = new TTestSignalInvokable(3); + pcntl_signal(SIGALRM, $ogHandler); + pcntl_alarm(3); + $now = time(); + + $this->dispatcher = new TTestSignalsDispatcher(); + + $alarmTime = TTestSignalsDispatcher::alarm(); + self::assertTrue($alarmTime == ($now + 3) || $alarmTime == ($now + 2)); + + self::assertEquals($alarmTime, TTestSignalsDispatcher::disarm($ogHandler, $alarmTime)); + + self::assertNull(TTestSignalsDispatcher::alarm()); + + $alarm1 = null; + $now = time(); + $time1 = TTestSignalsDispatcher::alarm(4, $f1 = function ($sender, $param) use (&$alarm1) {$alarm1 = $param;}); + self::assertTrue($time1 === $now + 4 || $time1 === $now + 5); + self::assertEquals($time1, TTestSignalsDispatcher::alarm()); + + $now = time(); + $time2 = TTestSignalsDispatcher::alarm(2, $f2 = function ($sender, $param) use (&$alarm1) {$alarm1 = $param;}); + self::assertTrue($time2 === $now + 2 || $time2 === $now + 3); + self::assertEquals($time2, TTestSignalsDispatcher::alarm()); + + self::assertEquals($time2, TTestSignalsDispatcher::disarm($f2)); + self::assertEquals($time1, TTestSignalsDispatcher::alarm()); + self::assertEquals($time1, TTestSignalsDispatcher::disarm($f1)); + + $this->dispatcher->detach(); + + // restore + pcntl_signal(SIGALRM, $originalAlarm); + } + + public function testAlarmDisarm() + { + if (!TSignalsDispatcher::hasSignals()) { + $this->markTestSkipped("skipping " . TSignalsDispatcher::class . "::alarm and ::disarm."); + return; + } + + self::assertEquals(SIG_DFL, pcntl_signal_get_handler(SIGALRM)); + + // Preset custom handlers, stored on attach, to be restored on detach + $handler = new TTestSignalInvokable(5); + pcntl_signal(SIGALRM, $handler); + + $this->dispatcher = new TTestSignalsDispatcher(); + + // no initial alarm. + self::assertNull(TTestSignalsDispatcher::alarm()); + + $alarm1 = null; + $alarmTime1 = TTestSignalsDispatcher::alarm(5, $f1 = function($sender, $param) use (&$alarm1) {$alarm1 = $param;} ); + $now = time(); + self::assertEquals($alarmTime1, TTestSignalsDispatcher::alarm()); + self::assertTrue($alarmTime1 === $now + 5 || $alarmTime1 === $now + 4); + + $alarm2 = null; + $alarmTime2 = TTestSignalsDispatcher::alarm(2, $f2 = function($sender, $param) use (&$alarm2) {$alarm2 = $param;} ); + $now = time(); + self::assertEquals($alarmTime2, TTestSignalsDispatcher::alarm()); + self::assertTrue($alarmTime2 === $now + 2 || $alarmTime2 === $now + 1); + self::assertGreaterThan($alarmTime2, $alarmTime1); + + self::assertNull(TTestSignalsDispatcher::disarm(-1, $f1)); + + self::assertEquals($alarmTime2, TTestSignalsDispatcher::disarm($f2)); + + self::assertEquals($alarmTime1, TTestSignalsDispatcher::alarm()); + + self::assertEquals($alarmTime1, TTestSignalsDispatcher::disarm($f1)); + + self::assertNull(TTestSignalsDispatcher::alarm()); + + $now = time(); + $alarmTime3 = TTestSignalsDispatcher::alarm(2); + + self::assertTrue($alarmTime3 === $now + 1 || $alarmTime3 === $now + 2); + self::assertEquals($alarmTime3, TTestSignalsDispatcher::alarm()); + + self::assertEquals($alarmTime3, TTestSignalsDispatcher::disarm($alarmTime3)); + + self::assertNull(TTestSignalsDispatcher::alarm()); + } + + public function testRing() + { + if (!TSignalsDispatcher::hasSignals()) { + $this->markTestSkipped("skipping " . TSignalsDispatcher::class . "::ring."); + return; + } + + $this->dispatcher = new TTestSignalsDispatcher(); + + $called = []; + $handler = function ($sender, $parameter) use (&$called) { + $called[] = $parameter->getAlarmTime(); + }; + + $time = $this->dispatcher->setupAlarms($handler); + + $param = new TSignalParameter(SIGALRM); + $this->dispatcher->ring($this->dispatcher, $param); + + self::assertLessThan(2, $called[0] - $time); + self::assertLessThan(2, $called[1] - $time); + self::assertEquals(2, count($called)); + } + + public function testDelegateChild() + { + $this->dispatcher = new TTestSignalsDispatcher(); + + $this->dispatcher->delegateChild($this->dispatcher, null); + + $param = new TSignalParameter(SIGCHLD); + $this->dispatcher->delegateChild($this->dispatcher, $param); + + $param->setParameter(['pid' => $pid = 55, 'code' => 0]); + $this->dispatcher->delegateChild($this->dispatcher, $param); + + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + + $called = false; + $this->dispatcher->attachPidHandler($pid, function($sender, $param) use (&$called) {$called = true;}); + + self::assertTrue($this->dispatcher->hasPidHandler($pid)); + + $this->dispatcher->delegateChild($this->dispatcher, $param); + + self::assertTrue($called); + self::assertTrue($this->dispatcher->hasPidHandler($pid)); + + $called = false; + + $param->setParameter(['pid' => $pid = 55, 'code' => 1]); + $this->dispatcher->delegateChild($this->dispatcher, $param); + + self::assertTrue($called); + self::assertFalse($this->dispatcher->hasPidHandler($pid)); + } + + public function testSyncDispatch() + { + if (!TTestSignalsDispatcher::hasSignals()) { + self::assertNull(TTestSignalsDispatcher::syncDispatch()); + return; + } + $this->dispatcher = new TTestSignalsDispatcher(); + + $called = false; + $sub = new TEventSubscription($this->dispatcher, SIGUSR1, function($sender, $param) use (&$called) {$called = true;}); + + $originalASync = TTestSignalsDispatcher::setAsyncSignals(false); + + self::assertTrue(TProcessHelper::sendSignal(SIGUSR1)); + self::assertFalse($called); + + self::assertTrue(TTestSignalsDispatcher::syncDispatch()); + self::assertTrue($called); + $called = false; + + self::assertTrue(TProcessHelper::sendSignal(SIGUSR1)); + self::assertFalse($called); + + self::assertFalse(TTestSignalsDispatcher::setAsyncSignals(true)); + self::assertTrue($called); + + TTestSignalsDispatcher::setAsyncSignals($originalASync); + } + + public function testAsyncSignals() + { + $oAsyncSignals = TTestSignalsDispatcher::getAsyncSignals(); + $originalASyncSignals = TTestSignalsDispatcher::setAsyncSignals(true); + + self::assertEquals($oAsyncSignals, $originalASyncSignals); + + if (!TSignalsDispatcher::hasSignals()) { + self::assertNull(TTestSignalsDispatcher::getAsyncSignals()); + self::assertNull(TTestSignalsDispatcher::setAsyncSignals(false)); + self::assertNull(TTestSignalsDispatcher::setAsyncSignals(true)); + } else { + self::assertTrue(TTestSignalsDispatcher::getAsyncSignals()); + self::assertTrue(TTestSignalsDispatcher::setAsyncSignals(false)); + self::assertFalse(TTestSignalsDispatcher::getAsyncSignals()); + self::assertFalse(TTestSignalsDispatcher::setAsyncSignals(true)); + self::assertTrue(TTestSignalsDispatcher::setAsyncSignals($originalASyncSignals)); + } + } + + public function testPriorHandlerPriority() + { + self::assertNull(TTestSignalsDispatcher::getPriorHandlerPriority()); + + TTestSignalsDispatcher::setPriorHandlerPriority(5); + self::assertEquals(5, TTestSignalsDispatcher::getPriorHandlerPriority()); + + TTestSignalsDispatcher::setPriorHandlerPriority(15); + self::assertEquals(15, TTestSignalsDispatcher::getPriorHandlerPriority()); + + TTestSignalsDispatcher::setPriorHandlerPriority(0); + self::assertEquals(0, TTestSignalsDispatcher::getPriorHandlerPriority()); + + TTestSignalsDispatcher::setPriorHandlerPriority(null); + self::assertNull(TTestSignalsDispatcher::getPriorHandlerPriority()); + } + +} + diff --git a/tests/unit/Util/TSysLogRouteTest.php b/tests/unit/Util/TSysLogRouteTest.php index 4476e04c5..29a79b888 100644 --- a/tests/unit/Util/TSysLogRouteTest.php +++ b/tests/unit/Util/TSysLogRouteTest.php @@ -51,8 +51,7 @@ public function testSysLogFacilities() $this->assertEquals(LOG_USER, $route->getFacility()); - //if (TProcessHelper::isSystemWindows()) { - if (strncasecmp(php_uname('s'), 'win', 3) === 0) { + if (\Prado\Util\Helpers\TProcessHelper::isSystemWindows()) { return; }