Skip to content

Commit a9bb93b

Browse files
andrei0x309nicolas-grekas
authored andcommitted
[Process] allow setting options esp. "create_new_console" to detach a subprocess
1 parent d158a45 commit a9bb93b

File tree

4 files changed

+99
-5
lines changed

4 files changed

+99
-5
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
CHANGELOG
22
=========
33

4+
5.2.0
5+
-----
6+
7+
* added `Process::setOptions()` to set `Process` specific options
8+
* added option `create_new_console` to allow a subprocess to continue
9+
to run after the main script exited, both on Linux and on Windows
10+
411
5.1.0
512
-----
613

Process.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Process implements \IteratorAggregate
7171
private $incrementalErrorOutputOffset = 0;
7272
private $tty = false;
7373
private $pty;
74+
private $options = ['suppress_errors' => true, 'bypass_shell' => true];
7475

7576
private $useFileHandles = false;
7677
/** @var PipesInterface */
@@ -196,7 +197,11 @@ public static function fromShellCommandline(string $command, string $cwd = null,
196197

197198
public function __destruct()
198199
{
199-
$this->stop(0);
200+
if ($this->options['create_new_console'] ?? false) {
201+
$this->processPipes->close();
202+
} else {
203+
$this->stop(0);
204+
}
200205
}
201206

202207
public function __clone()
@@ -303,10 +308,7 @@ public function start(callable $callback = null, array $env = [])
303308
$commandline = $this->replacePlaceholders($commandline, $env);
304309
}
305310

306-
$options = ['suppress_errors' => true];
307-
308311
if ('\\' === \DIRECTORY_SEPARATOR) {
309-
$options['bypass_shell'] = true;
310312
$commandline = $this->prepareWindowsCommandLine($commandline, $env);
311313
} elseif (!$this->useFileHandles && $this->isSigchildEnabled()) {
312314
// 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 = [])
332334
throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd));
333335
}
334336

335-
$this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $options);
337+
$this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
336338

337339
if (!\is_resource($this->process)) {
338340
throw new RuntimeException('Unable to launch a new process.');
@@ -1220,6 +1222,32 @@ public function getStartTime(): float
12201222
return $this->starttime;
12211223
}
12221224

1225+
/**
1226+
* Defines options to pass to the underlying proc_open().
1227+
*
1228+
* @see https://php.net/proc_open for the options supported by PHP.
1229+
*
1230+
* Enabling the "create_new_console" option allows a subprocess to continue
1231+
* to run after the main process exited, on both Windows and *nix
1232+
*/
1233+
public function setOptions(array $options)
1234+
{
1235+
if ($this->isRunning()) {
1236+
throw new RuntimeException('Setting options while the process is running is not possible.');
1237+
}
1238+
1239+
$defaultOptions = $this->options;
1240+
$existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console'];
1241+
1242+
foreach ($options as $key => $value) {
1243+
if (!\in_array($key, $existingOptions)) {
1244+
$this->options = $defaultOptions;
1245+
throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions)));
1246+
}
1247+
$this->options[$key] = $value;
1248+
}
1249+
}
1250+
12231251
/**
12241252
* Returns whether TTY is supported on the current operating system.
12251253
*/

Tests/CreateNewConsoleTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Process\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Process\Process;
16+
17+
/**
18+
* @author Andrei Olteanu <andrei@flashsoft.eu>
19+
*/
20+
class CreateNewConsoleTest extends TestCase
21+
{
22+
public function testOptionCreateNewConsole()
23+
{
24+
$this->expectNotToPerformAssertions();
25+
try {
26+
$process = new Process(['php', __DIR__.'/ThreeSecondProcess.php']);
27+
$process->setOptions(['create_new_console' => true]);
28+
$process->disableOutput();
29+
$process->start();
30+
} catch (\Exception $e) {
31+
$this->fail($e);
32+
}
33+
}
34+
35+
public function testItReturnsFastAfterStart()
36+
{
37+
// The started process must run in background after the main has finished but that can't be tested with PHPUnit
38+
$startTime = microtime(true);
39+
$process = new Process(['php', __DIR__.'/ThreeSecondProcess.php']);
40+
$process->setOptions(['create_new_console' => true]);
41+
$process->disableOutput();
42+
$process->start();
43+
$this->assertLessThan(3000, $startTime - microtime(true));
44+
}
45+
}

Tests/ThreeSecondProcess.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
echo 'Worker started';
13+
sleep(3);
14+
echo 'Worker done';

0 commit comments

Comments
 (0)