Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 105 additions & 2 deletions system/CLI/SignalTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@
/**
* Signal Trait
*
* Provides PCNTL signal handling capabilities for CLI commands.
* Requires the PCNTL extension (Unix only).
* Provides signal handling capabilities for CLI commands.
*
* On Unix it requires the PCNTL extension. On Windows, where PCNTL does not
* exist, only Ctrl+C and Ctrl+Break are supported, through
* `sapi_windows_set_ctrl_handler()`. Both are reported as SIGINT (2).
*/
trait SignalTrait
{
/**
* The number of SIGINT on every platform that defines it. The SIG*
* constants come from the PCNTL extension, which Windows does not have.
* Ctrl+C and Ctrl+Break are reported to handlers as this value.
*/
private const INTERRUPT_SIGNAL = 2;

/**
* Whether the process should continue running (false = termination requested).
*/
Expand All @@ -47,6 +57,11 @@ trait SignalTrait
*/
private array $signalMethodMap = [];

/**
* Whether the Windows console control handler is registered.
*/
private bool $windowsCtrlHandlerRegistered = false;

/**
* Cached result of PCNTL extension availability.
*/
Expand Down Expand Up @@ -86,9 +101,38 @@ protected function isPosixAvailable(): bool
return self::$isPosixAvailable;
}

/**
* Check if the Windows console control handler is available.
*
* It is the Windows counterpart of PCNTL, and handles Ctrl+C and Ctrl+Break.
*/
protected function isWindowsCtrlHandlerAvailable(): bool
{
return is_windows() && function_exists('sapi_windows_set_ctrl_handler');
}

/**
* Add or remove the Windows console control handler.
*
* Wraps `sapi_windows_set_ctrl_handler()` so it can be replaced in tests,
* as the function is only available on Windows.
*
* @param callable(int): void $handler
*/
protected function setWindowsCtrlHandler(callable $handler, bool $add): bool
{
// Static analysis on other platforms does not know the function.
/** @psalm-suppress UndefinedFunction */
return sapi_windows_set_ctrl_handler($handler, $add); // @codeCoverageIgnore
}

/**
* Register signal handlers.
*
* On Windows only Ctrl+C and Ctrl+Break can be handled. They are reported
* as SIGINT (2), and the SIG* constants are not available there, so call it
* without arguments or pass `2`. Any other signal is ignored.
*
* @param list<int> $signals List of signals to handle
* @param array<int, string> $methodMap Optional signal-to-method mapping
*/
Expand All @@ -97,6 +141,10 @@ protected function registerSignals(
array $methodMap = [],
): void {
if (! $this->isPcntlAvailable()) {
if ($this->isWindowsCtrlHandlerAvailable()) {
$this->registerWindowsCtrlHandler($signals, $methodMap);
}

return;
}

Expand Down Expand Up @@ -131,6 +179,46 @@ protected function registerSignals(
}
}

/**
* Register the handler for Ctrl+C and Ctrl+Break on Windows.
*
* @param list<int> $signals List of signals to handle
* @param array<int, string> $methodMap Optional signal-to-method mapping
*/
private function registerWindowsCtrlHandler(array $signals, array $methodMap): void
{
if ($signals !== [] && ! in_array(self::INTERRUPT_SIGNAL, $signals, true)) {
return;
}

$this->signalMethodMap = $methodMap;

if ($this->windowsCtrlHandlerRegistered) {
return;
}

if ($this->setWindowsCtrlHandler([$this, 'handleWindowsCtrlEvent'], true)) {
$this->windowsCtrlHandlerRegistered = true;
$this->registeredSignals[] = self::INTERRUPT_SIGNAL;
} else {
CLI::write(lang('CLI.signals.failedSignal', [$this->getSignalName(self::INTERRUPT_SIGNAL)]), 'red');
}
}

/**
* Handle a Windows console control event (Ctrl+C or Ctrl+Break).
*
* Windows hands the handler no signal number, so the event is reported as SIGINT.
* Like on Unix, the process is no longer stopped by the event, and the command
* is expected to check `shouldTerminate()` and exit by itself.
*/
protected function handleWindowsCtrlEvent(int $event): void
{
$this->callCustomHandler(self::INTERRUPT_SIGNAL);

$this->running = false;
}

/**
* Handle incoming signals.
*/
Expand Down Expand Up @@ -302,6 +390,11 @@ protected function mapSignal(int $signal, string $method): void
*/
protected function getSignalName(int $signal): string
{
// The SIG* constants come from PCNTL, which Windows does not have.
if (! $this->isPcntlAvailable()) {
return $signal === self::INTERRUPT_SIGNAL ? 'SIGINT' : "Signal {$signal}";
}

return match ($signal) {
SIGTERM => 'SIGTERM',
SIGINT => 'SIGINT',
Expand All @@ -322,6 +415,16 @@ protected function getSignalName(int $signal): string
*/
protected function unregisterSignals(): void
{
if ($this->windowsCtrlHandlerRegistered) {
$this->setWindowsCtrlHandler([$this, 'handleWindowsCtrlEvent'], false);

$this->windowsCtrlHandlerRegistered = false;
$this->registeredSignals = [];
$this->signalMethodMap = [];

return;
}

if (! $this->isPcntlAvailable()) {
return;
}
Expand Down
61 changes: 61 additions & 0 deletions tests/_support/Commands/Legacy/SignalCommandWindows.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

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

namespace Tests\Support\Commands\Legacy;

/**
* Mock command that simulates Windows: no PCNTL or POSIX extension, but a console
* control handler. The handler is recorded instead of being installed,
* as `sapi_windows_set_ctrl_handler()` only exists on Windows.
*/
class SignalCommandWindows extends SignalCommand
{
/**
* Whether the simulated `sapi_windows_set_ctrl_handler()` succeeds.
*/
public bool $ctrlHandlerSucceeds = true;

/**
* The handler the command tried to add (`true`) or remove (`false`), in order.
*
* @var list<array{handler: callable, add: bool}>
*/
public array $ctrlHandlerCalls = [];

protected function isPcntlAvailable(): bool
{
return false;
}

protected function isPosixAvailable(): bool
{
return false;
}

protected function isWindowsCtrlHandlerAvailable(): bool
{
return true;
}

protected function setWindowsCtrlHandler(callable $handler, bool $add): bool
{
$this->ctrlHandlerCalls[] = ['handler' => $handler, 'add' => $add];

return $this->ctrlHandlerSucceeds;
}

public function testHandleWindowsCtrlEvent(int $event): void
{
$this->handleWindowsCtrlEvent($event);
}
}
Loading
Loading