From 0c982798e01e5a48765aa5352f96066e77e36efd Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Mon, 29 Sep 2014 16:24:34 +0200 Subject: [PATCH] Fixed bug #51800 proc_open on Windows hangs forever This loop can block for some minutes, theoretically. Practially however, this is a 99% non issue for a normal use case. This is required because read() is synchronous. The PHP streams API wants to fill its internal buffers, therefore it might try to read some more data than user has demanded. Also, for a case where we want to read X bytes, but neither enough data nor EOF arrives, read() will block until it could fill the buffer. If a counterpart station runs slowly or delivers not all the data at once, read() would still be waiting. If we quit too early, we possibly could loose some data from the pipe. Thus it has to emulate the read() behaviour, but obviously not completely, just to some grade. Reading big data amount is for sure an issue on any platforms, it depends on the pipe buffer size, which is controlled by the system. On Windows, the buffer size seems to be way too small, which causes buffer congestion and a dead lock. It is essential to read the pipe descriptors simultaneously and possibly in the same order as the opposite writes them. Thus, this will work with smaller buffer data sizes passed through pipes. As MSDN states, anonymous pipes don't support asynchronous operations. Neither anonymous pipes do support select() as they are not SOCKETs but file descriptors. Consequently - bigger data sizes will need a better solution based on threads. However it is much more expencive. Maybe a better solution could be exporting a part of the internal doing as a userspace function which could perform some kind of lookahead operation on the pipe descriptor. This is just the first stone, depending on the user feedback we might go for further improvements in this area. --- NEWS | 1 + .../tests/streams/proc_open_bug51800.phpt | 91 +++++++++++++++++++ .../streams/proc_open_bug51800_right.phpt | 75 +++++++++++++++ .../tests/streams/proc_open_bug60120.phpt | 67 ++++++++++++++ .../tests/streams/proc_open_bug64438.phpt | 66 ++++++++++++++ main/streams/plain_wrapper.c | 28 ++++++ 6 files changed, 328 insertions(+) create mode 100644 ext/standard/tests/streams/proc_open_bug51800.phpt create mode 100644 ext/standard/tests/streams/proc_open_bug51800_right.phpt create mode 100644 ext/standard/tests/streams/proc_open_bug60120.phpt create mode 100644 ext/standard/tests/streams/proc_open_bug64438.phpt diff --git a/NEWS b/NEWS index 2942c720ed..50d997875c 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,7 @@ PHP NEWS as 6.2 (instead of 6.3)). (Christian Wenz) . Fixed bug #67633 (A foreach on an array returned from a function not doing copy-on-write). (Nikita) + . Fixed bug #51800 (proc_open on Windows hangs forever). (Anatol) - FPM: . Fixed bug #65641 (PHP-FPM incorrectly defines the SCRIPT_NAME variable diff --git a/ext/standard/tests/streams/proc_open_bug51800.phpt b/ext/standard/tests/streams/proc_open_bug51800.phpt new file mode 100644 index 0000000000..024d9cd6c6 --- /dev/null +++ b/ext/standard/tests/streams/proc_open_bug51800.phpt @@ -0,0 +1,91 @@ +--TEST-- +Bug #51800 proc_open on Windows hangs forever +--SKIPIF-- + +--XFAIL-- +pipes have to be read/written simultaneously +--FILE-- + array("pipe", "rb"), // stdin + 1 => array("pipe", "wb"), // stdout + 2 => array("pipe", "wb") // stderr + ); + +/* create the proc file */ +$r = file_put_contents($callee, ' $status, + "stdout" => $stdout, + "stderr" => $stderr, +), strlen($stdout), strlen($stderr)); + +unlink($callee); + +?> +===DONE=== +--EXPECTF-- +array(3) { + ["status"]=> + int(0) + ["stdout"]=> + string(10000) "a%s" + ["stderr"]=> + string(10000) "b%s" +} +int(10000) +int(10000) +===DONE=== + diff --git a/ext/standard/tests/streams/proc_open_bug51800_right.phpt b/ext/standard/tests/streams/proc_open_bug51800_right.phpt new file mode 100644 index 0000000000..bab37a8ba4 --- /dev/null +++ b/ext/standard/tests/streams/proc_open_bug51800_right.phpt @@ -0,0 +1,75 @@ +--TEST-- +Bug #51800 proc_open on Windows hangs forever, the right way to do it +--FILE-- + array("pipe", "rb"), // stdin + 1 => array("pipe", "wb"), // stdout + 2 => array("pipe", "wb") // stderr + ); + +/* create the proc file */ +$r = file_put_contents($callee, ' $status, + "stdout" => $stdout, + "stderr" => $stderr, +), strlen($stdout), strlen($stderr)); + +unlink($callee); + +?> +===DONE=== +--EXPECTF-- +array(3) { + ["status"]=> + int(0) + ["stdout"]=> + string(10000) "a%s" + ["stderr"]=> + string(10000) "b%s" +} +int(10000) +int(10000) +===DONE=== + diff --git a/ext/standard/tests/streams/proc_open_bug60120.phpt b/ext/standard/tests/streams/proc_open_bug60120.phpt new file mode 100644 index 0000000000..978cbe62fe --- /dev/null +++ b/ext/standard/tests/streams/proc_open_bug60120.phpt @@ -0,0 +1,67 @@ +--TEST-- +Bug #60120 proc_open hangs with stdin/out with 2048+ bytes +--FILE-- + true, 'binary_pipes' => true, 'bypass_shell' => false)); +$process = proc_open($cmd, $descriptors, $pipes, getcwd(), array(), $options); + +foreach ($pipes as $pipe) { + stream_set_blocking($pipe, false); +} +$writePipes = array($pipes[0]); +$stdinLen = strlen($stdin); +$stdinOffset = 0; + +unset($pipes[0]); + +while ($pipes || $writePipes) { + $r = $pipes; + $w = $writePipes; + $e = null; + $n = stream_select($r, $w, $e, 60); + + if (false === $n) { + break; + } elseif ($n === 0) { + proc_terminate($process); + + } + if ($w) { + $written = fwrite($writePipes[0], (binary)substr($stdin, $stdinOffset), 8192); + if (false !== $written) { + $stdinOffset += $written; + } + if ($stdinOffset >= $stdinLen) { + fclose($writePipes[0]); + $writePipes = null; + } + } + + foreach ($r as $pipe) { + $type = array_search($pipe, $pipes); + $data = fread($pipe, 8192); + var_dump($data); + if (false === $data || feof($pipe)) { + fclose($pipe); + unset($pipes[$type]); + } + } +} + + +?> +===DONE=== +--EXPECTF-- +string(2049) "%s" +string(2049) "%s" +string(0) "" +string(0) "" +===DONE=== + diff --git a/ext/standard/tests/streams/proc_open_bug64438.phpt b/ext/standard/tests/streams/proc_open_bug64438.phpt new file mode 100644 index 0000000000..e3288518d7 --- /dev/null +++ b/ext/standard/tests/streams/proc_open_bug64438.phpt @@ -0,0 +1,66 @@ +--TEST-- +Bug #64438 proc_open hangs with stdin/out with 4097+ bytes +--FILE-- + true, 'binary_pipes' => true, 'bypass_shell' => false)); +$process = proc_open($cmd, $descriptors, $pipes, getcwd(), array(), $options); + +foreach ($pipes as $pipe) { + stream_set_blocking($pipe, false); +} +$writePipes = array($pipes[0]); +$stdinLen = strlen($stdin); +$stdinOffset = 0; + +unset($pipes[0]); + +while ($pipes || $writePipes) { + $r = $pipes; + $w = $writePipes; + $e = null; + $n = stream_select($r, $w, $e, 60); + + if (false === $n) { + break; + } elseif ($n === 0) { + proc_terminate($process); + + } + if ($w) { + $written = fwrite($writePipes[0], (binary)substr($stdin, $stdinOffset), 8192); + if (false !== $written) { + $stdinOffset += $written; + } + if ($stdinOffset >= $stdinLen) { + fclose($writePipes[0]); + $writePipes = null; + } + } + + foreach ($r as $pipe) { + $type = array_search($pipe, $pipes); + $data = fread($pipe, 8192); + var_dump($data); + if (false === $data || feof($pipe)) { + fclose($pipe); + unset($pipes[$type]); + } + } +} + +?> +===DONE=== +--EXPECTF-- +string(4097) "%s" +string(4097) "%s" +string(0) "" +string(0) "" +===DONE=== + diff --git a/main/streams/plain_wrapper.c b/main/streams/plain_wrapper.c index a50662b78e..20ec7c2423 100644 --- a/main/streams/plain_wrapper.c +++ b/main/streams/plain_wrapper.c @@ -348,6 +348,34 @@ static size_t php_stdiop_read(php_stream *stream, char *buf, size_t count TSRMLS assert(data != NULL); if (data->fd >= 0) { +#ifdef PHP_WIN32 + php_stdio_stream_data *self = (php_stdio_stream_data*)stream->abstract; + + if (self->is_pipe || self->is_process_pipe) { + HANDLE ph = (HANDLE)_get_osfhandle(data->fd); + int retry = 0; + DWORD avail_read = 0; + + do { + /* Look ahead to get the available data amount to read. Do the same + as read() does, however not blocking forever. In case it failed, + no data will be read (better than block). */ + if (!PeekNamedPipe(ph, NULL, 0, NULL, &avail_read, NULL)) { + break; + } + /* If there's nothing to read, wait in 100ms periods. */ + if (0 == avail_read) { + usleep(100000); + } + } while (0 == avail_read && retry++ < 180); + + /* Reduce the required data amount to what is available, otherwise read() + will block.*/ + if (avail_read < count) { + count = avail_read; + } + } +#endif ret = read(data->fd, buf, count); if (ret == (size_t)-1 && errno == EINTR) { -- 2.40.0