Files
2024-07-15 11:28:08 +02:00

584 lines
14 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Base;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Factory;
use Exception;
use Psr\Log\LogLevel;
use Throwable;
/**
* Base class for all Akeeba Engine parts.
*
* Parts are objects which perform a specific function during the backup process, e.g. backing up files or dumping
* database contents. They have a fully defined and controlled lifecycle, from initialization to finalization. The
* transition between lifecycle phases is handled by the `tick()` method which is essentially the only public interface
* to interacting with an engine part.
*/
abstract class Part
{
public const STATE_INIT = 0;
public const STATE_PREPARED = 1;
public const STATE_RUNNING = 2;
public const STATE_POSTRUN = 3;
public const STATE_FINISHED = 4;
public const STATE_ERROR = 99;
/**
* The current state of this part; see the constants at the top of this class
*
* @var int
*/
protected $currentState = self::STATE_INIT;
/**
* The name of the engine part (a.k.a. Domain), used in return table
* generation.
*
* @var string
*/
protected $activeDomain = "";
/**
* The step this engine part is in. Used verbatim in return table and
* should be set by the code in the _run() method.
*
* @var string
*/
protected $activeStep = "";
/**
* A more detailed description of the step this engine part is in. Used
* verbatim in return table and should be set by the code in the _run()
* method.
*
* @var string
*/
protected $activeSubstep = "";
/**
* Any configuration variables, in the form of an array.
*
* @var array
*/
protected $_parametersArray = [];
/**
* The database root key
*
* @var string
*/
protected $databaseRoot = [];
/**
* Should we log the step nesting?
*
* @var bool
*/
protected $nest_logging = false;
/**
* Embedded installer preferences
*
* @var object
*/
protected $installerSettings;
/**
* How much milliseconds should we wait to reach the min exec time
*
* @var int
*/
protected $waitTimeMsec = 0;
/**
* Should I ignore the minimum execution time altogether?
*
* @var bool
*/
protected $ignoreMinimumExecutionTime = false;
/**
* The last exception thrown during the tick() method's execution.
*
* @var null|Exception
*/
protected $lastException = null;
/**
* Public constructor
*
* @return void
*/
public function __construct()
{
// Fetch the installer settings
$this->installerSettings = (object) [
'installerroot' => 'installation',
'sqlroot' => 'installation/sql',
'databasesini' => 1,
'readme' => 1,
'extrainfo' => 1,
'password' => 0,
];
$config = Factory::getConfiguration();
$installerKey = $config->get('akeeba.advanced.embedded_installer');
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
// Fall back to default ANGIE installer if the selected installer is not found
if (!array_key_exists($installerKey, $installerDescriptors))
{
$installerKey = 'angie';
}
if (array_key_exists($installerKey, $installerDescriptors))
{
$this->installerSettings = (object) $installerDescriptors[$installerKey];
}
}
/**
* Nested logging of exceptions
*
* The message is logged using the specified log level. The detailed information of the Throwable and its trace are
* logged using the DEBUG level.
*
* If the Throwable is nested, its parents are logged recursively. This should create a thorough trace leading to
* the root cause of an error.
*
* @param Exception|Throwable $exception The Exception or Throwable to log
* @param string $logLevel The log level to use, default ERROR
*/
protected static function logErrorsFromException($exception, $logLevel = LogLevel::ERROR)
{
$logger = Factory::getLog();
$logger->log($logLevel, $exception->getMessage());
$logger->debug(sprintf('[%s] %s(%u) #%u %s', get_class($exception), $exception->getFile(), $exception->getLine(), $exception->getCode(), $exception->getMessage()));
foreach (explode("\n", $exception->getTraceAsString()) as $line)
{
$logger->debug(rtrim($line));
}
$previous = $exception->getPrevious();
if (!is_null($previous))
{
self::logErrorsFromException($previous, $logLevel);
}
}
/**
* The public interface to an engine part. This method takes care for
* calling the correct method in order to perform the initialisation -
* run - finalisation cycle of operation and return a proper response array.
*
* @param int $nesting
*
* @return array A response array
*/
public function tick($nesting = 0)
{
$configuration = Factory::getConfiguration();
$timer = Factory::getTimer();
$this->waitTimeMsec = 0;
$this->lastException = null;
/**
* Call the right action method, depending on engine part state.
*
* The action method may throw an exception to signal failure, hence the try-catch. If there is an exception we
* will set the part's state to STATE_ERROR and store the last exception.
*/
try
{
switch ($this->getState())
{
case self::STATE_INIT:
$this->_prepare();
break;
case self::STATE_PREPARED:
case self::STATE_RUNNING:
$this->_run();
break;
case self::STATE_POSTRUN:
$this->_finalize();
break;
}
}
catch (Exception $e)
{
$this->lastException = $e;
$this->setState(self::STATE_ERROR);
}
// If there is still time, we are not finished and there is no break flag set, re-run the tick()
// method.
$breakFlag = $configuration->get('volatile.breakflag', false);
if (
!in_array($this->getState(), [self::STATE_FINISHED, self::STATE_ERROR]) &&
($timer->getTimeLeft() > 0) &&
!$breakFlag &&
($nesting < 20) &&
($this->nest_logging)
)
{
// Nesting is only applied if $this->nest_logging == true (currently only Kettenrad has this)
$nesting++;
if ($this->nest_logging)
{
Factory::getLog()->debug("*** Batching successive steps (nesting level $nesting)");
}
return $this->tick($nesting);
}
// Return the output array
$out = $this->makeReturnTable();
// If it's not a nest-logged part (basically, anything other than Kettenrad) return the output array.
if (!$this->nest_logging)
{
return $out;
}
// From here on: things to do for nest-logged parts (i.e. Kettenrad)
if ($breakFlag)
{
Factory::getLog()->debug("*** Engine steps batching: Break flag detected.");
}
// Reset the break flag
$configuration->set('volatile.breakflag', false);
// Log that we're breaking the step
Factory::getLog()->debug("*** Batching of engine steps finished. I will now return control to the caller.");
// Detect whether I need server-side sleep
$serverSideSleep = $this->needsServerSideSleep();
// Enforce minimum execution time
if (!$this->ignoreMinimumExecutionTime)
{
$timer = Factory::getTimer();
$this->waitTimeMsec = (int) $timer->enforce_min_exec_time(true, $serverSideSleep);
}
// Send a Return Table back to the caller
return $out;
}
/**
* Returns a copy of the class's status array
*
* @return array The response array
*/
public function getStatusArray()
{
return $this->makeReturnTable();
}
/**
* Sends any kind of setup information to the engine part. Using this,
* we avoid passing parameters to the constructor of the class. These
* parameters should be passed as an indexed array and should be taken
* into account during the preparation process only. This function will
* set the error flag if it's called after the engine part is prepared.
*
* @param array $parametersArray The parameters to be passed to the engine part.
*
* @return void
*/
public function setup($parametersArray)
{
if ($this->currentState == self::STATE_PREPARED)
{
$this->setState(self::STATE_ERROR);
throw new ErrorException(__CLASS__ . ":: Can't modify configuration after the preparation of " . $this->activeDomain);
}
$this->_parametersArray = $parametersArray;
if (array_key_exists('root', $parametersArray))
{
$this->databaseRoot = $parametersArray['root'];
}
}
/**
* Returns the state of this engine part.
*
* @return int The state of this engine part.
*/
public function getState()
{
if (!is_null($this->lastException))
{
$this->currentState = self::STATE_ERROR;
}
return $this->currentState;
}
/**
* Translate the integer state to a string, used by consumers of the public Engine API.
*
* @param int $state The part state to translate to string
*
* @return string
*/
public function stateToString($state)
{
switch ($state)
{
case self::STATE_ERROR:
return 'error';
break;
case self::STATE_INIT:
return 'init';
break;
case self::STATE_PREPARED:
return 'prepared';
break;
case self::STATE_RUNNING:
return 'running';
break;
case self::STATE_POSTRUN:
return 'postrun';
break;
case self::STATE_FINISHED:
return 'finished';
break;
}
return 'init';
}
/**
* Get the current domain of the engine
*
* @return string The current domain
*/
public function getDomain()
{
return $this->activeDomain;
}
/**
* Get the current step of the engine
*
* @return string The current step
*/
public function getStep()
{
return $this->activeStep;
}
/**
* Get the current sub-step of the engine
*
* @return string The current sub-step
*/
public function getSubstep()
{
return $this->activeSubstep;
}
/**
* Implement this if your Engine Part can return the percentage of its work already complete
*
* @return float A number from 0 (nothing done) to 1 (all done)
*/
public function getProgress()
{
return 0;
}
/**
* Get the value of the minimum execution time ignore flag.
*
* DO NOT REMOVE. It is used by the Engine consumers.
*
* @return boolean
*/
public function isIgnoreMinimumExecutionTime()
{
return $this->ignoreMinimumExecutionTime;
}
/**
* Set the value of the minimum execution time ignore flag. When set, the nested logging parts (basically,
* Kettenrad) will ignore the minimum execution time parameter.
*
* DO NOT REMOVE. It is used by the Engine consumers.
*
* @param boolean $ignoreMinimumExecutionTime
*/
public function setIgnoreMinimumExecutionTime($ignoreMinimumExecutionTime)
{
$this->ignoreMinimumExecutionTime = $ignoreMinimumExecutionTime;
}
/**
* Runs any initialization code. Must set the state to STATE_PREPARED.
*
* @return void
*/
abstract protected function _prepare();
/**
* Runs any finalisation code. Must set the state to STATE_FINISHED.
*
* @return void
*/
abstract protected function _finalize();
/**
* Performs the main objective of this part. While still processing the state must be set to STATE_RUNNING. When the
* main objective is complete and we're ready to proceed to finalization the state must be set to STATE_POSTRUN.
*
* @return void
*/
abstract protected function _run();
/**
* Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately,
* in fear of timing out.
*
* @return void
*/
protected function setBreakFlag()
{
$registry = Factory::getConfiguration();
$registry->set('volatile.breakflag', true);
}
/**
* Sets the engine part's internal state, in an easy to use manner
*
* @param int $state The part state to set
*
* @return void
*/
protected function setState($state = self::STATE_INIT)
{
$this->currentState = $state;
}
/**
* Constructs a Response Array based on the engine part's state.
*
* @return array The Response Array for the current state
*/
protected function makeReturnTable()
{
$errors = [];
$e = $this->lastException;
while (!empty($e))
{
$errors[] = $e->getMessage();
$e = $e->getPrevious();
}
return [
'HasRun' => $this->currentState != self::STATE_FINISHED,
'Domain' => $this->activeDomain,
'Step' => $this->activeStep,
'Substep' => $this->activeSubstep,
'Error' => implode("\n", $errors),
'Warnings' => [],
'ErrorException' => $this->lastException,
];
}
/**
* Set the current domain of the engine
*
* @param string $new_domain The domain to set
*
* @return void
*/
protected function setDomain($new_domain)
{
$this->activeDomain = $new_domain;
}
/**
* Set the current step of the engine
*
* @param string $new_step The step to set
*
* @return void
*/
protected function setStep($new_step)
{
$this->activeStep = $new_step;
}
/**
* Set the current sub-step of the engine
*
* @param string $new_substep The sub-step to set
*
* @return void
*/
protected function setSubstep($new_substep)
{
$this->activeSubstep = $new_substep;
}
/**
* Do I need to apply server-side sleep for the time difference between the elapsed time and the minimum execution
* time?
*
* @return bool
*/
private function needsServerSideSleep()
{
/**
* If the part doesn't support tagging, i.e. I can't determine if this is a backend backup or not, I will always
* use server-side sleep.
*/
if (!method_exists($this, 'getTag'))
{
return true;
}
/**
* If this is not a backend backup I will always use server-side sleep. That is to say that legacy front-end,
* remote JSON API and CLI backups must always use server-side sleep since they do not support client-side
* sleep.
*/
if (!in_array($this->getTag(), ['backend']))
{
return true;
}
return Factory::getConfiguration()->get('akeeba.basic.clientsidewait', 0) == 0;
}
}