[BUGFIX] Flush opcode caches while saving PHP files. 24/27024/20
authorAlexander Opitz <opitz.alexander@googlemail.com>
Thu, 23 Jan 2014 13:43:36 +0000 (14:43 +0100)
committerMarkus Klein <klein.t3@mfc-linz.at>
Tue, 4 Mar 2014 16:05:06 +0000 (17:05 +0100)
After manipulating PHP files, which we include with "require" we should
clear the opcode cache, if there is one installed.

So we introduce OpcodeCacheUtility to handle the clearing of the opcode
cache. Also to have a way to give feedback to the install tool which
can show the quality of the opcode cache in use. It also checks if an
opcode cache is enabled in the configuration, not only if the extension
is installed.

Use of this opcode cache clearing is added to the ConfigurationManager,
PackageManager and the cache (Simple)FileBackend.

Make use of this data in the SystemEnvironmentCheck.

Resolves: #55252
Releases: 6.2, 6.1, 6.0
Change-Id: I881f3fbe055c9566663c2c3c238de62ae30f7149
Reviewed-on: https://review.typo3.org/27024
Reviewed-by: Markus Klein
Tested-by: Markus Klein
typo3/sysext/core/Classes/Cache/Backend/FileBackend.php
typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php
typo3/sysext/core/Classes/Configuration/ConfigurationManager.php
typo3/sysext/core/Classes/Package/PackageManager.php
typo3/sysext/core/Classes/Utility/OpcodeCacheUtility.php [new file with mode: 0644]
typo3/sysext/install/Classes/SystemEnvironment/Check.php

index 611c310..c218640 100644 (file)
@@ -174,6 +174,9 @@ class FileBackend extends \TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend implem
                if ($result === FALSE) {
                        throw new \TYPO3\CMS\Core\Cache\Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1222361632);
                }
+               if ($this->cacheEntryFileExtension === '.php') {
+                       \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::clearAllActive($cacheEntryPathAndFilename);
+               }
        }
 
        /**
index 7ce916f..9467a66 100644 (file)
@@ -242,6 +242,9 @@ class SimpleFileBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend im
                }
                $cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
                rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename);
+               if ($this->cacheEntryFileExtension === '.php') {
+                       \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::clearAllActive($cacheEntryPathAndFilename);
+               }
        }
 
        /**
index 5e2d116..84def50 100644 (file)
@@ -306,7 +306,7 @@ class ConfigurationManager {
                        );
                }
                $configuration = Utility\ArrayUtility::sortByKeyRecursive($configuration);
-               return Utility\GeneralUtility::writeFile(
+               $result = Utility\GeneralUtility::writeFile(
                        $localConfigurationFile,
                        '<?php' . LF .
                                'return ' .
@@ -317,6 +317,10 @@ class ConfigurationManager {
                        '?>',
                        TRUE
                );
+
+               Utility\OpcodeCacheUtility::clearAllActive($localConfigurationFile);
+
+               return $result;
        }
 
        /**
index 5dd09df..41045e0 100644 (file)
@@ -620,4 +620,15 @@ class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO
                $this->packages = $newPackages;
        }
 
+       /**
+        * Saves the current content of $this->packageStatesConfiguration to the
+        * PackageStates.php file.
+        *
+        * @return void
+        */
+       protected function sortAndSavePackageStates() {
+               parent::sortAndSavePackageStates();
+
+               \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::clearAllActive($this->packageStatesPathAndFilename);
+       }
 }
diff --git a/typo3/sysext/core/Classes/Utility/OpcodeCacheUtility.php b/typo3/sysext/core/Classes/Utility/OpcodeCacheUtility.php
new file mode 100644 (file)
index 0000000..efe6c73
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+namespace TYPO3\CMS\Core\Utility;
+
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2014 Alexander Opitz <opitz@pluspol-interactive.de>
+ * All rights reserved
+ *
+ * This script is part of the TYPO3 project. The TYPO3 project is
+ * free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * The GNU General Public License can be found at
+ * http://www.gnu.org/copyleft/gpl.html.
+ * A copy is found in the text file GPL.txt and important notices to the license
+ * from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ * This script is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+/**
+ * Class with helper functions for clearing the PHP opcache.
+ * It auto detects the opcache system and invalidates/resets it.
+ * http://forge.typo3.org/issues/55252
+ * Supported opcaches are: OPcache (PHP 5.5), APC, WinCache, XCache, eAccelerator, ZendOptimizerPlus
+ *
+ * @author Alexander Opitz <opitz@pluspol-interactive.de>
+ */
+class OpcodeCacheUtility {
+
+       /**
+        * All supported cache types
+        * @var array|null
+        */
+       static protected $supportedCaches = NULL;
+
+       /**
+        * Holds all currently active caches
+        * @var array|null
+        */
+       static protected $activeCaches = NULL;
+
+       /**
+        * Initialize the cache properties
+        */
+       static protected function initialize() {
+               $apcVersion = phpversion('apc');
+
+               static::$supportedCaches = array(
+                       // The ZendOpcache aka OPcache since PHP 5.5
+                       // http://php.net/manual/de/book.opcache.php
+                       'OPcache' => array(
+                               'active' => extension_loaded('Zend OPcache') && ini_get('opcache.enable') === '1',
+                               'version' => phpversion('Zend OPcache'),
+                               'canReset' => TRUE, // opcache_reset() ... it seems that it doesn't reset for current run.
+                               // From documentation this function exists since first version (7.0.0) but from Changelog
+                               // this function exists since 7.0.2
+                               // http://pecl.php.net/package-changelog.php?package=ZendOpcache&release=7.0.2
+                               'canInvalidate' => function_exists('opcache_invalidate'),
+                               'error' => FALSE,
+                               'clearCallback' => function ($fileAbsPath) {
+                                       if (function_exists('opcache_invalidate')) {
+                                               opcache_invalidate($fileAbsPath);
+                                       } else {
+                                               opcache_reset();
+                                       }
+                               }
+                       ),
+
+                       // The Alternative PHP Cache aka APC
+                       // http://www.php.net/manual/de/book.apc.php
+                       'APC' => array(
+                               // Currently APCu identifies itself both as "apcu" and "apc" (for compatibility) although it doesn't
+                               // provide the APC-opcache functionality
+                               'active' => extension_loaded('apc') && !extension_loaded('apcu') && ini_get('apc.enabled') === '1',
+                               'version' => $apcVersion,
+                               // apc_clear_cache() since APC 2.0.0 so default yes. In cli it do not clear the http cache.
+                               'canReset' => TRUE,
+                               'canInvalidate' => self::canApcInvalidate(),
+                               // Versions lower then 3.1.7 are known as malfunction
+                               'error' => $apcVersion && VersionNumberUtility::convertVersionNumberToInteger($apcVersion) < 3001007,
+                               'clearCallback' => function ($fileAbsPath) {
+                                       if (static::$supportedCaches['APC']['canInvalidate']) {
+                                               // This may output a warning like: PHP Warning: apc_delete_file(): Could not stat file
+                                               // This warning isn't true, this means that apc was unable to generate the cache key
+                                               // which depends on the configuration of APC.
+                                               apc_delete_file($fileAbsPath);
+                                       } else {
+                                               apc_clear_cache('opcode');
+                                       }
+                               }
+                       ),
+
+                       // http://www.php.net/manual/de/book.wincache.php
+                       'WinCache' => array(
+                               'active' => extension_loaded('wincache') && ini_get('wincache.ocenabled') === '1',
+                               'version' => phpversion('wincache'),
+                               'canReset' => FALSE,
+                               'canInvalidate' => TRUE, // wincache_refresh_if_changed()
+                               'error' => FALSE,
+                               'clearCallback' => function ($fileAbsPath) {
+                                       wincache_refresh_if_changed(array($fileAbsPath));
+                               }
+                       ),
+
+                       // http://xcache.lighttpd.net/
+                       'XCache' => array(
+                               'active' => extension_loaded('xcache'),
+                               'version' => phpversion('xcache'),
+                               'canReset' => TRUE, // xcache_clear_cache()
+                               'canInvalidate' => FALSE,
+                               'error' => FALSE,
+                               'clearCallback' => function ($fileAbsPath) {
+                                       xcache_clear_cache(XC_TYPE_PHP);
+                               }
+                       ),
+
+                       // https://github.com/eaccelerator/eaccelerator
+                       //
+                       // @see https://github.com/eaccelerator/eaccelerator/blob/master/doc/php/info.php
+               // Only possible if we are in eaccelerator.admin_allowed_path and we can only remove data
+                       // "that isn't used in the current requests"
+                       'eAccelerator' => array(
+                               'active' => extension_loaded('eAccelerator'),
+                               'version' => phpversion('eaccelerator'),
+                               'canReset' => FALSE,
+                               'canInvalidate' => FALSE,
+                               'error' => TRUE, // eAccelerator is more or less out of date and not functional for what we need.
+                               'clearCallback' => function ($fileAbsPath) {
+                                       eaccelerator_clear();
+                               }
+                       ),
+
+                       // https://github.com/zendtech/ZendOptimizerPlus
+                       // http://files.zend.com/help/Zend-Server/zend-server.htm#zendoptimizerplus.html
+                       'ZendOptimizerPlus' => array(
+                               'active' => extension_loaded('Zend Optimizer+') && ini_get('zend_optimizerplus.enable') === '1',
+                               'version' => phpversion('Zend Optimizer+'),
+                               'canReset' => TRUE, // accelerator_reset()
+                               'canInvalidate' => FALSE,
+                               'error' => FALSE,
+                               'clearCallback' => function ($fileAbsPath) {
+                                       accelerator_reset();
+                               }
+                       ),
+               );
+
+               static::$activeCaches = array();
+               // Cache the active ones
+               foreach (static::$supportedCaches as $opcodeCache => $properties) {
+                       if ($properties['active']) {
+                               static::$activeCaches[$opcodeCache] = $properties;
+                       }
+               }
+       }
+
+       /**
+        * Clears a file from an opcache, if one exists.
+        *
+        * @param string $fileAbsPath The file as absolute path to be cleared.
+        *
+        * @return void
+        */
+       static public function clearAllActive($fileAbsPath) {
+               foreach (static::getAllActive() as $properties) {
+                       $callback = $properties['clearCallback'];
+                       $callback($fileAbsPath);
+               }
+       }
+
+       /**
+        * Returns all supported and active opcaches
+        *
+        * @return array Array filled with supported and active opcaches
+        */
+       static public function getAllActive() {
+               if (static::$activeCaches === NULL) {
+                       static::initialize();
+               }
+               return static::$activeCaches;
+       }
+
+       /**
+        * Checks if the APC configuration is useable to clear cache of one file.
+        * https://bugs.php.net/bug.php?id=66819
+        *
+        * @return bool Returns TRUE if file can be invalidated and FALSE if complete cache needs to be removed
+        */
+       static public function canApcInvalidate() {
+               // apc_delete_file() should exists since APC 3.1.1 but you never know so default is no
+               $canInvalidate = FALSE;
+
+               if (function_exists('apc_delete_file')) {
+                       // Deleting files from cache depends on generating the cache key.
+                       // This cache key generation depends on unnecessary configuration options
+                       // http://git.php.net/?p=pecl/caching/apc.git;a=blob;f=apc_cache.c;h=d15cf8c1b4b9d09b9bac75b16c062c8b40458dda;hb=HEAD#l931
+
+                       // If stat=0 then canonicalized path may be used
+                       $stat = (int)ini_get('apc.stat');
+                       // If canonicalize (default = 1) then file_update_protection isn't checked
+                       $canonicalize = (int)ini_get('apc.canonicalize');
+                       // If file file_update_protection is checked, then we will fail, 'cause we generated the file and then try to
+                       // remove it. But the file is not older than file_update_protection and therefore hash generation will stop with error.
+                       $protection = (int)ini_get('apc.file_update_protection');
+
+                       if ($protection === 0 || ($stat === 0 && $canonicalize === 1)) {
+                               $canInvalidate = TRUE;
+                       }
+               }
+
+               return $canInvalidate;
+       }
+}
index 138e678..a5573bb 100644 (file)
@@ -743,29 +743,59 @@ class Check {
         * @return Status\StatusInterface
         */
        protected function checkSomePhpOpcodeCacheIsLoaded() {
-               if (
-                       // Currently APCu identifies itself both as "apcu" and "apc" (for compatibility) although it doesn't provide the APC-opcache functionality
-                       extension_loaded('eaccelerator')
-                       || extension_loaded('xcache')
-                       || (extension_loaded('apc') && !extension_loaded('apcu'))
-                       || extension_loaded('Zend Optimizer+')
-                       || extension_loaded('Zend OPcache')
-                       || extension_loaded('wincache')
-               ) {
-                       $status = new Status\OkStatus();
-                       $status->setTitle('A PHP opcode cache is loaded');
-               } else {
+               $opcodeCaches = \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::getAllActive();
+               if (count($opcodeCaches) === 0) {
                        $status = new Status\WarningStatus();
                        $status->setTitle('No PHP opcode cache loaded');
                        $status->setMessage(
                                'PHP opcode caches hold a compiled version of executed PHP scripts in' .
                                ' memory and do not require to recompile any script on each access.' .
                                ' This can be a massive performance improvement and can put load off a' .
-                               ' server in general, a parse time reduction by factor three for full cached' .
+                               ' server in general. A parse time reduction by factor three for fully cached' .
                                ' pages can be achieved easily if using some opcode cache.' .
                                ' If in doubt choosing one, APC runs well and can be used as data' .
                                ' cache layer in TYPO3 CMS as additional feature.'
                        );
+               } else {
+                       $status = new Status\OkStatus();
+                       $message = '';
+
+                       foreach ($opcodeCaches as $opcodeCache => $properties) {
+                               $message .= 'Name: ' . $opcodeCache . ' Version: ' . $properties['version'];
+                               $message .= LF;
+
+                               if ($properties['error']) {
+                                       // Set status to error if not already set
+                                       if ($status->getSeverity() !== 'error') {
+                                               $status = new Status\ErrorStatus();
+                                       }
+                                       $message .= ' This opcode cache is marked as malfunctioning by the TYPO3 CMS Team.';
+                               } elseif ($properties['canInvalidate']) {
+                                       $message .= ' This opcode cache should work correctly and has good performance.';
+                               } else {
+                                       // Set status to warning if not already error set
+                                       if ($status->getSeverity() !== 'error' || $status->getSeverity() !== 'warning') {
+                                               $status = new Status\WarningStatus();
+                                       }
+                                       $message .= ' This opcode cache may work correctly but has medium performance.';
+                               }
+                               $message .= LF;
+                       }
+
+                       // Set title of status depending on serverity
+                       switch ($status->getSeverity()) {
+                               case 'error':
+                                       $status->setTitle('A possibly malfunctioning PHP opcode cache is loaded');
+                                       break;
+                               case 'warning':
+                                       $status->setTitle('A PHP opcode cache is loaded, which may cause problems');
+                                       break;
+                               case 'ok':
+                               default:
+                                       $status->setTitle('A PHP opcode cache is loaded');
+                                       break;
+                       }
+                       $status->setMessage($message);
                }
                return $status;
        }