diff --git a/php/public/scroll-into-view.js b/php/public/scroll-into-view.js index 2c676911..3e4b9574 100644 --- a/php/public/scroll-into-view.js +++ b/php/public/scroll-into-view.js @@ -4,6 +4,10 @@ const observer = new MutationObserver((records) => { // function being present. if (node && typeof(node.scrollIntoView) === 'function') { node.scrollIntoView(); + if (node.classList.contains('progress-indicator')) { + node.previousSibling.append('.'); + node.remove(); + } } }); observer.observe(document, {childList: true, subtree: true}); diff --git a/php/src/Controller/DockerController.php b/php/src/Controller/DockerController.php index 8401b11a..06734186 100644 --- a/php/src/Controller/DockerController.php +++ b/php/src/Controller/DockerController.php @@ -435,7 +435,14 @@ readonly class DockerController { // if it'll actually pull an image), but which should not need to know anything about the // wanted markup or formatting. $addToStreamingResponseBody = function (Container $container, string $message) use ($nonbufResp) : void { - $nonbufResp->getBody()->write("
{$container->displayName}: {$message}
"); + // If the message is a single dot we treat it as a progress indicator and send a specific, empty + // HTML element, which gets special treatment by the Javascript code. + if ($message === '.') { + $content = ""; + } else { + $content = "
{$container->displayName}: {$message}
"; + } + $nonbufResp->getBody()->write($content); }; return $addToStreamingResponseBody; diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 73b28e83..ffff4a23 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -10,6 +10,7 @@ use AIO\ContainerDefinitionFetcher; use AIO\Data\ConfigurationManager; use AIO\Data\DataConst; use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Utils; use GuzzleHttp\Exception\RequestException; use http\Env\Response; @@ -572,41 +573,24 @@ readonly class DockerActionManager { // libcurl and thus the curl option set when creating the client doesn't apply. $pullResponse = $this->guzzleClient->post($url, ['proxy' => 'unix:///var/run/docker.sock', 'stream' => true]); $pullBody = $pullResponse->getBody(); - $buffer = ''; $pullErrors = []; - $lastHeartbeat = 0; + $lastHeartbeat = time(); while (!$pullBody->eof()) { - $chunk = $pullBody->read(self::PULL_STREAM_READ_CHUNK_SIZE); - if ($chunk === '') { + $line = Utils::readLine($pullBody); + $event = json_decode($line, true); + if (!is_array($event)) { continue; } - $buffer .= $chunk; - // Guard against malformed responses that contain no newlines. - if (strlen($buffer) > self::PULL_STREAM_MAX_BUFFER_SIZE && strpos($buffer, "\n") === false) { - error_log('Docker pull response buffer exceeded 1 MB without a newline, discarding buffer for ' . $imageName); - $buffer = ''; - continue; - } - while (($newlinePos = strpos($buffer, "\n")) !== false) { - $line = trim(substr($buffer, 0, $newlinePos)); - $buffer = substr($buffer, $newlinePos + 1); - if ($line === '') { - continue; - } - $event = json_decode($line, true); - if (!is_array($event)) { - continue; - } - if (isset($event['error'])) { - $pullErrors[] = $event['error']; - } elseif ($addToStreamingResponseBody !== null) { - // Write a heartbeat at most once every 5 seconds so the reverse - // proxy sees continuous data and does not close the connection. - $now = time(); - if ($now - $lastHeartbeat >= self::PULL_HEARTBEAT_INTERVAL_SECONDS) { - $addToStreamingResponseBody($container, "Pulling image"); - $lastHeartbeat = $now; - } + if (isset($event['error'])) { + $pullErrors[] = $event['error']; + } elseif ($addToStreamingResponseBody !== null) { + // Write a heartbeat at most once every 5 seconds so the reverse + // proxy sees continuous data and does not close the connection. + $now = time(); + $interval = time() - $lastHeartbeat; + if ($interval >= self::PULL_HEARTBEAT_INTERVAL_SECONDS) { + $addToStreamingResponseBody($container, "."); + $lastHeartbeat = $now; } } }