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
1 change: 1 addition & 0 deletions docs/changelogs/v4.8.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ Release Date: Unreleased
- Likewise, the `command()` function now also supports the above enhancements for command-line option parsing when using the function to run commands from code.
- Added `make:request` generator command to scaffold [Form Request](../incoming/form_requests.md#form-requests) classes.
- Added `key:rotate` command to demote the current `encryption.key` to `encryption.previousKeys` in **.env** and generate a new key. See [Rotating with the `key:rotate` Command](../libraries/encryption.md#rotating-with-the-keyrotate-command).
- `SignalTrait` now handles Ctrl+C and Ctrl+Break on Windows, through `sapi_windows_set_ctrl_handler()`, so a command can stop gracefully there too. The events are reported as `SIGINT` (`2`). See [Windows](../cli/cli_signals.md#windows).
- Added `AbstractCommand::callSilently()` to invoke another command with its output discarded, restoring the prior output afterwards. See [Calling Silently](../cli/cli_modern_commands.md#calling-silently).
- The output side of the `CLI` class now lives in instance classes, with `CLI` delegating to them: `Output` (`print()`, `write()`, `error()`, `newLine()`, `deferNewLine()`, `beep()`, `clearScreen()`, `color()`, `wrap()`), `Terminal` (dimensions and color support), `ProgressBar`, `Table`, and `Color` (ANSI codes and `Color::width()`). `CLI::getOutput()` and `CLI::getTerminal()` return the shared instances. See [The Classes Behind the Facade](../cli/cli_library.md#the-classes-behind-the-facade).
- Modern commands can write through `$this->getOutput()`, the same `Output` used by the static `CLI` methods. `AbstractCommand::run()` and `Commands::runCommand()` accept an `Output` to run against, which `Console` passes down and sub-commands inherit. See [Writing Output](../cli/cli_modern_commands.md#writing-output).
Expand Down
16 changes: 14 additions & 2 deletions docs/cli/cli_signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,25 @@ These signals are handled directly by the kernel and will terminate or suspend y

Signal handling requires:

- **Unix-based system** (Linux, macOS, BSD) - Windows is not supported
- **Unix-based system** (Linux, macOS, BSD) - see [Windows](#windows) for Windows
- **PCNTL extension** - for signal registration and handling
- **POSIX extension** - required for pause/resume functionality (SIGTSTP/SIGCONT)

!!! note "Note"
On systems without these extensions, the SignalTrait will gracefully degrade and disable signal handling.

### Windows

Windows has no PCNTL extension and no Unix signals. Since v4.8.0, `SignalTrait` handles the one thing Windows can deliver to a PHP script: the console control events produced by **Ctrl+C** and **Ctrl+Break**. They are handled through PHP's `sapi_windows_set_ctrl_handler()` and reported to your command as `SIGINT` (`2`), so `isRunning()`, `shouldTerminate()`, custom handlers, and `onInterruption()` work as they do on Unix.

- Call `registerSignals()` without arguments. The `SIG*` constants come from the PCNTL extension, which is not available on Windows. If you pass a list, it must contain `2`. Every other signal is ignored.
- To map the event to your own method, use `2` as the key: `$this->registerSignals([], [2 => 'handleInterrupt']);`.
- `withSignalsBlocked()` runs the operation, but cannot block anything. The event does not stop the process by itself once a handler is registered, so a critical section is still completed. Your handler runs when the event arrives.
- Pause and resume (`SIGTSTP` / `SIGCONT`), and every other signal, do not exist on Windows.

!!! important "Important"
Once a handler is registered, Ctrl+C and Ctrl+Break no longer end the process. Your command must check `shouldTerminate()` and return, as on Unix.

## Using the SignalTrait

The `SignalTrait` provides a comprehensive signal handling system for CLI commands. To use it, simply add the trait to your command class and register signals in your command's `run()` method:
Expand Down Expand Up @@ -491,7 +503,7 @@ $this->registerSignals(
```

!!! note "Note"
Requires the PCNTL extension. On Windows, signal handling is automatically disabled.
Requires the PCNTL extension. On Windows, only Ctrl+C and Ctrl+Break are handled, as `SIGINT` (`2`). See [Windows](#windows).

#### isRunning()

Expand Down
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