diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -189,6 +189,7 @@ 'PhutilConsoleMetrics' => 'console/PhutilConsoleMetrics.php', 'PhutilConsoleMetricsSignalHandler' => 'future/exec/PhutilConsoleMetricsSignalHandler.php', 'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php', + 'PhutilConsoleProgressSink' => 'progress/PhutilConsoleProgressSink.php', 'PhutilConsoleServer' => 'console/PhutilConsoleServer.php', 'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php', 'PhutilConsoleSkip' => 'console/view/PhutilConsoleSkip.php', @@ -366,6 +367,7 @@ 'PhutilProcessQuery' => 'filesystem/PhutilProcessQuery.php', 'PhutilProcessRef' => 'filesystem/PhutilProcessRef.php', 'PhutilProcessRefTestCase' => 'filesystem/__tests__/PhutilProcessRefTestCase.php', + 'PhutilProgressSink' => 'progress/PhutilProgressSink.php', 'PhutilProseDiff' => 'utils/PhutilProseDiff.php', 'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php', 'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php', @@ -851,6 +853,7 @@ 'PhutilConsoleMetrics' => 'Phobject', 'PhutilConsoleMetricsSignalHandler' => 'PhutilSignalHandler', 'PhutilConsoleProgressBar' => 'Phobject', + 'PhutilConsoleProgressSink' => 'PhutilProgressSink', 'PhutilConsoleServer' => 'Phobject', 'PhutilConsoleServerChannel' => 'PhutilChannelChannel', 'PhutilConsoleSkip' => 'PhutilConsoleLogLine', @@ -1033,6 +1036,7 @@ 'PhutilProcessQuery' => 'Phobject', 'PhutilProcessRef' => 'Phobject', 'PhutilProcessRefTestCase' => 'PhutilTestCase', + 'PhutilProgressSink' => 'Phobject', 'PhutilProseDiff' => 'Phobject', 'PhutilProseDiffTestCase' => 'PhutilTestCase', 'PhutilProseDifferenceEngine' => 'Phobject', diff --git a/src/future/http/HTTPSFuture.php b/src/future/http/HTTPSFuture.php --- a/src/future/http/HTTPSFuture.php +++ b/src/future/http/HTTPSFuture.php @@ -25,6 +25,7 @@ private $downloadPath; private $downloadHandle; private $parser; + private $progressSink; /** * Create a temp file containing an SSL cert, and use it for this session. @@ -154,6 +155,15 @@ return $this; } + public function setProgressSink(PhutilProgressSink $progress_sink) { + $this->progressSink = $progress_sink; + return $this; + } + + public function getProgressSink() { + return $this->progressSink; + } + /** * Attach a file to the request. * @@ -406,6 +416,11 @@ $streaming_parser->setWriteHandle($this->downloadHandle); } + $progress_sink = $this->getProgressSink(); + if ($progress_sink) { + $streaming_parser->setProgressSink($progress_sink); + } + $this->parser = $streaming_parser; } } else { @@ -499,6 +514,16 @@ } } + $sink = $this->getProgressSink(); + if ($sink) { + $status = head($this->result); + if ($status->isError()) { + $sink->didFailWork(); + } else { + $sink->didCompleteWork(); + } + } + $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall($this->profilerCallID, array()); diff --git a/src/parser/http/PhutilHTTPResponseParser.php b/src/parser/http/PhutilHTTPResponseParser.php --- a/src/parser/http/PhutilHTTPResponseParser.php +++ b/src/parser/http/PhutilHTTPResponseParser.php @@ -8,6 +8,7 @@ private $buffer; private $state = 'headers'; private $writeHandle; + private $progressSink; public function setFollowLocationHeaders($follow_location_headers) { $this->followLocationHeaders = $follow_location_headers; @@ -27,6 +28,15 @@ return $this->writeHandle; } + public function setProgressSink(PhutilProgressSink $progress_sink) { + $this->progressSink = $progress_sink; + return $this; + } + + public function getProgressSink() { + return $this->progressSink; + } + public function readBytes($bytes) { if ($this->state == 'discard') { return $this; @@ -154,8 +164,15 @@ if ($this->state == 'body') { if (strlen($this->buffer)) { - $this->response->appendBody($this->buffer); + $bytes = $this->buffer; $this->buffer = ''; + + $this->response->appendBody($bytes); + + $sink = $this->getProgressSink(); + if ($sink) { + $sink->didMakeProgress(strlen($bytes)); + } } break; } diff --git a/src/progress/PhutilConsoleProgressSink.php b/src/progress/PhutilConsoleProgressSink.php new file mode 100644 --- /dev/null +++ b/src/progress/PhutilConsoleProgressSink.php @@ -0,0 +1,115 @@ +shouldPublishToConsole()) { + return; + } + + $completed = $this->getCompletedWork(); + $total = $this->getTotalWork(); + + if ($total !== null) { + $percent = ($completed / $total); + $percent = min(1, $percent); + $percent = max(0, $percent); + } else { + $percent = null; + } + + // TODO: In TTY mode, draw a nice ASCII progress bar. + + if ($percent !== null) { + $marker = sprintf('% 3.1f%%', 100 * $percent); + } else { + $marker = sprintf('% 16d', $completed); + } + + $message = pht('[%s] Working...', $marker); + + if ($this->isTTY()) { + $this->overwriteLine($message); + } else { + $this->printLine($message."\n"); + } + + $this->didPublishToConsole(); + } + + protected function publishCompletion() { + $this->printLine("\n"); + } + + protected function publishFailure() { + $this->printLine("\n"); + } + + private function shouldPublishToConsole() { + if (!$this->lastUpdate) { + return true; + } + + // Limit the number of times per second we actually write to the console. + if ($this->isTTY()) { + $writes_per_second = 5; + } else { + $writes_per_second = 0.5; + } + + $now = microtime(true); + if (($now - $this->lastUpdate) < (1.0 / $writes_per_second)) { + return false; + } + + return true; + } + + private function didPublishToConsole() { + $this->lastUpdate = microtime(true); + } + + private function isTTY() { + if ($this->isTTY === null) { + $this->isTTY = (function_exists('posix_isatty') && posix_isatty(STDERR)); + } + return $this->isTTY; + } + + private function getWidth() { + if ($this->width === null) { + $width = phutil_console_get_terminal_width(); + $width = min(nonempty($width, 78), 78); + $this->width = $width; + } + + return $this->width; + } + + private function overwriteLine($line) { + $head = "\r"; + $tail = ''; + + if ($this->lineWidth) { + $line_len = strlen($line); + + if ($line_len < $this->lineWidth) { + $tail = str_repeat(' ', $this->lineWidth - $line_len); + } + + $this->lineWidth = strlen($line); + } + + $this->printLine($head.$line.$tail); + } + + private function printLine($line) { + fprintf(STDERR, '%s', $line); + } +} diff --git a/src/progress/PhutilProgressSink.php b/src/progress/PhutilProgressSink.php new file mode 100644 --- /dev/null +++ b/src/progress/PhutilProgressSink.php @@ -0,0 +1,54 @@ +isRunning = true; + } + + public function __destruct() { + if ($this->isRunning) { + $this->didFailWork(); + } + } + + final public function setTotalWork($total_work) { + $this->totalWork = $total_work; + return $this; + } + + final public function getTotalWork() { + return $this->totalWork; + } + + final public function getCompletedWork() { + return $this->completedWork; + } + + final public function didMakeProgress($amount = 1) { + if ($this->isRunning) { + $this->completedWork += $amount; + $this->publishProgress(); + } + } + + final public function didCompleteWork() { + $this->isRunning = false; + $this->publishCompletion(); + } + + final public function didFailWork() { + $this->isRunning = false; + $this->publishFailure(); + } + + abstract protected function publishProgress(); + abstract protected function publishCompletion(); + abstract protected function publishFailure(); + +}