Skip to content

Commit 442c470

Browse files
committed
bug symfony#10420 [2.3][Process] Make Process::start non-blocking on Windows platform (romainneutron)
This PR was merged into the 2.3 branch. Discussion ---------- [2.3][Process] Make Process::start non-blocking on Windows platform | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | symfony#9755 | License | MIT This PR is a proposition that solves issue symfony#9755. Let me sum-up what problems we have on Windows platform: - We can not use pipes with `proc_open` on Windows because `stream_set_blocking` does not work on this platform, resulting in blocking pipes and a blocking `Process::start` (see https://bugs.php.net/bug.php?id=51800, https://bugs.php.net/bug.php?id=47918 and https://bugs.php.net/bug.php?id=50856). - We can not use file handles for both `STDOUT` and `STDERR` as it might produce corrupted content (see https://bugs.php.net/bug.php?id=65650). - We currently use file handles for `STDOUT` and pipe for `STDERR` but it makes `Process::start` blocking. The solution I propose here is to use native redirections, write `STDERR` / `STDOUT` in temporary files, read these files, finally cleanup. It works pretty well, all tests pass on Windows. Better : [previous tests that were specific to this platform](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Process/Tests/AbstractProcessTest.php#L720-725) disappear as unix one are now okay :). The drawback of this is the need of using `taskkill`: When stopping a process (`Process::stop` or in case of a timeout reached) `proc_terminate` does not kill the underlying process properly, only the `cmd` parent. The underlying process run by Process still runs after the `proc_terminate` call, having a lock on the temporary files we're using for getting the output, avoiding PHP to remove them (or any user on system) until the underlying process has finish to run). So I use the `taskkill` Windows command to terminate the underlying process. This works well but I've to admit it's not pretty. If we do not use this hack, let's face that the underlying process is not stopped. Last but not least, let's deal with the case we're running on Windows platform with PHP having --enable-sigchild environment (I don't know if it can really happen) In this case: - we can not retrieve the `pid` - we can not `taskkill` the underlying process - it runs - locks remain on temporary files, we might not be able to remove them We can't really deal with this. Feedback more than welcome Commits ------- 1f5bf32 [Process] Make Process::start non-blocking on Windows platform
2 parents 02088bc + 1f5bf32 commit 442c470

File tree

3 files changed

+63
-12
lines changed

3 files changed

+63
-12
lines changed

src/Symfony/Component/Process/Process.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,11 @@ public function start($callback = null)
232232
$commandline = $this->commandline;
233233

234234
if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->enhanceWindowsCompatibility) {
235-
$commandline = 'cmd /V:ON /E:ON /C "'.$commandline.'"';
235+
$commandline = 'cmd /V:ON /E:ON /C "('.$commandline.')"';
236+
foreach ($this->processPipes->getFiles() as $offset => $filename) {
237+
$commandline .= ' '.$offset.'>'.$filename;
238+
}
239+
236240
if (!isset($this->options['bypass_shell'])) {
237241
$this->options['bypass_shell'] = true;
238242
}
@@ -618,6 +622,12 @@ public function stop($timeout = 10, $signal = null)
618622
{
619623
$timeoutMicro = microtime(true) + $timeout;
620624
if ($this->isRunning()) {
625+
if (defined('PHP_WINDOWS_VERSION_BUILD') && !$this->isSigchildEnabled()) {
626+
exec(sprintf("taskkill /F /T /PID %d 2>&1", $this->getPid()), $output, $exitCode);
627+
if ($exitCode > 0) {
628+
throw new RuntimeException('Unable to kill the process');
629+
}
630+
}
621631
proc_terminate($this->process);
622632
do {
623633
usleep(1000);

src/Symfony/Component/Process/ProcessPipes.php

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class ProcessPipes
2121
/** @var array */
2222
public $pipes = array();
2323
/** @var array */
24+
private $files = array();
25+
/** @var array */
2426
private $fileHandles = array();
2527
/** @var array */
2628
private $readBytes = array();
@@ -39,27 +41,29 @@ public function __construct($useFiles, $ttyMode)
3941
// Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big.
4042
// Workaround for this problem is to use temporary files instead of pipes on Windows platform.
4143
//
42-
// Please note that this work around prevents hanging but
43-
// another issue occurs : In some race conditions, some data may be
44-
// lost or corrupted.
45-
//
4644
// @see https://bugs.php.net/bug.php?id=51800
4745
if ($this->useFiles) {
48-
$this->fileHandles = array(
49-
Process::STDOUT => tmpfile(),
46+
$this->files = array(
47+
Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'),
48+
Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'),
5049
);
51-
if (false === $this->fileHandles[Process::STDOUT]) {
52-
throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable');
50+
foreach ($this->files as $offset => $file) {
51+
$this->fileHandles[$offset] = fopen($this->files[$offset], 'rb');
52+
if (false === $this->fileHandles[$offset]) {
53+
throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable');
54+
}
5355
}
5456
$this->readBytes = array(
5557
Process::STDOUT => 0,
58+
Process::STDERR => 0,
5659
);
5760
}
5861
}
5962

6063
public function __destruct()
6164
{
6265
$this->close();
66+
$this->removeFiles();
6367
}
6468

6569
/**
@@ -105,11 +109,13 @@ public function closeUnixPipes()
105109
public function getDescriptors()
106110
{
107111
if ($this->useFiles) {
112+
// We're not using pipe on Windows platform as it hangs (https://bugs.php.net/bug.php?id=51800)
113+
// We're not using file handles as it can produce corrupted output https://bugs.php.net/bug.php?id=65650
114+
// So we redirect output within the commandline and pass the nul device to the process
108115
return array(
109116
array('pipe', 'r'),
110-
$this->fileHandles[Process::STDOUT],
111-
// Use a file handle only for STDOUT. Using for both STDOUT and STDERR would trigger https://bugs.php.net/bug.php?id=65650
112-
array('pipe', 'w'),
117+
array('file', 'NUL', 'w'),
118+
array('file', 'NUL', 'w'),
113119
);
114120
}
115121

@@ -128,6 +134,20 @@ public function getDescriptors()
128134
);
129135
}
130136

137+
/**
138+
* Returns an array of filenames indexed by their related stream in case these pipes use temporary files.
139+
*
140+
* @return array
141+
*/
142+
public function getFiles()
143+
{
144+
if ($this->useFiles) {
145+
return $this->files;
146+
}
147+
148+
return array();
149+
}
150+
131151
/**
132152
* Reads data in file handles and pipes.
133153
*
@@ -322,4 +342,17 @@ private function hasSystemCallBeenInterrupted()
322342
// stream_select returns false when the `select` system call is interrupted by an incoming signal
323343
return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call');
324344
}
345+
346+
/**
347+
* Removes temporary files
348+
*/
349+
private function removeFiles()
350+
{
351+
foreach ($this->files as $filename) {
352+
if (file_exists($filename)) {
353+
@unlink($filename);
354+
}
355+
}
356+
$this->files = array();
357+
}
325358
}

src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ public function testExitCodeIsAvailableAfterSignal()
112112
$this->markTestSkipped('Signal is not supported in sigchild environment');
113113
}
114114

115+
public function testStartAfterATimeout()
116+
{
117+
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
118+
$this->markTestSkipped('Restarting a timed-out process on Windows is not supported in sigchild environment');
119+
}
120+
parent::testStartAfterATimeout();
121+
}
122+
115123
/**
116124
* {@inheritdoc}
117125
*/

0 commit comments

Comments
 (0)