mirror of
https://github.com/getgrav/grav.git
synced 2025-10-26 07:56:07 +01:00
@@ -10,7 +10,9 @@
|
|||||||
namespace Grav\Common\Recovery;
|
namespace Grav\Common\Recovery;
|
||||||
|
|
||||||
use Grav\Common\Filesystem\Folder;
|
use Grav\Common\Filesystem\Folder;
|
||||||
|
use Grav\Common\Grav;
|
||||||
use Grav\Common\Yaml;
|
use Grav\Common\Yaml;
|
||||||
|
use RocketTheme\Toolbox\Event\Event;
|
||||||
use function bin2hex;
|
use function bin2hex;
|
||||||
use function dirname;
|
use function dirname;
|
||||||
use function file_get_contents;
|
use function file_get_contents;
|
||||||
@@ -32,6 +34,7 @@ use const E_COMPILE_ERROR;
|
|||||||
use const E_CORE_ERROR;
|
use const E_CORE_ERROR;
|
||||||
use const E_ERROR;
|
use const E_ERROR;
|
||||||
use const E_PARSE;
|
use const E_PARSE;
|
||||||
|
use const E_USER_ERROR;
|
||||||
use const GRAV_ROOT;
|
use const GRAV_ROOT;
|
||||||
use const JSON_PRETTY_PRINT;
|
use const JSON_PRETTY_PRINT;
|
||||||
use const JSON_UNESCAPED_SLASHES;
|
use const JSON_UNESCAPED_SLASHES;
|
||||||
@@ -47,6 +50,8 @@ class RecoveryManager
|
|||||||
private $rootPath;
|
private $rootPath;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $userPath;
|
private $userPath;
|
||||||
|
/** @var bool */
|
||||||
|
private $failureCaptured = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $context Container or root path.
|
* @param mixed $context Container or root path.
|
||||||
@@ -77,6 +82,15 @@ class RecoveryManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
register_shutdown_function([$this, 'handleShutdown']);
|
register_shutdown_function([$this, 'handleShutdown']);
|
||||||
|
$events = null;
|
||||||
|
try {
|
||||||
|
$events = Grav::instance()['events'] ?? null;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$events = null;
|
||||||
|
}
|
||||||
|
if ($events && method_exists($events, 'addListener')) {
|
||||||
|
$events->addListener('onFatalException', [$this, 'onFatalException']);
|
||||||
|
}
|
||||||
$this->registered = true;
|
$this->registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +117,7 @@ class RecoveryManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->closeUpgradeWindow();
|
$this->closeUpgradeWindow();
|
||||||
|
$this->failureCaptured = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,35 +127,49 @@ class RecoveryManager
|
|||||||
*/
|
*/
|
||||||
public function handleShutdown(): void
|
public function handleShutdown(): void
|
||||||
{
|
{
|
||||||
|
if ($this->failureCaptured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$error = $this->resolveLastError();
|
$error = $this->resolveLastError();
|
||||||
if (!$error) {
|
if (!$error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$type = $error['type'] ?? 0;
|
$this->processFailure($error);
|
||||||
if (!$this->isFatal($type)) {
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle uncaught exceptions bubbled to the top-level handler.
|
||||||
|
*
|
||||||
|
* @param \Throwable $exception
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handleException(\Throwable $exception): void
|
||||||
|
{
|
||||||
|
if ($this->failureCaptured) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = $error['file'] ?? '';
|
$error = [
|
||||||
$plugin = $this->detectPluginFromPath($file);
|
'type' => E_ERROR,
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
$context = [
|
'file' => $exception->getFile(),
|
||||||
'created_at' => time(),
|
'line' => $exception->getLine(),
|
||||||
'message' => $error['message'] ?? '',
|
|
||||||
'file' => $file,
|
|
||||||
'line' => $error['line'] ?? null,
|
|
||||||
'type' => $type,
|
|
||||||
'plugin' => $plugin,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!$this->shouldEnterRecovery($context)) {
|
$this->processFailure($error);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->activate($context);
|
/**
|
||||||
if ($plugin) {
|
* @param Event $event
|
||||||
$this->quarantinePlugin($plugin, $context);
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onFatalException(Event $event): void
|
||||||
|
{
|
||||||
|
$exception = $event['exception'] ?? null;
|
||||||
|
if ($exception instanceof \Throwable) {
|
||||||
|
$this->handleException($exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +201,41 @@ class RecoveryManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $error
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function processFailure(array $error): void
|
||||||
|
{
|
||||||
|
$type = (int)($error['type'] ?? 0);
|
||||||
|
if (!$this->isFatal($type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $error['file'] ?? '';
|
||||||
|
$plugin = $this->detectPluginFromPath($file);
|
||||||
|
|
||||||
|
$context = [
|
||||||
|
'created_at' => time(),
|
||||||
|
'message' => $error['message'] ?? '',
|
||||||
|
'file' => $file,
|
||||||
|
'line' => $error['line'] ?? null,
|
||||||
|
'type' => $type,
|
||||||
|
'plugin' => $plugin,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$this->shouldEnterRecovery($context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->activate($context);
|
||||||
|
if ($plugin) {
|
||||||
|
$this->quarantinePlugin($plugin, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->failureCaptured = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return last recorded recovery context.
|
* Return last recorded recovery context.
|
||||||
*
|
*
|
||||||
@@ -265,7 +329,7 @@ class RecoveryManager
|
|||||||
*/
|
*/
|
||||||
private function isFatal(int $type): bool
|
private function isFatal(int $type): bool
|
||||||
{
|
{
|
||||||
return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true);
|
return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_USER_ERROR], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -403,6 +467,7 @@ class RecoveryManager
|
|||||||
$path = $this->windowPath();
|
$path = $this->windowPath();
|
||||||
Folder::create(dirname($path));
|
Folder::create(dirname($path));
|
||||||
file_put_contents($path, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
file_put_contents($path, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
||||||
|
$this->failureCaptured = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Grav\Common\Filesystem\Folder;
|
use Grav\Common\Filesystem\Folder;
|
||||||
use Grav\Common\Recovery\RecoveryManager;
|
use Grav\Common\Recovery\RecoveryManager;
|
||||||
|
use RocketTheme\Toolbox\Event\Event;
|
||||||
|
|
||||||
class RecoveryManagerTest extends \Codeception\TestCase\Test
|
class RecoveryManagerTest extends \Codeception\TestCase\Test
|
||||||
{
|
{
|
||||||
@@ -120,6 +121,38 @@ class RecoveryManagerTest extends \Codeception\TestCase\Test
|
|||||||
self::assertFileDoesNotExist($quarantine);
|
self::assertFileDoesNotExist($quarantine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHandleExceptionCreatesFlag(): void
|
||||||
|
{
|
||||||
|
$manager = new RecoveryManager($this->tmpDir);
|
||||||
|
$manager->markUpgradeWindow('core-upgrade', ['scope' => 'core']);
|
||||||
|
|
||||||
|
$manager->handleException(new \RuntimeException('Unhandled failure'));
|
||||||
|
|
||||||
|
$flag = $this->tmpDir . '/user/data/recovery.flag';
|
||||||
|
self::assertFileExists($flag);
|
||||||
|
$context = json_decode(file_get_contents($flag), true);
|
||||||
|
self::assertSame('Unhandled failure', $context['message']);
|
||||||
|
self::assertArrayHasKey('plugin', $context);
|
||||||
|
self::assertNull($context['plugin']);
|
||||||
|
|
||||||
|
$manager->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOnFatalExceptionDispatchesToHandler(): void
|
||||||
|
{
|
||||||
|
$manager = new RecoveryManager($this->tmpDir);
|
||||||
|
$manager->markUpgradeWindow('core-upgrade', ['scope' => 'core']);
|
||||||
|
|
||||||
|
$manager->onFatalException(new Event(['exception' => new \RuntimeException('Event failure')]));
|
||||||
|
|
||||||
|
$flag = $this->tmpDir . '/user/data/recovery.flag';
|
||||||
|
self::assertFileExists($flag);
|
||||||
|
$context = json_decode(file_get_contents($flag), true);
|
||||||
|
self::assertSame('Event failure', $context['message']);
|
||||||
|
|
||||||
|
$manager->clear();
|
||||||
|
}
|
||||||
|
|
||||||
public function testHandleShutdownIgnoresNonFatalErrors(): void
|
public function testHandleShutdownIgnoresNonFatalErrors(): void
|
||||||
{
|
{
|
||||||
$manager = new class($this->tmpDir) extends RecoveryManager {
|
$manager = new class($this->tmpDir) extends RecoveryManager {
|
||||||
|
|||||||
Reference in New Issue
Block a user