Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
DefaultSignalHandler
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
5 / 5
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 signals
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 terminateWorkers
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 propagationSignal
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of fast-forward/fork.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/fork
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Fork\Signal;
20
21use FastForward\Fork\Manager\ForkManagerInterface;
22
23/**
24 * Propagates termination-oriented signals to workers managed by the master process.
25 *
26 * This handler is intended to provide a default shutdown strategy for signals that
27 * require worker termination or graceful process unwinding.
28 */
29 class DefaultSignalHandler implements SignalHandlerInterface
30{
31    /**
32     * Defines the default signals subscribed by the handler.
33     *
34     * @var array<int, Signal> signals listened to by default
35     */
36    public const array DEFAULT_SIGNALS = [Signal::Interrupt, Signal::Terminate, Signal::Quit];
37
38    /**
39     * Indicates whether the handler is already processing a signal.
40     *
41     * This flag prevents re-entrant shutdown handling when multiple signals are
42     * received during an ongoing termination sequence.
43     */
44    private bool $handling = false;
45
46    /**
47     * Creates a new default signal handler instance.
48     *
49     * @param array<int, Signal> $signals signals to subscribe to on the manager process
50     * @param bool $waitForWorkers whether the master process should wait for workers after propagation
51     * @param bool $exitOnSignal whether the master process should exit after handling the signal
52     * @param Signal $escalationSignal signal used when a second signal interrupts an in-progress shutdown
53     */
54    public function __construct(
55        private  array $signals = self::DEFAULT_SIGNALS,
56        private  bool $waitForWorkers = true,
57        private  bool $exitOnSignal = true,
58        private  Signal $escalationSignal = Signal::Kill,
59    ) {}
60
61    /**
62     * Returns the signals subscribed by this handler.
63     *
64     * @return array<int, Signal> the configured list of subscribed signals
65     */
66    public function signals(): array
67    {
68        return $this->signals;
69    }
70
71    /**
72     * Handles a received signal for the current manager context.
73     *
74     * When invoked in the master process, the handler propagates an appropriate
75     * signal to managed workers. If signal handling is already in progress, the
76     * escalation signal is sent instead to accelerate shutdown.
77     *
78     * @param ForkManagerInterface $manager the fork manager receiving the signal
79     * @param Signal $signal the signal that triggered the handler
80     */
81    public function __invoke(ForkManagerInterface $manager, Signal $signal): void
82    {
83        $mustExit = $manager->isWorker() || $this->exitOnSignal;
84
85        if ($this->handling) {
86            $this->terminateWorkers($manager, $this->escalationSignal);
87
88            // @codeCoverageIgnoreStart
89            if ($mustExit) {
90                exit($signal->exitStatus());
91            }
92
93            // @codeCoverageIgnoreEnd
94
95            return;
96        }
97
98        $this->handling = true;
99
100        try {
101            $this->terminateWorkers($manager, $this->propagationSignal($signal));
102
103            if ($this->waitForWorkers && $manager->isMaster()) {
104                $manager->wait();
105            }
106        } finally {
107            $this->handling = false;
108        }
109
110        // @codeCoverageIgnoreStart
111        if ($mustExit) {
112            exit($signal->exitStatus());
113        }
114
115        // @codeCoverageIgnoreEnd
116    }
117
118    /**
119     * Sends the provided signal to all workers managed by the master process.
120     *
121     * No action is taken when the current process is not the master process.
122     *
123     * @param ForkManagerInterface $manager the manager responsible for worker coordination
124     * @param Signal $signal the signal to send to managed workers
125     */
126    private function terminateWorkers(ForkManagerInterface $manager, Signal $signal): void
127    {
128        if (! $manager->isMaster()) {
129            return;
130        }
131
132        $manager->kill($signal);
133    }
134
135    /**
136     * Converts interactive termination signals to the signal used for worker propagation.
137     *
138     * Interrupt, quit, and terminate signals are normalized to a graceful termination
139     * request for workers. Any other signal is returned unchanged.
140     *
141     * @param Signal $signal the original signal received by the handler
142     *
143     * @return Signal the normalized signal to propagate to workers
144     */
145    private function propagationSignal(Signal $signal): Signal
146    {
147        return match ($signal) {
148            Signal::Interrupt, Signal::Quit, Signal::Terminate => Signal::Terminate,
149            default => $signal,
150        };
151    }
152}