diff --git a/src/Symfony/Component/Process/CHANGELOG.md b/src/Symfony/Component/Process/CHANGELOG.md index e26819b5bca17..f7b68b5dcb489 100644 --- a/src/Symfony/Component/Process/CHANGELOG.md +++ b/src/Symfony/Component/Process/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `Process::setIgnoredSignals()` to disable signal propagation to the child process + 6.4 --- diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index c95afabc0a5f4..0b5932017790d 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -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; @@ -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(); } @@ -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. */ @@ -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.'); diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index d027cc507e783..653fa6d84abc2 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -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)) {