1<?php declare(strict_types=1);
2
3/*
4 * This file is part of the Monolog package.
5 *
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Monolog;
13
14use Psr\Log\LoggerInterface;
15use Psr\Log\LogLevel;
16use ReflectionExtension;
17
18/**
19 * Monolog POSIX signal handler
20 *
21 * @author Robert Gust-Bardon <robert@gust-bardon.org>
22 *
23 * @phpstan-import-type Level from \Monolog\Logger
24 * @phpstan-import-type LevelName from \Monolog\Logger
25 */
26class SignalHandler
27{
28    /** @var LoggerInterface */
29    private $logger;
30
31    /** @var array<int, callable|string|int> SIG_DFL, SIG_IGN or previous callable */
32    private $previousSignalHandler = [];
33    /** @var array<int, int> */
34    private $signalLevelMap = [];
35    /** @var array<int, bool> */
36    private $signalRestartSyscalls = [];
37
38    public function __construct(LoggerInterface $logger)
39    {
40        $this->logger = $logger;
41    }
42
43    /**
44     * @param  int|string $level           Level or level name
45     * @param  bool       $callPrevious
46     * @param  bool       $restartSyscalls
47     * @param  bool|null  $async
48     * @return $this
49     *
50     * @phpstan-param Level|LevelName|LogLevel::* $level
51     */
52    public function registerSignalHandler(int $signo, $level = LogLevel::CRITICAL, bool $callPrevious = true, bool $restartSyscalls = true, ?bool $async = true): self
53    {
54        if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) {
55            return $this;
56        }
57
58        $level = Logger::toMonologLevel($level);
59
60        if ($callPrevious) {
61            $handler = pcntl_signal_get_handler($signo);
62            $this->previousSignalHandler[$signo] = $handler;
63        } else {
64            unset($this->previousSignalHandler[$signo]);
65        }
66        $this->signalLevelMap[$signo] = $level;
67        $this->signalRestartSyscalls[$signo] = $restartSyscalls;
68
69        if ($async !== null) {
70            pcntl_async_signals($async);
71        }
72
73        pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls);
74
75        return $this;
76    }
77
78    /**
79     * @param mixed $siginfo
80     */
81    public function handleSignal(int $signo, $siginfo = null): void
82    {
83        static $signals = [];
84
85        if (!$signals && extension_loaded('pcntl')) {
86            $pcntl = new ReflectionExtension('pcntl');
87            // HHVM 3.24.2 returns an empty array.
88            foreach ($pcntl->getConstants() ?: get_defined_constants(true)['Core'] as $name => $value) {
89                if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) {
90                    $signals[$value] = $name;
91                }
92            }
93        }
94
95        $level = $this->signalLevelMap[$signo] ?? LogLevel::CRITICAL;
96        $signal = $signals[$signo] ?? $signo;
97        $context = $siginfo ?? [];
98        $this->logger->log($level, sprintf('Program received signal %s', $signal), $context);
99
100        if (!isset($this->previousSignalHandler[$signo])) {
101            return;
102        }
103
104        if ($this->previousSignalHandler[$signo] === SIG_DFL) {
105            if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch')
106                && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill')
107            ) {
108                $restartSyscalls = $this->signalRestartSyscalls[$signo] ?? true;
109                pcntl_signal($signo, SIG_DFL, $restartSyscalls);
110                pcntl_sigprocmask(SIG_UNBLOCK, [$signo], $oldset);
111                posix_kill(posix_getpid(), $signo);
112                pcntl_signal_dispatch();
113                pcntl_sigprocmask(SIG_SETMASK, $oldset);
114                pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls);
115            }
116        } elseif (is_callable($this->previousSignalHandler[$signo])) {
117            $this->previousSignalHandler[$signo]($signo, $siginfo);
118        }
119    }
120}
121