Skip to content

Commit 1f5bf32

Browse files
committed
[Process] Make Process::start non-blocking on Windows platform
1 parent 02088bc commit 1f5bf32

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)