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
3declare(strict_types=1);
4
5namespace 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 */
20final 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}