timelimt on recovery status

Signed-off-by: Andy Miller <rhuk@mac.com>
This commit is contained in:
Andy Miller
2025-10-16 09:08:53 -06:00
parent 7192cfe549
commit c56d24c0d7
5 changed files with 140 additions and 0 deletions

View File

@@ -253,6 +253,15 @@ switch ($command) {
echo " File: {$file}\n";
echo " Line: {$line}\n";
echo " Created: {$created}\n";
$window = $manager->getUpgradeWindow();
if ($window) {
$expires = isset($window['expires_at']) ? date('c', (int)$window['expires_at']) : 'unknown';
$reason = $window['reason'] ?? '(unknown)';
echo " Window: active ({$reason}, expires {$expires})\n";
} else {
echo " Window: inactive\n";
}
exit(0);
default:

View File

@@ -20,6 +20,7 @@ use function is_array;
use function is_file;
use function json_decode;
use function json_encode;
use function max;
use function md5;
use function preg_match;
use function random_bytes;
@@ -100,6 +101,8 @@ class RecoveryManager
if (is_file($flag)) {
@unlink($flag);
}
$this->closeUpgradeWindow();
}
/**
@@ -121,6 +124,10 @@ class RecoveryManager
$file = $error['file'] ?? '';
$plugin = $this->detectPluginFromPath($file);
if (!$plugin) {
return;
}
$context = [
'created_at' => time(),
'message' => $error['message'] ?? '',
@@ -130,6 +137,10 @@ class RecoveryManager
'plugin' => $plugin,
];
if (!$this->shouldEnterRecovery($context)) {
return;
}
$this->activate($context);
if ($plugin) {
$this->quarantinePlugin($plugin, $context);
@@ -286,6 +297,63 @@ class RecoveryManager
return $this->rootPath . '/system/recovery.flag';
}
/**
* @return string
*/
private function windowPath(): string
{
return $this->rootPath . '/system/recovery.window';
}
/**
* @return array|null
*/
private function resolveUpgradeWindow(): ?array
{
$path = $this->windowPath();
if (!is_file($path)) {
return null;
}
$decoded = json_decode(file_get_contents($path), true);
if (!is_array($decoded)) {
@unlink($path);
return null;
}
$expiresAt = (int)($decoded['expires_at'] ?? 0);
if ($expiresAt > 0 && $expiresAt < time()) {
@unlink($path);
return null;
}
return $decoded;
}
/**
* @param array $context
* @return bool
*/
private function shouldEnterRecovery(array $context): bool
{
$window = $this->resolveUpgradeWindow();
if (null === $window) {
return false;
}
$scope = $window['scope'] ?? null;
if ($scope === 'plugin') {
$expected = $window['plugin'] ?? null;
if ($expected && ($context['plugin'] ?? null) !== $expected) {
return false;
}
}
return true;
}
/**
* @return string
*/
@@ -314,4 +382,54 @@ class RecoveryManager
{
return error_get_last();
}
/**
* Begin an upgrade window; during this window fatal plugin errors may trigger recovery mode.
*
* @param string $reason
* @param array $metadata
* @param int $ttlSeconds
* @return void
*/
public function markUpgradeWindow(string $reason, array $metadata = [], int $ttlSeconds = 604800): void
{
$ttl = max(60, $ttlSeconds);
$createdAt = time();
$payload = $metadata + [
'reason' => $reason,
'created_at' => $createdAt,
'expires_at' => $createdAt + $ttl,
];
file_put_contents($this->windowPath(), json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
}
/**
* @return bool
*/
public function isUpgradeWindowActive(): bool
{
return $this->resolveUpgradeWindow() !== null;
}
/**
* @return array|null
*/
public function getUpgradeWindow(): ?array
{
return $this->resolveUpgradeWindow();
}
/**
* @return void
*/
public function closeUpgradeWindow(): void
{
$window = $this->windowPath();
if (is_file($window)) {
@unlink($window);
}
}
}

View File

@@ -222,6 +222,13 @@ class SelfupgradeCommand extends GpmCommand
$io->newLine();
$io->writeln("Preparing to upgrade to v<cyan>{$remote}</cyan>..");
/** @var \Grav\Common\Recovery\RecoveryManager $recovery */
$recovery = Grav::instance()['recovery'];
$recovery->markUpgradeWindow('core-upgrade', [
'scope' => 'core',
'target_version' => $remote,
]);
$io->write(" |- Downloading upgrade [{$this->formatBytes($update['size'])}]... 0%");
$this->file = $this->download($update);

View File

@@ -12,6 +12,7 @@ namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Upgrader;
use Grav\Common\Grav;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
@@ -212,6 +213,10 @@ class UpdateCommand extends GpmCommand
}
}
/** @var \Grav\Common\Recovery\RecoveryManager $recovery */
$recovery = Grav::instance()['recovery'];
$recovery->markUpgradeWindow('package-update', ['scope' => 'core']);
// finally update
$install_command = $this->getApplication()->find('install');

View File

@@ -56,6 +56,7 @@ class RecoveryManagerTest extends \Codeception\TestCase\Test
}
};
$manager->markUpgradeWindow('core-upgrade', ['scope' => 'core']);
$manager->handleShutdown();
$flag = $this->tmpDir . '/system/recovery.flag';