Lines
96.00%
24 / 25
Functions and Methods
80.00%
4 / 5
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| SignalStop | 96.00% | 24 / 25 | 80.00% | 4 / 5 | 13 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| __destruct | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 3 | |||||
| install | 91.66% | 11 / 12 | 0.00% | 0 / 1 | 3.01 | |||||
| received | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | |||||
| describe | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 4 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Tomaj\Hermes\Shutdown; | |
| 6 | ||
| 7 | /** | |
| 8 | * Graceful stop on SIGTERM and SIGINT while Dispatcher::handle() runs (needs ext-pcntl). | |
| 9 | * | |
| 10 | * The first signal only sets a flag. Signals are dispatched synchronously at the safe points | |
| 11 | * (after a message, and at each driver shutdown check), so the dispatcher stops after the current | |
| 12 | * message and an idle driver stops at its next check. Further signals are ignored until then; | |
| 13 | * SIGKILL still ends the process at once. | |
| 14 | * | |
| 15 | * Signals belong to the process, so the flag is static. The object returned by install() | |
| 16 | * restores the previous handlers and clears the flag when it is destroyed. | |
| 17 | * | |
| 18 | * @internal | |
| 19 | */ | |
| 20 | final class SignalStop | |
| 21 | { | |
| 22 | private static ?int $signal = null; | |
| 23 | ||
| 24 | private static bool $listening = false; | |
| 25 | ||
| 26 | /** @var array<int, callable|int> */ | |
| 27 | private array $previous = []; | |
| 28 | ||
| 29 | private bool $previousAsync = false; | |
| 30 | ||
| 31 | private function __construct() {} | |
| 32 | ||
| 33 | public function __destruct() | |
| 34 | { | |
| 35 | // a signal stops only the handle() call it arrived in; drivers used afterwards run normally | |
| 36 | self::$signal = null; | |
| 37 | foreach ($this->previous as $signal => $handler) { | |
| 38 | pcntl_signal($signal, $handler); | |
| 39 | } | |
| 40 | if ($this->previous !== []) { | |
| 41 | pcntl_async_signals($this->previousAsync); | |
| 42 | self::$listening = false; | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | public static function install(): self | |
| 47 | { | |
| 48 | self::$signal = null; | |
| 49 | $guard = new self(); | |
| 50 | if (!function_exists('pcntl_signal')) { | |
| 51 | return $guard; | |
| 52 | } | |
| 53 | ||
| 54 | // Synchronous delivery: received() dispatches at the safe points. With async delivery a | |
| 55 | // SIGTERM that arrives inside php-amqplib's socket wait was sometimes never handled. | |
| 56 | $guard->previousAsync = pcntl_async_signals(false); | |
| 57 | foreach ([SIGTERM, SIGINT] as $signal) { | |
| 58 | $guard->previous[$signal] = pcntl_signal_get_handler($signal); | |
| 59 | pcntl_signal($signal, static function (int $received): void { | |
| 60 | self::$signal ??= $received; | |
| 61 | }); | |
| 62 | } | |
| 63 | self::$listening = true; | |
| 64 | ||
| 65 | return $guard; | |
| 66 | } | |
| 67 | ||
| 68 | public static function received(): ?int | |
| 69 | { | |
| 70 | if (self::$listening) { | |
| 71 | pcntl_signal_dispatch(); | |
| 72 | } | |
| 73 | ||
| 74 | return self::$signal; | |
| 75 | } | |
| 76 | ||
| 77 | public static function describe(int $signal): string | |
| 78 | { | |
| 79 | return match ($signal) { | |
| 80 | SIGTERM => 'SIGTERM', | |
| 81 | SIGINT => 'SIGINT', | |
| 82 | default => "signal {$signal}", | |
| 83 | }; | |
| 84 | } | |
| 85 | } |