1 <?php
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
92 class phpMorphy_Semaphore_Win extends phpMorphy_Semaphore {
93 const DIR_NAME = 'phpmorphy_semaphore';
94 const USLEEP_TIME = 100000;
95 const MAX_SLEEP_TIME = 5000000;
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 {
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
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
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
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
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
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 ($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 () {
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 ($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 ($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.
-