Changeset View
Standalone View
src/future/exec/ExecFuture.php
| Show All 39 Lines | final class ExecFuture extends Future { | ||||
| private $readBufferSize; | private $readBufferSize; | ||||
| private $stdoutSizeLimit = PHP_INT_MAX; | private $stdoutSizeLimit = PHP_INT_MAX; | ||||
| private $stderrSizeLimit = PHP_INT_MAX; | private $stderrSizeLimit = PHP_INT_MAX; | ||||
| private $profilerCallID; | private $profilerCallID; | ||||
| private $killedByTimeout; | private $killedByTimeout; | ||||
| private $useWindowsFileStreams = false; | |||||
| private $windowsStdoutTempFile = null; | |||||
| private $windowsStderrTempFile = null; | |||||
| private static $descriptorSpec = array( | private static $descriptorSpec = array( | ||||
| 0 => array('pipe', 'r'), // stdin | 0 => array('pipe', 'r'), // stdin | ||||
| 1 => array('pipe', 'w'), // stdout | 1 => array('pipe', 'w'), // stdout | ||||
| 2 => array('pipe', 'w'), // stderr | 2 => array('pipe', 'w'), // stderr | ||||
| ); | ); | ||||
| /* -( Creating ExecFutures )----------------------------------------------- */ | /* -( Creating ExecFutures )----------------------------------------------- */ | ||||
| ▲ Show 20 Lines • Show All 168 Lines • ▼ Show 20 Lines | public function updateEnv($key, $value) { | ||||
| } else { | } else { | ||||
| $this->env[$key] = $value; | $this->env[$key] = $value; | ||||
| } | } | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| /** | |||||
| * 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 )------------------------------------------ */ | /* -( Interacting With Commands )------------------------------------------ */ | ||||
| /** | /** | ||||
| * Read and return output from stdout and stderr, if any is available. This | * Read and return output from stdout and stderr, if any is available. This | ||||
| * method keeps a read cursor on each stream, but the entire streams are | * method keeps a read cursor on each stream, but the entire streams are | ||||
| * still returned when the future resolves. You can call read() again after | * still returned when the future resolves. You can call read() again after | ||||
| * resolving the future to retrieve only the parts of the streams you did not | * resolving the future to retrieve only the parts of the streams you did not | ||||
| ▲ Show 20 Lines • Show All 321 Lines • ▼ Show 20 Lines | private function readAndDiscard($stream, $limit, $description, $length) { | ||||
| if ($length <= 0) { | if ($length <= 0) { | ||||
| return ''; | return ''; | ||||
| } | } | ||||
| do { | do { | ||||
| $data = fread($stream, min($length, 64 * 1024)); | $data = fread($stream, min($length, 64 * 1024)); | ||||
| if (false === $data) { | if (false === $data) { | ||||
| throw new Exception('Failed to read from '.$description); | throw new Exception('Failed to read from '.$description); | ||||
| } | } | ||||
epriestley: This seems completely crazy -- do feof(), fseek(), ftell(), etc., not work? I'd expect them to… | |||||
| $read_bytes = strlen($data); | $read_bytes = strlen($data); | ||||
| if ($read_bytes > 0 && $limit > 0) { | if ($read_bytes > 0 && $limit > 0) { | ||||
| if ($read_bytes > $limit) { | if ($read_bytes > $limit) { | ||||
| $data = substr($data, 0, $limit); | $data = substr($data, 0, $limit); | ||||
| } | } | ||||
| $output .= $data; | $output .= $data; | ||||
| $limit -= strlen($data); | $limit -= strlen($data); | ||||
| } | } | ||||
Not Done Inline Actions$real_length is only written to and never read, so something is awry in any case. epriestley: $real_length is only written to and never read, so something is awry in any case. | |||||
| if (strlen($output) >= $length) { | if (strlen($output) >= $length) { | ||||
| break; | break; | ||||
| } | } | ||||
| } while ($read_bytes > 0); | } while ($read_bytes > 0); | ||||
| return $output; | return $output; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 69 Lines • ▼ Show 20 Lines | if (!$this->pipes) { | ||||
| // NOTE: See note above about Phage. | // NOTE: See note above about Phage. | ||||
| if (class_exists('PhutilErrorTrap')) { | if (class_exists('PhutilErrorTrap')) { | ||||
| $trap = new PhutilErrorTrap(); | $trap = new PhutilErrorTrap(); | ||||
| } else { | } else { | ||||
| $trap = null; | $trap = null; | ||||
| } | } | ||||
| $spec = self::$descriptorSpec; | |||||
| if ($this->useWindowsFileStreams) { | |||||
Not Done Inline ActionsThese variables are unused. epriestley: These variables are unused. | |||||
| $this->windowsStdoutTempFile = new TempFile(); | |||||
| $this->windowsStderrTempFile = new TempFile(); | |||||
| $spec = array( | |||||
| 0 => self::$descriptorSpec[0], // stdin | |||||
Not Done Inline ActionsThese objects are local and not preserved, so I would expect that we'll try to destroy the temporary files immediately upon exiting this function, and fail. In net, I'd expect that we will never remove these files. Do these files get removed? Roughly, I'd expect that you need to save these to $this->something. That might also resolve the unlink issue on its own. We should make every reasonable effort to unlink these files. The default permissions of TempFile also aren't as locked-down as they could be, but it seems like these files should be as tightly locked as possible. Not sure if we can reasonably do anything or what chmod() translates to under Windows, though. epriestley: These objects are local and not preserved, so I would expect that we'll try to destroy the… | |||||
| 1 => fopen($this->windowsStdoutTempFile, 'wb'), // stdout | |||||
| 2 => fopen($this->windowsStderrTempFile, 'wb'), // stderr | |||||
| ); | |||||
| if (!$spec[1] || !$spec[2]) { | |||||
Not Done Inline ActionsThese calls should be tested for errors. epriestley: These calls should be tested for errors. | |||||
| throw new Exception(pht( | |||||
| 'Unable to create temporary files for '. | |||||
| 'Windows stdout / stderr streams')); | |||||
| } | |||||
| } | |||||
Not Done Inline Actionspht() epriestley: pht() | |||||
| $proc = @proc_open( | $proc = @proc_open( | ||||
| $unmasked_command, | $unmasked_command, | ||||
| self::$descriptorSpec, | $spec, | ||||
| $pipes, | $pipes, | ||||
| $cwd, | $cwd, | ||||
| $env); | $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 | |||||
Not Done Inline ActionsAs above. epriestley: As above. | |||||
| ); | |||||
| if (!$pipes[1] || !$pipes[2]) { | |||||
| throw new Exception(pht( | |||||
| 'Unable to open temporary files for '. | |||||
| 'reading Windows stdout / stderr streams')); | |||||
Not Done Inline Actionspht() epriestley: pht() | |||||
| } | |||||
| } | |||||
| if ($trap) { | if ($trap) { | ||||
| $err = $trap->getErrorsAsString(); | $err = $trap->getErrorsAsString(); | ||||
| $trap->destroy(); | $trap->destroy(); | ||||
| } else { | } else { | ||||
| $err = error_get_last(); | $err = error_get_last(); | ||||
| } | } | ||||
| if (!is_resource($proc)) { | if (!is_resource($proc)) { | ||||
| throw new Exception("Failed to proc_open(): {$err}"); | throw new Exception("Failed to proc_open(): {$err}"); | ||||
| } | } | ||||
| $this->pipes = $pipes; | $this->pipes = $pipes; | ||||
| $this->proc = $proc; | $this->proc = $proc; | ||||
| list($stdin, $stdout, $stderr) = $pipes; | list($stdin, $stdout, $stderr) = $pipes; | ||||
| if (!phutil_is_windows()) { | if (!phutil_is_windows()) { | ||||
| // On Windows, there's no such thing as nonblocking interprocess I/O. | // On Windows, we redirect process standard output and standard error | ||||
| // Just leave the sockets blocking and hope for the best. Some features | // through temporary files, and then use stream_select to determine | ||||
| // will not work. | // if there's more data to read. | ||||
| if ((!stream_set_blocking($stdout, false)) || | if ((!stream_set_blocking($stdout, false)) || | ||||
| (!stream_set_blocking($stderr, false)) || | (!stream_set_blocking($stderr, false)) || | ||||
| (!stream_set_blocking($stdin, false))) { | (!stream_set_blocking($stdin, false))) { | ||||
| $this->__destruct(); | $this->__destruct(); | ||||
| throw new Exception('Failed to set streams nonblocking.'); | throw new Exception('Failed to set streams nonblocking.'); | ||||
| } | } | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 50 Lines • ▼ Show 20 Lines | if ($max_stderr_read_bytes > 0) { | ||||
| $this->stderr .= $this->readAndDiscard( | $this->stderr .= $this->readAndDiscard( | ||||
| $stderr, | $stderr, | ||||
| $this->getStderrSizeLimit() - strlen($this->stderr), | $this->getStderrSizeLimit() - strlen($this->stderr), | ||||
| 'stderr', | 'stderr', | ||||
| $max_stderr_read_bytes); | $max_stderr_read_bytes); | ||||
| } | } | ||||
| if (!$status['running']) { | if (!$status['running']) { | ||||
| if ($this->useWindowsFileStreams) { | |||||
| fclose($stdout); | |||||
| fclose($stderr); | |||||
| } | |||||
| $this->result = array( | $this->result = array( | ||||
| $status['exitcode'], | $status['exitcode'], | ||||
| $this->stdout, | $this->stdout, | ||||
| $this->stderr, | $this->stderr, | ||||
| ); | ); | ||||
| $this->closeProcess(); | $this->closeProcess(); | ||||
| return true; | return true; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 127 Lines • Show Last 20 Lines | |||||
This seems completely crazy -- do feof(), fseek(), ftell(), etc., not work? I'd expect them to work properly since these are standard files.