As heartbeat send a dot regularly

Rather than repeating the message, send a "magic" dot, which gets
appended to the previous line.

Previously the heartbeats weren't sent regulary because reading the data
into a buffer caused a lag.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
This commit is contained in:
Pablo Zmdl
2026-05-21 13:22:32 +02:00
parent d58db9438e
commit b0ab901b31
3 changed files with 27 additions and 32 deletions

View File

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

View File

@@ -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("<div>{$container->displayName}: {$message}</div>");
// 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 = "<span class='progress-indicator'></span>";
} else {
$content = "<div>{$container->displayName}: {$message}</div>";
}
$nonbufResp->getBody()->write($content);
};
return $addToStreamingResponseBody;

View File

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