mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-06-10 08:37:02 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c7a1bd90e | |||
| 7b7c610458 | |||
| b9dea64ba2 | |||
| b0ab901b31 | |||
| d58db9438e | |||
| 877968d8e5 |
@@ -4,6 +4,10 @@ const observer = new MutationObserver((records) => {
|
|||||||
// function being present.
|
// function being present.
|
||||||
if (node && typeof(node.scrollIntoView) === 'function') {
|
if (node && typeof(node.scrollIntoView) === 'function') {
|
||||||
node.scrollIntoView();
|
node.scrollIntoView();
|
||||||
|
if (node.classList.contains('progress-indicator')) {
|
||||||
|
node.previousSibling.append('.');
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
observer.observe(document, {childList: true, subtree: true});
|
observer.observe(document, {childList: true, subtree: true});
|
||||||
|
|||||||
@@ -410,6 +410,11 @@ readonly class DockerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function startStreamingResponse(Response $response) : Response {
|
private function startStreamingResponse(Response $response) : Response {
|
||||||
|
// Ensure the script keeps running even if the client connection drops (e.g. due to a
|
||||||
|
// reverse proxy read timeout during a long image pull). Without this, PHP would abort
|
||||||
|
// on the first write after the connection is gone, leaving only some containers started.
|
||||||
|
ignore_user_abort(true);
|
||||||
|
|
||||||
$nonbufResp = $response
|
$nonbufResp = $response
|
||||||
->withBody(new NonBufferedBody())
|
->withBody(new NonBufferedBody())
|
||||||
->withHeader('Content-Type', 'text/html; charset=utf-8')
|
->withHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
@@ -430,7 +435,14 @@ readonly class DockerController {
|
|||||||
// if it'll actually pull an image), but which should not need to know anything about the
|
// if it'll actually pull an image), but which should not need to know anything about the
|
||||||
// wanted markup or formatting.
|
// wanted markup or formatting.
|
||||||
$addToStreamingResponseBody = function (Container $container, string $message) use ($nonbufResp) : void {
|
$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;
|
return $addToStreamingResponseBody;
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ use AIO\ContainerDefinitionFetcher;
|
|||||||
use AIO\Data\ConfigurationManager;
|
use AIO\Data\ConfigurationManager;
|
||||||
use AIO\Data\DataConst;
|
use AIO\Data\DataConst;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Psr7\Utils;
|
||||||
use GuzzleHttp\Exception\RequestException;
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use http\Env\Response;
|
use http\Env\Response;
|
||||||
|
|
||||||
readonly class DockerActionManager {
|
readonly class DockerActionManager {
|
||||||
private const string API_VERSION = 'v1.44';
|
private const string API_VERSION = 'v1.44';
|
||||||
|
private const int PULL_HEARTBEAT_INTERVAL_SECONDS = 4;
|
||||||
private Client $guzzleClient;
|
private Client $guzzleClient;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -560,10 +562,45 @@ readonly class DockerActionManager {
|
|||||||
$maxRetries = 3;
|
$maxRetries = 3;
|
||||||
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
||||||
try {
|
try {
|
||||||
$this->guzzleClient->post($url);
|
// Use streaming so we can write heartbeat messages to the response while the
|
||||||
|
// image is being pulled. Without this, a long pull produces no output and a
|
||||||
|
// reverse proxy (nginx) can drop the connection after its read timeout expires.
|
||||||
|
// Once the connection is gone, PHP aborts on the next write and all consecutive
|
||||||
|
// containers are never started.
|
||||||
|
// We have to specify the proxy again, since when streaming, Guzzle apparently doesn't use
|
||||||
|
// 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();
|
||||||
|
$pullErrors = [];
|
||||||
|
$lastHeartbeat = time();
|
||||||
|
while (!$pullBody->eof()) {
|
||||||
|
$line = Utils::readLine($pullBody);
|
||||||
|
$event = json_decode($line, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
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();
|
||||||
|
$interval = time() - $lastHeartbeat;
|
||||||
|
if ($interval >= self::PULL_HEARTBEAT_INTERVAL_SECONDS) {
|
||||||
|
$addToStreamingResponseBody($container, ".");
|
||||||
|
$lastHeartbeat = $now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($pullErrors !== []) {
|
||||||
|
throw new \Exception(implode('; ', $pullErrors));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
} catch (RequestException $e) {
|
} catch (\Exception $e) {
|
||||||
$message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $e->getResponse()?->getBody()->getContents();
|
$errorDetails = $e instanceof RequestException
|
||||||
|
? $e->getResponse()?->getBody()->getContents()
|
||||||
|
: $e->getMessage();
|
||||||
|
$message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $errorDetails;
|
||||||
if ($attempt === $maxRetries) {
|
if ($attempt === $maxRetries) {
|
||||||
if ($imageIsThere === false) {
|
if ($imageIsThere === false) {
|
||||||
throw new \Exception($message);
|
throw new \Exception($message);
|
||||||
|
|||||||
Reference in New Issue
Block a user