Changeset View
Changeset View
Standalone View
Standalone View
src/future/exec/ExecFuture.php
| Show All 36 Lines | final class ExecFuture extends PhutilExecutableFuture { | ||||
| 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 $windowsStdoutTempFile = null; | ||||
| private $windowsStderrTempFile = null; | private $windowsStderrTempFile = null; | ||||
| private $terminateTimeout; | private $terminateTimeout; | ||||
| private $didTerminate; | private $didTerminate; | ||||
| private $killTimeout; | private $killTimeout; | ||||
| private static $descriptorSpec = array( | private static $descriptorSpec = array( | ||||
| ▲ Show 20 Lines • Show All 122 Lines • ▼ Show 20 Lines | /* -( Configuring Execution )---------------------------------------------- */ | ||||
| * @return this | * @return this | ||||
| */ | */ | ||||
| public function setReadBufferSize($read_buffer_size) { | public function setReadBufferSize($read_buffer_size) { | ||||
| $this->readBufferSize = $read_buffer_size; | $this->readBufferSize = $read_buffer_size; | ||||
| 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 375 Lines • ▼ Show 20 Lines | /* -( Internals )---------------------------------------------------------- */ | ||||
| */ | */ | ||||
| public function isReady() { | public function isReady() { | ||||
| // NOTE: We have soft dependencies on PhutilServiceProfiler and | // NOTE: We have soft dependencies on PhutilServiceProfiler and | ||||
| // PhutilErrorTrap here. These dependencies are soft to avoid the need to | // PhutilErrorTrap here. These dependencies are soft to avoid the need to | ||||
| // build them into the Phage agent. Under normal circumstances, these | // build them into the Phage agent. Under normal circumstances, these | ||||
| // classes are always available. | // classes are always available. | ||||
| if (!$this->pipes) { | if (!$this->pipes) { | ||||
| $is_windows = phutil_is_windows(); | |||||
| // NOTE: See note above about Phage. | // NOTE: See note above about Phage. | ||||
| if (class_exists('PhutilServiceProfiler')) { | if (class_exists('PhutilServiceProfiler')) { | ||||
| $profiler = PhutilServiceProfiler::getInstance(); | $profiler = PhutilServiceProfiler::getInstance(); | ||||
| $this->profilerCallID = $profiler->beginServiceCall( | $this->profilerCallID = $profiler->beginServiceCall( | ||||
| array( | array( | ||||
| 'type' => 'exec', | 'type' => 'exec', | ||||
| 'command' => (string)$this->command, | 'command' => (string)$this->command, | ||||
| )); | )); | ||||
| } | } | ||||
| if (!$this->start) { | if (!$this->start) { | ||||
| // We might already have started the timer via initiating resolution. | // We might already have started the timer via initiating resolution. | ||||
| $this->start = microtime(true); | $this->start = microtime(true); | ||||
| } | } | ||||
| $unmasked_command = $this->command; | $unmasked_command = $this->command; | ||||
| if ($unmasked_command instanceof PhutilCommandString) { | if ($unmasked_command instanceof PhutilCommandString) { | ||||
| $unmasked_command = $unmasked_command->getUnmaskedString(); | $unmasked_command = $unmasked_command->getUnmaskedString(); | ||||
| } | } | ||||
| $pipes = array(); | $pipes = array(); | ||||
| if (phutil_is_windows()) { | |||||
| // See T4395. proc_open under Windows uses "cmd /C [cmd]", which will | |||||
| // strip the first and last quote when there aren't exactly two quotes | |||||
| // (and some other conditions as well). This results in a command that | |||||
| // looks like `command" "path to my file" "something something` which is | |||||
| // clearly wrong. By surrounding the command string with quotes we can | |||||
| // be sure this process is harmless. | |||||
| if (strpos($unmasked_command, '"') !== false) { | |||||
| $unmasked_command = '"'.$unmasked_command.'"'; | |||||
| } | |||||
| } | |||||
| if ($this->hasEnv()) { | if ($this->hasEnv()) { | ||||
| $env = $this->getEnv(); | $env = $this->getEnv(); | ||||
| } else { | } else { | ||||
| $env = null; | $env = null; | ||||
| } | } | ||||
| $cwd = $this->getCWD(); | $cwd = $this->getCWD(); | ||||
| // 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; | $spec = self::$descriptorSpec; | ||||
| if ($this->useWindowsFileStreams) { | if ($is_windows) { | ||||
| $this->windowsStdoutTempFile = new TempFile(); | $stdout_file = new TempFile(); | ||||
| $this->windowsStderrTempFile = new TempFile(); | $stderr_file = new TempFile(); | ||||
| $spec = array( | $stdout_handle = fopen($stdout_file, 'wb'); | ||||
| 0 => self::$descriptorSpec[0], // stdin | if (!$stdout_handle) { | ||||
| 1 => fopen($this->windowsStdoutTempFile, 'wb'), // stdout | throw new Exception( | ||||
| 2 => fopen($this->windowsStderrTempFile, 'wb'), // stderr | pht( | ||||
| ); | 'Unable to open stdout temporary file ("%s") for writing.', | ||||
| $stdout_file)); | |||||
| } | |||||
| if (!$spec[1] || !$spec[2]) { | $stderr_handle = fopen($stderr_file, 'wb'); | ||||
| throw new Exception(pht( | if (!$stderr_handle) { | ||||
| 'Unable to create temporary files for '. | throw new Exception( | ||||
| 'Windows stdout / stderr streams')); | pht( | ||||
| 'Unable to open stderr temporary file ("%s") for writing.', | |||||
| $stderr_file)); | |||||
| } | } | ||||
| $spec = array( | |||||
| 0 => self::$descriptorSpec[0], | |||||
| 1 => $stdout_handle, | |||||
| 2 => $stderr_handle, | |||||
| ); | |||||
| } | } | ||||
| $proc = @proc_open( | $proc = @proc_open( | ||||
| $unmasked_command, | $unmasked_command, | ||||
| $spec, | $spec, | ||||
| $pipes, | $pipes, | ||||
| $cwd, | $cwd, | ||||
| $env); | $env, | ||||
| array( | |||||
| if ($this->useWindowsFileStreams) { | 'bypass_shell' => true, | ||||
| 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(pht( | |||||
| 'Unable to open temporary files for '. | |||||
| 'reading Windows stdout / stderr streams')); | |||||
| } | |||||
| } | |||||
| 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_windows) { | |||||
| fclose($stdout_handle); | |||||
| fclose($stderr_handle); | |||||
| } | |||||
| if (!is_resource($proc)) { | if (!is_resource($proc)) { | ||||
| // When you run an invalid command on a Linux system, the "proc_open()" | |||||
| // works and then the process (really a "/bin/sh -c ...") exits after | |||||
| // it fails to resolve the command. | |||||
| // When you run an invalid command on a Windows system, we bypass the | |||||
| // shell and the "proc_open()" itself fails. Throw a "CommandException" | |||||
| // here for consistency with the Linux behavior in this common failure | |||||
| // case. | |||||
| throw new CommandException( | |||||
| pht( | |||||
| 'Call to "proc_open()" to open a subprocess failed: %s', | |||||
| $err), | |||||
| $this->command, | |||||
| 1, | |||||
| '', | |||||
| ''); | |||||
| } | |||||
| if ($is_windows) { | |||||
| $stdout_handle = fopen($stdout_file, 'rb'); | |||||
| if (!$stdout_handle) { | |||||
| throw new Exception( | throw new Exception( | ||||
| pht( | pht( | ||||
| 'Failed to `%s`: %s', | 'Unable to open stdout temporary file ("%s") for reading.', | ||||
| 'proc_open()', | $stdout_file)); | ||||
| $err)); | } | ||||
| $stderr_handle = fopen($stderr_file, 'rb'); | |||||
| if (!$stderr_handle) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Unable to open stderr temporary file ("%s") for reading.', | |||||
| $stderr_file)); | |||||
| } | |||||
| $pipes = array( | |||||
| 0 => $pipes[0], | |||||
| 1 => $stdout_handle, | |||||
| 2 => $stderr_handle, | |||||
| ); | |||||
| $this->windowsStdoutTempFile = $stdout_file; | |||||
| $this->windowsStderrTempFile = $stderr_file; | |||||
| } | } | ||||
| $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 (!$is_windows) { | ||||
| // On Windows, we redirect process standard output and standard error | // On Windows, we redirect process standard output and standard error | ||||
| // through temporary files, and then use stream_select to determine | // through temporary files. Files don't block, so we don't need to make | ||||
| // if there's more data to read. | // these streams nonblocking. | ||||
| 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(pht('Failed to set streams nonblocking.')); | throw new Exception(pht('Failed to set streams nonblocking.')); | ||||
| } | } | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 61 Lines • ▼ Show 20 Lines | if (!$status['running']) { | ||||
| // consider the subprocess to have exited until we've read everything. | // consider the subprocess to have exited until we've read everything. | ||||
| // See T9724 for context. | // See T9724 for context. | ||||
| if (feof($stdout) && feof($stderr)) { | if (feof($stdout) && feof($stderr)) { | ||||
| $is_done = true; | $is_done = true; | ||||
| } | } | ||||
| } | } | ||||
| if ($is_done) { | if ($is_done) { | ||||
| if ($this->useWindowsFileStreams) { | |||||
| fclose($stdout); | |||||
| fclose($stderr); | |||||
| } | |||||
| // If the subprocess got nuked with `kill -9`, we get a -1 exitcode. | // If the subprocess got nuked with `kill -9`, we get a -1 exitcode. | ||||
| // Upgrade this to a slightly more informative value by examining the | // Upgrade this to a slightly more informative value by examining the | ||||
| // terminating signal code. | // terminating signal code. | ||||
| $err = $status['exitcode']; | $err = $status['exitcode']; | ||||
| if ($err == -1) { | if ($err == -1) { | ||||
| if ($status['signaled']) { | if ($status['signaled']) { | ||||
| $err = 128 + $status['termsig']; | $err = 128 + $status['termsig']; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 63 Lines • ▼ Show 20 Lines | foreach ($this->pipes as $pipe) { | ||||
| @fclose($pipe); | @fclose($pipe); | ||||
| } | } | ||||
| } | } | ||||
| $this->pipes = array(null, null, null); | $this->pipes = array(null, null, null); | ||||
| if ($this->proc) { | if ($this->proc) { | ||||
| @proc_close($this->proc); | @proc_close($this->proc); | ||||
| $this->proc = null; | $this->proc = null; | ||||
| } | } | ||||
| $this->stdin = null; | $this->stdin = null; | ||||
| unset($this->windowsStdoutTempFile); | |||||
| unset($this->windowsStderrTempFile); | |||||
| if ($this->profilerCallID !== null) { | if ($this->profilerCallID !== null) { | ||||
| $profiler = PhutilServiceProfiler::getInstance(); | $profiler = PhutilServiceProfiler::getInstance(); | ||||
| $profiler->endServiceCall( | $profiler->endServiceCall( | ||||
| $this->profilerCallID, | $this->profilerCallID, | ||||
| array( | array( | ||||
| 'err' => $this->result ? idx($this->result, 0) : null, | 'err' => $this->result ? idx($this->result, 0) : null, | ||||
| )); | )); | ||||
| $this->profilerCallID = null; | $this->profilerCallID = null; | ||||
| ▲ Show 20 Lines • Show All 100 Lines • Show Last 20 Lines | |||||