public function isReady()

in src/future/exec/ExecFuture.php [583:826]


  public function isReady() {
    // NOTE: We have soft dependencies on PhutilServiceProfiler and
    // PhutilErrorTrap here. These dependencies are soft to avoid the need to
    // build them into the Phage agent. Under normal circumstances, these
    // classes are always available.

    if (!$this->pipes) {

      // NOTE: See note above about Phage.
      if (class_exists('PhutilServiceProfiler')) {
        $profiler = PhutilServiceProfiler::getInstance();
        $this->profilerCallID = $profiler->beginServiceCall(
          array(
            'type'    => 'exec',
            'command' => (string)$this->command,
          ));
      }

      if (!$this->start) {
        // We might already have started the timer via initiating resolution.
        $this->start = microtime(true);
      }

      $unmasked_command = $this->command;
      if ($unmasked_command instanceof PhutilCommandString) {
        $unmasked_command = $unmasked_command->getUnmaskedString();
      }

      $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()) {
        $env = $this->getEnv();
      } else {
        $env = null;
      }

      $cwd = $this->getCWD();
      if (null === $cwd) {
        $cwd = getcwd();
      }

      // NOTE: See note above about Phage.
      if (class_exists('PhutilErrorTrap')) {
        $trap = new PhutilErrorTrap();
      } else {
        $trap = 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(pht(
            'Unable to create temporary files for '.
            'Windows stdout / stderr streams'));
        }
      }

      $proc = @proc_open(
        $unmasked_command,
        $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(pht(
            'Unable to open temporary files for '.
            'reading Windows stdout / stderr streams'));
        }
      }

      if ($trap) {
        $err = $trap->getErrorsAsString();
        $trap->destroy();
      } else {
        $err = error_get_last();
      }

      if (!is_resource($proc)) {
        throw new Exception(
          pht(
            'Failed to `%s`: %s',
            'proc_open()',
            $err));
      }

      $this->pipes = $pipes;
      $this->proc  = $proc;

      list($stdin, $stdout, $stderr) = $pipes;

      if (!phutil_is_windows()) {

        // 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)) ||
            (!stream_set_blocking($stdin,  false))) {
          $this->__destruct();
          throw new Exception(pht('Failed to set streams nonblocking.'));
        }
      }

      $this->tryToCloseStdin();

      return false;
    }

    if (!$this->proc) {
      return true;
    }

    list($stdin, $stdout, $stderr) = $this->pipes;

    while (isset($this->stdin) && $this->stdin->getByteLength()) {
      $write_segment = $this->stdin->getAnyPrefix();

      $bytes = fwrite($stdin, $write_segment);
      if ($bytes === false) {
        throw new Exception(pht('Unable to write to stdin!'));
      } else if ($bytes) {
        $this->stdin->removeBytesFromHead($bytes);
      } else {
        // Writes are blocked for now.
        break;
      }
    }

    $this->tryToCloseStdin();

    // Read status before reading pipes so that we can never miss data that
    // arrives between our last read and the process exiting.
    $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,
        $this->getStdoutSizeLimit() - strlen($this->stdout),
        'stdout',
        $max_stdout_read_bytes);
    }

    if ($max_stderr_read_bytes > 0) {
      $this->stderr .= $this->readAndDiscard(
        $stderr,
        $this->getStderrSizeLimit() - strlen($this->stderr),
        'stderr',
        $max_stderr_read_bytes);
    }

    $is_done = false;
    if (!$status['running']) {
      // We may still have unread bytes on stdout or stderr, particularly if
      // this future is being buffered and streamed. If we do, we don't want to
      // consider the subprocess to have exited until we've read everything.
      // See T9724 for context.
      if (feof($stdout) && feof($stderr)) {
        $is_done = true;
      }
    }

    if ($is_done) {
      if ($this->useWindowsFileStreams) {
        fclose($stdout);
        fclose($stderr);
      }

      // If the subprocess got nuked with `kill -9`, we get a -1 exitcode.
      // Upgrade this to a slightly more informative value by examining the
      // terminating signal code.
      $err = $status['exitcode'];
      if ($err == -1) {
        if ($status['signaled']) {
          $err = 128 + $status['termsig'];
        }
      }

      $this->result = array(
        $err,
        $this->stdout,
        $this->stderr,
      );
      $this->closeProcess();
      return true;
    }

    $elapsed = (microtime(true) - $this->start);

    if ($this->terminateTimeout && ($elapsed >= $this->terminateTimeout)) {
      if (!$this->didTerminate) {
        $this->killedByTimeout = true;
        $this->sendTerminateSignal();
        return false;
      }
    }

    if ($this->killTimeout && ($elapsed >= $this->killTimeout)) {
      $this->killedByTimeout = true;
      $this->resolveKill();
      return true;
    }

  }