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;
}
}
}