Skip to content

Commit

Permalink
feature #53968 [Process] allow to ignore signals when executing a pro…
Browse files Browse the repository at this point in the history
…cess (joelwurtz)

This PR was merged into the 7.1 branch.

Discussion
----------

[Process] allow to ignore signals when executing a process

| Q             | A
| ------------- | ---
| Branch?       | 7.1
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        |
| License       | MIT

This PR allow to a process to ignore signals dispatched by PHP

This can be useful when using symfony messenger where the handler would execute a Process and receives a SIGTERM, actually the handler would fail with a `ProcessSignaledException` instead of waiting for the handler to terminate and gracefully shutdown.

Please note that ignoring a signal will also disallow to send this signal with the `posix_kill` function

Commits
-------

7bb6ecf feat(process): allow to ignore signals when executing a process
  • Loading branch information
fabpot committed Apr 5, 2024
2 parents fdedd3b + 7bb6ecf commit 4c1d8eb
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/Symfony/Component/Process/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.1
---

* Add `Process::setIgnoredSignals()` to disable signal propagation to the child process

6.4
---

Expand Down
34 changes: 34 additions & 0 deletions src/Symfony/Component/Process/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class Process implements \IteratorAggregate
private bool $tty = false;
private bool $pty;
private array $options = ['suppress_errors' => true, 'bypass_shell' => true];
private array $ignoredSignals = [];

private WindowsPipes|UnixPipes $processPipes;

Expand Down Expand Up @@ -346,9 +347,23 @@ public function start(?callable $callback = null, array $env = []): void

return true;
});

$oldMask = [];

if (\function_exists('pcntl_sigprocmask')) {
// we block signals we want to ignore, as proc_open will use fork / posix_spawn which will copy the signal mask this allow to block
// signals in the child process
pcntl_sigprocmask(\SIG_BLOCK, $this->ignoredSignals, $oldMask);
}

try {
$process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
} finally {
if (\function_exists('pcntl_sigprocmask')) {
// we restore the signal mask here to avoid any side effects
pcntl_sigprocmask(\SIG_SETMASK, $oldMask);
}

restore_error_handler();
}

Expand Down Expand Up @@ -1206,6 +1221,20 @@ public function setOptions(array $options): void
}
}

/**
* Defines a list of posix signals that will not be propagated to the process.
*
* @param list<\SIG*> $signals
*/
public function setIgnoredSignals(array $signals): void
{
if ($this->isRunning()) {
throw new RuntimeException('Setting ignored signals while the process is running is not possible.');
}

$this->ignoredSignals = $signals;
}

/**
* Returns whether TTY is supported on the current operating system.
*/
Expand Down Expand Up @@ -1455,6 +1484,11 @@ private function resetProcessData(): void
*/
private function doSignal(int $signal, bool $throwException): bool
{
// Signal seems to be send when sigchild is enable, this allow blocking the signal correctly in this case
if ($this->isSigchildEnabled() && \in_array($signal, $this->ignoredSignals)) {
return false;
}

if (null === $pid = $this->getPid()) {
if ($throwException) {
throw new LogicException('Cannot send signal on a non running process.');
Expand Down
30 changes: 30 additions & 0 deletions src/Symfony/Component/Process/Tests/ProcessTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,36 @@ public function testNotTerminableInputPipe()
$this->assertFalse($process->isRunning());
}

public function testIgnoringSignal()
{
if (!\function_exists('pcntl_signal')) {
$this->markTestSkipped('pnctl extension is required.');
}

$process = $this->getProcess('sleep 10');
$process->setIgnoredSignals([\SIGTERM]);

$process->start();
$process->stop(timeout: 0.2);

$this->assertNotSame(\SIGTERM, $process->getTermSignal());
}

// This test ensure that the previous test is reliable, in case of the sleep command ignoring the SIGTERM signal
public function testNotIgnoringSignal()
{
if (!\function_exists('pcntl_signal')) {
$this->markTestSkipped('pnctl extension is required.');
}

$process = $this->getProcess('sleep 10');

$process->start();
$process->stop(timeout: 0.2);

$this->assertSame(\SIGTERM, $process->getTermSignal());
}

private function getProcess(string|array $commandline, ?string $cwd = null, ?array $env = null, mixed $input = null, ?int $timeout = 60): Process
{
if (\is_string($commandline)) {
Expand Down

0 comments on commit 4c1d8eb

Please sign in to comment.