• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 20376548327

19 Dec 2025 04:48PM UTC coverage: 84.548% (-0.003%) from 84.551%
20376548327

push

github

web-flow
refactor: remove `ignore-platform-req` (#9847)

* refactor: remove `ignore-platform-req`

* Remove ignore-platform-req=php from composer update

* Remove PHP version check from composer.json

Removed a PHP version check from post-autoload-dump command.

21531 of 25466 relevant lines covered (84.55%)

197.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.55
/system/CLI/SignalTrait.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\CLI;
15

16
use Closure;
17

18
/**
19
 * Signal Trait
20
 *
21
 * Provides PCNTL signal handling capabilities for CLI commands.
22
 * Requires the PCNTL extension (Unix only).
23
 */
24
trait SignalTrait
25
{
26
    /**
27
     * Whether the process should continue running (false = termination requested).
28
     */
29
    private bool $running = true;
30

31
    /**
32
     * Whether signals are currently blocked.
33
     */
34
    private bool $signalsBlocked = false;
35

36
    /**
37
     * Array of registered signals.
38
     *
39
     * @var list<int>
40
     */
41
    private array $registeredSignals = [];
42

43
    /**
44
     * Signal-to-method mapping.
45
     *
46
     * @var array<int, string>
47
     */
48
    private array $signalMethodMap = [];
49

50
    /**
51
     * Cached result of PCNTL extension availability.
52
     */
53
    private static ?bool $isPcntlAvailable = null;
54

55
    /**
56
     * Cached result of POSIX extension availability.
57
     */
58
    private static ?bool $isPosixAvailable = null;
59

60
    /**
61
     * Check if PCNTL extension is available (cached).
62
     */
63
    protected function isPcntlAvailable(): bool
64
    {
65
        if (self::$isPcntlAvailable === null) {
21✔
66
            if (is_windows()) {
4✔
67
                self::$isPcntlAvailable = false;
×
68
            } else {
69
                self::$isPcntlAvailable = extension_loaded('pcntl');
4✔
70
                if (! self::$isPcntlAvailable) {
4✔
71
                    CLI::write(lang('CLI.signals.noPcntlExtension'), 'yellow');
×
72
                }
73
            }
74
        }
75

76
        return self::$isPcntlAvailable;
21✔
77
    }
78

79
    /**
80
     * Check if POSIX extension is available (cached).
81
     */
82
    protected function isPosixAvailable(): bool
83
    {
84
        if (self::$isPosixAvailable === null) {
8✔
85
            self::$isPosixAvailable = is_windows() ? false : extension_loaded('posix');
1✔
86
        }
87

88
        return self::$isPosixAvailable;
8✔
89
    }
90

91
    /**
92
     * Register signal handlers.
93
     *
94
     * @param list<int>          $signals   List of signals to handle
95
     * @param array<int, string> $methodMap Optional signal-to-method mapping
96
     */
97
    protected function registerSignals(
98
        array $signals = [],
99
        array $methodMap = [],
100
    ): void {
101
        if (! $this->isPcntlAvailable()) {
8✔
102
            return;
1✔
103
        }
104

105
        if ($signals === []) {
7✔
106
            $signals = [SIGTERM, SIGINT, SIGHUP, SIGQUIT];
×
107
        }
108

109
        if (! $this->isPosixAvailable() && (in_array(SIGTSTP, $signals, true) || in_array(SIGCONT, $signals, true))) {
7✔
110
            CLI::write(lang('CLI.signals.noPosixExtension'), 'yellow');
1✔
111
            $signals = array_diff($signals, [SIGTSTP, SIGCONT]);
1✔
112

113
            // Remove from method map as well
114
            unset($methodMap[SIGTSTP], $methodMap[SIGCONT]);
1✔
115

116
            if ($signals === []) {
1✔
117
                return;
×
118
            }
119
        }
120

121
        // Enable async signals for immediate response
122
        pcntl_async_signals(true);
7✔
123

124
        $this->signalMethodMap = $methodMap;
7✔
125

126
        foreach ($signals as $signal) {
7✔
127
            if (pcntl_signal($signal, [$this, 'handleSignal'])) {
7✔
128
                $this->registeredSignals[] = $signal;
7✔
129
            } else {
130
                $signal = $this->getSignalName($signal);
×
131
                CLI::write(lang('CLI.signals.failedSignal', [$signal]), 'red');
×
132
            }
133
        }
134
    }
135

136
    /**
137
     * Handle incoming signals.
138
     */
139
    protected function handleSignal(int $signal): void
140
    {
141
        $this->callCustomHandler($signal);
3✔
142

143
        // Apply standard Unix signal behavior for registered signals
144
        switch ($signal) {
145
            case SIGTERM:
3✔
146
            case SIGINT:
1✔
147
            case SIGQUIT:
×
148
            case SIGHUP:
×
149
                $this->running = false;
3✔
150
                break;
3✔
151

152
            case SIGTSTP:
×
153
                // Restore default handler and re-send signal to actually suspend
154
                pcntl_signal(SIGTSTP, SIG_DFL);
×
155
                posix_kill(posix_getpid(), SIGTSTP);
×
156
                break;
×
157

158
            case SIGCONT:
×
159
                // Re-register SIGTSTP handler after resume
160
                pcntl_signal(SIGTSTP, [$this, 'handleSignal']);
×
161
                break;
×
162
        }
163
    }
164

165
    /**
166
     * Call custom signal handler if one is mapped for this signal.
167
     * Falls back to generic onInterruption() method if no explicit mapping exists.
168
     */
169
    private function callCustomHandler(int $signal): void
170
    {
171
        // Check for explicit mapping first
172
        $method = $this->signalMethodMap[$signal] ?? null;
3✔
173

174
        if ($method !== null && method_exists($this, $method)) {
3✔
175
            $this->{$method}($signal);
1✔
176

177
            return;
1✔
178
        }
179

180
        // If no explicit mapping, try generic catch-all method
181
        if (method_exists($this, 'onInterruption')) {
2✔
182
            $this->onInterruption($signal);
2✔
183
        }
184
    }
185

186
    /**
187
     * Check if command should terminate.
188
     */
189
    protected function shouldTerminate(): bool
190
    {
191
        return ! $this->running;
3✔
192
    }
193

194
    /**
195
     * Check if the process is currently running (not terminated).
196
     */
197
    protected function isRunning(): bool
198
    {
199
        return $this->running;
2✔
200
    }
201

202
    /**
203
     * Request immediate termination.
204
     */
205
    protected function requestTermination(): void
206
    {
207
        $this->running = false;
2✔
208
    }
209

210
    /**
211
     * Reset all states (for testing or restart scenarios).
212
     */
213
    protected function resetState(): void
214
    {
215
        $this->running = true;
1✔
216

217
        // Unblock signals if they were blocked
218
        if ($this->signalsBlocked) {
1✔
219
            $this->unblockSignals();
×
220
        }
221
    }
222

223
    /**
224
     * Execute a callable with ALL signals blocked to prevent ANY interruption during critical operations.
225
     *
226
     * This blocks ALL interruptible signals including:
227
     * - Termination signals (SIGTERM, SIGINT, etc.)
228
     * - Pause/resume signals (SIGTSTP, SIGCONT)
229
     * - Custom signals (SIGUSR1, SIGUSR2)
230
     *
231
     * Only SIGKILL (unblockable) can still terminate the process.
232
     * Use this for database transactions, file operations, or any critical atomic operations.
233
     *
234
     * @template TReturn
235
     *
236
     * @param Closure():TReturn $operation
237
     *
238
     * @return TReturn
239
     */
240
    protected function withSignalsBlocked(Closure $operation)
241
    {
242
        $this->blockSignals();
12✔
243

244
        try {
245
            return $operation();
12✔
246
        } finally {
247
            $this->unblockSignals();
12✔
248
        }
249
    }
250

251
    /**
252
     * Block ALL interruptible signals during critical sections.
253
     * Only SIGKILL (unblockable) can terminate the process.
254
     */
255
    protected function blockSignals(): void
256
    {
257
        if (! $this->signalsBlocked && $this->isPcntlAvailable()) {
12✔
258
            // Block ALL signals that could interrupt critical operations
259
            pcntl_sigprocmask(SIG_BLOCK, [
12✔
260
                SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals
12✔
261
                SIGTSTP, SIGCONT,                 // Pause/resume signals
12✔
262
                SIGUSR1, SIGUSR2,                 // Custom signals
12✔
263
                SIGPIPE, SIGALRM,                 // Other common signals
12✔
264
            ]);
12✔
265
            $this->signalsBlocked = true;
12✔
266
        }
267
    }
268

269
    /**
270
     * Unblock previously blocked signals.
271
     */
272
    protected function unblockSignals(): void
273
    {
274
        if ($this->signalsBlocked && $this->isPcntlAvailable()) {
12✔
275
            // Unblock the same signals we blocked
276
            pcntl_sigprocmask(SIG_UNBLOCK, [
12✔
277
                SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals
12✔
278
                SIGTSTP, SIGCONT,                 // Pause/resume signals
12✔
279
                SIGUSR1, SIGUSR2,                 // Custom signals
12✔
280
                SIGPIPE, SIGALRM,                 // Other common signals
12✔
281
            ]);
12✔
282
            $this->signalsBlocked = false;
12✔
283
        }
284
    }
285

286
    /**
287
     * Check if signals are currently blocked.
288
     */
289
    protected function signalsBlocked(): bool
290
    {
291
        return $this->signalsBlocked;
2✔
292
    }
293

294
    /**
295
     * Add or update signal-to-method mapping at runtime.
296
     */
297
    protected function mapSignal(int $signal, string $method): void
298
    {
299
        $this->signalMethodMap[$signal] = $method;
1✔
300
    }
301

302
    /**
303
     * Get human-readable signal name.
304
     */
305
    protected function getSignalName(int $signal): string
306
    {
307
        return match ($signal) {
308
            SIGTERM => 'SIGTERM',
3✔
309
            SIGINT  => 'SIGINT',
3✔
310
            SIGHUP  => 'SIGHUP',
2✔
311
            SIGQUIT => 'SIGQUIT',
2✔
312
            SIGUSR1 => 'SIGUSR1',
2✔
313
            SIGUSR2 => 'SIGUSR2',
1✔
314
            SIGPIPE => 'SIGPIPE',
1✔
315
            SIGALRM => 'SIGALRM',
1✔
316
            SIGTSTP => 'SIGTSTP',
1✔
317
            SIGCONT => 'SIGCONT',
1✔
318
            default => "Signal {$signal}",
3✔
319
        };
320
    }
321

322
    /**
323
     * Unregister all signals (cleanup).
324
     */
325
    protected function unregisterSignals(): void
326
    {
327
        if (! $this->isPcntlAvailable()) {
1✔
328
            return;
×
329
        }
330

331
        foreach ($this->registeredSignals as $signal) {
1✔
332
            pcntl_signal($signal, SIG_DFL);
1✔
333
        }
334

335
        $this->registeredSignals = [];
1✔
336
        $this->signalMethodMap   = [];
1✔
337
    }
338

339
    /**
340
     * Check if signals are registered.
341
     */
342
    protected function hasSignals(): bool
343
    {
344
        return $this->registeredSignals !== [];
3✔
345
    }
346

347
    /**
348
     * Get list of registered signals.
349
     *
350
     * @return list<int>
351
     */
352
    protected function getSignals(): array
353
    {
354
        return $this->registeredSignals;
4✔
355
    }
356

357
    /**
358
     * Get comprehensive process state information.
359
     *
360
     * @return array{
361
     *      pid: int,
362
     *      running: bool,
363
     *      pcntl_available: bool,
364
     *      registered_signals: int,
365
     *      registered_signals_names: array<int, string>,
366
     *      signals_blocked: bool,
367
     *      explicit_mappings: int,
368
     *      memory_usage_mb: float,
369
     *      memory_peak_mb: float,
370
     *      session_id?: false|int,
371
     *      process_group?: false|int,
372
     *      has_controlling_terminal?: bool
373
     *  }
374
     */
375
    protected function getProcessState(): array
376
    {
377
        $pid   = getmypid();
4✔
378
        $state = [
4✔
379
            // Process identification
380
            'pid'     => $pid,
4✔
381
            'running' => $this->running,
4✔
382

383
            // Signal handling status
384
            'pcntl_available'          => $this->isPcntlAvailable(),
4✔
385
            'registered_signals'       => count($this->registeredSignals),
4✔
386
            'registered_signals_names' => array_map([$this, 'getSignalName'], $this->registeredSignals),
4✔
387
            'signals_blocked'          => $this->signalsBlocked,
4✔
388
            'explicit_mappings'        => count($this->signalMethodMap),
4✔
389

390
            // System resources
391
            'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
4✔
392
            'memory_peak_mb'  => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
4✔
393
        ];
4✔
394

395
        // Add terminal control info if POSIX extension is available
396
        if ($this->isPosixAvailable()) {
4✔
397
            $state['session_id']               = posix_getsid($pid);
4✔
398
            $state['process_group']            = posix_getpgid($pid);
4✔
399
            $state['has_controlling_terminal'] = posix_isatty(STDIN);
4✔
400
        }
401

402
        return $state;
4✔
403
    }
404
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc