From a9bb93b437c4ebb719ff46e33623aaa2139514cd Mon Sep 17 00:00:00 2001 From: Andrei O Date: Wed, 8 Jul 2020 00:04:30 +0300 Subject: [PATCH] [Process] allow setting options esp. "create_new_console" to detach a subprocess --- CHANGELOG.md | 7 ++++++ Process.php | 38 ++++++++++++++++++++++++---- Tests/CreateNewConsoleTest.php | 45 ++++++++++++++++++++++++++++++++++ Tests/ThreeSecondProcess.php | 14 +++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 Tests/CreateNewConsoleTest.php create mode 100644 Tests/ThreeSecondProcess.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f3a0202..31b9ee6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +5.2.0 +----- + + * added `Process::setOptions()` to set `Process` specific options + * added option `create_new_console` to allow a subprocess to continue + to run after the main script exited, both on Linux and on Windows + 5.1.0 ----- diff --git a/Process.php b/Process.php index e1efb9cc..5dd38c4b 100644 --- a/Process.php +++ b/Process.php @@ -71,6 +71,7 @@ class Process implements \IteratorAggregate private $incrementalErrorOutputOffset = 0; private $tty = false; private $pty; + private $options = ['suppress_errors' => true, 'bypass_shell' => true]; private $useFileHandles = false; /** @var PipesInterface */ @@ -196,7 +197,11 @@ public static function fromShellCommandline(string $command, string $cwd = null, public function __destruct() { - $this->stop(0); + if ($this->options['create_new_console'] ?? false) { + $this->processPipes->close(); + } else { + $this->stop(0); + } } public function __clone() @@ -303,10 +308,7 @@ public function start(callable $callback = null, array $env = []) $commandline = $this->replacePlaceholders($commandline, $env); } - $options = ['suppress_errors' => true]; - if ('\\' === \DIRECTORY_SEPARATOR) { - $options['bypass_shell'] = true; $commandline = $this->prepareWindowsCommandLine($commandline, $env); } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild @@ -332,7 +334,7 @@ public function start(callable $callback = null, array $env = []) throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } - $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $options); + $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); if (!\is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); @@ -1220,6 +1222,32 @@ public function getStartTime(): float return $this->starttime; } + /** + * Defines options to pass to the underlying proc_open(). + * + * @see https://php.net/proc_open for the options supported by PHP. + * + * Enabling the "create_new_console" option allows a subprocess to continue + * to run after the main process exited, on both Windows and *nix + */ + public function setOptions(array $options) + { + if ($this->isRunning()) { + throw new RuntimeException('Setting options while the process is running is not possible.'); + } + + $defaultOptions = $this->options; + $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console']; + + foreach ($options as $key => $value) { + if (!\in_array($key, $existingOptions)) { + $this->options = $defaultOptions; + throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); + } + $this->options[$key] = $value; + } + } + /** * Returns whether TTY is supported on the current operating system. */ diff --git a/Tests/CreateNewConsoleTest.php b/Tests/CreateNewConsoleTest.php new file mode 100644 index 00000000..4d43fb8d --- /dev/null +++ b/Tests/CreateNewConsoleTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; + +/** + * @author Andrei Olteanu + */ +class CreateNewConsoleTest extends TestCase +{ + public function testOptionCreateNewConsole() + { + $this->expectNotToPerformAssertions(); + try { + $process = new Process(['php', __DIR__.'/ThreeSecondProcess.php']); + $process->setOptions(['create_new_console' => true]); + $process->disableOutput(); + $process->start(); + } catch (\Exception $e) { + $this->fail($e); + } + } + + public function testItReturnsFastAfterStart() + { + // The started process must run in background after the main has finished but that can't be tested with PHPUnit + $startTime = microtime(true); + $process = new Process(['php', __DIR__.'/ThreeSecondProcess.php']); + $process->setOptions(['create_new_console' => true]); + $process->disableOutput(); + $process->start(); + $this->assertLessThan(3000, $startTime - microtime(true)); + } +} diff --git a/Tests/ThreeSecondProcess.php b/Tests/ThreeSecondProcess.php new file mode 100644 index 00000000..e483b4b9 --- /dev/null +++ b/Tests/ThreeSecondProcess.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +echo 'Worker started'; +sleep(3); +echo 'Worker done';