diff --git a/CHANGELOG.md b/CHANGELOG.md index dc84c0a53..0345b6353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ Yii Framework 2 debug extension Change Log ========================================== -2.1.26 under development ------------------------- +2.2.0 under development +----------------------- +- Enh #489: Adds data storage functionality that allows you to replace file storage with any available storage (evgenybukharev) - Enh #430: Allow to configure toolbar position via `Module::$toolbarPosition` property (sasha-x) - Bug #528: Fix `yii\debug\Panel::getTraceLine()` to handle backtrace for internal PHP functions (zymeli) diff --git a/README.md b/README.md index 231f1c2c8..a82334b25 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,28 @@ You will see a debugger toolbar showing at the bottom of every page of your appl You can click on the toolbar to see more detailed debug information. +Data Storage +----- + +You can save debug data in any storage, for this you need to implement the yii\debug\components\data\DataStorage interface, and configure the module. +Solves the problem of correct operation of the debug panel in case the application is located on several servers. + +```php +return [ + 'bootstrap' => ['debug'], + 'modules' => [ + 'debug' => [ + 'class' => 'yii\debug\Module', + 'dataStorageConfig'=>[ + 'class'=>'yii\debug\components\data\CacheDataStorage', + ], + ], + // ... + ], + ... +]; +``` + Open Files in IDE ----- diff --git a/src/LogTarget.php b/src/LogTarget.php index 5d94fdda3..f4ffe6cc1 100644 --- a/src/LogTarget.php +++ b/src/LogTarget.php @@ -45,17 +45,14 @@ public function __construct($module, $config = []) /** * Exports log messages to a specific destination. * Child classes must implement this method. - * @throws \yii\base\Exception */ public function export() { - $path = $this->module->dataPath; - FileHelper::createDirectory($path, $this->module->dirMode); - $summary = $this->collectSummary(); - $dataFile = "$path/{$this->tag}.data"; + $data = []; $exceptions = []; + foreach ($this->module->panels as $id => $panel) { try { $panelData = $panel->save(); @@ -71,47 +68,25 @@ public function export() $data['summary'] = $summary; $data['exceptions'] = $exceptions; - file_put_contents($dataFile, serialize($data)); - if ($this->module->fileMode !== null) { - @chmod($dataFile, $this->module->fileMode); - } - - $indexFile = "$path/index.data"; - $this->updateIndexFile($indexFile, $summary); + $this->module->getDataStorage()->setData($this->tag, $data); } /** - * @see DefaultController * @return array + * @see DefaultController */ public function loadManifest() { - $indexFile = $this->module->dataPath . '/index.data'; - - $content = ''; - $fp = @fopen($indexFile, 'r'); - if ($fp !== false) { - @flock($fp, LOCK_SH); - $content = fread($fp, filesize($indexFile)); - @flock($fp, LOCK_UN); - fclose($fp); - } - - if ($content !== '') { - return array_reverse(unserialize($content), true); - } - - return []; + return $this->module->getDataStorage()->getDataManifest(); } /** - * @see DefaultController * @return array + * @see DefaultController */ public function loadTagToPanels($tag) { - $dataFile = $this->module->dataPath . "/$tag.data"; - $data = unserialize(file_get_contents($dataFile)); + $data = $this->module->getDataStorage()->getData($tag); $exceptions = $data['exceptions']; foreach ($this->module->panels as $id => $panel) { if (isset($data[$id])) { @@ -124,49 +99,11 @@ public function loadTagToPanels($tag) $panel->setError($exceptions[$id]); } } + $this->module->getDataStorage()->setData($this->tag, $data); return $data; } - /** - * Updates index file with summary log data - * - * @param string $indexFile path to index file - * @param array $summary summary log data - * @throws \yii\base\InvalidConfigException - */ - private function updateIndexFile($indexFile, $summary) - { - if (!@touch($indexFile) || ($fp = @fopen($indexFile, 'r+')) === false) { - throw new InvalidConfigException("Unable to open debug data index file: $indexFile"); - } - @flock($fp, LOCK_EX); - $manifest = ''; - while (($buffer = fgets($fp)) !== false) { - $manifest .= $buffer; - } - if (!feof($fp) || empty($manifest)) { - // error while reading index data, ignore and create new - $manifest = []; - } else { - $manifest = unserialize($manifest); - } - - $manifest[$this->tag] = $summary; - $this->gc($manifest); - - ftruncate($fp, 0); - rewind($fp); - fwrite($fp, serialize($manifest)); - - @flock($fp, LOCK_UN); - @fclose($fp); - - if ($this->module->fileMode !== null) { - @chmod($indexFile, $this->module->fileMode); - } - } - /** * Processes the given log messages. * This method will filter the given messages with [[levels]] and [[categories]]. @@ -174,7 +111,6 @@ private function updateIndexFile($indexFile, $summary) * @param array $messages log messages to be processed. See [[\yii\log\Logger::messages]] for the structure * of each message. * @param bool $final whether this method is called at the end of the current application - * @throws \yii\base\Exception */ public function collect($messages, $final) { @@ -184,54 +120,6 @@ public function collect($messages, $final) } } - /** - * Removes obsolete data files - * @param array $manifest - */ - protected function gc(&$manifest) - { - if (count($manifest) > $this->module->historySize + 10) { - $n = count($manifest) - $this->module->historySize; - foreach (array_keys($manifest) as $tag) { - $file = $this->module->dataPath . "/$tag.data"; - @unlink($file); - if (isset($manifest[$tag]['mailFiles'])) { - foreach ($manifest[$tag]['mailFiles'] as $mailFile) { - @unlink(Yii::getAlias($this->module->panels['mail']->mailPath) . "/$mailFile"); - } - } - unset($manifest[$tag]); - if (--$n <= 0) { - break; - } - } - $this->removeStaleDataFiles($manifest); - } - } - - /** - * Remove staled data files i.e. files that are not in the current index file - * (may happen because of corrupted or rotated index file) - * - * @param array $manifest - * @since 2.0.11 - */ - protected function removeStaleDataFiles($manifest) - { - $storageTags = array_map( - function ($file) { - return pathinfo($file, PATHINFO_FILENAME); - }, - FileHelper::findFiles($this->module->dataPath, ['except' => ['index.data']]) - ); - - $staledTags = array_diff($storageTags, array_keys($manifest)); - - foreach ($staledTags as $tag) { - @unlink($this->module->dataPath . "/$tag.data"); - } - } - /** * Collects summary data of current request. * @return array diff --git a/src/Module.php b/src/Module.php index 2f774ea40..e688f5706 100644 --- a/src/Module.php +++ b/src/Module.php @@ -10,19 +10,22 @@ use Yii; use yii\base\Application; use yii\base\BootstrapInterface; -use yii\helpers\Html; -use yii\helpers\IpHelper; +use yii\base\InvalidConfigException; +use yii\debug\components\data\DataStorage; +use yii\di\Instance; use yii\helpers\Json; -use yii\helpers\Url; -use yii\web\ForbiddenHttpException; +use yii\helpers\IpHelper; use yii\web\Response; +use yii\helpers\Html; +use yii\helpers\Url; use yii\web\View; +use yii\web\ForbiddenHttpException; /** * The Yii Debug Module provides the debug toolbar and debugger * * @author Qiang Xue - * @since 2.0 + * @since 2.0 */ class Module extends \yii\base\Module implements BootstrapInterface { @@ -76,30 +79,20 @@ class Module extends \yii\base\Module implements BootstrapInterface * @since 2.0.7 */ public $defaultPanel = 'log'; + /** - * @var string the directory storing the debugger data files. This can be specified using a path alias. - */ - public $dataPath = '@runtime/debug'; - /** - * @var int the permission to be set for newly created debugger data files. - * This value will be used by PHP [[chmod()]] function. No umask will be applied. - * If not set, the permission will be determined by the current environment. - * @since 2.0.6 - */ - public $fileMode; - /** - * @var int the permission to be set for newly created directories. - * This value will be used by PHP [[chmod()]] function. No umask will be applied. - * Defaults to 0775, meaning the directory is read-writable by owner and group, - * but read-only for other users. - * @since 2.0.6 + * @var DataStorage */ - public $dirMode = 0775; + private $dataStorage; + /** - * @var int the maximum number of debug data files to keep. If there are more files generated, - * the oldest ones will be removed. + * Config options by DataStorage + * @var string[] */ - public $historySize = 50; + public $dataStorageConfig = [ + 'class' => 'yii\debug\components\data\FileDataStorage', + ]; + /** * @var int the debug bar default height, as a percentage of the total screen height * @since 2.1.1 @@ -117,6 +110,7 @@ class Module extends \yii\base\Module implements BootstrapInterface * You may want to enable the debug logs if you want to investigate how the debug module itself works. */ public $enableDebugLogs = false; + /** * @var bool whether to disable IP address restriction warning triggered by checkAccess function * @since 2.0.14 @@ -226,7 +220,8 @@ public static function setYiiLogo($logo) public function init() { parent::init(); - $this->dataPath = Yii::getAlias($this->dataPath); + + $this->dataStorage = Instance::ensure($this->dataStorageConfig + ['module' => $this],'yii\debug\components\data\DataStorage'); $this->initPanels(); } @@ -490,6 +485,14 @@ protected function corePanels() return $corePanels; } + /** + * @return DataStorage + */ + public function getDataStorage() + { + return $this->dataStorage; + } + /** * {@inheritdoc} * @since 2.0.7 diff --git a/src/components/data/CacheDataStorage.php b/src/components/data/CacheDataStorage.php new file mode 100644 index 000000000..b657fcd1a --- /dev/null +++ b/src/components/data/CacheDataStorage.php @@ -0,0 +1,159 @@ +cache = Instance::ensure($this->cacheComponent,'yii\caching\Cache'); + } + + /** + * @param string $tag + * + * @return array + */ + public function getData($tag) + { + return $this->cache->exists($this->cacheDebugDataKey . $tag) ? unserialize($this->cache->get($this->cacheDebugDataKey . $tag)) : []; + } + + /** + * @param string $tag + * @param array $data + * + * @return mixed|void + */ + public function setData($tag, $data) + { + $this->cache->set($this->cacheDebugDataKey . $tag, serialize($data), $this->dataDuration); + $this->updateIndex($tag, $data['summary'] ?: []); + } + + /** + * @param $forceReload + * + * @return array|mixed + */ + public function getDataManifest($forceReload = false) + { + return $this->cache->exists($this->cacheDebugManifestKey) ? unserialize($this->cache->get($this->cacheDebugManifestKey)) : []; + } + + /** + * @param Module $module + * + * @return mixed|void + */ + public function setModule($module) + { + $this->module = $module; + } + + /** + * @param string $tag + * @param $summary + * + * @return void + */ + private function updateIndex($tag, $summary) + { + $manifest = $this->cache->get($this->cacheDebugManifestKey); + + if (empty($manifest)) { + $manifest = []; + } else { + $manifest = unserialize($manifest); + } + + $manifest[$tag] = $summary; + $this->gc($manifest); + + $this->cache->set($this->cacheDebugManifestKey, serialize($manifest), $this->manifestDuration); + } + + + /** + * Removes obsolete data files + * + * @param array $manifest + */ + protected function gc(&$manifest) + { + if (count($manifest) > $this->historySize + 10) { + $n = count($manifest) - $this->historySize; + foreach (array_keys($manifest) as $tag) { + if (isset($manifest[$tag]['mailFiles'])) { + foreach ($manifest[$tag]['mailFiles'] as $mailFile) { + @unlink(Yii::getAlias($this->module->panels['mail']->mailPath) . "/$mailFile"); + } + } + unset($manifest[$tag]); + if (--$n <= 0) { + break; + } + } + } + } +} diff --git a/src/components/data/DataStorage.php b/src/components/data/DataStorage.php new file mode 100644 index 000000000..6155b86db --- /dev/null +++ b/src/components/data/DataStorage.php @@ -0,0 +1,37 @@ +dataPath = Yii::getAlias($this->dataPath); + } + + /** + * @param $tag + * + * @return array + */ + public function getData($tag) + { + $dataFile = $this->dataPath . "/$tag.data"; + return unserialize(file_get_contents($dataFile)); + } + + /** + * @param string $tag + * @param array $data + * + * @return void + * @throws InvalidConfigException + * @throws \yii\base\Exception + */ + public function setData($tag, $data) + { + $path = $this->dataPath; + FileHelper::createDirectory($path, $this->dirMode); + $dataFile = "$path/{$tag}.data"; + + file_put_contents($dataFile, serialize($data)); + if ($this->fileMode !== null) { + @chmod($dataFile, $this->fileMode); + } + + $indexFile = "$path/index.data"; + $this->updateIndexFile($indexFile, $tag, $data['summary'] ?: []); + } + + /** + * @param $forceReload + * + * @return array|mixed + */ + public function getDataManifest($forceReload = false) + { + if ($this->_manifest === null || $forceReload) { + if ($forceReload) { + clearstatcache(); + } + $indexFile = $this->dataPath . '/index.data'; + + $content = ''; + $fp = @fopen($indexFile, 'r'); + if ($fp !== false) { + @flock($fp, LOCK_SH); + $content = fread($fp, filesize($indexFile)); + @flock($fp, LOCK_UN); + fclose($fp); + } + + if ($content !== '') { + $this->_manifest = array_reverse(unserialize($content), true); + } else { + $this->_manifest = []; + } + } + + return $this->_manifest; + } + + + /** + * Updates index file with summary log data + * + * @param string $indexFile path to index file + * @param array $summary summary log data + * + * @throws \yii\base\InvalidConfigException + */ + private function updateIndexFile($indexFile, $tag, $summary) + { + touch($indexFile); + if (($fp = @fopen($indexFile, 'r+')) === false) { + throw new InvalidConfigException("Unable to open debug data index file: $indexFile"); + } + @flock($fp, LOCK_EX); + $manifest = ''; + while (($buffer = fgets($fp)) !== false) { + $manifest .= $buffer; + } + if (!feof($fp) || empty($manifest)) { + // error while reading index data, ignore and create new + $manifest = []; + } else { + $manifest = unserialize($manifest); + } + + $manifest[$tag] = $summary; + $this->gc($manifest); + + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, serialize($manifest)); + + @flock($fp, LOCK_UN); + @fclose($fp); + + if ($this->fileMode !== null) { + @chmod($indexFile, $this->fileMode); + } + } + + /** + * Removes obsolete data files + * @param array $manifest + */ + protected function gc(&$manifest) + { + if (count($manifest) > $this->historySize + 10) { + $n = count($manifest) - $this->historySize; + foreach (array_keys($manifest) as $tag) { + $file = $this->dataPath . "/$tag.data"; + @unlink($file); + if (isset($manifest[$tag]['mailFiles'])) { + foreach ($manifest[$tag]['mailFiles'] as $mailFile) { + @unlink(Yii::getAlias($this->module->panels['mail']->mailPath) . "/$mailFile"); + } + } + unset($manifest[$tag]); + if (--$n <= 0) { + break; + } + } + $this->removeStaleDataFiles($manifest); + } + } + + /** + * Remove staled data files i.e. files that are not in the current index file + * (may happen because of corrupted or rotated index file) + * + * @param array $manifest + * @since 2.0.11 + */ + protected function removeStaleDataFiles($manifest) + { + $storageTags = array_map( + function ($file) { + return pathinfo($file, PATHINFO_FILENAME); + }, + FileHelper::findFiles($this->dataPath, ['except' => ['index.data']]) + ); + + $staledTags = array_diff($storageTags, array_keys($manifest)); + + foreach ($staledTags as $tag) { + @unlink($this->dataPath . "/$tag.data"); + } + } + + /** + * @param Module $module + * + * @return mixed|void + */ + public function setModule($module) + { + $this->module = $module; + } +} diff --git a/src/controllers/DefaultController.php b/src/controllers/DefaultController.php index 99e9559f6..280377cf4 100644 --- a/src/controllers/DefaultController.php +++ b/src/controllers/DefaultController.php @@ -8,18 +8,19 @@ namespace yii\debug\controllers; use Yii; -use yii\debug\models\search\Debug; use yii\web\Controller; use yii\web\NotFoundHttpException; +use yii\debug\models\search\Debug; use yii\web\Response; /** * Debugger controller provides browsing over available debug logs. * - * @see \yii\debug\Panel + * + * @see \yii\debug\Panel * * @author Qiang Xue - * @since 2.0 + * @since 2.0 */ class DefaultController extends Controller { @@ -36,11 +37,6 @@ class DefaultController extends Controller */ public $summary; - /** - * @var array - */ - private $_manifest; - /** * {@inheritdoc} @@ -57,7 +53,6 @@ public function actions() /** * {@inheritdoc} - * @throws \yii\web\BadRequestHttpException */ public function beforeAction($action) { @@ -65,24 +60,14 @@ public function beforeAction($action) return parent::beforeAction($action); } - /** - * Index action - * - * @return string - * @throws NotFoundHttpException - */ public function actionIndex() { $searchModel = new Debug(); - $dataProvider = $searchModel->search($_GET, $this->getManifest()); + $manifest = $this->module->getDataStorage()->getDataManifest(); + $dataProvider = $searchModel->search($_GET, $manifest); // load latest request - $tags = array_keys($this->getManifest()); - - if (empty($tags)) { - throw new \Exception("No debug data have been collected yet, try browsing the website first."); - } - + $tags = array_keys($manifest); $tag = reset($tags); $this->loadData($tag); @@ -90,21 +75,24 @@ public function actionIndex() 'panels' => $this->module->panels, 'dataProvider' => $dataProvider, 'searchModel' => $searchModel, - 'manifest' => $this->getManifest(), + 'manifest' => $manifest, ]); } /** - * @see \yii\debug\Panel - * @param string|null $tag debug data tag. + * @param string|null $tag debug data tag. * @param string|null $panel debug panel ID. + * * @return mixed response. * @throws NotFoundHttpException if debug data not found. + * @see \yii\debug\Panel */ public function actionView($tag = null, $panel = null) { + $manifest = $this->module->getDataStorage()->getDataManifest(); + if ($tag === null) { - $tags = array_keys($this->getManifest()); + $tags = array_keys($manifest); $tag = reset($tags); } $this->loadData($tag); @@ -121,19 +109,12 @@ public function actionView($tag = null, $panel = null) return $this->render('view', [ 'tag' => $tag, 'summary' => $this->summary, - 'manifest' => $this->getManifest(), + 'manifest' => $manifest, 'panels' => $this->module->panels, 'activePanel' => $activePanel, ]); } - /** - * Toolbar action - * - * @param string $tag - * @return string - * @throws NotFoundHttpException - */ public function actionToolbar($tag) { $this->loadData($tag, 5); @@ -165,24 +146,9 @@ public function actionDownloadMail($file) } /** - * @param bool $forceReload - * @return array - */ - protected function getManifest($forceReload = false) - { - if ($this->_manifest === null || $forceReload) { - if ($forceReload) { - clearstatcache(); - } - $this->_manifest = $this->module->logTarget->loadManifest(); - } - - return $this->_manifest; - } - - /** - * @param string $tag debug data tag. - * @param int $maxRetry maximum numbers of tag retrieval attempts. + * @param string $tag debug data tag. + * @param int $maxRetry maximum numbers of tag retrieval attempts. + * * @throws NotFoundHttpException if specified tag not found. */ public function loadData($tag, $maxRetry = 0) @@ -191,9 +157,19 @@ public function loadData($tag, $maxRetry = 0) // which may be delayed in some environment if xdebug is enabled. // See: https://github.com/yiisoft/yii2/issues/1504 for ($retry = 0; $retry <= $maxRetry; ++$retry) { - $manifest = $this->getManifest($retry > 0); + $manifest = $this->module->getDataStorage()->getDataManifest($retry > 0); if (isset($manifest[$tag])) { - $data = $this->module->logTarget->loadTagToPanels($tag); + $data=$this->module->getDataStorage()->getData($tag); + $exceptions = isset($data['exceptions'])?$data['exceptions']:[]; + foreach ($this->module->panels as $id => $panel) { + if (isset($data[$id])) { + $panel->tag = $tag; + $panel->load(unserialize($data[$id])); + } + if (isset($exceptions[$id])) { + $panel->setError($exceptions[$id]); + } + } $this->summary = $data['summary']; return;