Version: current

      1 <?php
      2  /**
      3  * This file is part of phpMorphy library
      4  *
      5  * Copyright c 2007-2008 Kamaev Vladimir <heromantor@users.sourceforge.net>
      6  *
      7  * This library is free software; you can redistribute it and/or
      8  * modify it under the terms of the GNU Lesser General Public
      9  * License as published by the Free Software Foundation; either
     10  * version 2 of the License, or (at your option) any later version.
     11  *
     12  * This library is distributed in the hope that it will be useful,
     13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     15  * Lesser General Public License for more details.
     16  *
     17  * You should have received a copy of the GNU Lesser General Public
     18  * License along with this library; if not, write to the
     19  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
     20  * Boston, MA 02111-1307, USA.
     21  */
     22
     23 if(!defined('PHPMORPHY_SHM_SEGMENT_SIZE')) {
     24     define('PHPMORPHY_SHM_SEGMENT_SIZE', 1024 * 1024 * 24);
     25 }
     26
     27 if(!defined('PHPMORPHY_SHM_SEGMENT_ID')) {
     28     define('PHPMORPHY_SHM_SEGMENT_ID', 0x54358308);
     29 }
     30
     31 if(!defined('PHPMORPHY_SEMAPHORE_KEY')) {
     32     define('PHPMORPHY_SEMAPHORE_KEY', PHPMORPHY_SHM_SEGMENT_ID + 1);
     33 }
     34
     35 if(!defined('PHPMORPHY_SHM_HEADER_MAX_SIZE')) {
     36     define('PHPMORPHY_SHM_HEADER_MAX_SIZE', 1024 * 32);
     37 }
     38
     39 interface phpMorphy_Shm_Cache_Interface {
     40     function close();
     41     function get($filePath);
     42     function clear();
     43     function delete($filePath);
     44     function reload($filePath);
     45     function reloadIfExists($filePath);
     46     function free();
     47 }
     48
     49 class phpMorphy_Shm_Cache_FileDescriptor {
     50     private
     51         $shm_id,
     52         $file_size,
     53         $offset;
     54
     55     function __construct($shmId, $fileSize, $offset) {
     56         $this->shm_id = $shmId;
     57         $this->file_size = $fileSize;
     58         $this->offset = $offset;
     59     }
     60
     61     function getShmId() { return $this->shm_id; }
     62     function getFileSize() { return $this->file_size; }
     63     function getOffset() { return $this->offset; }
     64 }
     65
     66 abstract class phpMorphy_Semaphore {
     67     abstract function lock();
     68     abstract function unlock();
     69
     70     static function create($key, $empty = false) {
     71         if(!$empty) {
     72             if (0 == strcasecmp($GLOBALS['__phpmorphy_substr'](PHP_OS, 0, 3), 'WIN')) {
     73                 $clazz = 'phpMorphy_Semaphore_Win';
     74             } else {
     75                 $clazz = 'phpMorphy_Semaphore_Nix';
     76             }
     77         } else {
     78             $clazz = 'phpMorphy_Semaphore_Empty';
     79         }
     80
     81         return new $clazz($key);
     82     }
     83 };
     84
     85 class phpMorphy_Semaphore_Empty extends phpMorphy_Semaphore {
     86     function lock() { }
     87     function unlock() { }
     88     function remove() { }
     89 };
     90
     91 // TODO: implement this
     92 class phpMorphy_Semaphore_Win extends phpMorphy_Semaphore {
     93     const DIR_NAME = 'phpmorphy_semaphore';
     94     const USLEEP_TIME = 100000; // 0.1s
     95     const MAX_SLEEP_TIME = 5000000; // 5sec
     96
     97     protected $dir_path;
     98
     99     protected function __construct($key) {
    100         $this->dir_path = $this->getTempDir() . DIRECTORY_SEPARATOR . self::DIR_NAME . "_$key";
    101
    102         register_shutdown_function(array($this, 'unlock'));
    103     }
    104
    105     protected function getTempDir() {
    106         if(false === ($result = getenv('TEMP'))) {
    107             if(false === ($result = getenv('TMP'))) {
    108                 throw new phpMorphy_Exception("Can`t get temporary directory");
    109             }
    110         }
    111
    112         return $result;
    113     }
    114
    115     function lock() {
    116         for($i = 0; $i < self::MAX_SLEEP_TIME; $i += self::USLEEP_TIME) {
    117             if(!file_exists($this->dir_path)) {
    118                 if(false !== @mkdir($this->dir_path, 0644)) {
    119                     return true;
    120                 }
    121             }
    122
    123             usleep(self::USLEEP_TIME);
    124         }
    125
    126         throw new phpMorphy_Exception("Can`t acquire semaphore");
    127     }
    128
    129     function unlock() {
    130         @rmdir($this->dir_path);
    131     }
    132
    133     function remove() {
    134     }
    135 }
    136
    137 class phpMorphy_Semaphore_Nix extends phpMorphy_Semaphore {
    138     const DEFAULT_PERM = 0644;
    139
    140     private $sem_id = false;
    141
    142     protected function __construct($key) {
    143         if(false === ($this->sem_id = sem_get($key, 1, self::DEFAULT_PERM, true))) {
    144             throw new phpMorphy_Exception("Can`t get semaphore for '$key' key");
    145         }
    146     }
    147
    148     function lock() {
    149         if(false === sem_acquire($this->sem_id)) {
    150             throw new phpMorphy_Exception("Can`t acquire semaphore");
    151         }
    152     }
    153
    154     function unlock() {
    155         if(false === sem_release($this->sem_id)) {
    156             throw new phpMorphy_Exception("Can`t release semaphore");
    157         }
    158     }
    159
    160     function remove() {
    161         sem_remove($this->sem_id);
    162     }
    163 }
    164
    165 class phpMorphy_Shm_Header {
    166     protected
    167         $max_size,
    168         $segment_id,
    169         $files_map = array(),
    170         $free_map = array();
    171
    172     function __construct($segmentId, $maxSize) {
    173         $this->max_size = (int)$maxSize;
    174         $this->segment_id = $segmentId;
    175
    176         $this->clear();
    177     }
    178
    179     function lookup($filePath) {
    180         if(!$this->exists($filePath)) {
    181             throw new phpMorphy_Exception("'$filePath' not found in shm");
    182         }
    183
    184         return $this->files_map[$this->normalizePath($filePath)];
    185     }
    186
    187     function exists($filePath) {
    188         return isset($this->files_map[$this->normalizePath($filePath)]);
    189     }
    190
    191     function register($filePath, $fh) {
    192         if($this->exists($filePath)) {
    193             throw new phpMorphy_Exception("Can`t register, '$filePath' already exists");
    194         }
    195
    196         if(false === ($stat = fstat($fh))) {
    197             throw new phpMorphy_Exception("Can`t fstat '$filePath' file");
    198         }
    199
    200         $file_size = $stat['size'];
    201
    202         $offset = $this->getBlock($file_size);
    203
    204         $entry = array(
    205             'offset' => $offset,
    206             'mtime' => $stat['mtime'],
    207             'size' => $file_size,
    208             'shm_id' => $this->segment_id
    209         );
    210
    211         $this->files_map[$this->normalizePath($filePath)] = $entry;
    212
    213         return $entry;
    214     }
    215
    216     function delete($filePath) {
    217         $data = $this->lookup($filePath);
    218
    219         unset($this->files_map[$this->normalizePath($filePath)]);
    220
    221         $this->freeBlock($data['offset'], $data['size']);
    222     }
    223
    224     function clear() {
    225         $this->files_map = array();
    226         $this->free_map = array(0 => $this->max_size);
    227     }
    228
    229     function getAllFiles() {
    230         return $this->files_map;
    231     }
    232
    233     protected function registerBlock($offset, $size) {
    234         $old_size = $this->free_map[$offset];
    235
    236         if($old_size < $size) {
    237             throw new phpMorphy_Exception("Too small free block for register(free = $old_size, need = $size)");
    238         }
    239
    240         unset($this->free_map[$offset]);
    241
    242         if($old_size > $size) {
    243             $this->free_map[$offset + $size] = $old_size - $size;
    244         }
    245     }
    246
    247     protected function freeBlock($offset, $size) {
    248         $this->free_map[$offset] = $size;
    249         $this->defrag();
    250     }
    251
    252     protected function defrag() {
    253         ksort($this->free_map);
    254
    255         $map_count = count($this->free_map);
    256
    257         if($map_count < 2) {
    258             return;
    259         }
    260
    261         $keys = array_keys($this->free_map);
    262         $i = 0;
    263         $prev_offset = $keys[$i];
    264
    265         for($i++; $i < $map_count; $i++) {
    266             $offset = $keys[$i];
    267
    268             if($prev_offset + $this->free_map[$prev_offset] == $offset) {
    269                 // merge
    270                 $this->free_map[$prev_offset] += $this->free_map[$offset];
    271
    272                 unset($this->free_map[$offset]);
    273             } else {
    274                 $prev_offset = $offset;
    275             }
    276         }
    277     }
    278
    279     protected function getBlock($fileSize) {
    280         foreach($this->free_map as $offset => $size) {
    281             if($size >= $fileSize) {
    282                 $this->registerBlock($offset, $fileSize);
    283
    284                 return $offset;
    285             }
    286         }
    287
    288         throw new phpMorphy_Exception("Can`t find free space for $size block");
    289     }
    290
    291     protected function normalizePath($path) {
    292         return $path;
    293     }
    294 }
    295
    296 class phpMorphy_Shm_Cache implements phpMorphy_Shm_Cache_Interface {
    297     const DEFAULT_MODE = 0644;
    298     const READ_BLOCK_SIZE = 8192;
    299
    300     protected static $EXTENSION_PRESENT = null;
    301
    302     protected
    303         $options,
    304         $semaphore,
    305         $segment
    306         ;
    307
    308     function __construct($options = array(), $clear = false) {
    309         if(!isset(self::$EXTENSION_PRESENT)) {
    310             self::$EXTENSION_PRESENT = extension_loaded('shmop');
    311         }
    312
    313         if(!self::$EXTENSION_PRESENT) {
    314             throw new phpMorphy_Exception("shmop extension needed");
    315         }
    316
    317         $this->options = $options = $this->repairOptions($options);
    318
    319         $this->semaphore = phpMorphy_Semaphore::create($options['semaphore_key'], $options['no_lock']);
    320
    321         $this->segment = $this->getSegment($options['segment_id'], $options['segment_size']);
    322
    323         if($clear) {
    324             $this->semaphore->remove();
    325             $this->initHeaderObject($this->segment);
    326         }
    327     }
    328
    329     static function clearSemaphore($semaphoreId = null) {
    330         $semaphoreId = isset($semaphoreId) ? $semaphoreId : PHPMORPHY_SEMAPHORE_KEY;
    331
    332         $sem = phpMorphy_Semaphore::create($semaphoreId);
    333         return $sem->remove();
    334     }
    335
    336     protected function repairOptions($options) {
    337         $defaults = array(
    338             'semaphore_key' => PHPMORPHY_SEMAPHORE_KEY,
    339             'segment_id' => PHPMORPHY_SHM_SEGMENT_ID,
    340             'segment_size' => PHPMORPHY_SHM_SEGMENT_SIZE,
    341             'with_mtime' => false,
    342             'header_max_size' => PHPMORPHY_SHM_HEADER_MAX_SIZE,
    343             'no_lock' => false,
    344         );
    345
    346         return (array)$options + $defaults;
    347     }
    348
    349     function close() {
    350         if(isset($this->segment)) {
    351             shmop_close($this->segment);
    352             $this->segment = null;
    353         }
    354     }
    355
    356     protected function safeInvoke($filePath, $method) {
    357         $this->lock();
    358
    359         try {
    360             $header = $this->readHeader();
    361
    362             $result = $this->$method($filePath, $header);
    363
    364             // writeHeader is atomic
    365             $this->writeHeader($this->segment, $header);
    366
    367             $this->unlock();
    368
    369             return $result;
    370         } catch (Exception $e) {
    371             $this->unlock();
    372
    373             throw $e;
    374         }
    375     }
    376
    377     protected function doGet($filePath, $header) {
    378         $result = array();
    379         foreach((array)$filePath as $file) {
    380             $result[$file] = $this->getSingleFile($header, $file);
    381         }
    382
    383         if(!is_array($filePath)) {
    384             $result = $result[$filePath];
    385         }
    386
    387         return $result;
    388     }
    389
    390     function get($filePath) {
    391         if(!is_array($filePath)) {
    392             return $this->createFileDescriptor($this->safeInvoke($filePath, 'doGet'));
    393         } else {
    394             $result = array();
    395
    396             foreach($this->safeInvoke($filePath, 'doGet') as $file => $item) {
    397                 $result[$file] = $this->createFileDescriptor($item);
    398             }
    399
    400             return $result;
    401         }
    402     }
    403
    404
    405     protected function getSingleFile($header, $filePath) {
    406         try {
    407             $fh = false;
    408
    409             if(false !== $header->exists($filePath)) {
    410                 $result = $header->lookup($filePath);
    411
    412                 if(!$this->options['with_mtime']) {
    413                     return $result;
    414                 }
    415
    416                 if(false === ($mtime = filemtime($filePath))) {
    417                     throw new phpMorphy_Exception("Can`t get mtime attribute for '$filePath' file");
    418                 }
    419
    420                 if($result['mtime'] === $mtime) {
    421                     return $result;
    422                 }
    423
    424                 $fh = $this->openFile($filePath);
    425
    426                 // update
    427                 $header->delete($filePath);
    428                 $result = $header->register($filePath, $fh);
    429
    430                 $this->saveFile($fh, $result['offset']);
    431
    432                 fclose($fh);
    433
    434                 return $result;
    435             }
    436
    437             // register
    438             $fh = $this->openFile($filePath);
    439
    440             $result = $header->register($filePath, $fh);
    441
    442             $this->saveFile($fh, $result['offset']);
    443
    444             fclose($fh);
    445
    446             return $result;
    447         } catch (Exception $e) {
    448             if(isset($fh) && $fh !== false) {
    449                 fclose($fh);
    450             }
    451
    452             throw $e;
    453         }
    454     }
    455
    456     protected function doClear($filePath, $header) {
    457         $header->clear();
    458     }
    459
    460     function clear() {
    461         $this->safeInvoke(null, 'doClear');
    462     }
    463
    464     protected function doDelete($filePath, $header) {
    465         foreach((array)$filePath as $file) {
    466             $hdr->delete($file);
    467         }
    468     }
    469
    470     function delete($filePath) {
    471         $this->safeInvoke($filePath, 'doDelete');
    472     }
    473
    474     protected function doReload($filePath, $header) {
    475         $return = array();
    476
    477         foreach((array)$filePath as $file) {
    478             $fh = $this->openFile($file);
    479
    480             // update
    481             $hdr->delete($file);
    482             $result = $hdr->register($file, $fh);
    483
    484             $this->saveFile($fh, $result['offset']);
    485
    486             fclose($fh);
    487             $fh = false;
    488
    489             $return[$file] = $result;
    490         }
    491
    492         if(!is_array($filePath)) {
    493             $return = $return[$filePath];
    494         }
    495
    496         return $return;
    497     }
    498
    499     function reload($filePath) {
    500         if(!is_array($filePath)) {
    501             return $this->createFileDescriptor($this->safeInvoke($filePath, 'doReload'));
    502         } else {
    503             $result = array();
    504
    505             foreach($this->safeInvoke($filePath, 'doReload') as $file => $item) {
    506                 $result[$file] = $this->createFileDescriptor($item);
    507             }
    508
    509             return $result;
    510         }
    511     }
    512
    513     function reloadIfExists($filePath) {
    514         try {
    515             return $this->reload($filePath);
    516         } catch (Exception $e) {
    517             return false;
    518         }
    519     }
    520
    521     function free() {
    522         $this->lock();
    523         if(false === shmop_delete($this->segment)) {
    524             throw new phpMorphy_Exception("Can`t delete $this->segment segment");
    525         }
    526
    527         $this->close();
    528
    529         $this->unlock();
    530     }
    531
    532     function getFilesList() {
    533         $this->lock();
    534
    535         $result = $this->readHeader()->getAllFiles();
    536
    537         $this->unlock();
    538
    539         return $result;
    540     }
    541
    542     protected function createFileDescriptor($result) {
    543         return new phpMorphy_Shm_Cache_FileDescriptor($this->segment, $result['size'], $this->options['header_max_size'] + $result['offset']);
    544     }
    545
    546     protected function openFile($filePath) {
    547         if(false === ($fh = fopen($filePath, 'rb'))) {
    548             throw new phpMorphy_Exception("Can`t open '$filePath' file");
    549         }
    550
    551         return $fh;
    552     }
    553
    554     protected function lock() {
    555         $this->semaphore->lock();
    556     }
    557
    558     protected function unlock() {
    559         $this->semaphore->unlock();
    560     }
    561
    562     protected function getFilesOffset() {
    563         return $this->options['header_max_size'];
    564     }
    565
    566     protected function getMaxOffset() {
    567         return $this->options['segment_size'] - 1;
    568     }
    569
    570     protected function saveFile($fh, $offset) {
    571         if(false === ($stat = fstat($fh))) {
    572             throw new phpMorphy_Exception("Can`t fstat '$filePath'");
    573         }
    574
    575         $file_size = $stat['size'];
    576         $chunk_size = self::READ_BLOCK_SIZE;
    577
    578         $max_offset = $offset + $file_size;
    579
    580         if($max_offset >= $this->getMaxOffset()) {
    581             throw new phpMorphy_Exception("Can`t write '$filePath' file to $offset offset, not enough space");
    582         }
    583
    584         $i = 0;
    585         while(!feof($fh)) {
    586             $data = fread($fh, $chunk_size);
    587             if(false === (shmop_write($this->segment, $data, $this->getFilesOffset() + $offset + $i))) {
    588                 throw new phpMorphy_Exception("Can`t write chunk of file '$filePath' to shm");
    589             }
    590
    591             $i += $chunk_size;
    592         }
    593     }
    594
    595     protected function getSegment($segmentId, $segmentSize) {
    596         $this->lock();
    597
    598         try {
    599             $shm_id = $this->openSegment($segmentId, $segmentSize, $is_new);
    600
    601             if($is_new) {
    602                 $this->initHeaderObject($shm_id, false);
    603             }
    604         } catch (Exception $e) {
    605             $this->unlock();
    606             throw $e;
    607         }
    608
    609         $this->unlock();
    610
    611         return $shm_id;
    612     }
    613
    614     protected function initHeaderObject($shmId, $lock = true) {
    615         if($lock) {
    616             $this->lock();
    617             $this->writeHeader($shmId, $this->createHeader($shmId));
    618             $this->unlock();
    619         } else {
    620             $this->writeHeader($shmId, $this->createHeader($shmId));
    621         }
    622     }
    623
    624     protected function readHeader() {
    625         if(false === ($data = shmop_read($this->segment, 0, $this->getFilesOffset()))) {
    626             throw new phpMorphy_Exception("Can`t read header for " . $this->segment);
    627         }
    628
    629         if(false === ($result = unserialize($data))) {
    630             throw new phpMorphy_Exception("Can`t unserialize header for " . $this->segment);
    631         }
    632
    633         return $result;
    634     }
    635
    636     protected function writeHeader($shmId, phpMorphy_Shm_Header $header) {
    637         $data = serialize($header);
    638
    639         if($GLOBALS['__phpmorphy_strlen']($data) > $this->getFilesOffset()) {
    640             throw new phpMorphy_Exception("Too long header, try increase PHPMORPHY_SHM_HEADER_MAX_SIZE");
    641         }
    642
    643         if(false === shmop_write($shmId, $data, 0)) {
    644             throw new phpMorphy_Exception("Can`t write shm header");
    645         }
    646     }
    647
    648     protected function createHeader($shmId) {
    649         return new phpMorphy_Shm_Header($shmId, $this->options['segment_size']);
    650     }
    651
    652     protected function openSegment($segmentId, $size, &$new = null) {
    653         $new = false;
    654
    655         if(false === ($handle = @shmop_open($segmentId, 'w', 0, 0))) {
    656             if(false === ($handle = shmop_open($segmentId, 'n', self::DEFAULT_MODE, $size))) {
    657                 throw new phpMorphy_Exception("Can`t create SHM segment with '$segmentId' id and $size size");
    658             }
    659
    660             $new = true;
    661         }
    662
    663         return $handle;
    664     }
    665 }
    666
    667

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -