diff --git a/src/future/exec/ExecFuture.php b/src/future/exec/ExecFuture.php --- a/src/future/exec/ExecFuture.php +++ b/src/future/exec/ExecFuture.php @@ -45,6 +45,10 @@ private $profilerCallID; private $killedByTimeout; + private $useWindowsFileStreams = false; + private $windowsStdoutTempFile = null; + private $windowsStderrTempFile = null; + private static $descriptorSpec = array( 0 => array('pipe', 'r'), // stdin 1 => array('pipe', 'w'), // stdout @@ -229,6 +233,21 @@ } + /** + * Set whether to use non-blocking streams on Windows. + * + * @param bool Whether to use non-blocking streams. + * @return this + * @task config + */ + public function setUseWindowsFileStreams($use_streams) { + if (phutil_is_windows()) { + $this->useWindowsFileStreams = $use_streams; + } + return $this; + } + + /* -( Interacting With Commands )------------------------------------------ */ @@ -564,7 +583,24 @@ } do { - $data = fread($stream, min($length, 64 * 1024)); + $real_length = $length; + + // On Windows, we must check to see if we can read without blocking + // from the stream, and even then we must read only 1 byte at a time. + if ($this->useWindowsFileStreams) { + $r = array($stream); + $w = array(); + $e = array(); + if (false === stream_select($r, $w, $e, 0)) { + throw new Exception('stream_select() failed'); + } + $real_length = 0; + if (in_array($stream, $r)) { + $real_length = 1; + } + } + + $data = fread($stream, min($real_length, 64 * 1024)); if (false === $data) { throw new Exception('Failed to read from '.$description); } @@ -662,13 +698,50 @@ } else { $trap = null; } + + $stdout_target = null; + $stderr_target = null; + + $spec = self::$descriptorSpec; + if ($this->useWindowsFileStreams) { + $this->windowsStdoutTempFile = new TempFile(); + $this->windowsStderrTempFile = new TempFile(); + + $spec = array( + 0 => self::$descriptorSpec[0], // stdin + 1 => fopen($this->windowsStdoutTempFile, 'wb'), // stdout + 2 => fopen($this->windowsStderrTempFile, 'wb'), // stderr + ); + + if (!$spec[1] || !$spec[2]) { + throw new Exception( + 'Unable to create temporary files for '. + 'Windows stdout / stderr streams'); + } + } $proc = @proc_open( $unmasked_command, - self::$descriptorSpec, + $spec, $pipes, $cwd, $env); + + if ($this->useWindowsFileStreams) { + fclose($spec[1]); + fclose($spec[2]); + $pipes = array( + 0 => head($pipes), // stdin + 1 => fopen($this->windowsStdoutTempFile, 'rb'), // stdout + 2 => fopen($this->windowsStderrTempFile, 'rb'), // stderr + ); + + if (!$pipes[1] || !$pipes[2]) { + throw new Exception( + 'Unable to open temporary files for '. + 'reading Windows stdout / stderr streams'); + } + } if ($trap) { $err = $trap->getErrorsAsString(); @@ -685,12 +758,12 @@ $this->proc = $proc; list($stdin, $stdout, $stderr) = $pipes; - + if (!phutil_is_windows()) { - // On Windows, there's no such thing as nonblocking interprocess I/O. - // Just leave the sockets blocking and hope for the best. Some features - // will not work. + // On Windows, we redirect process standard output and standard error + // through temporary files, and then use stream_select to determine + // if there's more data to read. if ((!stream_set_blocking($stdout, false)) || (!stream_set_blocking($stderr, false)) || @@ -732,14 +805,14 @@ $status = $this->procGetStatus(); $read_buffer_size = $this->readBufferSize; - + $max_stdout_read_bytes = PHP_INT_MAX; $max_stderr_read_bytes = PHP_INT_MAX; if ($read_buffer_size !== null) { $max_stdout_read_bytes = $read_buffer_size - strlen($this->stdout); $max_stderr_read_bytes = $read_buffer_size - strlen($this->stderr); } - + if ($max_stdout_read_bytes > 0) { $this->stdout .= $this->readAndDiscard( $stdout, @@ -755,8 +828,13 @@ 'stderr', $max_stderr_read_bytes); } - + if (!$status['running']) { + if ($this->useWindowsFileStreams) { + fclose($stdout); + fclose($stderr); + } + $this->result = array( $status['exitcode'], $this->stdout,